Lists of Books - A Practical Example - ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

Part IV. A Practical Example

Chapter 16. Lists of Books

This chapter will update the home page to display a list of featured books. Because the Book model contained both a list price and sale price, a savings label will be added to attract the user into buying the book! Another list of books needs to be displayed filtered to the category selected from the left-hand menu created in Chapter 15. Both the featured books and the filtered-by-category books will leverage a shared view as shown later in this chapter.

The Home Page

Before the books are displayed, I’ve made some minor changes to the home page that was previously scaffolded by Visual Studio. Example 16-1 contains the updated Views/Home/Index.cshtml view with the tweaks.

Example 16-1. Views/Home/Index.cshtml

@{

ViewBag.Title = "Home Page";

}

<div class="jumbotron">

<h1>Jamie's Shopping Cart</h1>

<p class="lead">Shop for your favorite books, we've got the best prices.</p>

<p><a href="@Url.Action("About")" class="btn btn-primary btn-lg">

Learn more »</a></p>

</div>

@Html.Action("Featured", "Books")

The view leverages the stylish jumbotron feature of Bootstrap. This is a great way to create a noticable call-to-action that users cannot miss. My jumbotron is quite simple in that it tells the users I have the best prices, and they can click a link to learn more.

After the jumbotron, the featured books are included via the Html.Action method, just like the cart summary and categories were included in the shared layout. This was done to leverage both a HomeController and a BooksController and keep each related to their respective objects. The next section will implement the featured books.

No changes are required to the HomeController because it does not contain any logic. It simply loads the view.

The Featured Books

To create the featured books, create a BooksController inside the Controllers folder. Example 16-2 contains the Featured action of the BooksController.

Example 16-2. BooksController

using ShoppingCart.Models;

using ShoppingCart.Services;

using ShoppingCart.ViewModels;

using System;

using System.Collections.Generic;

using System.Web;

using System.Web.Mvc;

namespace ShoppingCart.Controllers

{

public class BooksController : Controller

{

private readonly BookService _bookService = new BookService();

public BooksController()

{

AutoMapper.Mapper.CreateMap<Book, BookViewModel>();

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

AutoMapper.Mapper.CreateMap<Category, CategoryViewModel>();

}

[ChildActionOnly]

public PartialViewResult Featured()

{

var books = _bookService.GetFeatured();

return PartialView(

AutoMapper.Mapper.Map<List<Book>, List<BookViewModel>>(books)

);

}

protected override void Dispose(bool disposing)

{

if (disposing)

{

_bookService.Dispose();

}

base.Dispose(disposing);

}

}

}

Hopefully, the BooksController is looking quite familiar. I have once again followed the previously defined pattern. A BookService is instantiated (shown in Example 16-3) and is disposed of at the end of the request.

Next is the Featured action. Just like the cart summary and categories menu, this action is attributed with ChildActionOnly. The Featured action uses the BookService to retrieve the list of featured books. These books are then automapped to the BookViewModel and passed through to the view (shown in Example 16-4).

The BookService class can now be created inside the existing Services folder. Example 16-3 contains the BookService with the GetFeatured method.

Example 16-3. BookService

using ShoppingCart.DAL;

using ShoppingCart.Models;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

namespace ShoppingCart.Services

{

public class BookService : IDisposable

{

private ShoppingCartContext _db = new ShoppingCartContext();

public List<Book> GetFeatured()

{

return _db.Books.

Include("Author").

Where(b => b.Featured).

ToList();

}

public void Dispose()

{

_db.Dispose();

}

}

}

Like the previous services, the BookService implements the IDisposable interface and creates a ShoppingCartContext that is disposed of when the request is finished.

The GetFeatured method returns a list of books with the Featured Boolean set to true. The related Author object is also included so that it can be rendered along with the book.

It’s now time to create the view. If a Books folder was not created in the Views folder, create it now. Inside this folder create a new Featured view. Be sure to check the partial view because this is a Child Action Only view. Example 16-4 contains the nearly empty view (because the logic is contained within another shared view that is shown in Example 16-5).

Example 16-4. Views/Books/Featured.cshtml

@model List<ShoppingCart.ViewModels.BookViewModel>

@Html.Partial("_List", Model)

This view defines the model binding a list of BookViewModels. After this, another HtmlHelper method is used — Partial — that defines the name of the partial view to load. The second parameter is the optional data that the partial view can be data bound to.

The final piece of the featured books is the HTML code to list the books. In the Views/Books folder, create another partial view named _List. The underscore in the name is a common convention to help identify partial views not associated directly with a controller action. Example 16-5shows the _List.cshtml view.

Example 16-5. Views/Books/_List.cshtml

@model List<ShoppingCart.ViewModels.BookViewModel>

@{

const int maxPerRow = 3;

int counter = 0;

}

<div class="row">

@foreach (var book in Model)

