Click here to Skip to main content
15,348,965 members
Articles / Web Development / ASP.NET / ASP.NET Core
Article
Posted 12 Mar 2022

Stats

4K views
15 bookmarked

An Introduction to ASP.NET Core MVC through an Example (Part 2)

Rate me:
Please Sign up or sign in to vote.
3.38/5 (5 votes)
12 Mar 2022CPOL5 min read
An introduction to ASP.NET Core MVC
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:

C#
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:

Image 1

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

Image 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:

C#
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:

C#
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.

Razor
@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:

C#
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:

C#
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:

Razor
@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:

C#
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this 
                // for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            // old URL: http://localhost:44333/?bookPage=2
            // new URL: https://localhost:44333/Books/2
            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:

Image 3

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:

Image 4

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:

Razor
<!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:

Razor
@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:

Razor
@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:

C#
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:

Image 5

 

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Coding Notes
Software Developer
Vietnam Vietnam
I am a teacher, a software developer. I love reading, travelling and I am also blogging on ngocminhtran.com(in VietNamese).

Comments and Discussions

 
QuestionNo Pagination Buttons Pin
GµårÐïåñ29-Mar-22 17:07
professionalGµårÐïåñ29-Mar-22 17:07 
AnswerRe: No Pagination Buttons Pin
Coding Notes30-Mar-22 22:19
professionalCoding Notes30-Mar-22 22:19 
GeneralRe: No Pagination Buttons Pin
GµårÐïåñ31-Mar-22 6:50
professionalGµårÐïåñ31-Mar-22 6:50 
GeneralRe: No Pagination Buttons Pin
Coding Notes31-Mar-22 21:47
professionalCoding Notes31-Mar-22 21:47 
GeneralRe: No Pagination Buttons Pin
GµårÐïåñ1-Apr-22 7:29
professionalGµårÐïåñ1-Apr-22 7:29 
GeneralRe: No Pagination Buttons Pin
Coding Notes1-Apr-22 14:47
professionalCoding Notes1-Apr-22 14:47 
GeneralRe: No Pagination Buttons Pin
GµårÐïåñ1-Apr-22 20:47
professionalGµårÐïåñ1-Apr-22 20:47 
GeneralRe: No Pagination Buttons Pin
Coding Notes3-Apr-22 16:18
professionalCoding Notes3-Apr-22 16:18 
One of my students reflected that, when copying content, the following page-classes-enabled bootstrap attribute was very often wrong:
Razor
<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>

GeneralRe: No Pagination Buttons Pin
GµårÐïåñ4-Apr-22 19:57
professionalGµårÐïåñ4-Apr-22 19:57 
GeneralRe: No Pagination Buttons Pin
GµårÐïåñ5-Apr-22 8:53
professionalGµårÐïåñ5-Apr-22 8:53 
GeneralRe: No Pagination Buttons Pin
Member 1220295727-May-22 21:51
MemberMember 1220295727-May-22 21:51 
GeneralRe: No Pagination Buttons Pin
GµårÐïåñ28-May-22 8:03
professionalGµårÐïåñ28-May-22 8:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.