[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на C# станут решающим фактором. Переходите по ссылке 🔰.
У цьому уроці ми створимо CRUD-операції в ASP.NET Core Razor Pages, використовуючи патерн Repository. Додаток буде виконано в ASP.NET Core Razor Pages, а на рівні доступу до даних використовуватиметься Entity Framework Core «ORM».
Щоб створити проєкт RazorCrud, запустіть Visual Studio і виберіть у меню File (Файл) – New Project (Створити Проєкт). Вкажіть шаблон проєкту ASP.NET Core Web App (Razor Pages). Введіть RazorCrud, у полі Name, на наступній сторінці вкажіть Framework .Net 8.0 і натисніть кнопку Create:
Модель і Entity Framework Core
Entity Framework Core – це ORM, який ми будемо використовувати для зв’язку з базою даних і виконання CRUD-операцій. Наш репозиторій буде звертатися до Entity Framework Core, щоб можна було виконувати ці операції з базою даних.
Для взаємодії з MS SQL Server і виконання міграцій через EF Core, додамо в проєкт через Nuget пакети:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
Далі нам потрібно створити сутність, яка буде являти собою клас Assignment.cs. Ви повинні створити цей клас у папці Models. Тому спочатку створіть папку «Models» у корені додатка, а потім усередині неї створіть клас Assignment, з таким вмістом:
1 2 3 4 5 6 7 8 9 |
public class Assignment { public int Id { get; set; } public string Title { get; set; } public string Description { get; set; } public DateTime StartedDate { get; set; } public DateTime FinishedDate { get; set; } public string? Image { get; set; } } |
Для зберігання зображення в папці wwwroot створимо папку assignmentImages.
Створення класу контексту даних
У забезпеченні доступу до даних у БД інфраструктура Entity Framework Core покладається на клас контексту БД. Щоб забезпечити приклад додатка контекстом, додайте в папку Data файл класу на ім’я ApplicationContext.cs з таким кодом:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions<ApplicationContext> context) : base(context) { } public DbSet<Assignment> Assignments { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Assignment>().Property(u => u.StartedDate).HasDefaultValueSql("GETDATE()"); } } |
Коли інфраструктура Entity Framework Core використовується для збереження простої моделі даних на кшталт тієї, що визначена в додатку RazorCrud. Клас контексту БД відповідно простий – хоча ситуація зміниться зі зростанням складності моделі даних у наступних розділах.
Перейдемо у файл appsettings.json і пропишемо конфігурацію підключення та логування:
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\mssqllocaldb;Database=razorCrudDb;Trusted_Connection=True;" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } |
Рядки підключення визначаються у файлі appsettings.json, у коді вище наведено визначення рядка підключення для БД додатка RazorCrud. У проєкті застосовується версія LocalDB продукту SQL Server, яка спроєктована спеціально для розробників і не вимагає конфігурації або облікових даних.
Ви зобов’язані забезпечити, щоб рядок підключення був єдиним нерозривним рядком. Формат рядка підключення специфічний для кожного сервера баз даних.
Далі нам потрібно зареєструвати ApplicationContext у файлі Program.cs. Додамо наступний виділений рядок, який зареєструє наш контекст бази даних:
1 2 3 4 5 |
var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); |
Отже, давайте виконаємо міграцію. Перейдемо в Інструменти ➤ Менеджер пакетів NuGet ➤ Консоль менеджера пакетів і послідовно виконаємо такі команди:
PM> Add-Migration Initial
PM> Update-Database
Після завершення виконання команд міграції в додатку буде створено папку Migrations. Ця папка містить новостворені файли, які використовувалися для створення бази даних.
Далі перейдіть у меню View ➤ SQL Server Object Explorer, де знайдемо базу даних razorCrudDb. Ця база даних була створена, коли ми запускали міграцію. Ця база даних містить лише одну таблицю під назвою Assignments, яка відповідає сутності Assignment, створеній у нашому додатку ASP.NET Core Razor Pages.
Створення репозиторію
Паттерн репозиторій потребує двох речей:
- Інтерфейс
- Клас, який буде реалізовувати/успадковувати інтерфейс.
Інтерфейс міститиме всі методи, які працюватимуть із базою даних, наприклад CRUD. Зазвичай це методи для створення, читання, оновлення та видалення записів у базі даних. Іноді у нас буде ще кілька методів для фільтрації записів. Ми розглянемо їх пізніше.
Щоб створити інтерфейс сховища, створимо папку Interfaces і додамо в неї файл інтерфейсу на ім’я IRepository з таким вмістом:
1 2 3 4 |
public interface IRepository<T> where T : class { Task CreateAsync(T entity); } |
IRepository<T> – це загальний тип, у якому T обмежений як клас. Це означає, що T має бути посилальним типом, таким як клас, інтерфейс, делегат або масив.
Створимо папку Repository і додамо в неї файл класу на ім’я Repository.cs з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class Repository<T> : IRepository<T> where T : class { private ApplicationContext context; public Repository(ApplicationContext context) { this.context = context; } public async Task CreateAsync(T entity) { if (entity == null) { throw new ArgumentNullException(nameof(entity)); } context.Add(entity); await context.SaveChangesAsync(); } } |
Нарешті, нам потрібно зареєструвати інтерфейс для його реалізації в Program.cs. Код, який необхідно додати, показано нижче:
1 |
builder.Services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); |
Наведений вище код вказує ASP.NET Core на створення нового екземпляра Repository щоразу, коли присутня залежність IRepository. Typeof вказує, що тип може бути будь-яким, наприклад Movie, Teacher, Employee тощо.
Чудово, ми щойно завершили створення нашого шаблону Generic Repository. Залишилося тільки створити CRUD-операції в Razor Pages.
CRUD-операції з використанням патерну репозиторію
На деяких сторінках, ми будемо використовувати окремі стилі та скрипти. Якщо перейти у файл Views / Shared / _Layout.cshtml, можна побачити певну секцію для скриптів:
1 |
@await RenderSectionAsync("Scripts", required: false) |
Давайте так само, визначимо подібну секцію, для стилів, вгорі сторінки:
1 |
@await RenderSectionAsync("Styles", required: false) |
У кореневій папці програми є папка Pages. Нам потрібно створити нову сторінку Razor Page у цій папці Pages і назвати її Create.cshtml.
Для цього клацніть правою кнопкою миші папку Pages і виберіть Add ➤ New Item. Далі у вікні Add New Item вибериv Razor Page – Empty template і назвемо його Create.cshtml.
Визначимо у файлі Create.cshtml, наступний код:
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
@page @model CreateModel @using Microsoft.AspNetCore.Mvc.RazorPages; @using Models; @using RazorCrud.Interfaces @using System.ComponentModel.DataAnnotations @{ ViewData["Title"] = "Create a Assignment"; } @section Styles { <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet" type="text/css" /> <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.3.0/css/datepicker.css" rel="stylesheet" type="text/css" /> } <h1 class="bg-info text-white">Create a Assignment</h1> <div asp-validation-summary="All" class="text-danger"></div> <form method="post" enctype="multipart/form-data"> <div class="form-group"> <label asp-for="@Model.Assignment.Title"></label> <input type="text" asp-for="@Model.Assignment.Title" class="form-control" /> <span asp-validation-for="@Model.Assignment.Title" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="@Model.Assignment.Description"></label> <input type="text" asp-for="@Model.Assignment.Description" class="form-control" /> <span asp-validation-for="@Model.Assignment.Description" class="text-danger"></span> </div> <label asp-for="@Model.Assignment.FinishedDate"></label> <div id="datepicker" class="input-group date" data-date-format="dd-mm-yyyy"> <input type="text" asp-for="@Model.Assignment.FinishedDate" class="form-control" readonly /> <span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span> </div> <div class="form-group"> <label asp-for="@Model.Assignment.Image" class="control-label"></label> <input type="file" class="form-control" asp-for="@Model.Assignment.Image" accept="image/*" /> </div> <div class="mt-3"> <button type="submit" class="btn btn-primary">Create</button> </div> </form> @section Scripts { <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.3.0/js/bootstrap-datepicker.js"></script> <script> $(function () { $("#datepicker").datepicker({ autoclose: true, todayHighlight: true }).datepicker('update', new Date()); }); </script> } @functions { public class CreateModel : PageModel { private readonly IRepository<Assignment> assignmentRepository; public CreateModel(IRepository<Assignment> repository) { this.assignmentRepository = repository; } [BindProperty] public AssignmentViewModel Assignment { get; set; } public IActionResult OnGet() { return Page(); } public async Task<IActionResult> OnPostAsync([FromServices] IWebHostEnvironment appEnvironment) { if (ModelState.IsValid) { Assignment assignment = new Assignment { Title = Assignment.Title, Description = Assignment.Description, FinishedDate = Assignment.FinishedDate, }; if (Assignment.Image != null) { string fileName = Assignment.Image.FileName; if (fileName.Contains("\")) { fileName = fileName.Substring(fileName.LastIndexOf('\') + 1); } string imagePath = "/assignmentImages/" + Guid.NewGuid() + fileName; using (var fileStream = new FileStream(appEnvironment.WebRootPath + imagePath, FileMode.Create)) { await Assignment.Image.CopyToAsync(fileStream); } assignment.Image = imagePath; } await assignmentRepository.CreateAsync(assignment); return RedirectToPage("Create"); } else { return Page(); } } } public class AssignmentViewModel { [Key] public int Id { get; set; } [Required] public string Title { get; set; } [Required] public string Description { get; set; } public DateTime FinishedDate { get; set; } public IFormFile? Image { get; set; } } } |
Сторінка містить як код razor, так і код C#. Верхня частина містить код razor, а коди C# розміщені в тілі функцій. Застосовуваний @page у верхній частині вказує на те, що це Razor-сторінка і вона може обробляти http-запити.
Тепер запустимо проєкт і відкриємо цю сторінку в браузері за її назвою, тобто url цієї сторінки буде – https://localhost:44329/Create. Порт localhost для вас буде іншим:
Прив’язка вхідних елементів до моделі здійснюється за допомогою помічника тега asp-for:
1 2 3 |
<label asp-for="@Model.Assignment.FinishedDate"></label> <input type="text" asp-for="@Model.Assignment.FinishedDate" class="form-control" /> <span asp-validation-for="@Model.Assignment.FinishedDate" class="text-danger"></span> |
Помічники тегів дають змогу коду на стороні сервера створювати та відображати HTML-елементи. Помічник тегів asp-for додає 4 html-атрибути до елементів введення. Ви можете побачити ці атрибути, «проінспектувавши» їх у браузері.
Я також додав div перед формою на сторінці razor. Цей div показуватиме всі помилки валідації разом. Це робиться шляхом додавання тега-помічника asp-validation-summary=«All» у div.
Для роботи з календарем на сторінку так само додаються необхідні стилі та скрипти від Bootstrap. У секції Scripts, таким чином, окремий блок скриптів ініціалізує віджет вибору дати для елемента з ідентифікатором datepicker і встановлює поточну дату як обрану.
На сторінках razor є методи-обробники, які автоматично виконуються в результаті http-запиту. Існує 2 методи-обробники – OnGet і OnPostAsync.
Метод «OnGet» буде викликаний при http-запиті типу get на Create Razor Page, а «OnPostAsync», що є асинхронною версією OnPost, буде викликаний при http-запиті типу post на Create Razor Page.
Обробник OnGet() просто повертає сторінку razor. Обробник OnPostAsync(), через атрибут [FromServices], отримує залежність IWebHostEnvironment, яка надає доступ до шляхів до кореневої папки застосунку, папок, які містять статичні ресурси, а також шляхів до каталогів, які містять логи, конфігураційні файли та інші ресурси.
Далі перевіряє, чи дійсно стан моделі, перевіряє, чи є файл, і зберігає його в раніше створену нами папку.
Потім він викликає метод CreateAsync сховища. Цей метод CreateAsync надається разом з об’єктом задачі, щоб вона була додана в базу даних.
Не забуваємо, що ми можемо використовувати спочатку валідацію на стороні клієнта, через JavaScript. Для цього, в секцію Scripts, завантажимо часткове подання:
1 |
@await Html.PartialAsync("_ValidationScriptsPartial") |
Тепер створимо функціональність для отримання завдань. Отже, нам потрібно додати новий метод у наше сховище, який виконуватиме завдання читання. Перейдемо в інтерфейс IRepository і додайте метод ReadAllAsync, як показано нижче:
1 2 3 4 5 |
public interface IRepository<T> where T : class { <strong>Task<List<T>> ReadAllAsync();</strong> Task CreateAsync(T entity); } |
Метод ReadAllAsync повертає список типів T асинхронно. Далі нам необхідно реалізувати цей метод у класі «Repository.cs». Код показано нижче у виділеному вигляді:
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 |
public class Repository<T> : IRepository<T> where T : class { private ApplicationContext context; public Repository(ApplicationContext context) { this.context = context; } public async Task CreateAsync(T entity) { if (entity == null) { throw new ArgumentNullException(nameof(entity)); } context.Add(entity); await context.SaveChangesAsync(); } <strong> public async Task<List<T>> ReadAllAsync() { return await context.Set<T>().ToListAsync(); }</strong> } |
Метод використовує метод Set для створення об’єкта Microsoft.EntityFrameworkCore.DbSet для запиту сутності з бази даних. У загальному випадку він зчитуватиме сутність «T» з бази даних, а потім ми перетворимо її на список за допомогою методу ToListAsync().
Тепер створимо нову сторінку Razor Page у папці «Pages» з ім’ям Read.cshtml і додамо в неї такий код:
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 |
@page @model ReadModel @using Microsoft.AspNetCore.Mvc.RazorPages; @using Models; @using RazorCrud.Interfaces @{ ViewData["Title"] = "Assignments"; } <h1 class="bg-info text-white">Assignments</h1> <div class="mb-3"> <a asp-page="Create" class="btn btn-secondary">Create a Assignment</a> </div> <table class="table table-sm table-bordered"> <tr> <th>Id</th> <th>Title</th> <th>Description</th> <th>Image</th> </tr> @foreach (Assignment assignment in Model.Assignments) { <tr> <td>@assignment.Id</td> <td>@assignment.Title</td> <td>@assignment.Description</td> <td> @if (assignment.Image != null) { <img src="@assignment.Image" class="img-fluid" style="width:100px; height:100px;"> } </td> </tr> } </table> @functions { public class ReadModel : PageModel { private readonly IRepository<Assignment> assignmentRepository; public ReadModel(IRepository<Assignment> assignmentRepository) { this.assignmentRepository = assignmentRepository; } public List<Assignment> Assignments { get; set; } public async Task OnGet() { Assignments = await assignmentRepository.ReadAllAsync(); } } } |
Код цієї сторінки Razor Page доволі простий, спочатку я створив посилання на сторінку Razor Page «Create» за допомогою тега-помічника asp-page.
Далі у нас є HTML-таблиця, яка містить 3 стовпці для 3 полів сутності Assignment. У таблиці відображатимуться всі завдання, які наразі зберігаються в базі даних. У блоці функцій визначено властивість List<Assignment>, яка надається зі списком завдань методом OnGet().
Оскільки сторінка читання створена, ми повинні перенаправляти користувача на сторінку читання, коли створюється новий запис.
Перейдемо на сторінку Create.cshtml і змінимо останній рядок обробника OnPostAsync так, щоб він перенаправляв користувача на сторінку читання:
1 2 3 4 5 |
public async Task<IActionResult> OnPostAsync() { //... return RedirectToPage("Read"); } |
Перенаправлення здійснюється методом RedirectToPage(Read). Подивимося на зміни, які показані виділеним способом.
Пагінація
При додаванні більшої кількості записів, ми втратимо зручність роботи з ними. Це також призведе до такої проблеми, як уповільнення роботи сторінки.
Щоб вирішити цю проблему, ми створимо функцію Pagination. Отже, у папці Models, створимо нову папку Paging у корені додатка. У цю папку додамо 3 класи, а саме:
PagingInfo.cs – відстежує загальну кількість записів, поточну сторінку, елементи на сторінці та загальну кількість сторінок.
AssignmentList.cs – містить список записів поточної сторінки та PagingInfo.
PageLinkTagHelper.cs – це тег-помічник, який буде створювати посилання пейджинга всередині div.
Відкриємо клас PagingInfo.cs і визначимо такий вміст:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class PagingInfo { public int TotalItems { get; set; } public int ItemsPerPage { get; set; } public int CurrentPage { get; set; } public int TotalPages { get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); } } } |
Відкриємо клас AssignmentList.cs і визначимо такий вміст:
1 2 3 4 5 |
public class AssignmentList { public IEnumerable<Assignment> Assignments { get; set; } public PagingInfo PagingInfo { get; set; } } |
Відкриємо клас PageLinkTagHelper.cs і визначимо такий вміст:
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 123 124 125 126 |
[HtmlTargetElement("div", Attributes = "page-model")] public class PageLinkTagHelper : TagHelper { private IUrlHelperFactory urlHelperFactory; public PageLinkTagHelper(IUrlHelperFactory helperFactory) { urlHelperFactory = helperFactory; } [ViewContext] [HtmlAttributeNotBound] public ViewContext ViewContext { get; set; } public PagingInfo PageModel { get; set; } public string PageName { get; set; } /*Принимает все атрибуты, которые являются другими атрибутами страницы -* к примеру: page-other-category="@Model.allTotal" page-other-some="@Model.allTotal"*/ [HtmlAttributeName(DictionaryAttributePrefix = "page-other-")] public Dictionary<string, object> PageOtherValues { get; set; } = new Dictionary<string, object>(); public bool PageClassesEnabled { get; set; } = false; public string PageClass { get; set; } public string PageClassSelected { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext); TagBuilder result = new TagBuilder("div"); string anchorInnerHtml = ""; for (int i = 1; i <= PageModel.TotalPages; i++) { TagBuilder tag = new TagBuilder("a"); anchorInnerHtml = AnchorInnerHtml(i, PageModel); if (anchorInnerHtml == "..") tag.Attributes["href"] = "#"; else if (PageOtherValues.Keys.Count != 0) tag.Attributes["href"] = urlHelper.Page(PageName, AddDictionaryToQueryString(i)); else tag.Attributes["href"] = urlHelper.Page(PageName, new { id = i }); if (PageClassesEnabled) { tag.AddCssClass(PageClass); tag.AddCssClass(i == PageModel.CurrentPage ? PageClassSelected : ""); } tag.InnerHtml.Append(anchorInnerHtml); if (anchorInnerHtml != "") result.InnerHtml.AppendHtml(tag); } output.Content.AppendHtml(result.InnerHtml); } public IDictionary<string, object> AddDictionaryToQueryString(int i) { object routeValues = null; var dict = (routeValues != null) ? new RouteValueDictionary(routeValues) : new RouteValueDictionary(); dict.Add("id", i); foreach (string key in PageOtherValues.Keys) { dict.Add(key, PageOtherValues[key]); } var expandoObject = new ExpandoObject(); var expandoDictionary = (IDictionary<string, object>)expandoObject; foreach (var keyValuePair in dict) { expandoDictionary.Add(keyValuePair); } return expandoDictionary; } public static string AnchorInnerHtml(int i, PagingInfo pagingInfo) { string anchorInnerHtml = ""; if (pagingInfo.TotalPages <= 10) anchorInnerHtml = i.ToString(); else { if (pagingInfo.CurrentPage <= 5) { if ((i <= 8) || (i == pagingInfo.TotalPages)) anchorInnerHtml = i.ToString(); else if (i == pagingInfo.TotalPages - 1) anchorInnerHtml = ".."; } else if ((pagingInfo.CurrentPage > 5) && (pagingInfo.TotalPages - pagingInfo.CurrentPage >= 5)) { if ((i == 1) || (i == pagingInfo.TotalPages) || ((pagingInfo.CurrentPage - i >= -3) && (pagingInfo.CurrentPage - i <= 3))) anchorInnerHtml = i.ToString(); else if ((i == pagingInfo.CurrentPage - 4) || (i == pagingInfo.CurrentPage + 4)) anchorInnerHtml = ".."; } else if (pagingInfo.TotalPages - pagingInfo.CurrentPage < 5) { if ((i == 1) || (pagingInfo.TotalPages - i <= 7)) anchorInnerHtml = i.ToString(); else if (pagingInfo.TotalPages - i == 8) anchorInnerHtml = ".."; } } return anchorInnerHtml; } } |
PageLinkTagHelper виконує основну роботу зі створення посилань пагінації. Давайте розглянемо, як він працює.
Помічники тегів повинні успадковувати клас TagHelper і перевизначати функцію Process. Функція process – це місце, де ми пишемо наш код помічника тега. У нашому випадку ми створимо якірні теги для посилань на сторінку і покажемо їх усередині div.
Клас-помічник тега застосовується з атрибутом HtmlTargetElement, який вказує, що він застосовуватиметься до будь-якого div, що має атрибут page-model:
1 |
[HtmlTargetElement("div", Attributes = "page-model")] |
Клас-помічник тега визначає низку властивостей, які отримуватимуть значення від Razor Page. Цими властивостями є PageModel, PageName, PageClassesEnabled, PageClass і PageClassSelected.
Є ще одна властивість типу ViewContext, яка пов’язується з даними контексту подання, що включають дані маршрутизації, ViewData, ViewBag, TempData, ModelState, поточний HTTP-запит тощо.
1 2 3 |
[ViewContext] [HtmlAttributeNotBound] public ViewContext ViewContext { get; set; } |
Використання атрибута [HtmlAttributeNotBound] в основному говорить про те, що цей атрибут не є тим, який ви збираєтеся встановити за допомогою допоміжного атрибута тега на сторінці razor.
Помічник тегів отримує об’єкт IUrlHelperFactory з функції ін’єкції залежностей і використовує його для створення тегів якорів сторінок. Існує також функція AnchorInnerHtml, яка займається створенням тексту для посилань на сторінку.
Наступне, що нам потрібно зробити, це зробити цей тег-помічник доступним для сторінок razor, що ми можемо зробити, додавши наведений нижче рядок коду в файл _ViewImports.cshtml:
1 |
@addTagHelper RazorCrud.Models.Paging.*, RazorCrud |
Директива @addTagHelper робить помічники тегів доступними для сторінки Razor. Перший параметр після @addTagHelper вказує, які помічники тегів потрібно завантажити, я використовував синтаксис підстановки («*») у MovieCrud.Paging.*, тож це означає, що потрібно завантажити всі помічники тегів, які мають простір імен MovieCrud.Paging або будь-який простір імен, який починається з MovieCrud.Paging.
А другий параметр «RazorCrud» вказує збірку, що містить помічники тегів. Це ім’я програми.
Тепер нам потрібно інтегрувати цей тег-помічник на сторінку Read Razor Page. Тому спочатку нам потрібно додати новий метод у наш репозиторій. Додамо метод ReadAllFilterAsync в інтерфейс IRepository:
1 2 3 4 5 6 |
public interface IRepository<T> where T : class { <strong>Task<(List<T>, int)> ReadAllFilterAsync(int skip, int take);</strong> Task<List<T>> ReadAllAsync(); Task CreateAsync(T entity); } |
Цей метод повертає кортеж типу List<T> і int. Очевидно, це означає, що він поверне список записів поточної сторінки і загальну кількість записів у базі даних. Крім того, він приймає 2 параметри – skip і take, які допомагають нам побудувати логіку для отримання тільки записів поточної сторінки.
Потім додамо реалізацію цього методу в клас Repository.cs, як показано нижче:
1 2 3 4 5 6 7 8 9 10 11 12 |
public async Task<(List<T>, int)> ReadAllFilterAsync(int skip, int take) { var all = context.Set<T>(); var relevant = await all.Skip(skip).Take(take).ToListAsync(); var total = all.Count(); (List<T>, int) result = (relevant, total); return result; } |
Бачите, що тепер ми отримуємо тільки записи поточної сторінки за допомогою методів Linq Skip і Take. Потім повертаємо записи разом із кількістю всіх записів у кортежі.
Нарешті, перейдемо до файлу Read.cshtml і внесемо такі зміни:
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 |
@page "{id:int?}" @model ReadModel @using Microsoft.AspNetCore.Mvc.RazorPages; @using Models; @using RazorCrud.Interfaces @using RazorCrud.Models.Paging @{ ViewData["Title"] = "Assignments"; } <h1 class="bg-info text-white">Assignments</h1> <div class="mb-3"> <a asp-page="Create" class="btn btn-secondary">Create a Assignment</a> </div> <table class="table table-sm table-bordered"> <tr> <th>Id</th> <th>Title</th> <th>Description</th> <th>Image</th> </tr> @foreach (Assignment assignment in Model.AssignmentList.Assignments) { <tr> <td>@assignment.Id</td> <td>@assignment.Title</td> <td>@assignment.Description</td> <td> @if (assignment.Image != null) { <img src="@assignment.Image" class="img-fluid" style="width:100px; height:100px;"> } </td> </tr> } </table> <div class="pagingDiv" page-model="Model.AssignmentList.PagingInfo" page-name="Read" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div> @functions { public class ReadModel : PageModel { private readonly IRepository<Assignment> assignmentRepository; public ReadModel(IRepository<Assignment> assignmentRepository) { this.assignmentRepository = assignmentRepository; } public AssignmentList AssignmentList { get; set; } public async Task OnGet(int id) { AssignmentList = new AssignmentList(); int pageSize = 3; PagingInfo pagingInfo = new PagingInfo(); pagingInfo.CurrentPage = id == 0 ? 1 : id; pagingInfo.ItemsPerPage = pageSize; var skip = pageSize * (Convert.ToInt32(pagingInfo.CurrentPage) - 1); var resultTuple = await assignmentRepository.ReadAllFilterAsync(skip, pageSize); pagingInfo.TotalItems = resultTuple.Item2; AssignmentList.Assignments = resultTuple.Item1; AssignmentList.PagingInfo = pagingInfo; } } } |
Для визначення стилів пагінації перейдемо до папки wwwroot/css/site.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 25 26 27 28 29 30 31 32 33 34 35 |
.pagingDiv { background: #f2f2f2; } .pagingDiv > a { display: inline-block; padding: 0px 9px; margin-right: 4px; border-radius: 3px; border: solid 1px #c0c0c0; background: #e9e9e9; box-shadow: inset 0px 1px 0px rgba(255,255,255, .8), 0px 1px 3px rgba(0,0,0, .1); font-size: .875em; font-weight: bold; text-decoration: none; color: #717171; text-shadow: 0px 1px 0px rgba(255,255,255, 1); } .pagingDiv > a:hover { background: #fefefe; background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FEFEFE), to(#f0f0f0)); background: -moz-linear-gradient(0% 0% 270deg,#FEFEFE, #f0f0f0); } .pagingDiv > a.active { border: none; background: #616161; box-shadow: inset 0px 0px 8px rgba(0,0,0, .5), 0px 1px 0px rgba(255,255,255, .8); color: #f0f0f0; text-shadow: 0px 0px 3px rgba(0,0,0, .5); } |
Давайте розберемося в цих змінах по порядку. Зверху в директиву сторінки додається id маршруту:
1 |
@page "{id:int?}" |
Це робиться тому, що номер сторінки буде вказано в url як останній сегмент, наприклад:
1 2 3 4 |
https://localhost:44329/Read/1 https://localhost:44329/Read/2 https://localhost:44329/Read/3 https://localhost:44329/Read/10 |
Наведений вище тип маршрутизації створюється ASP.NET Core за замовчуванням. Наступна зміна – додавання властивості AssignmentList у блок функцій:
1 |
public AssignmentList AssignmentList { get; set; } |
Ця властивість потім використовується в циклі foreach, який створює рядки таблиці із записів. Model.AssignmentList.Assignments буде містити список доручень.
Після цього я додав div, що містить атрибут page-model, і тому помічник тегів перетворює цей div на посилання пагінації.
У класі-помічнику тегів у мене є властивість PageOtherValues, визначена як тип словника:
1 |
public Dictionary<string, object> PageOtherValues { get; set; } = new Dictionary<string, object>(); |
Ця властивість отримує значення в типі Dictionary з атрибутів, що починаються з «page-other-». Прикладами таких атрибутів можуть бути:
1 2 3 |
page-other-other page-other-data page-other-name |
Значення цих атрибутів будуть додані в рядок запиту url. Цю роботу виконує функція AddDictionaryToQueryString, визначена в класі. Хоча я не використовував її, вона може бути корисною, якщо ви хочете додати більше можливостей у клас-помічник тегів.
Переходимо до обробника OnGet, який тепер отримує як параметр значення номера сторінки. Нагадаємо, що деякий час тому ми додали його в директиву page.
Я встановив розмір сторінки рівним 3, який ви можете змінити на свій розсуд.
Поточна сторінка та елементи на сторінці додаються в об’єкт класу PagingInfo. Також обчислюється значення початкового запису для сторінки і додається в змінну skip:
1 |
var skip = pageSize * (Convert.ToInt32(pagingInfo.CurrentPage) - 1); |
Далі ми викликаємо метод ReadAllFilterAsync зі значенням skip і кількістю записів для вибірки (тобто pagesize).
Метод повертає кортеж, значення якого витягують і передають у TotalItems з pagingInfo і Assignment з AssignmentList.Assignments:
1 2 |
pagingInfo.TotalItems = resultTuple.Item2; AssignmentList.Assignments = resultTuple.Item1; |
Нарешті, ми присвоюємо властивості pagingInfo об’єкта AssignmentList значення pagingInfo:
1 |
AssignmentList.PagingInfo = pagingInfo; |
Давайте додамо кілька записів і перевіримо роботу пагінації:
Шановні ентузіасти програмування на C#!
З найкращими побажаннями,
[Леонід/ Dijix Company]
Доопрацювання інших Crud операцій
Перейдемо до функції Update, як і раніше, додамо новий методи ReadAsync і UpdateAsync в інтерфейс IRepository:
1 2 |
Task<T> ReadAsync(int id); Task UpdateAsync(T entity); |
Також реалізуємо ці методи в класі Repository.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public async Task<T> ReadAsync(int id) { return await context.Set<T>().FindAsync(id); } public async Task UpdateAsync(T entity) { if (entity == null) { throw new ArgumentNullException(nameof(entity)); } context.Update(entity); await context.SaveChangesAsync(); } |
У вищенаведеному методі ReadAsync, використовується метод FindAsync() з Entity Framework Core і передається id сутності, яку потрібно прочитати з бази даних. Метод UpdateAsync оновлює сутність у базі даних.
Після цього нам потрібно створити нову сторінку Razor Page під назвою Update.cshtml у папці «Pages». Потім додамо в неї наступний код:
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
@page @model UpdateModel @using Microsoft.AspNetCore.Mvc.RazorPages; @using Models; @using RazorCrud.Interfaces @using System.ComponentModel.DataAnnotations @{ ViewData["Title"] = "Update a Assignment"; } @section Styles { <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet" type="text/css" /> <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.3.0/css/datepicker.css" rel="stylesheet" type="text/css" /> } <h1 class="bg-info text-white">Update a Assignment</h1> <a asp-page="Read" class="btn btn-secondary">View all Assignments</a> <div asp-validation-summary="All" class="text-danger"></div> <form method="post" enctype="multipart/form-data"> <input type="hidden" asp-for="@Model.Assignment.Id" /> <input type="hidden" asp-for="@Model.Assignment.ImagePath" /> <div class="form-group"> <label asp-for="@Model.Assignment.Title"></label> <input type="text" asp-for="@Model.Assignment.Title" class="form-control" /> <span asp-validation-for="@Model.Assignment.Title" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="@Model.Assignment.Description"></label> <input type="text" asp-for="@Model.Assignment.Description" class="form-control" /> <span asp-validation-for="@Model.Assignment.Description" class="text-danger"></span> </div> <label asp-for="@Model.Assignment.FinishedDate"></label> <div id="datepicker" class="input-group date" data-date-format="dd-mm-yyyy"> <input type="text" asp-for="@Model.Assignment.FinishedDate" class="form-control" readonly /> <span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span> </div> <div class="form-group"> <label asp-for="@Model.Assignment.Image" class="control-label"></label> <input type="file" class="form-control" asp-for="@Model.Assignment.Image" accept="image/*" /> @if (Model.Assignment.ImagePath != null) { <img src="@Model.Assignment.ImagePath" class="img-fluid" style="width:100px; height:100px;"> } </div> <div class="mt-3"> <button type="submit" class="btn btn-primary">Update</button> </div> </form> @section Scripts { <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.3.0/js/bootstrap-datepicker.js"></script> <script> $(function () { $("#datepicker").datepicker({ autoclose: true, todayHighlight: true }).datepicker('update', new Date()); }); </script> @await Html.PartialAsync("_ValidationScriptsPartial") } @functions { public class UpdateModel : PageModel { private readonly IRepository<Assignment> assignmentRepository; public UpdateModel(IRepository<Assignment> assignmentRepository) { this.assignmentRepository = assignmentRepository; } [BindProperty] public AssignmentViewModel Assignment { get; set; } public async Task<IActionResult> OnGet(int id) { var assignment = await assignmentRepository.ReadAsync(id); if (assignment != null) { Assignment = new AssignmentViewModel { Id = assignment.Id, Title = assignment.Title, Description = assignment.Description, ImagePath = assignment.Image, FinishedDate = assignment.FinishedDate }; return Page(); } else { return RedirectToPage("Read"); } } public async Task<IActionResult> OnPostAsync([FromServices] IWebHostEnvironment appEnvironment) { if (ModelState.IsValid) { Assignment assignment = new Assignment { Id = Assignment.Id, Title = Assignment.Title, Description = Assignment.Description, FinishedDate = Assignment.FinishedDate, }; if (Assignment.Image != null) { if (System.IO.File.Exists(appEnvironment.WebRootPath + Assignment.ImagePath)) { System.IO.File.Delete(appEnvironment.WebRootPath + Assignment.ImagePath); } string fileName = Assignment.Image.FileName; if (fileName.Contains("\")) { fileName = fileName.Substring(fileName.LastIndexOf('\') + 1); } string imagePath = "/assignmentImages/" + Guid.NewGuid() + fileName; using (var fileStream = new FileStream(appEnvironment.WebRootPath + imagePath, FileMode.Create)) { await Assignment.Image.CopyToAsync(fileStream); } assignment.Image = imagePath; } await assignmentRepository.UpdateAsync(assignment); return RedirectToPage("Read"); } else { return Page(); } } } public class AssignmentViewModel { [Key] public int Id { get; set; } [Required] public string Title { get; set; } [Required] public string Description { get; set; } public DateTime FinishedDate { get; set; } public IFormFile? Image { get; set; } public string? ImagePath { get; set; } } } |
Код сторінки «Update» дуже схожий на код сторінки «Update», тільки з деякими змінами:
В обробнику OnGet() викликається сховище для отримання доручення, чий id обробник отримав як параметр. Ідентифікатор доручення буде відправлено в Update як параметр рядка запиту. Я показав такі посилання нижче:
https://localhost:7182/Update?id=1
https://localhost:7182/Update?id=10
https://localhost:7182/Update?id=100
Обробник OnGet має в параметрі (int id), і функція прив’язки моделі автоматично зв’яже цей параметр id зі значенням id, зазначеним у рядку запиту.
Далі, отримуємо запис через метод репозиторію ReadAsync і проєктуємо у властивість за типом AssignmentViewModel, для відображення на сторінці.
Після оновлення потрапляємо на обробник OnPostAsync, де перевіряємо наявність зображення і видаляємо попереднє, якщо воно є. Далі, використовуючи метод репозиторію UpdateAsync, оновлюємо запис.
Ще одна річ, яку нам потрібно зробити, – це пов’язати сторінку оновлення зі сторінкою читання. Таблиця в Read.cshml, у якій відображаються записи доручень, ідеально підходять для цього.
Додамо ще один стовпець для таблиці, до цього стовпця буде додано тег якоря, що посилатиметься на сторінку оновлення:
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 |
<table class="table table-sm table-bordered"> <tr> <th>Id</th> <th>Title</th> <th>Description</th> <th>Image</th> <strong> <th></th></strong> </tr> @foreach (Assignment assignment in Model.AssignmentList.Assignments) { <tr> <td>@assignment.Id</td> <td>@assignment.Title</td> <td>@assignment.Description</td> <td> @if (assignment.Image != null) { <img src="@assignment.Image" class="img-fluid" style="width:100px; height:100px;"> } </td> <strong> <td> <a class="btn btn-sm btn-primary" asp-page="Update" asp-route-id="@assignment.Id"> Update </a> </td></strong> </tr> } </table> |
Значення href тега anchor буде створено помічниками тегів asp-page і asp-route. У asp-page вказується ім’я сторінки, яке дорівнює Update, а в asp-route – ім’я маршруту, яке дорівнює id. Значення id додається в помічник тега з механізму циклу foreach.
Тепер додамо в інтерфейс IRepository, метод під назвою DeleteAsync. Цей метод приймає як параметр id сутності:
1 |
Task DeleteAsync(int id); |
Також реалізуємо цей метод у класі IRepository.cs:
1 2 3 4 5 6 7 8 9 10 11 12 |
public async Task DeleteAsync(int id) { var entity = await context.Set<T>().FindAsync(id); if (entity == null) { throw new ArgumentNullException(nameof(entity)); } context.Set<T>().Remove(entity); await context.SaveChangesAsync(); } |
У цьому методі ми використовуємо метод FindAsync сховища, щоб знайти сутність за її ідентифікатором. А потім видаляємо сутність методом Remove.
Далі перейдемо до сторінки Read. Нам не потрібно створювати нову сторінку Razor Page для операції Delete. Щоб створити CRUD-операцію Delete, додамо ще один стовпець у таблицю на Razor-сторінці Read.cshml. Цей стовпець міститиме кнопку видалення, натискання на яку призведе до видалення запису:
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 |
@page "{id:int?}" @model ReadModel @using Microsoft.AspNetCore.Mvc.RazorPages; @using Models; @using RazorCrud.Interfaces @using RazorCrud.Models.Paging @{ ViewData["Title"] = "Assignments"; } <h1 class="bg-info text-white">Assignments</h1> <div class="mb-3"> <a asp-page="Create" class="btn btn-secondary">Create a Assignment</a> </div> <table class="table table-sm table-bordered"> <tr> <th>Id</th> <th>Title</th> <th>Description</th> <th>Image</th> <th></th> <strong> <th></th></strong> </tr> @foreach (Assignment assignment in Model.AssignmentList.Assignments) { <tr> <td>@assignment.Id</td> <td>@assignment.Title</td> <td>@assignment.Description</td> <td> @if (assignment.Image != null) { <img src="@assignment.Image" class="img-fluid" style="width:100px; height:100px;"> } </td> <td> <a class="btn btn-sm btn-primary" asp-page="Update" asp-route-id="@assignment.Id"> Update </a> </td> <strong> <td> <form asp-page-handler="Delete" asp-route-id="@assignment.Id" method="post"> <button type="submit" class="btn btn-sm btn-danger"> Delete </button> </form> </td></strong> </tr> } </table> <div class="pagingDiv" page-model="Model.AssignmentList.PagingInfo" page-name="Read" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div> |
Нарешті, нам потрібно додати обробник OnPostDeleteAsync у файл Read.cshtml. Цей обробник буде викликатися при натисканні кнопки видалення:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public async Task<IActionResult> OnPostDeleteAsync([FromServices] IWebHostEnvironment appEnvironment, int id) { var currentAssignment = await assignmentRepository.ReadAsync(id); if (currentAssignment != null) { await assignmentRepository.DeleteAsync(id); if (System.IO.File.Exists(appEnvironment.WebRootPath + currentAssignment.Image)) { System.IO.File.Delete(appEnvironment.WebRootPath + currentAssignment.Image); } } return RedirectToPage("Read", new { id = 1 }); } |
Обробник OnPostDeleteAsync є асинхронним обробником, оскільки в його кінці стоїть «Async». Термін OnPost на початку його назви вказує на те, що це обробник типу Post. Спочатку, використовуючи метод DeleteAsync, видаляємо запис. Потім, перевіряємо чи є зображення, тоді видаляємо.
Запустимо додаток і перевіримо його роботу:
Фільтрація даних
Давайте створимо функцію для пошуку сутності за її ім’ям за допомогою LINQ виразів. Додамо новий метод ReadAllAsync в інтерфейс IRepository. Цей метод матиме параметр типу Expression<Func<T, bool>>, який можна використовувати для передачі виразу фільтрації:
1 |
Task<(List<T>, int)> ReadAllAsync(Expression<Func<T, bool>> filter, int skip, int take); |
Далі у файлі Repository.cs додамо цей метод:
1 2 3 4 5 6 7 8 9 10 |
public async Task<(List<T>, int)> ReadAllAsync(Expression<Func<T, bool>> filter, int skip, int take) { var all = context.Set<T>().Where(filter); var relevant = await all.Skip(skip).Take(take).ToListAsync(); var total = all.Count(); (List<T>, int) result = (relevant, total); return result; } |
Func<T, bool> – означає, що вираз фільтра буде працювати з сутностями типу «T» і повертає тип bool. Тому ми можемо застосувати цей вираз фільтрації до речення «Де», як показано нижче:
1 |
context.Set<T>().Where(filter) |
Для того щоб синхронізувати пагінацію і фільтрацію, змінимо сторінку Read.cshtml таким чином:
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
@page "{id:int?}" @model ReadModel @using Microsoft.AspNetCore.Mvc.RazorPages; @using Models; @using RazorCrud.Interfaces @using RazorCrud.Models.Paging @using System.Linq.Expressions @{ ViewData["Title"] = "Assignments"; } <h1 class="bg-info text-white">Assignments</h1> <div class="mb-3"> <a asp-page="Create" class="btn btn-secondary">Create a Assignment</a> </div> <strong><form method="get"> <div class="form-group"> <input type="hidden" asp-for="@Model.Page" /> <label asp-for="@Model.SearchTerm"></label> <input type="text" asp-for="@Model.SearchTerm" class="form-control" /> </div> <div class="form-group mt-2 mb-3"> <button type="submit" class="btn btn-primary">Search</button> </div> </form></strong> <table class="table table-sm table-bordered"> <tr> <th>Id</th> <th>Title</th> <th>Description</th> <th>Image</th> <th></th> <th></th> </tr> @foreach (Assignment assignment in Model.AssignmentList.Assignments) { <tr> <td>@assignment.Id</td> <td>@assignment.Title</td> <td>@assignment.Description</td> <td> @if (assignment.Image != null) { <img src="@assignment.Image" class="img-fluid" style="width:100px; height:100px;"> } </td> <td> <a class="btn btn-sm btn-primary" asp-page="Update" asp-route-id="@assignment.Id"> Update </a> </td> <td> <form asp-page-handler="Delete" asp-route-id="@assignment.Id" method="post"> <button type="submit" class="btn btn-sm btn-danger"> Delete </button> </form> </td> </tr> } </table> <strong><div class="pagingDiv" page-model="Model.AssignmentList.PagingInfo" page-name="Read" page-classes-enabled="true" page-class="paging" page-class-selected="active" page-other-searchTerm="@Model.SearchTerm"></div></strong> @functions { public class ReadModel : PageModel { private readonly IRepository<Assignment> assignmentRepository; public ReadModel(IRepository<Assignment> assignmentRepository) { this.assignmentRepository = assignmentRepository; } <strong> [BindProperty] public string SearchTerm { get; set; }</strong> <strong> [BindProperty] public int Page { get; set; }</strong> public AssignmentList AssignmentList { get; set; } public async Task OnGet(int id, string? searchTerm) { AssignmentList = new AssignmentList(); <strong> Page = id;</strong> int pageSize = 3; PagingInfo pagingInfo = new PagingInfo(); pagingInfo.CurrentPage = id == 0 ? 1 : id; pagingInfo.ItemsPerPage = pageSize; var skip = pageSize * (Convert.ToInt32(pagingInfo.CurrentPage) - 1); <strong>(List<Assignment>, int) resultTuple; if (searchTerm != null) { SearchTerm = searchTerm; Expression<Func<Assignment, bool>> filter = m => m.Title.Contains(searchTerm); resultTuple = await assignmentRepository.ReadAllAsync(filter, skip, pageSize); } else { resultTuple = await assignmentRepository.ReadAllFilterAsync(skip, pageSize); } </strong> pagingInfo.TotalItems = resultTuple.Item2; AssignmentList.Assignments = resultTuple.Item1; AssignmentList.PagingInfo = pagingInfo; } public async Task<IActionResult> OnPostDeleteAsync([FromServices] IWebHostEnvironment appEnvironment, int id) { var currentAssignment = await assignmentRepository.ReadAsync(id); if (currentAssignment != null) { await assignmentRepository.DeleteAsync(id); if (System.IO.File.Exists(appEnvironment.WebRootPath + currentAssignment.Image)) { System.IO.File.Delete(appEnvironment.WebRootPath + currentAssignment.Image); } } return RedirectToPage("Read", new { id = 1 }); } } } |
Для отримання запиту, на сторінці визначили властивість SearchTerm, значення якої передаємо в клас PageLinkTagHelper, у колекцію PageOtherValues:
1 2 |
[HtmlAttributeName(DictionaryAttributePrefix = "page-other-")] public Dictionary<string, object> PageOtherValues { get; set; } = new Dictionary<string, object>(); |
Цей код дає змогу визначити атрибути HTML із префіксом «page-other-» і зберігати їхні значення в словнику PageOtherValues. Значення з цієї колекції витягуються і додаються як параметри запиту:
1 2 |
else if (PageOtherValues.Keys.Count != 0) tag.Attributes["href"] = urlHelper.Page(PageName, AddDictionaryToQueryString(i)); |
Таким чином, у методі OnGet, сторінки Read.cshtml, ми можемо отримати ці значення через параметри:
1 |
public async Task OnGet(int id, string? searchTerm) |
Запустимо додаток і перевіримо його роботу:
На цьому все, сподіваюся вам було цікаво і просто. Пишіть у коментарях, якщо хочете продовження, в якому ми реалізуємо авторизацію та реєстрацію, поділ частин програми за ролями.
Я сподіваюся, що вам сподобалося читати цю статтю, і вона виявилася легкою для розуміння. Будь ласка, дайте мені знати, якщо у вас є якісь коментарі або виправлення.
Так само вам може бути цікава попередня стаття – Найкращі книги з C# на 2024 рік.
Ви хочете навчитися писати код мовою програмування C#?
Створювати різні інформаційні системи, що складаються з сайтів, мобільних клієнтів, десктопних додатків, телеграм-ботів тощо.
Переходьте до нас на сторінку Dijix і ознайомтеся з умовами навчання, ми спеціалізуємося тільки на індивідуальних заняттях, як для початківців, так і для просунутих програмістів. Ви можете взяти як одне заняття для опрацювання питання, що вас цікавить, так і кілька, для більш щільної роботи. Завдяки особистому кабінету, кожен студент підвищить якість свого навчання, у вашому розпорядженні:
- Доступ до пройденого матеріалу
- Тематичні статті
- Бібліотека книг
- Онлайн тестування
- Спілкування в закритих групах