[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на 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 генерує код для створення початкової схеми бази даних, що ґрунтується на моделі, зазначеній у класі ApplicationContext. Аргумент 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 |
Запустіть сервер, завдяки з’єднанню 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 і ознайомтеся з умовами навчання, ми спеціалізуємося тільки на індивідуальних заняттях, як для початківців, так і для просунутих програмістів. Ви можете взяти як одне заняття для опрацювання питання, що вас цікавить, так і кілька, для більш щільної роботи. Завдяки особистому кабінету, кожен студент підвищить якість свого навчання, у вашому розпорядженні:
- Доступ до пройденого матеріалу
- Тематичні статті
- Бібліотека книг
- Онлайн тестування
- Спілкування в закритих групах