{

counter++;

if (counter > maxPerRow)

{

counter = 0;

@Html.Raw("</div>")

@Html.Raw("<div class=\"row\">")

}

<div class="col-md-4">

<a href="@Url.Action("Details", "Books", new { book.Id })" class="thumbnail">

<img src="@book.ImageUrl" alt="@book.Title" title="@book.Title" />

<span class="label label-success">Save @book.SavePercentage %</span>

</a>

<h4><a href="@Url.Action("Details", "Books", new { book.Id })">

@book.Title</a></h4>

<p>@book.Author.FullName</p>

<p>Your Price: $@book.SalePrice</p>

<p>List Price: <span style="text-decoration: line-through">

$@book.ListPrice</span></p>

</div>

}

</div>

Like the Featured view, this view is also data bound to a list of BookViewModels. After this, a couple of variables are defined that will help split the books into rows of three. Before looping through the list of books, a div with the class of row is defined. This div is closed after the end of the loop. During the loop of books, the counter is incremented. When the counter surpasses the maxPerRow, the div with the class of row is closed, and the new div is created.

Next up is creating the column with the book information. Another div is created with the class of col-md-4. This will make each book use up to one-third of the screen width. The book’s thumbnail is added inside an HTML link. The image inside of the link is specially stylized because of the thumbnail class on the link. Under the image, a span tag is defined with the label and label-success classes that contain the book’s savings percentage.

And finally, the book’s title, author, and the prices are displayed with a strikethrough on the list price as shown in Figure 16-1.

Figure 16-1. Featured books

Filtered Books by Category

In the last chapter when the category menu was created, each category was linked to the Index action on the BooksController. The link also provided the categoryId. Example 16-6 contains an updated BooksController with the new Index action.

Example 16-6. Updated BooksController

using ShoppingCart.Models;

using ShoppingCart.Services;

using ShoppingCart.ViewModels;

using System;

using System.Collections.Generic;

using System.Web;

using System.Web.Mvc;

namespace ShoppingCart.Controllers

{

public class BooksController : Controller

{

private readonly BookService _bookService = new BookService();

public BooksController()

{

AutoMapper.Mapper.CreateMap<Book, BookViewModel>();

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

AutoMapper.Mapper.CreateMap<Category, CategoryViewModel>();

}

// GET: Books

public ActionResult Index(int categoryId)

{

var books = _bookService.GetByCategoryId(categoryId);

ViewBag.SelectedCategoryId = categoryId;

return View(

AutoMapper.Mapper.Map<List<Book>, List<BookViewModel>>(books)

);

}

[ChildActionOnly]

public PartialViewResult Featured()

{

var books = _bookService.GetFeatured();

return PartialView(

AutoMapper.Mapper.Map<List<Book>, List<BookViewModel>>(books)

);

}

protected override void Dispose(bool disposing)

{

if (disposing)

{

_bookService.Dispose();

}

base.Dispose(disposing);

}

}

}

The Index method leverages the BookService to fetch the list of books by the categoryId parameter. This list is then automapped and passed through to the view.

Recall that in the shared layout, when the Html.Action to include the category menu was called, the selectedCategoryId was populated with a ViewBag variable if it existed. This is the controller that sets the variable so that the selected category will be highlighted when the view is rendered.

The BookService requires updating to include the new GetByCategoryId function. This is shown in Example 16-7.

Example 16-7. Updated BookService

using ShoppingCart.DAL;

using ShoppingCart.Models;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

namespace ShoppingCart.Services

{

public class BookService : IDisposable

{

private ShoppingCartContext _db = new ShoppingCartContext();

public List<Book> GetByCategoryId(int categoryId)

{

return _db.Books.

Include("Author").

Where(b => b.CategoryId == categoryId).

OrderByDescending(b => b.Featured).

ToList();

}

public List<Book> GetFeatured()

{

return _db.Books.

Include("Author").

Where(b => b.Featured).

ToList();

}

public void Dispose()

{

_db.Dispose();

}

}

}

The GetByCategoryId function is quite similar to the GetFeatured function; however, this time the books are filtered where their CategoryId is equal to the categoryId parameter. The Author object is also included, and to provide more upsell, the books with the Featured flag set to true are displayed first.

To finish displaying the books, an Index view needs to be created in the Views/Books folder. This time ensure that the partial view is not checked! The completed view is displayed in Example 16-8.

Example 16-8. Views/Books/Index.cshtml

@model List<ShoppingCart.ViewModels.BookViewModel>

@{

ViewBag.Title = "Books";

}

<h2>Books</h2>

@if (Model.Count > 0) {

@Html.Partial("_List", Model)

} else {

<div class="alert alert-info">

There are currently no books to display.

</div>

}

Like the previously created book views, the Index view is also data bound to a list of BookViewModels. A Razor if statement is then used to show the partial _List view (shown in Example 16-5) to show the books. If there are no books, an alert is displayed letting the user know there are no books in this category.

When you run the completed example, you will see the category highlighted on the left, letting you easily know which category you are currently browsing.

Summary

Like the category menu, this entire chapter did not require any dynamic user interaction, so Knockout was not used. Bootstrap was leveraged throughout in an attempt to make the books visually appealing to the user with minimal effort.

Get ready for the next chapter. More Knockout will be required to add the ability to add a book to your shopping cart!