[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на C# станут решающим фактором. Переходите по ссылке 🔰.
В этой статье, мы продолжим писать магазин на Asp.Net Core и Entity Framework Core. В первой части мы добавили поддержку для сохранения данных в БД и выдачи запросов к ней. Для ознакомления с первой частью, перейдите по ссылке.
Магазин на Asp.Net Core MVC EF. Часть 2
Обновление контроллера и создание представления
Обновление только измененных свойств
Выполнение массовых обновлений
Изменение представлений и контроллера
Использование средства обнаружения изменений в файле
Модификация и удаление данных
В текущий момент приложение GameStore способно сохранять объекты Product в БД и выполнять запросы для их чтения обратно в память. Большинству приложений также требуется возможность вносить изменения в данные после того, как они были сохранены, включая полное удаление объектов. В этой главе будет добавлена поддержка для обновления и удаления объектов Product. Вдобавок обсуждаются проблемы, с которыми вполне вероятно вы столкнетесь при добавлении таких средств в собственные проекты, и объясняется, как их решить.
Инфраструктура Entity Framework Core поддерживает несколько способов обновления объектов. В текущей главе мы начнем с простейшего приема, при котором объект, созданный связывателем моделей MVC, применяется для полной замены объекта, хранящегося в БД.
Первым делом измените интерфейс IProduct, добавив к нему методы, которые можно использовать в остальном коде приложения для извлечения и обновления существующего объекта:
1 2 3 4 5 6 |
public interface IProduct { //... void Product GetProduct(int id); void UpdateProduct(Product product); } |
Метод GetProduct() предоставит одиночный объект Product по значению его первичного ключа. Метод UpdateProduct() получает объект Product и какого-либо результата не возвращает.
Реализуем данные методы, в классе репозитории ProductRepository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class ProductRepository : IProduct { //... public Product GetProduct(int id) { return _context.Products.Find(id); } public void UpdateProduct(Product product) { _context.Products.Update(product); _context.SaveChanges(); } } |
Объект DbSet<Product>, возвращаемый свойством Products контекста БД, предоставляет функциональное средство, которое необходимо для реализации новых методов. Метод Find() принимает значение первичного ключа и запрашивает у БД соответствующий ему объект. Метод Update() принимает объект Product и применяет его для обновления БД, заменяя объект в БД с тем же самым первичным ключом. Как и в случае всех операций, которые изменяют БД, после вызова Update() понадобится вызвать метод SaveChanges().
Помнить о необходимости вызова метода SaveChanges() может показаться трудновыполнимой задачей, но это быстро войдет в привычку. Такой подход означает, что вы можете подготовить множество изменений, вызывая методы объекта контекста, и затем одновременно отправить их БД с помощью единственного вызова SaveChanges().
Обновление контроллера и создание представления
Следующий шаг заключается в обновлении контроллера Home, чтобы добавить методы действий, которые позволят пользователю выбрать объект Product для редактирования и отправить изменения приложению.
Добавим 2 новых действия в HomeController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class HomeController : Controller { //... [HttpGet] public IActionResult UpdateProduct(int id) { return View(_products.GetProduct(id)); } [HttpPost] public IActionResult UpdateProduct(Product product) { _products.UpdateProduct(product); return RedirectToAction(nameof(Index)); } } |
Вы можете заметить, как методы действий сопоставляются со средствами, предоставляемыми хранилищем, посредством класса контекста БД. Чтобы снабдить контроллер представлением для новых действий, добавьте в папку Views/Horne файл по имени UpdateProduct.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 |
@{ ViewData["Title"] = "Обновление товара"; } @model Product <hЗ class="p-2 bg-primary text-white text-center">Обновление товара - @Model.Name</hЗ> <form asp-action="UpdateProduct" method="post"> <div class="form-group"> <label asp-for="Id"></label> <input asp-for="Id" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <label asp-for="Name"></label> <input asp-for="Name" class="form-control" /> </div> <div class="form-group"> <label asp-for="Category"></label> <input asp-for="Category" class="form-control" /> </div> <div class="form-group"> <label asp-for="PurchasePrice"></label> <input asp-for="PurchasePrice" class="form-control" /> </div> <div class="form-group"> <label asp-for="RetailPrice"></label> <input asp-for="RetailPrice" class="form-control" /> </div> <div class="text-center"> <button class="btn btn-primary" type="submit">Сохранить</button> <a asp-action="Index" class="btn btn-secondary">Отмена</a> </div> </form> |
Представление UpdateProduct.cshtml снабжает пользователя НТМL-формой, которую можно использовать для изменения свойств объекта Product за исключением свойства Id, применяемого в качестве первичного ключа. Первичные ключи нелегко изменить после того, как они были назначены, и если требуется другое значение ключа, то проще удалить объект и создать новый. По этой причине к элементу input добавлен атрибут readonly, который позволяет видеть значение свойства Id, но не изменять его. Либо поле с Id, можно вообще скрыть с глаз пользователя.
Для интеграции средства обновления с остальным кодом приложения добавим элемент button к каждому объекту Product, отображаемому представлением Index.cshtml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@if (Model.Count() == 0) { <div class="row"> <div class="col text-center p-2">Нет данных</div> </div> } else { @foreach (Product product in Model) { <div class="row р-2"> <div class="col">@product.Name</div> <div class="col">@product.Category</div> <div class="col text-right">@product.PurchasePrice</div> <div class="col text-right">@product.RetailPrice</div> <div class="col"> <a asp-action="UpdateProduct" asp-route-id="@product.Id" class=" btn btn-outline-primary"> Изменить </a> </div> </div> } } |
Запустите приложение, выберите любой товар и нажмите кнопку “Изменить”:
Измените любое значение на выбранном товаре и нажмите кнопку “Сохранить”:
Если вы просмотрите журнальные сообщения, сгенерированные приложением, то сможете увидеть, как выполняемые действия превращаются в SQL-команды, отправляемые серверу баз данных. Когда вы щелкаете на кнопке Edit, инфраструктура EntityFramework Core запрашивает у БД детали выбранного объекта с помощью следующей команды:
1 2 3 |
SELECT TOP(1) [p].[Id], [p].[Category], [p].[Name], [p].[PurchasePrice], [p].[RetailPrice] FROM[Products] AS[p] WHERE[p].[Id] = @__p_0 |
Метод Find(), транслируется в команду SELECT для одиночного объекта, который указан с использованием ключевого слова ТОР. После щелчка на кнопке Save инфраструктура Entity Framework Саге обновляет БД с помощью такой команды:
1 2 3 |
UPDATE[Products] SET[Category] = @p0, [Name] = @p1, [PurchasePrice] = @p2, [RetailPrice] = @p3 WHERE[Id] = @p4; SELECT @@ROWCOUNT; |
Метод Update() транслируется в SQL-команду UPDATE, которая сохраняет значения данных формы, полученные из НТTР-запроса.
Обновление только измененных свойств
Основные строительные блоки для выполнения обновлений на месте, но результат неэффективен, поскольку инфраструктура Entity Framework Core не располагает исходными данными для оценки, что конкретно изменилось, и не имеет другого выбора, кроме сохранения всех свойств. Чтобы взглянуть на проблему, щелкните на кнопке Edit для одного из товаров и затем щелкните на кнопке Save, не внося каких-либо изменений. Хотя новые значения данных не вводились, журнальные сообщения, сгенерированные приложением, говорят о том, что выпущенная инфраструктурой EntityFramework Core команда UPDATE посылает значения для всех свойств, которые определены в классе Product:
1 2 |
UPDATE[Products] SET[Category] = @p0, [Name] = @p1, [PurchasePrice] = @p2, [RetailPrice] = @p3 WHERE[Id] = @p4; |
Инфраструктура Entity Framework Core содержит средство обнаружения изменений, которое способно выявить, какие свойства были изменены. Для такого простого класса модели данных, каковым является Product, польза от этого средства невелика, но в случае более сложных моделей обнаружение изменений может быть важным.
Средство обнаружения изменений требует исходных данных, с которыми можно было бы сравнить данные, полученные от пользователя. Есть разные способы предоставления исходных данных (они описаны в главе 12), но здесь будет применен простейший подход, который предусматривает запрашивание у БД существующих данных.
Изменим метод UpdateProduct, класса ProductRepository, следующим образом:
1 2 3 4 5 6 7 8 9 10 |
public void UpdateProduct(Product product) { Product product2 = GetProduct(product.Id); product2.Name = product.Name; product2.Category = product.Category; product2.RetailPrice = product.RetailPrice; product2.PurchasePrice = product.PurchasePrice; //_context.Products.Update(product); _context.SaveChanges(); } |
Код объединяет в приложении две разных функции. Инфраструктура Entity Framework Core отслеживает изменения в объектах, которые создает из данных запросов, в то время как связыватель моделей MVC создает объекты из НТГР-данных. Упомянутые два источника объектов не объединены, и если не соблюдать осторожность при удержании их раздельными, то возникнут проблемы. Самый безопасный способ задействовать в своих интересах отслеживание изменений — запросить БД и затем скопировать значения из НТГР-данных, как было сделано.
Когда вызывается метод SaveChanges(), инфраструктура Entity Framework Core выяснит, значения каких свойств изменились, и обновит в БД только эти свойства.
Запустите приложение, выполните изменение закупочной стоимости любого продукта и просмотрите текущий запрос:
1 2 |
UPDATE[Products] SET[PurchasePrice] = @p0 WHERE[Id] = @p1; |
Выполнение массовых обновлений
Массовые обновления часто требуются в приложениях, где предусмотрены отдельные роли администраторов, которым необходимо вносить изменения во множество объектов в единственной операции. Точная природа обновлений будет варьироваться, но распространенные причины для массовых обновлений включают исправление ошибок в записях данных или переназначение объектов в новые категории, что может оказаться затратным по времени процессом в случае выполнения на индивидуальных объектах. С использованием инфраструктуры Entity Framework Core делать массовые обновления легко, но требуются небольшие усилия, чтобы обеспечить их гладкую работу с частью ASP.NEТ Core MVC приложения.
Изменение хранилища
Добавьте в интерфейс хранилища IProduct новый метод, который будет выполнять массовое обновление:
1 2 3 4 5 |
public interface IProduct { //... void UpdateAll(Product[] products); } |
Наконец, добавьте в репозиторий ProductRepository реализацию метода UpdateAll(), который будет обновлять БД с применением данных, полученных из НТТР-запроса:
1 2 3 4 5 6 7 8 9 10 |
public class ProductRepository : IProduct { //... public void UpdateAll(Product[] products) { _context.Products.UpdateRange(products); _context.SaveChanges(); } } |
Класс DbSet<T> предлагает методы для работы с индивидуальными объектами и с коллекциями объектов. В этом примере используется метод UpdateRange(), который является аналогом метода Update(), но работает с коллекциями. Когда вызывается метод SaveChanges(), инфраструктура Entity Framework Core отправляет последовательность SQL-команд UPDATE для обновления БД.
Изменение представлений и контроллера
Для добавления поддержки массовых обновлений измените, представление Index, включив в него кнопку Edit All (Редактировать все), которая нацелена на действие UpdateAll. Также добавьте свойство типа ViewBag по имени UpdateAll. Если его значением является true, тогда будет отображаться частичное представление InlineEditor.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 |
@{ ViewData["Title"] = "Все товары"; } @model IQueryable<Product> <h3 class="p-2 bg-bg-primary text-white text-center">Товары</h3> <div class="container"> @if (ViewBag.UpdateAll != true) { <div class="row"> <div class="col font-weight-bold">Название</div> <div class="col font-weight-bold">Категория</div> <div class="col font-weight-bold">Закупочная цена</div> <div class="col font-weight-bold">Розничная цена</div> <div class="col"></div> </div> <form asp-action="AddProduct" asp-controller="Home" method="post"> <div class="row"> <div class="col"> <input name="Name" class="form-control" required /> </div> <div class="col"> <input name="Category" class="form-control" required /> </div> <div class="col"> <input name="PurchasePrice" class="form-control" required /> </div> <div class="col"> <input name="RetailPrice" class="form-control" required /> </div> <div class="col"> <button type="submit" class="btn btn-primary">Добавить</button> </div> </div> </form> @if (Model.Count() == 0) { <div class="row"> <div class="col text-center p-2">Нет данных</div> </div> } else { @foreach (Product product in Model) { <div class="row р-2"> <div class="col">@product.Name</div> <div class="col">@product.Category</div> <div class="col text-right">@product.PurchasePrice</div> <div class="col text-right">@product.RetailPrice</div> <div class="col"> <a asp-action="UpdateProduct" asp-route-id="@product.Id" class="btn btn-outline-primary">Изменить</a> </div> </div> } } <div class="text-center"> <a asp-action="UpdateAll" class="btn btn-primary">Изменить все</a> </div> } else { @Html.PartialAsync("InlineEditor", Model) } </div> |
Создайте частичное представление, добавив в папку Views/Home файл по имени InlineEditor.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 |
@model IEnumerable<Product> <div class="row"> <div class="col-1 font-weight-bold">Id</div> <div class="col font-weight-bold">Название</div> <div class="col font-weight-bold">Категория</div> <div class="col font-weight-bold">Закупочная цена</div> <div class="col font-weight-bold">Розничная цена</div> </div> @{ int i = 0; } <form asp-action="UpdateAll" method="post"> @foreach (Product p in Model) { <div class="row"> <div class="col-1"> @p.Id <input type="hidden" name="Products[@i].Id" value="@p.Id"> </div> <div class="col"> @p.Name <input class="form-control" name="Products[@i].Name" value="@p.Name"> </div> <div class="col"> @p.Category <input class="form-control" name="Products[@i].Category" value="@p.Category"> </div> <div class="col"> @p.PurchasePrice <input class="form-control" name="Products[@i].PurchasePrice" value="@p.PurchasePrice"> </div> <div class="col"> @p.RetailPrice <input class="form-control" name="Products[@i].RetailPrice" value="@p.RetailPrice"> </div> </div> i++; } <div class="form-group text-center"> <button class="btn btn-primary" type="submit">Сохранить</button> <a asp-action="Index" class="btn btn-secondary">Отмена</a> </div> </form> |
Частичное представление создает набор элементов формы, имена которых следу ют соглашению, принятому в МVС для коллекции объектов, а потому свойству Id назначаются имена Products[0].Id, Products[1].Id и т.д. Установка имен элементов input требует счетчика, что порождает неуклюжую смесь выражений Razor и С#.
Добавьте в контроллер Ноше методы действий, которые позволят пользователю начать процесс массового редактирования и отправить данные:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class HomeController : Controller { //... [HttpGet] public IActionResult UpdateAll() { ViewBag.UpdateAll = true; return View(nameof(Index), _products.GetAllProducts()); } [HttpPost] public IActionResult UpdateAll(Product[] products) { _products.UpdateAll(products); return RedirectToAction(nameof(Index)); } } |
На данном этапе, структура проекта выглядит следующим образом:
Запустите приложение и выполните массовое редактирование товаров:
Использование средства обнаружения изменений в файле
В предыдущем коде не применяется средство обнаружения изменений Entity Framework Core, т.е. будут обновляться все свойства всех объектов Product. Чтобы обновлять только изменившиеся значения, изменит метод UpdateAll, в классе ProductRepository, следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class ProductRepository : IProduct { //... public void UpdateAll(Product[] products) { // _context.Products.UpdateRange(products); Dictionary<int, Product> data = products.ToDictionary(e => e.Id); IEnumerable<Product> baseline = _context.Products.Where(e => data.Keys.Contains(e.Id)); foreach (Product product in baseline) { Product requestProduct = data[product.Id]; product.Name = requestProduct.Name; product.Category = requestProduct.Category; product.RetailPrice = requestProduct.RetailPrice; product.PurchasePrice = requestProduct.PurchasePrice; } _context.SaveChanges(); } } |
Процесс выполнения обновления может выглядеть запутанным. Сначала создается словарь объектов Product, полученных от связывателя моделей MVC, с применением свойства Id для ключей. Коллекция ключей используется для запрашивания соответствующих объектов в БД. Далее происходит перечисление объектов БД с копированием значений свойств из объектов НТТР-запроса. После вызова метода SaveChanges() инфраструктура EF выполняет обнаружение изменений и обновляет только те свойства, значения которых изменились.
Запустите приложение и выполните массовое редактирование товаров. Просмотрите журнальные сообщения, сгенерированные приложением. Сравните запросы с разными версиями метода UpdateAll.
Удаление данных
Удаление объектов из БД — процесс простой, хотя с разрастанием модели данных он может усложниться. В интерфейс IProduct, добавим следующий метод:
1 2 3 4 5 |
public interface IProduct { //... void DeleteProduct(Product product); } |
В классе ProductRepository, реализуем данный метод:
1 2 3 4 5 6 7 8 9 10 |
public class ProductRepository : IProduct { //... public void DeleteProduct(Product product) { _context.Products.Remove(product); _context.SaveChanges(); } } |
Класс DbSet<T> имеет методы Remove() и RemoveRange(), предназначенные для удаления одного и нескольких объектов из БД. Как и в случае других операций, модифицирующих БД, данные не удаляются до тех пор, пока не будет вызван метод SaveChanges().
Добавьте в контроллер Home метод действия, который получает из НТТР запроса детали подлежащего удалению объекта Product и передает их хранилищу:
1 2 3 4 5 6 7 8 9 10 |
public class HomeController : Controller { //... [HttpPost] public IActionResult DeleteProduct(Product product) { _products.DeleteProduct(product); return RedirectToAction(nameof(Index)); } } |
В представление Index, добавим элемент form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@foreach (Product product in Model) { <div class="row р-2"> <div class="col">@product.Name</div> <div class="col">@product.Category</div> <div class="col text-right">@product.PurchasePrice</div> <div class="col text-right">@product.RetailPrice</div> <div class="col"> <form asp-action="DeleteProduct" method="post"> <input type="hidden" name="Id" value="@product.Id"> <button type="submit" class="btn btn-outline-danger">Удалить</button> </form> <a asp-action="UpdateProduct" asp-route-id="@product.Id" class="btn btn-outline-primary">Изменить</a> </div> </div> } |
Обратите внимание, что форма содержит единственный элемент input для свойства Id. Это все, что инфраструктура Entity Framework Core применяет для удаления объекта из БД, хотя операция выполняется над полным объектом Product. Вместо отправки дополнительных данных, которые не планируется использовать, было послано только значение первичного ключа, которое связыватель моделей MVC применит для создания объекта Product, оставляя все другие свойства равными null или стандартному значению для типа.
Запустите приложение и выполните удаление любого продукта.
Итог
В главе к приложению GameStore была добавлена поддержка для обновления и удаления объектов. Вы узнали, каким образом модифицировать индивидуальные объекты и выполнять массовые обновления, а также как снабжать инфраструктуру Entity Framework Core исходными данными для ее средства обнаружения изменений. Кроме того, было показано, как удалять данные, что делается просто в случае модели данных с единственным классом и усложняется с разрастанием модели данных. В следующем уроке модель данных для приложения GameStore будет расширена.
На этом статья «Магазин на Asp.Net Core MVC EF 2», подошла к концу, надеюсь вам было интересно. Вы можете скачать исходный код в моем репозитории — Github.
Поделитесь вашим опытом в комментариях, как вы создаете магазин на Asp.Net Core MVC EF?
Так же вам может быть интересна предыдущая статья:
Вы хотите научится писать код на языке программирования C#?
Создавать различные информационные системы, состоящие из сайтов, мобильных клиентов, десктопных приложений, телеграмм-ботов и т.д.
Переходите к нам на страницу Dijix и ознакомьтесь с условиями обучения, мы специализируемся только на индивидуальных занятиях, как для начинающих, так и для более продвинутых программистов. Вы можете взять как одно занятие для проработки интересующего Вас вопроса, так и несколько, для более плотной работы. Благодаря личному кабинету, каждый студент повысит качество своего обучения, в вашем распоряжении:
- Доступ к пройденному материалу
- Тематические статьи
- Библиотека книг
- Онлайн тестирование
- Общение в закрытых группах
Живи в своем мире, программируй в нашем.
В HomeController используется два раза UpdateProduct и они начинают конфликтовать, лучше второй метод переименовать во что-то по типу UpdateProductAction и соответственно к нему обращаться в html файле.
Плюсом, в интерфейсах не создается GetProduct, его надо самостоятельно прописать Product GetProduct(int id);
Ну и желательно везде сделать проверку на NULL, чтобы не было недоразумений потом у пользователей.
Добрый день. Вы правы на счет метода GetProduct.
Метод UpdateProduct перегружается, поэтому конфликтов быть не должно.
Извиняюсь, я тогда не знал, что перед каждым методом надо ставить атрибут [HttpGet] или [HttpPost], поэтому у меня они конфликтовали. Я писал все методы под одним из атрибутов
Добрый день.
public interface IProduct
{
//…
voidProduct GetProduct(int id); <- может public ?void UpdateProduct(Product product);
}
Модификатор ‘public’ в интерфейсе можно не указывать.