Lesson Tuesday

Items in our to do list are now associated with Categorys. Our application will prompt users to first create categories and then create Items that belong to them. As such, there will no longer be any Items not associated with a parent Category. This has several ramifications for how we'll organize our front end.

  • Users will no longer be able to see Items without first clicking on a Category. Items will only be listed on the Category detail page, which is the Show() route on CategoriesController.

  • In order to create a new Item, users first have to select the Category to which an Item will belong.

RESTful Routing with Objects Within Objects


This new setup also affects our RESTful routing. Just to review:

RESTful routing...maps HTTP methods (GET, POST, PATCH, DELETE) and CRUD actions (Create, Read, Update, Destroy) together in a conventional pattern...routes completing common actions (like creating, updating, or deleting objects) have names and paths that accurately reflect what they're doing, with which CRUD and HTTP verbs, upon what type of object.

Because RESTful routing demands we communicate this information in the names of our routes and their paths, our own routes and paths must now communicate that Items are nested within Categorys.

General RESTful Routing with Objects Within Objects

RESTful routing conventions for applications that use objects within objects look like this:

However, not all applications use all routes depicted here. Following RESTful routing doesn't require we use all routes. It just requires that the routes we do need in our applications follow these conventions.

To Do List RESTful Routing with Objects Within Objects

For our to do list, we'll use these routes. Notice how they differ from our current setup:

URL paths for all Item routes now include a fragment detailing their parent Category before the fragment containing Item information.

For example, the path to display an Item detail page used to be /items/{id}. It is now /categories/{categoryId}/items/{itemId}. This is to denote that Items now belong to a specific Category. It also denotes which Category they belong to by including the Category's ID right in the URL path.

RESTful Routing & Objects Within Objects Refactor


Let's tweak our application to follow objects within objects RESTful standards.

Item Index Route

As discussed, users will no longer see Items without first clicking on a parent Category. Items will only be listed on the Category detail page (its Show() route). That means we no longer need an Index() route on ItemsController. Remove this route from ItemsController.cs and delete its corresponding view at Views/Items/Index.cshtml now.

Item Show Route (Detail Page)

Instead, the user will click a Category from the Category Index() route to view its details on the Category Show() route. This page already lists all Items that belong to that Category:

ToDoList/Views/Categories/Show.cshtml
...

@foreach (Item item in @Model["items"])
{
  <li><a href='/categories/@Model["category"].Id/items/@item.Id'>@item.Description</a></li>
}

...

When the user clicks an Item from this list, they'll navigate to the Item's detail (show) page. Notice the path in this link is /categories/@Model["category"].Id/items/@item.Id, which follows RESTful standards depicted in the tables above.

Let's update the existing Show() route on the ItemsController to handle this updated path:

ToDoList/Controllers/ItemsController.cs
...

  [HttpGet("/categories/{categoryId}/items/{itemId}")]
  public ActionResult Show(int categoryId, int itemId)
  {
    Item item = Item.Find(itemId);
    Category category = Category.Find(categoryId);
    Dictionary<string, object> model = new Dictionary<string, object>();
    model.Add("item", item);
    model.Add("category", category);
    return View(model);
  }

...
  • The path now includes Category information, which ensures our routes are now RESTfully named.

  • Because the path includes both Item and Category IDs, we can locate the correct parent and child objects and pass them to our view in a Dictionary.

Let's also update this route's view to account for these changes:

ToDoList/Views/Items/Show.cshtml
...

<h1>To Do List Item Details: </h1>

<h2>@Model["item"].Description</h2>
<h3>Category: @Model["category"].Name</h3>

<a href='/categories/@Model["category"].Id/items/new'>Add another item</a>
<a href='/categories'>View all categories</a>

...

Because our model data resides in a Dictionary named model, we use square bracket notation to access the two objects. We also add a line detailing which Category this Item belongs to.

If we attempt to view this new page in the browser, we'll get an error. That's because we need to add Items to a Category before we can click one and view its details. However, the Add a new item link on our category detail page is not working. This is because the link looks like this:

