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