- retrieving items from the database;
- adding new items to the database;
- updating existing items in the database;
- deleting existing item from the database;
-
Add a new controller to the
Controllers
folder calledAdminController
public class AdminController : Controller { private IStoreRepository repository; public AdminController(IStoreRepository repo) { repository = repo; } public IActionResult Index() { return View(repository.Products); } }
The controller constructor declares a dependency on the
IStoreRepository
interface, which will be resolved when instances are created. The controller defines a single action method,Index
, that calls theView
method to select the default view for the action, passing the set of products in the database as the view model.
-
Add a new layout file, called
_AdminLayout
, to theViews/Shared
folder with the following code.<!DOCTYPE html> <html lang="en"> <head> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>@ViewBag.Title</title> <link href="/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" /> </head> <body> <nav class="navbar bg-light navbar-expand-sm"> <div class="container-fluid"> <span class="navbar-brand mb-0">MVCStore</span> </div> </nav> <div class="container-fluid"> @RenderBody() </div> @await RenderSectionAsync("Scripts", required: false) </body> </html>
For navbars that never collapse, add the
.navbar-expand
class on the navbar. Link: https://getbootstrap.com/docs/4.0/components/navbar/ -
Add a veiew corrsponding to the
Index
action in theAdmin
controller.@model IEnumerable<Product> @{ ViewBag.Title = "Admin"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h1>Products</h1> <a asp-action="Create" class="btn btn-primary mb-3">Add Product</a> <table class="table table-striped table-bordered table-sm"> <tr> <th class="text-right">ID</th> <th>Name</th> <th class="text-right">Price</th> <th class="text-center">Actions</th> </tr> @foreach (var item in Model) { <tr> <td class="text-right">@item.ProductID</td> <td>@item.Name</td> <td class="text-right">@item.Price</td> <td class="text-center"> <a asp-action="Edit" class="btn btn-sm btn-warning" asp-route-productId="@item.ProductID"> Edit </a> <form asp-action="Delete" method="post" style="display: inline"> <input type="hidden" name="ProductId" value="@item.ProductID" /> <button type="submit" class="btn btn-danger btn-sm"> Delete </button> </form> </td> </tr> } </table>
-
Add an
Edit
action on theAdminController
public IActionResult Edit(int productId) { var product = repository.Products.FirstOrDefault(p => p.ProductID == productId); return View(product); }
-
Add the corresponding view. Update the content of the view as follows:
@model MVCStore.Models.Product @{ ViewBag.Title = "Edit"; Layout = "_AdminLayout"; } <h1>Edit product</h1> <hr /> <form asp-action="Edit"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input type="hidden" asp-for="ProductID" /> <div class="form-group"> <label asp-for="Name" class="control-label"></label> <input asp-for="Name" class="form-control" /> <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Description" class="control-label"></label> <input asp-for="Description" class="form-control" /> <span asp-validation-for="Description" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Price" class="control-label"></label> <input asp-for="Price" class="form-control" /> <span asp-validation-for="Price" class="text-danger"></span> </div> <div> <input type="submit" value="Save" class="btn btn-primary" /> <a asp-action="Index" class="btn btn-secondary">Back to List</a> </div> </form>
-
Add a
SaveProduct
method in theIStoreRepository
interface.public interface IStoreRepository { IEnumerable<Product> Products { get; } Task SaveProductAsync(Product product); }
-
Implement the
SaveProduct
method as follows.public async Task SaveProductAsync(Product product) { if (product.ProductID == 0) { context.Products.Add(product); } else { Product dbEntry = context.Products .FirstOrDefault(p => p.ProductID == product.ProductID); if (dbEntry != null) { dbEntry.Name = product.Name; dbEntry.Description = product.Description; dbEntry.Price = product.Price; } } await context.SaveChangesAsync(); }
-
Add the
Edit
action that will handle POST requests on theAdminController
[HttpPost] public async Task<IActionResult> Edit(Product product) { if (ModelState.IsValid) { await repository.SaveProductAsync(product); TempData["message"] = $"{product.Name} has been saved"; return RedirectToAction("Index"); } else { // there is something wrong with the data values return View(product); } }
Notice the
TempData
object -
Update the
_AdminLayout.cshtml
layout file in order to display the confirmation message.@if (TempData["message"] != null) { <div class="alert alert-success">@TempData["message"]</div> }
-
Update the
Product
class as follows.public class Product { public int ProductID { get; set; } [Required(ErrorMessage = "Please enter a product name")] public string Name { get; set; } [Required(ErrorMessage = "Please enter a description")] public string Description { get; set; } [Required] [Range(0.01, double.MaxValue,ErrorMessage = "Please enter a positive price")] [Column(TypeName = "decimal(8, 2)")] public decimal Price { get; set; } public string Category { get; set; } }
-
Add a
Create
action to theAdminController
class.public IActionResult Create(){ return View("Edit", new Product()); }
-
Add a
DeleteProduct
method to theIStoreRepository
interface.Task<Product> DeleteProductAsync(int productID);
-
Implement the method in the
EFProductRepository
class.public async Task<Product> DeleteProductAsync(int productID) { Product dbEntry = context.Products .FirstOrDefault(p => p.ProductID == productID); if (dbEntry != null) { context.Products.Remove(dbEntry); await context.SaveChangesAsync(); } return dbEntry; }
-
Add the corresponding action to the
AdminController
[HttpPost] public async Task<IActionResult> Delete(int productId) { Product deletedProduct = await repository.DeleteProductAsync(productId); if (deletedProduct != null) { TempData["message"] = $"{deletedProduct.Name} was deleted"; } return RedirectToAction("Index"); }
-
Add a new unit test class in the
MVCStore.Tests
project calledAdminControllerTests
. Add the follwing test method.[Fact] public void Can_Delete_Valid_Products() { // Arrange - create a Product Product prod = new Product { ProductID = 2, Name = "Test" }; // Arrange - create the mock repository Mock<IStoreRepository> mock = new Mock<IStoreRepository>(); mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1"}, prod, new Product {ProductID = 3, Name = "P3"}, }.AsQueryable<Product>()); // Arrange - create the controller AdminController target = new AdminController(mock.Object); // Act - delete the product target.Delete(prod.ProductID); // Assert - ensure that the repository delete method was // called with the correct Product mock.Verify(m => m.DeleteProductAsync(prod.ProductID)); }