ToDoList/Views/Categories/Show.cshtml
...

<p><a href='/categories/@Model["category"].Id/items/new'>Add a new item</a></p>

...

It links to the path /categories/{categoryId}/items/new. If we check our table above, we know this follows RESTful conventions. We just haven't updated our ItemsController's New() route to use this path. Let's do this next.

New Item Route

We'll update the New() route on our ItemsController to look like this:

ToDoList/Controllers/ItemsController.cs
...

  [HttpGet("/categories/{categoryId}/items/new")]
  public ActionResult New(int categoryId)
  {
     Category category = Category.Find(categoryId);
     return View(category);
  }

...

The path now includes the ID of the Category we're adding a new Item to. Because it's in curly braces, we can grab this in our route's parameter to locate the Category object and pass it into the corresponding view.

Let's update this route's corresponding view so it displays the Category we're adding an Item to:

ToDoList/Views/Items/New.cshtml
...

<h1>Add a new item to @Model.Name</h1>

<form action="/categories/@Model.Id/items" method="post">
  <input id="categoryId" name="categoryId" type="hidden" value="@Model.Id">
  <label for="itemDescription">Item Description</label>
  <input id="itemDescription" name="itemDescription" type="text">
  <button type='submit'>Add Item</button>
</form>

...
  • We list the Category we're adding an Item to above the form.

  • We've updated the form's action attribute to /categories/@Model.Id/items so it follows RESTful standards. This means we'll have to update the Create() route that processes this form's submissions, which we'll do soon.

  • We've added a new input to our form, too: <input id='categoryId' name='categoryId' type='hidden' value='@Model.Id'>. This will pass the Category ID alongside the user's form input to the Create() route that processes this form's submission. However, since its input type is hidden, it won't display anything to the user.

Why do we need to include the Category's ID like this? This is because we now save all new Items into a corresponding Category. When we create a new Item with this form, we'll need to know which Category it belongs to. Let's update the Create() route that processes this form's submissions next.

Item Create() Route

Because new Items all belong to Categorys, the act of creating a new Item now alters our Category objects. As such, it's more accurate to say it's related to our Category model now.

To follow best practices, we'll move the ItemsController's Create() route to the CategoriesController. This is standard practice in applications that use objects within objects like ours. We'll also update this route to accommodate our new objects within objects relationship:

ToDoList/Controllers/CategoriesController.cs
...
    [HttpPost("/categories/{categoryId}/items")]
    public ActionResult Create(int categoryId, string itemDescription)
    {
      Dictionary<string, object> model = new Dictionary<string, object>();
      Category foundCategory = Category.Find(categoryId);
      Item newItem = new Item(itemDescription);
      foundCategory.AddItem(newItem);
      List<Item> categoryItems = foundCategory.Items;
      model.Add("items", categoryItems);
      model.Add("category", foundCategory);
      return View("Show", model);
    }
...
  • We update this method's path to follow RESTful convention.

  • The method now takes two arguments: the categoryId we passed into a hidden form field and an itemDescription that contains the user's form input.

  • We create a new empty Dictionary named model.

  • Using the categoryId provided as an argument, we locate the Category object our new Item should belong to and call it foundCategory.

  • We then create a new Item object with the user's form input.

  • We add the newItem to the foundCategory with our existing AddItem() method.

  • We retrieve all other Items that correspond to this category and add it to our model. We do this because the view we'll render at the end of this route requires this information.

  • We also add the foundCategory to our model.

  • Finally, we pass in our model data to View(), instructing it to render the Category detail page, which is the Show.cshtml view.

Even though CategoriesController already has a Create() route, they won't get mixed up because they have distinctly different paths. If this is confusing, it's fine to add a comment until it becomes second nature:

ToDoList/Controllers/CategoriesController.cs
...

// This one creates new Items within a given Category, not new Categories:

