In the last lesson, we set up a many-to-many structure in our classes, created a join class, and configured and migrated our code into our database. Now we're ready to create controllers that will handle our new application structure. In this lesson, we'll focus on adding read functionality to the ItemsController
, including the following:
TagsController
with READ functionality for all tags (the Index()
action) and an individual tag (the Details()
action).TagsController
and Index()
Action and ViewWithin the ToDoList
production directory, create a new file called TagsController.cs
within the Controllers
directory and add the following code:
using Microsoft.AspNetCore.Mvc;
using ToDoList.Models;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace ToDoList.Controllers
{
public class TagsController : Controller
{
private readonly ToDoListContext _db;
public TagsController(ToDoListContext db)
{
_db = db;
}
public ActionResult Index()
{
return View(_db.Tags.ToList());
}
}
}
The Index()
route should look familiar to those we've created in the CategoriesController.cs
and ItemsController.cs
with one shortcut: instead of saving the list of tags to a variable and passing that into the View()
method, we pass in the method call _db.Tags.ToList()
directly as the argument to View()
.
Next, let's create the corresponding Index.cshtml
. Within the Views
directory, create another subdirectory called Tags
and a file within called Index.cshtml
with the following code:
@{
Layout = "_Layout";
}
@using ToDoList.Models;
<h1>Tags</h1>
@if (@Model.Count == 0)
{
<h3>No tags have been added yet!</h3>
}
@foreach (Tag tag in Model)
{
<li>@Html.ActionLink($"{tag.Title}", "Details", new { id = tag.TagId })</li>
}
<p>@Html.ActionLink("Add new tag", "Create")</p>
<p>@Html.ActionLink("Home", "Index", "Home")</p>
Next, let's add a link to our tags from the homepage of our To Do List app. Open up ToDoList/Views/Home/Index.cshtml
and add the following action link:
...
<p>@Html.ActionLink("See all tags", "Index", "Tags")</p>
Details()
Action and ViewNext, let's add the ability to see the details of a single tag. In the view, we'll list the title of the tag as well as every item that is currently associated with that tag. That means we're going to have to deliver the tag object to the Details.cshtml
view, as well as the join entities the tag is associated with.
First, let's add the Details()
action to the TagsController.cs
:
...
public ActionResult Details(int id)
{
Tag thisTag = _db.Tags
.Include(tag => tag.JoinEntities)
.ThenInclude(join => join.Item)
.FirstOrDefault(tag => tag.TagId == id);
return View(thisTag);
}
...
Notice the new method we are using: ThenInclude()
. Let's go over what's happening here.
Our _db.Tags
expression gives us a list of Tag
objects from the database. However, if we completed the query now (using the FirstOrDefault()
method), we'd simply have an Tag
without its related Category
s.
We need to .Include(tag => tag.JoinEntities)
to load the JoinEntities
property of each Tag
. However, the JoinEntities
property on an Tag
is just a collection of join entities (List<ItemTag>
), which are tracked by ids: ItemTagId
, TagId
, and ItemId
. These are not the actual item objects related to a Tag
.
We need the actual Item
objects themselves, so we use ThenInclude()
method to load the Item
object associated with each ItemTag
. Remember that an ItemTag
is simply a reference to a relationship. Each ItemTag
includes the id of a Tag
as well as the id of an Item
. With .ThenInclude(join => join.Item)
, we actually fetch the associated Item
object for each ItemTag
join entity.
Next, let's create the Tags/Details.cshtml
view. Again, we'll make sure to display the Tag
details as well as all of the Item
objects associated with each Tag.
@{
Layout = "_Layout";
}
@using ToDoList.Models
@model ToDoList.Models.Tag
<h2>Tag Details</h2>
<hr />
<h3>@Html.DisplayNameFor(model => model.Title): @Html.DisplayFor(model => model.Title)</h3>
@if(@Model.JoinEntities.Count == 0)
{
<p>This tag does not belong to any items.</p>
}
else
{
<h4>Items the tag belongs to:</h4>
<ul>
@foreach(ItemTag join in Model.JoinEntities)
{
<li>@join.Item.Description</li>
}
</ul>
}
<p>@Html.ActionLink("Back to list", "Index")</p>
Most of this code should look familiar, so we'll point out a few important pieces:
@using
directive for ToDoList.Models
so that we can reference the ItemTag
class in our foreach
loop.@model
directive for ToDoList.Models.Tag
so that we can use the strongly typed HTML helper methods Html.DisplayNameFor()
and @Html.DisplayFor
. @Model.JoinEntities.Count
in our conditional: with this line of code, we are checking if the List<ItemTag>
that we save to the Tag.JoinEntities
property is empty, and if so, we deliver a message to the user stating that there are no items associated with the tag we're looking at.List<ItemTag>
is not empty, then we loop through the Tag.JoinEntities
property and display each item's description: <li>@join.Item.Description</li>
. A few notes:
join
represents a single ItemTag
join entity.ItemTag.Item
property, which contains all of the Item
object's data.Next, we'll update the Details.cshtml
views for both Categories
and Items
to display tags.
We'll start with updating Views/Items/Details.cshtml
. Right now this view shows the description of the item along with the category it belongs to. After the update we make, the view will also include a list of tags that are associated with the item.
Here's the updated code:
@{
Layout = "_Layout";
}
@using ToDoList.Models
@model ToDoList.Models.Item
<h2>Item Details</h2>
<hr />
<h3>@Html.DisplayNameFor(model => model.Description): @Html.DisplayFor(model => model.Description)</h3>
<h3>@Html.DisplayNameFor(model => model.Category): @Html.DisplayFor(model => model.Category.Name)</h3>
@if(@Model.JoinEntities.Count == 0)
{
<p>This item does not have any tags yet!</p>
}
else
{
<h4>This item has the following tags:</h4>
<ul>
@foreach(ItemTag join in Model.JoinEntities)
{
<li>Tag: @join.Tag.Title</li>
}
</ul>
}
<p>@Html.ActionLink("Back to list", "Index")</p>
<p>@Html.ActionLink("Edit Item or Category", "Edit", new { id = Model.ItemId })</p>
<p>@Html.ActionLink("Delete Item", "Delete", new { id = Model.ItemId })</p>
The above addition looks very similar to how we display a list of items that belong to a tag. The main difference is in the naming. In summary, if the Item.JoinEntities.Count
is equal to zero, then there are no tags that are associated with the item, so we deliver a message to the user about this. Otherwise, we loop through the Item.JoinEntities
property, and for each join entity we access the Tag
property to display the tag's title.
With our view ready to display join entities, we now need to update the Details()
action in the ItemsController.cs
to fetch the join entities and tags from the database when we get the data for the item. Here's the update we'll make:
...
public ActionResult Details(int id)
{
Item thisItem = _db.Items
.Include(item => item.Category)
.Include(item => item.JoinEntities)
.ThenInclude(join => join.Tag)
.FirstOrDefault(item => item.ItemId == id);
return View(thisItem);
}
...
What we've done is add a new Include()
method to fetch the join entities, and a ThenInclude()
method to fetch the actual tag object for each join entity.
Notice how we list an Include()
method for each navigation property in the Item
class: Item.Category
and Item.JoinEntities
. We can do this for as many navigation properties as we have and need to fetch.
As always, we end our database query with FirstOrDefault()
if we want to fetch one object, or ToList()
if we want to fetch a list of objects. There are many other methods we can use like OrderBy()
or ToDictionary()
. If you have not already done so, check out the MS Docs on the System.Linq.Enumerable
class methods to learn about other methods we can use to query our database.
Next, we'll update our category details view to display not just the items that belong to each category, but also the tags that belong to each item. This is what the finished product will look like:
To make the above possible, we'll need a loop within a loop and some additional code to format our tags to display inline. Here's the updated code:
@{
Layout = "_Layout";
}
@model ToDoList.Models.Category;
@using ToDoList.Models;
@using System.Collections.Generic;
<h2>Category Details</h2>
<hr />
<h3>@Html.DisplayNameFor(model => model.Name): @Html.DisplayFor(model => model.Name)</h3>
@if(@Model.Items.Count == 0)
{
<p>This category does not contain any items</p>
}
else
{
<h4>Items the category contains:</h4>
<ul>
@foreach(Item item in Model.Items)
{
string tags = "";
@if(item.JoinEntities.Count == 0)
{
tags = "This item does not have any tags.";
}
else
{
List<string> tagList = new List<string>();
@foreach(ItemTag join in item.JoinEntities)
{
tagList.Add(join.Tag.Title);
}
tags = String.Join(", ", tagList);
}
<li>@item.Description | Tags: @tags</li>
}
</ul>
}
<p>@Html.ActionLink("Back to categories", "Index")</p>
<p>@Html.ActionLink("Edit Category", "Edit", new { id = Model.CategoryId })</p>
<p>@Html.ActionLink("Delete Category", "Delete", new { id = Model.CategoryId })</p>
<p>@Html.ActionLink("Add new item", "Create", "Items")</p>
We'll focus on understanding the new code:
First notice that we've added a new @using
directive for the System.Collections.Generic;
namespace that allows use to use the List<T>
type.
Next, notice how we've refactored the first (outer) foreach
loop:
Item.JoinEntities
property, and if not, then we display a message "This item does not have any tags."
.tags
, and the String.Join()
method:
tags
to hold the display value for tags. It will be either a message saying there are no tags, or all of the tags associated with the item, separated by a comma.tags
variable is set to the item's tags separated by a comma, we create this string by doing the following:
List<string>
called tagList
. tagList
.String.Join()
to join each list item in tagList
into a string, separating each item with a comma and space.Phew! That is a lot of new logic. Note that you can format your code however you like and you don't need to create complicated formatting in your own projects. You also don't need to display each item's tags on a category's detail page. However, you should consider what is best for a user's experience as far as navigating a site and accessing information. As always, have fun and try exploring something new.
With our category Details
view ready to display each item's join entities, we now need to revisit our Details()
action in the CategoriesController.cs
to fetch not only a list of items, but each's item's tags.
Here's the update we'll make:
...
public ActionResult Details(int id)
{
Category thisCategory = _db.Categories
.Include(cat => cat.Items)
.ThenInclude(item => item.JoinEntities)
.ThenInclude(join => join.Tag)
.FirstOrDefault(category => category.CategoryId == id);
return View(thisCategory);
}
...
Category
has only one navigation property, Category.Items
; this is why there is only one Include()
method call. If we want to access each item's tag(s), we need to use a series of ThenInclude()
method calls to get the Item.JoinEntities
data for each item, and then the JoinEntity.Tag
tag data for each join entity.
We should now be able to run our application and navigate from the homepage to the tags index view. However, in order to view our new tag's details page (and the updates to our category and item details page), we'll have to first add some tags. Let's do that next.