[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на C# станут решающим фактором. Переходите по ссылке 🔰.
В этом уроке, мы рассмотрим как быстро и просто реализовать стандартный набор CRUD операцией в проекте Blazor Server.
Приложение будет управлять базой данных AnimalsDb, а его главная страница в конце будет выглядеть, как показано выше.
В этом проекте, я покажу альтернативные способы создания компонентов на основе «Scaffolding Tools».
Scaffolding Tools — мощный набор инструментов, который позволяет автоматически создавать базовый код для веб-приложений на основе существующей базы данных или моделей данных. Они упрощают и ускоряют процесс разработки, позволяя генерировать стандартные CRUD (Create, Read, Update, Delete) операции и контроллеры для управления данными.
Создание и настройка проекта
Чтобы создать проект Animals, запустите Visual Studio и выберите в меню File (Файл) — New Project (Создать Проект). Укажите шаблон проекта Blazor Web App.
Введите Animals, в поле Name, на следующей странице укажите .Net 8.0, Intractive render mode: Server, отметим checkbox для пункта – Include sample pages. Нажмите кнопку Create:
На данный момент у нас есть начальный проект. Давайте запустим проект (Crtl + F5), чтобы убедиться, что все в порядке:
Модель и Репозиторий
Модель для приложения Animals будет основана на списке фильмов. Для этого реализуем класс Animals. Мы будем использовать этот класс с Entity Framework Core (EF Core) для работы с базой данных. EF Core — это фреймворк объектно-реляционного отображения (ORM), который упрощает код доступа к данным. Классы моделей не имеют зависимости от EF Core. Они просто определяют свойства данных, которые будут храниться в базе данных.
Создайте папку Models и добавьте в нее файл класса по имени Animal.cs со следующим содержимым:
1 2 3 4 5 6 7 8 9 |
public class Animal { public int Id { get; set; } public string Name { get; set; } public string Gender { get; set; } public double Weight { get; set; } public AnimalType AnimalType { get; set; } public DateTime CreatedAt { get; set; } = DateTime.Now; } |
Затем добавьте еще один класс под названием AnimalType.cs, как показано ниже:
1 2 3 4 5 6 7 8 9 |
public enum AnimalType { Mammals, Reptiles, Amphibians, Birds, Insects, Fish } |
Пришло время создать все необходимые компоненты, используя scaffolding. Нажмем правой клавишей по папке Components / Pages и нажмем правой клавишей мышки Add – New Scaffoldet Item – Razor Components using Entity Framework (CRUD):
В следующем окне, выбираем следующие пункты:
Внимание, данная опция доступна только с Visual Studio preview 17.9. Нажимаем кнопку «Add».
Если в ходе добавления возникла ошибка, со следующим содержимым:
«install the package microsoft.visualstudio.web.codegeneration.design and try again»
Перейдите в Dependencies – Manage NuGet Packages – Browse и укажите в поиске:
Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design
Установите данную библиотеку. ВАЖНО! Если библиотека уже установлена и ошибка по-прежнему возникает, попробуйте пересобрать проект или же переустановить данную библиотеку вручную.
На этом этапе работает инструмент scaffolding. Автоматическое создание контекста базы данных и методов и представлений CRUD (create, read, update, and delete) действий известно как scaffolding.
Созданные компоненты Razor добавляются в папку Pages проекта в сгенерированную папку, названную по имени класса модели. Сгенерированный компонент Index использует QuickGrid для отображения данных, о нем поговорим немного позже.
В нашем случае, в папке Pages, появится папка AnimalPages, со следующими компонентами для CRUD операций:
В корне проекта можно увидеть новую папку Data, со сгенерированным классом ApplicationContext:
1 2 3 4 5 6 7 8 9 10 |
public class ApplicationContext : DbContext { public ApplicationContext (DbContextOptions<ApplicationContext> options) : base(options) { } public DbSet<Animals.Models.Animal> Animal { get; set; } = default!; } |
ApplicationContext координирует функциональность EF Core (Create, Read, Update, Delete и т.д.) для модели Animal.
ASP.NET Core построен с использованием инъекции зависимостей (DI). Службы (например, контекст БД EF Core) регистрируются с помощью DI во время запуска приложения. Компоненты, которым требуются эти службы, получают их через параметры конструктора:
1 2 |
builder.Services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("ApplicationContext") ?? throw new InvalidOperationException("Connection string 'ApplicationContext' not found."))); |
Имя строки подключения передается в контекст путем вызова метода на объекте DbContextOptions. Для локальной разработки система конфигурации ASP.NET Core считывает строку подключения из файла appsettings.json, который уже успешно сгенерирован с необходимым содержимым, перейдем в файл и измени название базы данных на AnimalDb:
1 2 3 |
"ConnectionStrings": { "ApplicationContext": "Server=(localdb)\\mssqllocaldb;Database=AnimalDb;Trusted_Connection=True;MultipleActiveResultSets=true" } |
Так же, в классе Program, можно увидеть подключение следующего компонента:
1 |
builder.Services.AddQuickGridEntityFrameworkAdapter(); |
Компонент QuickGrid — это компонент Razor для быстрого и эффективного отображения данных в табличной форме. QuickGrid представляет собой простой и удобный компонент сетки данных для распространенных сценариев визуализации сетки и служит эталонной архитектурой и базой производительности для создания компонентов сетки данных. QuickGrid отличается высокой степенью оптимизации и использует передовые методы для достижения оптимальной производительности рендеринга.
Он используется на страницах, где отображается сводная информация, к примеру, откроем компонент Index.razor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@page "/animals" @using Microsoft.AspNetCore.Components.QuickGrid @inject Animals.Data.ApplicationContext DB @using Animals.Models <PageTitle>Index</PageTitle> <h1>Index</h1> <p> <a href="animals/create">Create New</a> </p> <QuickGrid Class="table" Items="DB.Animal"> <PropertyColumn Property="animal => animal.Name" /> <PropertyColumn Property="animal => animal.Gender" /> <PropertyColumn Property="animal => animal.Weight" /> <PropertyColumn Property="animal => animal.AnimalType" /> <PropertyColumn Property="animal => animal.CreatedAt" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> |
EF Core’s ApplicationContext предоставляет свойство DbSet<TEntity> для каждой таблицы в базе данных. Указываем нужное свойство в параметре Items.
В следующем примере в качестве источника данных используется DbSet<TEntity> (таблица) Animal:
1 |
<QuickGrid Class="table" Items="DB.Animal"> |
Вы также можете использовать любой оператор LINQ, поддерживаемый EF, для фильтрации данных перед передачей их в параметр Items.
К примеру, отобразим только подвид рыб:
1 |
<QuickGrid Class="table" Items="DB.Animal.Where(e=>e.AnimalType == AnimalType.Fish)"> |
QuickGrid распознает экземпляры IQueryable, поставляемые EF, и умеет выполнять запросы асинхронно для повышения эффективности.
Учитывайте, что компоненты созданные с помощью скаффолдера, требуют рендеринга на стороне сервера (SSR), поэтому они не поддерживаются при работе с WebAssembly.
На данном этапе, на страницах Create.razor и Edit.razor, будет ошибка, связанная со свойством перечислением AnimalType, исправим ее, заменим элемент InputText:
1 |
<InputText id="animaltype" @bind-Value="Animal.AnimalType" class="form-control" /> |
На элемент InputSelect:
1 2 3 4 5 6 |
<InputSelect id="animaltype" @bind-Value="Animal.AnimalType" class="form-control"> @foreach (var value in Enum.GetValues<AnimalType>()) { <option value="@value">@value</option> } </InputSelect> |
Так же, давайте облегчим пагинацию, указав ссылку на маршрутизируем компонент Index.razor, в компоненте NavMenu.razor:
1 2 3 4 5 |
<div class="nav-item px-3"> <NavLink class="nav-link" href="animals"> <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Animals </NavLink> </div> |
Создание базы данных
Создадим базу данных с помощью функции EF Core Migrations. Миграции позволяет нам создать базу данных, соответствующую нашей модели данных, и обновлять схему базы данных при изменении модели данных.
Откройте Tools -> NuGet Package Manager > Package Manager Console (PMC) и выполните следующую команду в PMC:
PM> Add-Migration Initial
Команда Add-Migration генерирует код для создания начальной схемы базы данных, которая основана на модели, указанной в классе TvShowsContext. Аргумент Initial — это имя миграции, можно использовать любое имя.
После выполнения команды в папке Migrations будет создан файл миграции:
В качестве следующего шага выполните следующую команду в Package Manager Console:
PM> Update-Database
Команда Update-Database запускает метод Up в файле Migrations/{time-stamp}_InitialCreate.cs, который создает базу данных.
Первоначальная настройка завершена, запустим приложение и проверим его работу:
Валидация данных
Применение компонента EditForm позволяет легко добавить валидацию к форме. Для валидации данных нам нужен валидатор — объект, который определяет, как проверять корректность данных. По умолчанию фреймворк Blazor предоставляет валидатор аннотаций данных — компонент DataAnnotationsValidator, который прикрепляет к форме поддержку валидации с помощью аннотаций данных. Как и в целом в .NET, аннотации данных в виде атрибутов позволяют установить правила валидации свойств модели.
По умолчанию на страницах Create.razor и Edit.razor, этот компонент уже добавлен:
1 |
<DataAnnotationsValidator /> |
На страницу, добавлен компонент ValidationSummary, который выводит все сообщения об ошибках. На уровне разметки html генерируется html-элемент <ul> с классом validation-errors:
1 |
<ValidationSummary class="text-danger" /> |
Так же, добавлен компонент ValidationMessage<T>, который отображает сообщение об ошибке для отдельного свойства модели. Этот компонент генерирует html-элемент <div> с классом validation-message, который содержит сообщение об ошибке.
С помощью свойства For компонент ValidationMessage устанавливает валидируемое свойство модели через передаваемое лямбда-выражение.
Все что нам остается сделать, то использовать атрибуты для нашего класса Animal:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Animal { public int Id { get; set; } [Required] public string? Name { get; set; } [Required] public string? Gender { get; set; } [Required] [Range(minimum:0.1, maximum: 300)] public double Weight { get; set; } public AnimalType AnimalType { get; set; } public DateTime CreatedAt { get; set; } = DateTime.Now; } |
Запустим приложение и проверим его работу:
Создание компонента оповещения
При выполнении, каких либо действий, удобно будет использовать компонент, для оповещения пользователя.
В папке Components, создадим папку Shared, внутри которой создадим компонент StatusMessage, со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@if (!string.IsNullOrEmpty(Message)) { var statusMessageClass = Message.StartsWith("Error") ? "danger" : "success"; <div class="alert bg-light alert-dismissible show text-@statusMessageClass" role="alert"> @((MarkupString)Message) <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } @code { [Parameter] public string? Message { get; set; } } |
Данный компонент построен на использовании Bootstrap. В нашем случае он сгодится для отображения сообщений 2-ух категорий: «Успешная операция» и «Ошибка». Немного позже мы используем его и вы увидите как это просто.
Создание компонента для наполнения тестовыми данными
Нам необходим компонент, который сможет заполнять БД тестовыми данными. Добавьте в папку Components / Pages, новую папку DataPages, а в ней новый компонент Seed.razor, со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
@page "/data/seed" @using Animals.Components.Shared @using Microsoft.EntityFrameworkCore @using System.ComponentModel.DataAnnotations @inject Animals.Data.ApplicationContext DB <PageTitle>Seed</PageTitle> <h3>Seed</h3> <hr /> <div class="row"> <StatusMessage Message="@message"></StatusMessage> <p>Specify the number of rows to be filled in.</p> <div class="col-md-4"> <EditForm method="post" Model="SeedModel" OnValidSubmit="CreateSeedData" FormName="createSeedData" Enhance> <DataAnnotationsValidator /> <ValidationSummary class="text-danger" /> <div class="mb-3"> <label for="count" class="form-label">Count:</label> <InputNumber id="count" @bind-Value="SeedModel.Count" class="form-control" /> <ValidationMessage For="() => SeedModel.Count" class="text-danger" /> </div> <button type="submit" class="btn btn-primary">Fill</button> </EditForm> </div> </div> @code { private string? message; [SupplyParameterFromForm] public SeedViewModel SeedModel{ get; set; } = new(); private void CreateSeedData() { ClearData(); if (SeedModel.Count > 0) { DB.Database.SetCommandTimeout(TimeSpan.FromMinutes(10)); DB.Database.ExecuteSqlRaw("DROP PROCEDURE IF EXISTS CreateSeedData"); DB.Database.ExecuteSqlRaw($@" CREATE PROCEDURE CreateSeedData @RowCount decimal AS BEGIN SET NOCOUNT ON DECLARE @i INT = 0; DECLARE @Gender NVARCHAR(10); BEGIN TRANSACTION WHILE @i <= @RowCount BEGIN IF(ROUND((RAND() * 1.0 ), 0) = 1) SET @Gender = 'Male'; ELSE SET @Gender = 'Female'; INSERT INTO Animal (Name,Gender,Weight,AnimalType,CreatedAt) VALUES (CONCAT('Name',' - ',@i), @Gender, ROUND(RAND() * (150-5+15),2), ROUND((RAND() * 5.0), 0), DATEADD(DAY, -(ABS(CHECKSUM(NEWID()) % 3600 )), getdate())) SET @i = @i + 1 END COMMIT END "); DB.Database.BeginTransaction(); DB.Database.ExecuteSqlRaw($"EXEC CreateSeedData @RowCount = {SeedModel.Count}"); DB.Database.CommitTransaction(); } message = $"The table has been successfully populated with {SeedModel.Count} items."; } private void ClearData() { DB.Database.SetCommandTimeout(TimeSpan.FromMinutes(10)); DB.Database.BeginTransaction(); DB.Database.ExecuteSqlRaw("DELETE FROM Animal"); DB.Database.CommitTransaction(); } public class SeedViewModel { [Required] [Range(minimum:1, maximum:1_000_00)] public int? Count { get; set; } } } |
Для работы с EntityFrameworkCore и компонентом StatusMessage, обязательно подключаем следующие пространства имен:
1 2 3 |
@using Animals.Components.Shared @using Microsoft.EntityFrameworkCore @using System.ComponentModel.DataAnnotations |
Для удобства пользователя, добавим ссылку на компонент в основном меню.
По умолчанию, Blazor применяет несколько иконок от библиотеки Bootstrap Icons. Их добавление можно найти в файле NavMenu.razor.css:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
.bi { display: inline-block; position: relative; width: 1.25rem; height: 1.25rem; margin-right: 0.75rem; top: -1px; background-size: cover; } .bi-house-door-fill-nav-menu { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); } .bi-plus-square-fill-nav-menu { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); } .bi-list-nested-nav-menu { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); } |
Подобным образом вы можете добавить описание своих иконок. В этом файле уберём подключения старых иконок и изменим класс .bi, следующим образом:
1 2 3 4 5 6 7 8 |
.bi { display: inline-block; position: relative; width: 1.25rem; height: 1rem; margin-right: 0.75rem; top: -15px; } |
Если вы захотите использовать все их иконки, то в файле App.razor, необходимо подключить их по прямой ссылке или загрузив в проект:
1 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> |
Перейдем в Components / Layout / NavMenu.razor и изменим его следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<div class="top-row ps-3 navbar navbar-dark"> <div class="container-fluid"> <a class="navbar-brand" href="">Animals</a> </div> </div> <input type="checkbox" title="Navigation menu" class="navbar-toggler" /> <div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()"> <nav class="flex-column"> <div class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="bi bi-house-door-fill" aria-hidden="true"></span> Home </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="bi bi-plus-square-fill" aria-hidden="true"></span> Counter </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="weather"> <span class="bi bi-list-nested" aria-hidden="true"></span> Weather </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="animals"> <span class="bi bi-gitlab" aria-hidden="true"></span> Animals </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="data/seed"> <span class="bi bi-cloud-arrow-down-fill" aria-hidden="true"></span> Seed </NavLink> </div> </nav> </div> |
Обратите внимание, я убрал использование класса -nav-menu, для всех ссылок и в качестве иконок, указал нужные мне из подключенной библиотеки.
Запустим приложение и проверим его работу:
Пагинация компонента QuickGrid
Используя компонент Seed, заполним таблицу на 10_000 рядков, после чего перейдем в компонент Animals. Обратите внимание на скорость загрузки данных, оно занимает минимум 5 секунд и в этом время пользователь видит пустую таблицу (он может подумать, что данных попросту нет).
Первое что мы можем сделать, то как-то уведомить пользователя о том, что данные загружаются. Для этого, изменим компонент Index.razor, следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
@page "/animals" @attribute [StreamRendering] @using Microsoft.AspNetCore.Components.QuickGrid @inject Animals.Data.ApplicationContext DB @using Animals.Models @using Microsoft.EntityFrameworkCore <PageTitle>Index</PageTitle> <h1>Index</h1> <p> <a href="animals/create">Create New</a> </p> @if (animals == null) { <p>Loading...</p> } else { <QuickGrid Class="table" Items="animals"> <PropertyColumn Property="animal => animal.Name" /> <PropertyColumn Property="animal => animal.Gender" /> <PropertyColumn Property="animal => animal.Weight" /> <PropertyColumn Property="animal => animal.AnimalType" /> <PropertyColumn Property="animal => animal.CreatedAt" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> } @code { private IQueryable<Animal> animals; protected override async Task OnInitializedAsync() { animals = (await DB.Animal.ToListAsync()).AsQueryable(); } } |
Вверху страницы, был добавлен атрибут StreamRendering:
1 |
@attribute [StreamRendering] |
Этот атрибут действует только в рендере, поддерживающем потоковый рендеринг (например, рендеринг HTML на стороне сервера из конечной точки Razor Component).
Потоковый рендеринг означает, что контент компонента будет отправлен клиенту по мере готовности, а не ожидать, пока весь компонент будет полностью готов к отображению.
Использование потокового рендеринга может улучшить производительность и опыт пользователя, особенно для больших или медленно загружающихся компонентов, так как пользователь может видеть и взаимодействовать с частью компонента, даже если он еще не полностью загружен.
Именно за счет использование данного атрибута, при запуске компонента, мы можем видеть процесс загрузки:
Как только коллекций animals будет заполнена, мы отобразим ее в таблице.
Получение и отображение большого количества элементов может быть дорогостоящим. Если объем отображаемых данных может быть большим, следует использовать либо пагинацию, либо виртуализацию.
Чтобы включить пагинацию, необходимо создать экземпляр PaginationState и передать его в качестве свойства Pagination грида. Чтобы обеспечить пользовательский интерфейс для пагинации, мы можем использовать встроенный компонент Paginator или создать собственный пользовательский интерфейс, который считывает и изменяет экземпляр PaginationState.
Изменим компонент Index.razor, следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
@page "/animals" @attribute [StreamRendering] @using Microsoft.AspNetCore.Components.QuickGrid @inject Animals.Data.ApplicationContext DB @using Animals.Models @using Microsoft.EntityFrameworkCore @rendermode RenderMode.InteractiveServer <PageTitle>Index</PageTitle> <h1>Index</h1> <p> <a href="animals/create">Create New</a> </p> @if (animals == null) { <p>Loading...</p> } else { <div class="page-size-chooser"> Items per page: <select @bind="@pagination.ItemsPerPage"> <option>5</option> <option>10</option> <option>20</option> <option>50</option> </select> </div> <QuickGrid Class="table" Items="animals" Pagination="@pagination"> <PropertyColumn Property="animal => animal.Name" /> <PropertyColumn Property="animal => animal.Gender" /> <PropertyColumn Property="animal => animal.Weight" /> <PropertyColumn Property="animal => animal.AnimalType" /> <PropertyColumn Property="animal => animal.CreatedAt" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> } @code { private IQueryable<Animal> animals; private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; protected override async Task OnInitializedAsync() { animals = (await DB.Animal.ToListAsync()).AsQueryable(); } } |
В первую очередь, необходимо указать, что компонент работает с интерактивным рендерингом на сервере:
1 |
@rendermode RenderMode.InteractiveServer |
Запустиd сервер, благодаря соединению SignalR между сервером и клиентом будет происходить взаимодействие. Это позволит в данном случае изменить количество записей на страницу, с помощью выпадающего списка, привязанному к свойству ItemsPerPage, объекта pagination:
1 2 3 4 5 6 7 8 9 |
<div class="page-size-chooser"> Items per page: <select @bind="@pagination.ItemsPerPage"> <option>5</option> <option>10</option> <option>20</option> <option>50</option> </select> </div> |
Как я уже говорил, чтобы включить пагинацию, необходимо создать экземпляр PaginationState:
1 |
private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; |
И передать его в качестве свойства Pagination грида:
1 |
<QuickGrid Class="table" Items="animals" Pagination="@pagination"> |
Запустим приложение и проверим его работу:
Осталось добавить кнопки пагинации, в компоненте Index.razor, за компонентом QuickGrid, добавим компонент Paginator:
1 |
<Paginator State="@pagination" /> |
Запустим приложение и проверим его работу:
Вы можете настроить внешний вид Paginator, предоставив шаблон SummaryTemplate. Если вам нужна дополнительная настройка, вы можете создать свой собственный альтернативный пользовательский интерфейс, который будет работать с PaginationState. К примеру, отобразим все кнопки пагинации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
@page "/animals" @attribute [StreamRendering] @using Microsoft.AspNetCore.Components.QuickGrid @inject Animals.Data.ApplicationContext DB @using Animals.Models @using Microsoft.EntityFrameworkCore @rendermode RenderMode.InteractiveServer <PageTitle>Index</PageTitle> <h1>Index</h1> <p> <a href="animals/create">Create New</a> </p> @if (animals == null) { <p>Loading...</p> } else { <div class="page-size-chooser"> Items per page: <select @bind="@pagination.ItemsPerPage"> <option>5</option> <option>10</option> <option>20</option> <option>50</option> </select> </div> <QuickGrid Class="table" Items="animals" Pagination="@pagination"> <PropertyColumn Property="animal => animal.Name" /> <PropertyColumn Property="animal => animal.Gender" /> <PropertyColumn Property="animal => animal.Weight" /> <PropertyColumn Property="animal => animal.AnimalType" /> <PropertyColumn Property="animal => animal.CreatedAt" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> <div class="page-buttons"> Page: @if (pagination.TotalItemCount.HasValue) { for (var pageIndex = 0; pageIndex <= pagination.LastPageIndex; pageIndex++) { var capturedIndex = pageIndex; <button @onclick="@(() => GoToPageAsync(capturedIndex))" class="@PageButtonClass(capturedIndex)" aria-current="@AriaCurrentValue(capturedIndex)" aria-label="Go to page @(pageIndex + 1)"> @(pageIndex + 1) </button> } } </div> } @code { private IQueryable<Animal> animals; private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; protected override async Task OnInitializedAsync() { animals = (await DB.Animal.ToListAsync()).AsQueryable(); pagination.TotalItemCountChanged += (sender, eventArgs) => StateHasChanged(); } private async Task GoToPageAsync(int pageIndex) { await pagination.SetCurrentPageIndexAsync(pageIndex); } private string? PageButtonClass(int pageIndex) => pagination.CurrentPageIndex == pageIndex ? "current" : null; private string? AriaCurrentValue(int pageIndex) => pagination.CurrentPageIndex == pageIndex ? "page" : null; } |
Сортировка компонента QuickGrid
Сортировкой управляют столбцы.
Для столбца PropertyColumn сортировка включается, если установить Sortable=«true». Это следует делать только в том случае, если ваш базовый источник данных поддерживает сортировку по выражению свойства этого столбца.
Для столбца TemplateColumn сортировка включается, если вы зададите значение параметра SortBy этого столбца.
Для пользовательского столбца, унаследованного от ColumnBase, сортировка включается, если вы установите Sortable=«true» или переопределите IsSortableByDefault и вернете true.
Давайте применим сортировку для всех столбцов, для этого в компоненте Index, изменим содержимое каждой колонки QuickGrid, добавив атрибут Sortable со значение true:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<QuickGrid Class="table" Items="animals" Pagination="@pagination"> <PropertyColumn Property="animal => animal.Name" Sortable="true" /> <PropertyColumn Property="animal => animal.Gender" Sortable="true" /> <PropertyColumn Property="animal => animal.Weight" Sortable="true" /> <PropertyColumn Property="animal => animal.AnimalType" Sortable="true" /> <PropertyColumn Property="animal => animal.CreatedAt" Sortable="true" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> |
Как видите, все просто. Запустим приложение и проверим его работу.
Фильтрация компонента QuickGrid
QuickGrid не нуждается во встроенных API для фильтрации, поэтому мы можем легко настроить собственный пользовательский интерфейс для управления критериями включения и затем использовать его с источником данных.
Пользовательский интерфейс для управления критериями фильтрации может быть размещен в любом месте и построен с использованием обычных компонентов Blazor и привязки. Мы можем использовать функцию ColumnOptions в QuickGrid, чтобы поместить пользовательский интерфейс фильтрации во всплывающее окно, связанное с определенным столбцом.
Давайте реализуем возможность поиска по свойству Name, через всплывающее окно элемента QuickGrid, для этого изменим компонент Index.razor, следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
@page "/animals" @attribute [StreamRendering] @using Microsoft.AspNetCore.Components.QuickGrid @inject Animals.Data.ApplicationContext DB @using Animals.Models @using Microsoft.EntityFrameworkCore @rendermode RenderMode.InteractiveServer <PageTitle>Index</PageTitle> <h1>Index</h1> <p> <a href="animals/create">Create New</a> </p> @if (animals == null) { <p>Loading...</p> } else { <div class="page-size-chooser"> Items per page: <select @bind="@pagination.ItemsPerPage"> <option>5</option> <option>10</option> <option>20</option> <option>50</option> </select> </div> <QuickGrid Class="table" Items="filteredAnimals" Pagination="@pagination"> <PropertyColumn Property="animal => animal.Name" Sortable="true"> <ColumnOptions> <div class="search-box"> <input type="search" autofocus @bind="nameFilter" @bind:event="oninput" placeholder="Animal name..." /> </div> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="animal => animal.Gender" Sortable="true" /> <PropertyColumn Property="animal => animal.Weight" Sortable="true" /> <PropertyColumn Property="animal => animal.AnimalType" Sortable="true" /> <PropertyColumn Property="animal => animal.CreatedAt" Sortable="true" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> <Paginator State="@pagination" /> } @code { private IQueryable<Animal> animals; private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; private string nameFilter; private IQueryable<Animal> filteredAnimals { get { var result = animals; if (!string.IsNullOrEmpty(nameFilter)) { result = result.Where(c => c.Name.Contains(nameFilter, StringComparison.CurrentCultureIgnoreCase)); } return result; } } protected override async Task OnInitializedAsync() { animals = (await DB.Animal.ToListAsync()).AsQueryable(); } } |
Вначале мы добавили приватное поле класса, которое содержит фильтр по имени для животных, а также приватное свойство класса, которое возвращает отфильтрованный набор животных. Оно использует LINQ-запрос для фильтрации animals, основываясь на nameFilter.
Метод filteredAnimals() применяет фильтрацию к списку животных на основе значения nameFilter. Если nameFilter не пустое, фильтрация осуществляется по имени животного, игнорируя регистр символов. Фильтрованный результат возвращается из свойства filteredAnimals.
После чего, для атрибута Items, компонента QuickGrid, применяется коллекция filteredAnimals.
Для колонки animal.Name, добавляется модальное окно с помещенным внутри полем для ввода текста, которое привязано к полю nameFilter. Запустим приложение и проверим его работу:
По такому же принципу, мы можем добавлять возможности фильтрации для остальных колонок, к примеру, добавим фильтр для колонки Weight:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
@page "/animals" @attribute [StreamRendering] @using Microsoft.AspNetCore.Components.QuickGrid @inject Animals.Data.ApplicationContext DB @using Animals.Models @using Microsoft.EntityFrameworkCore @rendermode RenderMode.InteractiveServer <PageTitle>Index</PageTitle> <h1>Index</h1> <p> <a href="animals/create">Create New</a> </p> @if (animals == null) { <p>Loading...</p> } else { <div class="page-size-chooser"> Items per page: <select @bind="@pagination.ItemsPerPage"> <option>5</option> <option>10</option> <option>20</option> <option>50</option> </select> </div> <QuickGrid Class="table" Items="filteredAnimals" Pagination="@pagination"> <PropertyColumn Property="animal => animal.Name" Sortable="true"> <ColumnOptions> <div class="search-box"> <input type="search" autofocus @bind="nameFilter" @bind:event="oninput" placeholder="Animal name..." /> </div> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="animal => animal.Gender" Sortable="true" /> <PropertyColumn Property="animal => animal.Weight" Sortable="true"> <ColumnOptions> <div class="search-box"> <input type="range" autofocus @bind="weightFilter" @bind:event="oninput" min="0" max="160" /> <span class="inline-block w-10">@weightFilter</span> </div> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="animal => animal.AnimalType" Sortable="true" /> <PropertyColumn Property="animal => animal.CreatedAt" Sortable="true" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> <Paginator State="@pagination" /> } @code { private IQueryable<Animal> animals; private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; private string nameFilter; private double weightFilter; private IQueryable<Animal> filteredAnimals { get { var result = animals; if (!string.IsNullOrEmpty(nameFilter)) { result = result.Where(c => c.Name.Contains(nameFilter, StringComparison.CurrentCultureIgnoreCase)); } if (weightFilter > 0) { result = result.Where(e => e.Weight >= weightFilter); } return result; } } protected override async Task OnInitializedAsync() { animals = (await DB.Animal.ToListAsync()).AsQueryable(); } } |
Давайте создадим выпадающий список для выбора определенного типа животного, для начала изменим перечисление AnimalType, добавив ему константу All:
1 2 3 4 5 6 7 8 9 10 |
public enum AnimalType { Mammals, Reptiles, Amphibians, Birds, Insects, Fish, All } |
Теперь изменим компонент Index.razor, следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
@page "/animals" @attribute [StreamRendering] @using Microsoft.AspNetCore.Components.QuickGrid @inject Animals.Data.ApplicationContext DB @using Animals.Models @using Microsoft.EntityFrameworkCore @rendermode RenderMode.InteractiveServer <PageTitle>Index</PageTitle> <h1>Index</h1> <p> <a href="animals/create">Create New</a> </p> @if (animals == null) { <p>Loading...</p> } else { <div class="page-size-chooser"> Items per page: <select @bind="@pagination.ItemsPerPage"> <option>5</option> <option>10</option> <option>20</option> <option>50</option> </select> </div> <QuickGrid Class="table" Items="filteredAnimals" Pagination="@pagination"> <PropertyColumn Property="animal => animal.Name" Sortable="true"> <ColumnOptions> <div class="search-box"> <input type="search" autofocus @bind="nameFilter" @bind:event="oninput" placeholder="Animal name..." /> </div> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="animal => animal.Gender" Sortable="true" /> <PropertyColumn Property="animal => animal.Weight" Sortable="true"> <ColumnOptions> <div class="search-box"> <input type="range" autofocus @bind="weightFilter" @bind:event="oninput" min="0" max="160" /> <span class="inline-block w-10">@weightFilter</span> </div> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="animal => animal.AnimalType" Sortable="true"> <ColumnOptions> <div class="search-box"> <InputSelect @bind-Value="animalTypeFilter" class="form-control"> @foreach (var value in Enum.GetValues<AnimalType>()) { <option value="@value">@value</option> } </InputSelect> </div> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="animal => animal.CreatedAt" Sortable="true" /> <TemplateColumn Context="animal"> <a href="@($"animals/edit?id={animal.Id}")">Edit</a> | <a href="@($"animals/details?id={animal.Id}")">Details</a> | <a href="@($"animals/delete?id={animal.Id}")">Delete</a> </TemplateColumn> </QuickGrid> <Paginator State="@pagination" /> } @code { private IQueryable<Animal> animals; private PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; private string nameFilter; private double weightFilter; private AnimalType animalTypeFilter = AnimalType.All; private IQueryable<Animal> filteredAnimals { get { var result = animals; if (!string.IsNullOrEmpty(nameFilter)) { result = result.Where(c => c.Name.Contains(nameFilter, StringComparison.CurrentCultureIgnoreCase)); } if (weightFilter > 0) { result = result.Where(e => e.Weight >= weightFilter); } if (animalTypeFilter != AnimalType.All) { result = result.Where(e => e.AnimalType == animalTypeFilter); } return result; } } protected override async Task OnInitializedAsync() { animals = (await DB.Animal.ToListAsync()).AsQueryable(); } } |
Запустим приложение и проверим его работу:
Так как перечисление AnimalType приобрело новую константу All, не забудьте ее скрыть в компонентах Create.razor и Edit.razor:
1 2 3 4 5 6 |
<InputSelect id="animaltype" @bind-Value="Animal.AnimalType" class="form-control"> @foreach (var value in Enum.GetValues<AnimalType>().Where(e=>e != AnimalType.All)) { <option value="@value">@value</option> } </InputSelect> |
На этом все, надеюсь, вам все понравилось. Обязательно сохраните этот проект, он нам пригодится для следующей теме проекта.
Я надеюсь, что вам понравилось читать эту статью, и она оказалась легкой для понимания. Пожалуйста, дайте мне знать, если у вас есть какие-либо комментарии или исправления.
Так же вам может быть интересна предыдущая статья — Проект с использованием Razor Pages (CRUD операции).
Вы хотите научится писать код на языке программирования C#?
Создавать различные информационные системы, состоящие из сайтов, мобильных клиентов, десктопных приложений, телеграмм-ботов и т.д.
Переходите к нам на страницу Dijix и ознакомьтесь с условиями обучения, мы специализируемся только на индивидуальных занятиях, как для начинающих, так и для более продвинутых программистов. Вы можете взять как одно занятие для проработки интересующего Вас вопроса, так и несколько, для более плотной работы. Благодаря личному кабинету, каждый студент повысит качество своего обучения, в вашем распоряжении:
- Доступ к пройденному материалу
- Тематические статьи
- Библиотека книг
- Онлайн тестирование
- Общение в закрытых группах
Живи в своем мире, программируй в нашем.