Lesson Weekend

CRUD: Deleting Items


We've now added all necessary CRUD functionality to our ItemsController with the exception of delete functionality. Let's go ahead and uncomment the Delete() and the DeleteConfirmed() routes in our items controller. We'll also uncomment the items delete view (Views/Items/Delete). They should look like this:

Controllers/ItemsController.cs
...
public ActionResult Delete(int id)
{
    var thisItem = _db.Items.FirstOrDefault(item => item.ItemId == id);
    return View(thisItem);
}

[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
    var thisItem = _db.Items.FirstOrDefault(item => item.ItemId == id);
    _db.Items.Remove(thisItem);
    _db.SaveChanges();
    return RedirectToAction("Index");
}
...
Views/Items/Delete.cshtml
@{
  Layout = "_Layout";
}

@model ToDoList.Models.Item

<h2>Are you sure you want to delete this Item?</h2>

@Html.DisplayNameFor(model => model.Description): @Html.DisplayFor(model => model.Description)
@using (Html.BeginForm())
{
    <input type="submit" value="Delete" />
}
@Html.ActionLink("Back to List", "Index")

Utilizing a concept called cascade delete, Entity Framework Core will automatically remove entries in the join table when we delete an entity. Say we have an Item with description "Do the dishes" and we add it to a Category named "Kitchen", thus creating an entry in the join table. If we were to delete either the item or the category, the corresponding entry in the CategoryItem join table will also be deleted since the ItemId or CategoryId of the deleted item or category no longer exists.

Removing a Join Entity


At some point, we might want to remove a Category from an Item without deleting the Item itself. For instance, let's say an Item has a Morning and Evening category because that Item must be done every morning and evening. However, due to an update in our workflow, we no longer need to do the task in the Evening. Our application should include this functionality as well.

We'll start by adding a form to our Items/Details.cshtml view to handle the POST request necessary to delete the entry:

Views/Items/Details.cshtml
...
  <ul>
  @foreach(var join in Model.JoinEntities)
  {
    <li>@join.Category.Name</li>
    @using (Html.BeginForm("DeleteCategory", "Items"))
    {
      @Html.Hidden("joinId", @join.CategoryItemId)
      <input type="submit" value="Delete"/>
    }
  }
  </ul>
...
  • We add the form to the foreach loop block so that we can create a separate delete button for each category. We pass in two arguments to our BeginForm() method. The first argument is the route method that we'd like to invoke and the second argument is the controller (Note that .NET appends "Controller" to whatever string you pass in).

  • We also pass through a Hidden() method with two arguments. The first argument is the name of the route parameter variable we'd like to pass and the second is the actual value of that parameter. In this case, since we want to delete the CategoryItem entry, we'll pass through the CategoryItemId that we have access to as a variable called joinId.

Next, we'll create the corresponding POST route in our items controller:

...
[HttpPost]
public ActionResult DeleteCategory(int joinId)
{
    var joinEntry = _db.CategoryItem.FirstOrDefault(entry => entry.CategoryItemId == joinId);
    _db.CategoryItem.Remove(joinEntry);
    _db.SaveChanges();
    return RedirectToAction("Index");
}
...

This route will find the entry in the join table by using the join entry's CategoryItemId. The CategoryItemId is being passed in through the variable joinId in our route's parameter and came from the BeginForm() HTML helper method in our details view.

Since we've hooked up the new form and route action method, we should now be able to successfully remove categories from an item without deleting the category or item.

Startup and Route Configuration

We use the name joinId in the DeleteCategory() route instead of id because .NET automatically utilizes the value in the URL query if we name the variable id. For example, if we named the parameter id instead of joinId and the details URL was something like /Items/Details/6, then the value of id would be 6, which is the ItemId and not the CategoryItemId that we wanted from our Hidden() method.

The reason .NET uses this convention is due to our configuration in the Startup.cs file. Take a look at the following code in the Startup file:

ToDoList/Startup.cs
...
      app.UseEndpoints(routes =>
      {
        routes.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
      });
...

The template option tells .NET how to treat routes. This configuration, known as conventional routing, matches a path like /Items/Details/6 to its specific controller action by looking for the Details action route in the Items controller. Then it binds the value of id to 6. We won't change routes in this class, but be aware that .NET routing conventions can be configured. If you're interested in learning more, check out the documentation on the subject.

Repository Reference

Follow the link below to view how a sample version of the project should look at this point. Note that this is a link to a specific commit in the repository.

Example GitHub Repo for To Do List

Lesson 8 of 14
Last updated more than 3 months ago.