Introduction to ASP.NET Core part 10: the details page and more on view models

Introduction

In the previous post we looked more closely at routing in .NET Core MVC. There are two ways two declare the routing rules: imperatively via Startup.cs or declaratively in the controllers using attributes. The routing rules can be distributed with this solution. General routing rules like the Default route can be placed in Startup.cs, whereas controller and action method specific rules can be declared in the controllers directly.

In this post we’ll continue building on our demo application. We’ll add a details page for the books and we’ll also see our first Razor HTML helper method in action.

The Book entity

Recall that we put a BooksController into the Controllers folder in our DotNetCoreBookstore demo application earlier in this series. We have connected it to a View which lists a couple of Book view models:

Listing all book view models in ASP.NET Core MVC

Before we move on with some other MVC related stuff let’s simulate that we’re extracting the list of books from a data store instead of a private function in BooksController.cs like it is now.

It’s quite common that pure data store related actions are carried out by repositories. In our case there will be a Book repository. A repository is a normal C# class, there’s nothing special about it, but its role is very specific. The controller won’t normally communicate with a repository though. Instead there’s a layer in between which is called the service layer. The main responsibility of the service layer is to delegate the work necessary to retrieve the objects that the controller needs to other services and repositories. A service class is supposed to shield the complexities of working with the back end from the front end. We have not much complexity in our demo project yet. That’s in fact desirable so that we can go step by step without being sidetracked by a lot of new material. Also, services and repositories are normally placed in their respective C# library projects, but here we’ll keep everything within the web project. After all this course is about .NET Core and only marginally about software architecture.

Our first objective is to refactor our code in BookController.cs so that it extracts the list of books from a Book repository via a Book service.

We have no entity class for a Book. We started with a view model a while ago, so now we’ll need a domain object as well. Add a folder called Domains to the web project and a C# class called Book into it:

public class Book
{
	public int Id { get; set; }
	public string Title { get; set; }
	public string Author { get; set; }
	public int NumberOfPages { get; set; }
	public decimal Price { get; set; }
	public Genre Genre { get; set; }
}

…where Genre is an enum:

public enum Genre
{
	Unknown,
	DotNet,
	Java,
	JavaScript,
	Python,
	SoftwareArchitecture,
	Database
}

…which is also located in the Domains folder.

Next add a folder called Repositories to the web project root and insert the following interface into it:

using System.Collections.Generic;

namespace DotNetCoreBookstore.Repositories
{
    public interface IBookRepository
    {
	IEnumerable<Book> GetAll();
    }
}

We’ll have a dummy implementation to begin with since we have no database yet. Add the following class into the Repositories folder:

using System.Collections.Generic;
using DotNetCoreBookstore.Domains;

namespace DotNetCoreBookstore.Repositories
{
	public class DebugBookRepository : IBookRepository
	{
		private List<Book> _testBooks;

		public DebugBookRepository()
		{
			_testBooks = new List<Book>();
			_testBooks.Add(new Book() { Id = 1, Author = "John Smith", Title = "C# for beginners", Genre = Genre.DotNet, NumberOfPages = 400, Price = 100 });
			_testBooks.Add(new Book() { Id = 2, Author = "Jane Cook", Title = "Java for beginners", Genre = Genre.Java, NumberOfPages = 450, Price = 150 });
			_testBooks.Add(new Book() { Id = 3, Author = "Mary Stone", Title = "F# for beginners", Genre = Genre.DotNet, NumberOfPages = 300, Price = 90 });
			_testBooks.Add(new Book() { Id = 4, Author = "Andrew Cooper", Title = "Software architecture", Genre = Genre.SoftwareArchitecture, NumberOfPages = 380, Price = 185 });
			_testBooks.Add(new Book() { Id = 5, Author = "Susan Williams", Title = "SOLID principles", Genre = Genre.SoftwareArchitecture, NumberOfPages = 410, Price = 190 });
		}

		public IEnumerable<Book> GetAll()
		{
			return _testBooks;
		}
	}
}

That should be easy to follow. We build a list of books in the constructor and return them all in the GetAll function.

Let’s take care of the book service next. Add a folder called Services with the following interface:

using DotNetCoreBookstore.Domains;
using System.Collections.Generic;

namespace DotNetCoreBookstore.Services
{
	public interface IBookService
    {
		IEnumerable<Book> GetAll();
    }
}

…and the following implementation:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DotNetCoreBookstore.Domains;
using DotNetCoreBookstore.Repositories;

