In this article, you will learn how to add pagination, how to use tag helpers, what a partial view is and how to use it, and so on.
Introduction
As mentioned in Part 1, the Index.cshtml view displays the books in the database on a single page. In this article, I will add support for pagination so that the view displays a smaller number of books on a page, and the user can move from page to page to view the overall catalog. I will also style the content by using the Bootstrap.
Background
In this article, we will be familiar with some techniques that are used to built the core infrastructure for the BooksStore
application such as how to add pagination, how to use tag helpers, what a partial view is and how to use it, how to create the mapping between the HTML attribute name format and the C# property name format, how to style the content with the Bootstrap, so on.
Using the Code
Adding Pagination
We’re going to add a parameter to the Index
method in the Home
controller with following code:
public class HomeController : Controller
{
private IBooksStoreRepository repository;
public int PageSize = 3;
public HomeController(IBooksStoreRepository repo)
{
repository = repo;
}
public IActionResult Index(int bookPage = 1)
=> View(repository.Books
.OrderBy(b => b.BookID)
.Skip((bookPage - 1) * PageSize)
.Take(PageSize));
}
The preceding code:
- The
PageSize
field specifies that we want three books per page. - We get the
Book
objects, order them by the primary key (BookID
), skip over the books that occur before the start of the current page, and take the number of books specified by the PageSize
field.
We can navigate through the catalog of books by using query strings. Run application and we will see that there are now three books shown on the page:
If we want to view another page, we can append query string parameters to the end of the URL, like this:
http://localhost:44333/?bookPage=2
We need to render some page links at the bottom of each list of books so that customers can navigate between pages. To do this, we are going to create a tag helper, which generates the HTML markup for the links we require.
To support the tag helper, we are going to create a view model class, which is used specifically to pass data between a controller and a view by creating a Models/ViewModels folder in the BooksStore
project, add to it a class file named PagingInfo.cs, and define the class with the following code:
using System;
namespace BooksStore.Models.ViewModels
{
public class PagingInfo
{
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages =>
(int)Math.Ceiling((decimal)TotalItems / ItemsPerPage);
}
}
In the preceding code, we want to pass information to the view about the number of pages available, the current page, and the total number of books in the repository.
Now we are going to create a folder named MyTagHelper
in the BooksStore
project and add to it a class file called MyPageLink.cs with the following code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using BooksStore.Models.ViewModels;
namespace BooksStore.MyTagHelper
{
[HtmlTargetElement("div", Attributes = "page-model")]
public class MyPageLink : TagHelper
{
private IUrlHelperFactory urlHelperFactory;
public MyPageLink(IUrlHelperFactory helperFactory)
{
urlHelperFactory = helperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public PagingInfo PageModel { get; set; }
public string PageAction { get; set; }
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
for (int i = 1; i <= PageModel.TotalPages; i++)
{
TagBuilder tag = new TagBuilder("a");
tag.Attributes["href"] = urlHelper.Action(PageAction,
new { bookPage = i });
tag.InnerHtml.Append(i.ToString());
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
}
}
This tag helper populates a div
element with a
elements that correspond to pages of books. Most ASP.NET Core components, such as controllers and views, are discovered automatically, but tag helpers have to be registered. We are going to add a statement to the _ViewImports.cshtml file in the Views folder that tells ASP.NET Core to look for tag helper classes in the BooksStore
project.
@using BooksStore
@using BooksStore.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BooksStore
We also added an @using
expression so that we can refer to the view model classes in views without having to qualify their names with the namespace.
We have to provide an instance of the PagingInfo
view model class to the view. To do this, we are going to add a class file called BooksListViewModel.cs to the Models/ViewModels folder of the BooksStore
project with the following code:
using System.Collections.Generic;
namespace BooksStore.Models.ViewModels
{
public class BooksListViewModel
{
public IEnumerable<Book> Books { get; set; }
public PagingInfo PagingInfo { get; set; }
}
}
We can update the Index
action method in the HomeController
class to use the BooksListViewModel
class to provide the view with details of the books to display on the page and with details of the pagination with the following code:
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using BooksStore.Models;
using BooksStore.Models.ViewModels;
...
public IActionResult Index(int bookPage = 1)
=> View(new BooksListViewModel
{
Books = repository.Books
.OrderBy(p => p.BookID)
.Skip((bookPage - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo
{
CurrentPage = bookPage,
ItemsPerPage = PageSize,
TotalItems = repository.Books.Count()
}
});
We created the view model that contains the paging information, updated the controller so that it passes this information to the view. Now we are going to change the @model
directive to match the new model view type and add an HTML element that the tag helper will process to create the page links, as the following markup:
@model BooksStore.Models.ViewModels.BooksListViewModel
@foreach (var p in Model.Books)
{
<div>
<h3>@p.Title</h3>
@p.Description
@p.Genre
<h4>@p.Price.ToString("c")</h4>
</div>
}
<div page-model="@Model.PagingInfo" page-action="Index"></div>
Because page links still use the query string to pass page information to the server, like this http://localhost/?bookPage=2, we can add a new route in the Startup
class to improve the URLs with the following code:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("pagination",
"Books/{bookPage}",
new { Controller = "Home", action = "Index" });
endpoints.MapDefaultControllerRoute();
});
SeedData.EnsurePopulated(app);
}
}
This is the only alteration required to change the URL scheme for book pagination. Run the application and click one of the pagination links:
Styling the Content
We are going to use the Bootstrap to provide the CSS styles we will apply to the application. In Visual Studio 2019, we can find the Bootstrap in the wwwroot / lib folder:
Razor layouts provide common content so that it doesn’t have to be repeated in multiple views. We change the _Layout.cshtml file in the Views/Shared folder to include the Bootstrap CSS stylesheet in the content sent to the browser and define a common header that will be used throughout the BooksStore
application with the following markup:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>BooksStore</title>
<link href="~/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div>
<div class="bg-dark text-white p-2">
<span class="navbar-brand ml-2">BOOKS STORE</span>
</div>
<div class="row m-1 p-1">
<div id="categories" class="col-3">
The BooksStore homepage helps you explore Earth's Biggest Bookstore
without ever leaving the comfort of your couch.
</div>
<div class="col-9">
@RenderBody()
</div>
</div>
</div>
</body>
</html>
Next, we are going to add a Razor View called BookTemplate.cshtml to the Views/Shared
folder and added the markup shown below:
@model Book
<div class="card" style="width: 100%;">
<div class="card-body">
<h5 class="card-title">
@Model.Title
<span class="badge badge-pill badge-primary">
<small>@Model.Price.ToString("c")</small>
</span>
</h5>
<h6 class="card-subtitle mb-2 text-muted">@Model.Genre</h6>
<p class="card-text">@Model.Description</p>
</div>
</div>
We created a partial view, which is a fragment of content that you can embed into another view, rather like a template. Now we need to update the Index.cshtml file in the Views/Home folder so that it uses the partial view:
@model BooksStore.Models.ViewModels.BooksListViewModel
@foreach (var p in Model.Books)
{
<partial name="BookTemplate" model="p" />
}
<div page-model="@Model.PagingInfo" page-action="Index" page-classes-enabled="true"
page-class="btn" page-class-normal="btn-outline-dark"
page-class-selected="btn-primary" class="btn-group pull-right m-1">
</div>
We need to define custom attributes on the div
element that specify the classes that we require, and these correspond to properties we added to the tag helper class, which are then used to style the a
elements that are produced. To do this, we are going to make some changes to the MyPageLink
class with the following code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using BooksStore.Models.ViewModels;
namespace BooksStore.MyTagHelper
{
[HtmlTargetElement("div", Attributes = "page-model")]
public class MyPageLink : TagHelper
{
private IUrlHelperFactory urlHelperFactory;
public MyPageLink(IUrlHelperFactory helperFactory)
{
urlHelperFactory = helperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public PagingInfo PageModel { get; set; }
public string PageAction { get; set; }
public bool PageClassesEnabled { get; set; } = false;
public string PageClass { get; set; }
public string PageClassNormal { get; set; }
public string PageClassSelected { get; set; }
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
for (int i = 1; i <= PageModel.TotalPages; i++)
{
TagBuilder tag = new TagBuilder("a");
tag.Attributes["href"] = urlHelper.Action(PageAction,new { bookPage = i });
if (PageClassesEnabled)
{
tag.AddCssClass(PageClass);
tag.AddCssClass(i == PageModel.CurrentPage
? PageClassSelected : PageClassNormal);
}
tag.InnerHtml.Append(i.ToString());
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
}
}
The values of the attributes are automatically used to set the tag helper property values, with the mapping between the HTML attribute name format (page-class-normal
) and the C# property name format (PageClassNormal
) taken into account. This allows tag helpers to respond differently based on the attributes of an HTML element, creating a more flexible way to generate content in an ASP.NET Core application.
Run the application:
Points of Interest
We added support for pagination so that the view displays a smaller number of books on a page, and the user can move from page to page to view the overall catalog. We also used the Bootstrap to style the appearance of the application. In the next article, we will provide the navigation by genre feature to the application.
History
- 12th March, 2022: Initial version