[HttpPost("/categories/{categoryId}/items")]
public ActionResult Create(int categoryId, string itemDescription)
{
  Dictionary<string, object> model = new Dictionary<string, object>();
  Category foundCategory = Category.Find(categoryId);
  Item newItem = new Item(itemDescription);
  foundCategory.AddItem(newItem);
  List<Item> categoryItems = foundCategory.Items;
  model.Add("items", categoryItems);
  model.Add("category", foundCategory);
  return View("Show", model);
}

...

Homepage

Finally, instead of linking to Item options from the homepage, let's link to Category options like this:

ToDoList/Views/Home/Index.cshtml
...

<h1>Welcome to the To Do List!</h1>
<h3><a href='/categories'>View categories</a></h3>
<h3><a href='/categories/new'>Add a new category</a></h3>

...

After following all steps in this lesson, our CategoriesController will now look like this:

ToDoList/Controllers/CategoriesController.cs
using System.Collections.Generic;
using System;
using Microsoft.AspNetCore.Mvc;
using ToDoList.Models;

namespace ToDoList.Controllers
{
  public class CategoriesController : Controller
  {

    [HttpGet("/categories")]
    public ActionResult Index()
    {
      List<Category> allCategories = Category.GetAll();
      return View(allCategories);
    }

    [HttpGet("/categories/new")]
    public ActionResult New()
    {
      return View();
    }

    [HttpPost("/categories")]
    public ActionResult Create(string categoryName)
    {
      Category newCategory = new Category(categoryName);
      return RedirectToAction("Index");
    }

    [HttpGet("/categories/{id}")]
    public ActionResult Show(int id)
    {
      Dictionary<string, object> model = new Dictionary<string, object>();
      Category selectedCategory = Category.Find(id);
      List<Item> categoryItems = selectedCategory.Items;
      model.Add("category", selectedCategory);
      model.Add("items", categoryItems);
      return View(model);
    }

    // This one creates new Items within a given Category, not new Categories:
    [HttpPost("/categories/{categoryId}/items")]
    public ActionResult Create(int categoryId, string itemDescription)
    {
      Dictionary<string, object> model = new Dictionary<string, object>();
      Category foundCategory = Category.Find(categoryId);
      Item newItem = new Item(itemDescription);
      foundCategory.AddItem(newItem);
      List<Item> categoryItems = foundCategory.Items;
      model.Add("items", categoryItems);
      model.Add("category", foundCategory);
      return View("Show", model);
    }

  }
}

Our ItemsController looks like this:

ToDoList/Controllers/ItemsController.cs
using Microsoft.AspNetCore.Mvc;
using ToDoList.Models;
using System.Collections.Generic;

namespace ToDoList.Controllers
{
  public class ItemsController : Controller
  {

    [HttpGet("/categories/{categoryId}/items/new")]
    public ActionResult New(int categoryId)
    {
       Category category = Category.Find(categoryId);
       return View(category);
    }

    [HttpGet("/categories/{categoryId}/items/{itemId}")]
    public ActionResult Show(int categoryId, int itemId)
    {
      Item item = Item.Find(itemId);
      Category category = Category.Find(categoryId);
      Dictionary<string, object> model = new Dictionary<string, object>();
      model.Add("item", item);
      model.Add("category", category);
      return View(model);
    }

    [HttpPost("/items/delete")]
    public ActionResult DeleteAll()
    {
      Item.ClearAll();
      return View();
    }

  }
}

Again, notice how this follows the RESTful conventions depicted in the tables above.

We should now be able to build and run our application and navigate through it in the browser. Our front end can successfully manage our new objects within objects setup.

Visual Reference


If you're having a hard time conceptualizing how to plan your routes, controllers, and views, consider creating a diagram like the one below. Route names here don't follow RESTful convention exactly, but this is just a demonstration:

Note: Open the image in a separate tab to see it at full size.

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

RESTful routing conventions for applications that use objects within objects look like the image below.

Following RESTful routing doesn't require we use all routes. It just requires that the routes we do need in our applications follow these conventions.

Lesson 7 of 11
Last updated more than 3 months ago.