namespace DotNetCoreBookstore.Services
{
	public class DebugBookService : IBookService
	{
		private readonly IBookRepository _bookRepository;

		public DebugBookService(IBookRepository bookRepository)
		{
			if (bookRepository == null) throw new ArgumentNullException("Book repository");
			_bookRepository = bookRepository;
		}

		public IEnumerable<Book> GetAll()
		{
			try
			{
				return _bookRepository.GetAll();
			}
			catch
			{
				throw;
			}
		}
	}
}

That’s a very minimalist service implementation but again, we want to keep it simple. Our service class will delegate the database handling job to the repository.

We can now connect the BooksController with the Book service:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using DotNetCoreBookstore.Models;
using DotNetCoreBookstore.Services;

namespace DotNetCoreBookstore.Controllers
{
    public class BooksController : Controller
    {
		private readonly IBookService _bookService;

		public BooksController(IBookService bookService)
		{
			if (bookService == null) throw new ArgumentNullException("Book service!");
			_bookService = bookService;
		}

		public IActionResult Index()
                {			
			return View(GetBooks());
                }

		private IEnumerable<BookViewModel> GetBooks()
		{
			var books = _bookService.GetAll();
			return (from b in books select new BookViewModel() { Id = b.Id, Author = b.Author, Title = b.Title });			
		}
    }
}

Before we can test this we have to wire up the abstractions and the concrete types to inject into the dependent classes like we did earlier in this series. Add the following to ConfigureServices function of Startup.cs:

services.AddScoped<IBookRepository, DebugBookRepository>();
services.AddScoped<IBookService, DebugBookService>();

Run the application and navigate to /books. The same 5 books on offer should be listed in the screen as before, there should be no visible change for the end user.

The good thing about this setup is that we can replace the implementation of these interfaces later on and the controller won’t care at all, it will continue to call the abstractions and ignore the concrete classes.

Let’s also wrap the list of books in a view model. Add the following C# class into the Models folder:

using System.Collections.Generic;

namespace DotNetCoreBookstore.Models
{
	public class BookIndexViewModel
    {
		public IEnumerable<BookViewModel> BookViewModels { get; set; }
		public int TotalBooksOnOffer { get; set; }
	}
}

We’ll replace the current model injected into the Index.cshtml of the Books controller:

@model DotNetCoreBookstore.Models.BookIndexViewModel

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Our books</title>
</head>
<body>
    <h1>These are our books on offer. Total: @Model.TotalBooksOnOffer</h1>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Author</th>
                <th>Title</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var bookViewModel in Model.BookViewModels)
            {
                <tr>
                    <td>@bookViewModel.Id</td>
                    <td>@bookViewModel.Author</td>
                    <td>@bookViewModel.Title</td>
                </tr>
            }
        </tbody>
    </table>
</body>
</html>

Finally we need to update the Index action method of the Books controller so that we pass in the correct view model:

public IActionResult Index()
{
	BookIndexViewModel bookIndexViewModel = new BookIndexViewModel();
	var bookViewModels = GetBooks();
	bookIndexViewModel.BookViewModels = bookViewModels;
	bookIndexViewModel.TotalBooksOnOffer = bookViewModels.Count();
	return View(bookIndexViewModel);
}

Run the application and navigate to /books. You should see the updated header with the books count. The table should remain untouched.

The book details view

The Book domain has other interesting properties that the user may want to view. We’ll create another view model for that called the BookDetailsViewModel. Add the following object to the Models folder:

namespace DotNetCoreBookstore.Models
{
	public class BookDetailsViewModel : BookViewModel
    {
		public int NumberOfPages { get; set; }
		public decimal Price { get; set; }
		public string Genre { get; set; }
	}
}

It inherits from the book view model so that those properties will also be available for the details view.

The most common way of finding an entity in the data store is by its unique identifier. We’ll extend the book repository interface with a getter by ID:

Book GetBy(int id);

…and implement it with a simple LINQ query in DebugBookRepository:

public Book GetBy(int id)
{
	return (from b in _testBooks where b.Id == id select b).FirstOrDefault();
}

We’ll do the same in IBookService:

Book GetBy(int id);

…and implement it in DebugBookService by calling on the repository:

public Book GetBy(int id)
{
	try
	{
		return _bookRepository.GetBy(id);
	}
	catch
	{
		throw;
	}
}

We can add a Details action model to BooksController.cs. We know that it will need an ID so that it can find the correct book:

public IActionResult Details(int id)
{
	BookDetailsViewModel bookDetailsViewModel = new BookDetailsViewModel();
	var book = _bookService.GetBy(id);
	if (book != null)
	{
		bookDetailsViewModel.Author = book.Author;
		bookDetailsViewModel.Genre = book.Genre.ToString();
		bookDetailsViewModel.Id = book.Id;
		bookDetailsViewModel.NumberOfPages = book.NumberOfPages;
		bookDetailsViewModel.Price = book.Price;
		bookDetailsViewModel.Title = book.Title;
	}
	else
	{
		string na = "N/A";
		bookDetailsViewModel.Author = na;
		bookDetailsViewModel.Genre = na;
		bookDetailsViewModel.Id = -1;
		bookDetailsViewModel.NumberOfPages = 0;
		bookDetailsViewModel.Price = 0;
		bookDetailsViewModel.Title = na;
	}
	return View(bookDetailsViewModel);
}

We ask the book service to return a book entity for us. If it’s found then we populate the book details view model. Otherwise we build the empty view model with some defaults. The model is injected into a View that doesn’t exist yet. We’ll follow the naming conventions and add a Razor view file called Details.cshtml into the Views/Books folder:

@model DotNetCoreBookstore.Models.BookDetailsViewModel

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Details of @Model.Title</title>
</head>
<body>
    <h1>Details of @Model.Title</h1>

    <table>
        <thead>
            <tr>
                <th>Author</th>
                <th>Title</th>
                <th>Genre</th>
                <th>Pages</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>@Model.Author</td>
                <td>@Model.Title</td>
                <td>@Model.Genre</td>
                <td>@Model.NumberOfPages</td>
                <td>@Model.Price</td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Next we’ll add a link to each book view model on the books index page. The link will lead to details page of the book.

Razor views have access to a wide range of HTML helpers that dynamically create HTML tags. The ActionLink helper method is the one we need here. It builds a link with a href attribute using a couple of parameters like here:

@model DotNetCoreBookstore.Models.BookIndexViewModel

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Our books</title>
</head>
<body>
    <h1>These are our books on offer. Total: @Model.TotalBooksOnOffer</h1>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Author</th>
                <th>Title</th>
                <th>Details</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var bookViewModel in Model.BookViewModels)
            {
                <tr>
                    <td>@bookViewModel.Id</td>
                    <td>@bookViewModel.Author</td>
                    <td>@bookViewModel.Title</td>
                    <td>@Html.ActionLink("Details", "Details", new { id = bookViewModel.Id})</td>
                </tr>
            }
        </tbody>
    </table>
</body>
</html>

The HTML helpers can be accessed using @Html in Razor. The ActionLink function has a number of overloads to specify the details of the link. The parameters in the above example are the following:

  • “Details”: the link text
  • “Details”: the name of the action method to call
  • The anonymous object with an id property: this is the route value to be relayed to the action method which requires an id parameter

Other overloads of ActionLink let us define the controller name if it’s different from the one the current page is using. We can also provide HTML attributes for the link such as a class name in an anonymous object.

Run the application and navigate to /books. View the page source and you’ll see that ActionLink built the following type of anchor:

<a href="/books/Details/1">Details</a>

Click on it to come to the details page of the selected book. While on the details page you can also test an ID which doesn’t exist such as /books/details/20 to see how the default not-available-book is rendered.

Here’s an example of a more extended ActionLink:

<td>@Html.ActionLink("Details", "Details", "Books", new { id = bookViewModel.Id}, new { @class = "book-details-link"} )</td>

The “Books” parameter is the name of the controller. This is obviously not needed here since we stay on the same controller, I just wanted to demonstrate the usage. The last anonymous object sets the HTML attributes on the anchor tag. Since “class” is also a C# keyword we need to escape it with the at sign otherwise the code won’t compile. The above action link produces the following anchor tag:

<td><a class="book-details-link" href="/Books/Details/1">Details</a></td>

Whenever you’re not sure what kind of HTML Razor produces you can always look at the page source in the browser.

To finish off this post we’ll add a link to the details view to go back to the index page. The ActionLink function is the way to go in this case as well. Add the following code underneath the table in Details.cshtml:

@Html.ActionLink("Back to list", "Index")

We can now jump between the list of books and the details page of a selected book.

View the next part here.

View the list of MVC and Web API related posts here.

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

One Response to Introduction to ASP.NET Core part 10: the details page and more on view models

  1. savanrajput says:

    Hey , i read your blog it is very informative and helpful for many of us . Thankyou for sharing it .

Leave a comment

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.