Skip to content
  • iImagine
  • Register
  • Log In

Web Development School

Learning made easy.

  • Books
    • Beginning Web Development with ASP.Net Core & Client-Side Technologies
      • TOC
      • Part 1
        • Chapter 1: Static HTML – Designing the landing page
      • Part 2
        • Chapter 2: ASP.Net Core – Let’s talk Dynamic
        • Chapter 3: Introduction to ASP.Net Core MVC
          [ASP.Net Core v9]
      • Part 4
        • Chapter 7: Using Server Side & Client Side technologies together
          [ASP.Net Core v7 & Angular 15]
  • Environment Setup
    • Installing Angular
    • Installing Visual Studio 2022
    • Installing SQL Server 2022 Express
    • Installing Postman
    • Installing Git for Windows
  • Blog
  • iImagine WebSolutions
  • Events
  • Learning Videos
  • Toggle search form

Pagination: Create a Custom Tag Helper

Currently, when we run the application, all of the vehicle records are displayed on one page. At the moment we only have 12 records. But if we had hundreds, or thousands, or even millions of vehicles in the database a user would most likely balk at having to scroll and hunt for one she is interested in on a single page.

In this module we are going to implement a paging feature so that the user can search through pages of vehicles in a more digestible manor.

Table Of Contents
  1. Add a pageNumber parameter to the Home Controller
  2. Unit Test the Pagination feature
  3. Create the View Model
  4. Refactor the Index method results
  5. Create the Paging Tag Helper
  6. Register the custom Tag Helper
  7. Add a PageLinkTagHelper element to the View
  8. Style the Page Link buttons
  9. Add a paging route
  10. Fix the HomeController Tests
  11. Unit Test the PageLinkTagHelper component
  12. What's Next

Add a pageNumber parameter to the Home Controller

Modify the home controller with the following code.

FredsCars\Controllers\HomeController.cs

// using FredsCars.Data;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Controllers
{
    public class HomeController : Controller
    {
        private IVehicleRepository _repo;

        public int PageSize = 5;

        public HomeController(IVehicleRepository repo)
        {
            _repo = repo;
        }
        
        public async Task<ViewResult> Index(int pageNumber = 1)
        {
            return View(await _repo.Vehicles
                .Skip((pageNumber - 1) * PageSize)
                .Take(PageSize)
                .Include(v => v.VehicleType)
                .ToListAsync());
        }
    }
}

Restart the application and the results should look similar to the following. Only the first five vehicles from the database are rendered. This is the first page.

To get to the second page navigate to the following URL:
http://localhost:40080/?pageNumber=2

The question mark (?) in the URL signifies the start of a querystring. A QueryString is a part of a URL that contains key-value pairs used to pass information to the web application. It typically appears after a question mark (?) in the URL, with multiple parameters separated by ampersands (&). We will see examples of the ampersand separator when we add more QueryString parameters for sorting.

In the above URL, there is one key/value pair; pageNumber=2. So the value of 2 will be passed as an argument to the matching named parameter in the Home Controller Index method.

The next five vehicles are rendered for page 2.

To get to the last two records on the third page navigate to the following URL:
http://localhost:40080/?pageNumber=3

In the code above we modified the Home Controller first by declaring a public property of type int named PageSize and initialized it to the value 5.

public int PageSize = 5;

This public property is used to control how many vehicle records we take for each page. And, since it is a public property we can easily set it to any value we like from the calling code. This will make testing easier and also make the code more flexible. For instance, we may want to create a feature allowing the user to select how many vehicles show per page.

The final two changes are in the Index method. First we added an optional parameter of type int named pageNumber to the signature of the Index method.

public async Task<ViewResult> Index(int pageNumber = 1)

The value of pageNumber in the URL QueryStrings above will be passed to the pageNumber parameter in the Index method specifying which page number should be selected from the database. Matching URL QueryString values to action method parameters is done by the MVC model binding system. If there is no pageNumber value in the URL’s QueryString the pageNumber parameter will have a default value of one as declared in the Index method’s signature. This is what makes it an optional parameter.


The ASP.Net Core model binding system can also match route segment values to parameters in methods. For instance, the default route in Program.cs is:
{controller = home}/{action = index}/{id?}.
So, for a URL like vehicles/edit/1, the request would go to the Vehicles controller (which we have not created yet), then to the edit action method, and pass the optional id parameter value of one to an id parameter in the edit method.


The final change we made is in the body of the Index method.

return View(await _repo.Vehicles
    .Skip((pageNumber - 1) * PageSize)
    .Take(PageSize)
    .Include(v => v.VehicleType)
    .ToListAsync());

Here we used the LINQ methods Skip and Take against the Vehicles IQueryable to go to the page number specified, subtract one, and multiply the result times the page size.
So, if the page number is 2 and we subtract one, the result is one. If you multiply 1 times 5, you are skipping the first five records to start at the specified page number of 2. From there we take five records. Simple! Right? Don’t worry if paging logic seems a little difficult at first. Just follow the pattern and eventually it will start to make sense.

Unit Test the Pagination feature

At this point it would be a good idea to test the home controller again. First of all we want to test the new feature; pagination. Secondly we want to make sure our existing tests for the home controller are still working.

NOTE: Unit Tests are typically automated and run nightly in a team development environment. But, for our purposes we will keep an eye on the status of our tests manually throughout this book.

Add the following test called Can_Page_Results to the HomeControllerTests class by modifying the HomeControllerTests.cs file with the code below.

FredsCars.Tests\Controllers\HomeControllerTests.cs

... existing code ...
public class HomeControllerTests
{
    ... existing code ...
    [Fact]
    public async Task Can_Page_Results()
    {
        // Arrange
        // 1 - create a List<T> with test items
        var vehicles = new List<Vehicle>
        {
            new Vehicle
            {
                Id = 1,
                Make = "Make1",
                Model = "Model1",
                VehicleType = new VehicleType
                {
                    Id = 1,
                    Name = "Car"
                }
            },
            new Vehicle
            {
                Id = 2,
                Make = "Make2",
                Model = "Model2",
                VehicleType = new VehicleType
                {
                    Id = 2,
                    Name = "Car"
                }
            },
            new Vehicle
            {
                Id = 3,
                Make = "Make3",
                Model = "Model3",
                VehicleType = new VehicleType
                {
                    Id = 3,
                    Name = "Truck"
                }
            },
            new Vehicle
            {
                Id = 4,
                Make = "Make4",
                Model = "Model4",
                VehicleType = new VehicleType
                {
                    Id = 4,
                    Name = "Jeep"
                }
            },
            new Vehicle
            {
                Id = 5,
                Make = "Make5",
                Model = "Model5",
                VehicleType = new VehicleType
                {
                    Id = 5,
                    Name = "Jeep"
                }
            }
        };
    
        // 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension 
        var mockVehiclesIQueryable = vehicles.BuildMock();
    
        // 3 - build mock IVehicleRepository
        Mock<IVehicleRepository> mockVehicleRepo =
            new Mock<IVehicleRepository>();
        mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);
    
        HomeController controller =
            new HomeController(mockVehicleRepo.Object);
        controller.PageSize = 3;
    
        // Act
        IEnumerable<Vehicle>? result =
            (await controller.Index(2)).ViewData.Model
                as IEnumerable<Vehicle>;
    
        // Assert
        Vehicle[] vehicleArray = result?.ToArray()
            ?? Array.Empty<Vehicle>();
        Assert.True(vehicleArray.Length == 2);
        Assert.Equal("Make4", vehicleArray[0].Make);
        Assert.Equal("Make5", vehicleArray[1].Make);
    }
}
... existing code ...

In the unit test code above we are testing that the Home Controller’s Index method can now page through data utilizing the changes we made earlier in adding a PageSize property to the controller and utilizing the Skip and Take LINQ methods on the IQueryable.

This code is very similar to the first unit test we wrote for the home controller’s Index method. First, in the arrange section, we create a List<Vehicle> with test data, use the MockQueryable Moq extension to convert the vehicles list into an IQueryable that the asynchronous code in the Index method can deal with, and create a mock of the IVehicleRepository interface specifying its Vehicles property returns the test data from the mock Vehicles IQueryable. Finally we create an instance of the home controller and inject the mock Vehicle Repo object into its constructor. We also set the controller’s PageSize property to three for the test.

In the act section we asyncronously call the contoller’s Index method passing the value of 2 as the argument to the pageNumber parameter. From there we get the Model from the ViewData property of the returned ViewResult object, and convert the model to an IEnumerable<Vehicle>. Finally, we assign the IEnumerable<Vehicle> result to a variable called result.

Lastly, in the assert section, we convert the result to an array to make it easier to work with and assign it to a variable named vehicleArray of type Vehicle[] (or Vehicle array).
We first assert that the array’s length is 2 which it should be since the test data is comprised of five Vehicle objects in the List, the PageSize is three, and we called the second page.

We then assert that the Make property of the fist and second records in the result (the 4th and 5th records of the test data) are equal to “Make4” and “Make5”.


If you run the test in the Test Explorer, you should see a green circle with a checkmark indicating the test succeeded and is in a passing state.

And if you run all of the tests in the Test project all of the previous tests should pass.

The original test for Can_Access_VehicleList_FromVehicleRepo still passes even though we do not pass an argument to the controller’s Index method for the pageNumber.

IEnumerable<Vehicle>? result =
    (await controller.Index(/* no parameter passed here for pageNumber */)).ViewData.Model
        as IEnumerable<Vehicle>;

This is because we defined the pageNumber parameter as an optional parameter by giving it a default value of 1.

public async Task<ViewResult> Index(int pageNumber = 1)

Create the View Model

Now that we have paging working in the Home Controller’s Index method, we need a way to get all of the paging information down to the view so that it can actually implement the feature for the user.

This is where View Models come in.


In ASP.NET Core, a ViewModel is a class that serves as a data container we can use to pass information from a controller to a view.


Instead of just passing the Vehicles IQueryable property of the repository with the results down to the View as the model, we are going to embed this information into a ViewModel along with the paging information. That way we can pass all of the information we need down to the View in one fell swoop in one big strongly typed object.


Add a folder named ViewModels to the Models folder. In the new ViewModels folder create a class called PagingInfo and modify it with the code below.

FredsCars\Models\ViewModels\PagingInfo.cs

namespace FredsCars.Views.ViewModels
{
    public class PagingInfo
    {
        // Total number of items in the database
        public int TotalItemCount { get; set; }
        public int PageSize { get; set; }
        public int PageIndex { get; set; }

        public int TotalPageCount =>
            (int)Math.Ceiling((decimal)TotalItemCount / PageSize);
    }
}

The PagingInfo class shown above is not the actual ViewModel itself, but a container class for all of the paging info we need to pass to the view. We are going to embed this class and the Vehicle results into one View Model and pass that to the view.

The PagingInfo class will contain all of the information the View needs to implement paging:

  • TotalItemCount: the total number of records in the repository.
  • PageSize: number of items per page.
  • PageIndex: the current page.
  • TotalPageCount: a calculated property. We get the total number of pages by dividing the total item count by the page size. This may end up being a floating point number with a decimal like 2.3. So we use the C# Math.Ceiling method to round up to the next whole integer for instance, 3.

For the second part of our View Model, create a class named VehiclesListViewModel in the ViewModels folder and modify it with the code shown below.

FredsCars\Models\ViewModels\VehiclesListViewModel.cs

namespace FredsCars.Views.ViewModels
{
    public class VehiclesListViewModel
    {
        public List<Vehicle> Vehicles { get; set; } = new();

        public PagingInfo PagingInfo { get; set; } = new();
    }
}

In the View Model code above in the new class named VehiclesListViewModel, we have embedded a PagingInfo class property as well as a second property named Vehicles of type List<Vehicle> (List of Vehicle). We have initialized both properties to their respective types using the new method. The new method is syntactical sugar so we don’t have to spell out each property’s type when it can be inferred by C#. The following is how the longer form would look.

public List<Vehicle> Vehicles { get; set; } = new List<Vehicle>();
public PagingInfo PagingInfo { get; set; } = new PagingInfo();

Refactor the Index method results

Now that we have a ViewModel structure ready to go it is time to refactor the Index method to include the paging information in the result the view will need as well as the Vehicle IQueryable containing one page of Vehicles.

Modify the Home Controller with the following code.

FredsCars\Controllers\HomeController.cs

// using FredsCars.Data;
using FredsCars.Models.Repositories;
using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Controllers
{
    public class HomeController : Controller
    {
        private IVehicleRepository _repo;

        public int PageSize = 5;

        public HomeController(IVehicleRepository repo)
        {
            _repo = repo;
        }

        public async Task<ViewResult> Index(int pageNumber = 1)
            => View(new VehiclesListViewModel
            {
                Vehicles = await _repo.Vehicles
                    .Skip((pageNumber - 1) * PageSize)
                    .Take(PageSize)
                    .Include(v => v.VehicleType)
                    .ToListAsync(),
                PagingInfo = new PagingInfo
                {
                    PageIndex = pageNumber,
                    PageSize = this.PageSize,
                    TotalItemCount = _repo.Vehicles.Count()
                }
            });
        
    }
}

In the code above we have incorporated the paging information into the model we pass to the view.

First, we have the replaced the return word syntax for the Index method:

public async Task<ViewResult> Index(int pageNumber = 1)
{
    return View(/* model results */);
}

with a method bodied expression using the Lambda syntax and Lambda operator (=>):

public async Task<ViewResult> Index(int pageNumber = 1)
    => View(new VehiclesListViewModel
    {
        Vehicles = ...,
        PagingInfo = ...
        }
    });

Expression-bodied members were introduced to C# in version 6.0 (and have been expanded upon since) and provide a concise way to define methods, properties, and other members using lambda-like syntax.


We didn’t change anything about the way we retrieve a page set of Vehicle results. But, now instead of passing the results directly to the view we assign it to the Vehicles property of the new VehiclesListViewModel class.

public async Task<ViewResult> Index(int pageNumber = 1)
    => View(new VehiclesListViewModel
    {
        Vehicles = await _repo.Vehicles
            .Skip((pageNumber - 1) * PageSize)
            .Take(PageSize)
            .Include(v => v.VehicleType)
            .ToListAsync(),
        PagingInfo = new PagingInfo
        {
            PageIndex = pageNumber,
            PageSize = this.PageSize,
            TotalItemCount = _repo.Vehicles.Count()
        }
    });

At the same time we set all of the paging info in a new PagingInfo class and assign that to the PagingInfo property of the VehiclesListViewModel class. We set the PageIndex property to the incoming pageNumber parameter, the PageSize property to the similarly named class PageSize property using the this keyword to denote the class PageSize property rather than the PageInfo class PageSize property; Although Visual Studio dims the this keyword telling us that it is not needed and Visual Studio could infer our meaning without it. But it is still good to be explicit if you prefer.

Create the Paging Tag Helper

Now that we have all of the supporting structure in place we can create a custom tag helper to implement paging for the user to page through results.

Create a folder on the root of the FredsCars project called TagHelpers. In the TagHelpers folder create a new class called PageLinkTagHelper and modify its contents with the code below.

FredsCars\TagHelpers\PageLinkTagHelper.cs

using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace FredsCars.TagHelpers
{
    [HtmlTargetElement("div", Attributes = "page-model")]
    public class PageLinkTagHelper : TagHelper
    {
        private IUrlHelperFactory _urlHelperFactory;

        public PageLinkTagHelper(IUrlHelperFactory urlHelperFactory)
        {
            _urlHelperFactory = urlHelperFactory;
        }

        [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)
        {
            if (ViewContext != null && PageModel != null)
            {
                IUrlHelper urlHelper
                    = _urlHelperFactory.GetUrlHelper(ViewContext);
                TagBuilder divTag = new TagBuilder("div");
                for (int i = 1; i < PageModel.TotalPageCount; i++)
                {
                    TagBuilder aTag = new TagBuilder("a");
                    aTag.Attributes["href"] = urlHelper.Action(PageAction,
                        new { pageNumber = i });
                    aTag.InnerHtml.Append(i.ToString());
                    divTag.InnerHtml.AppendHtml(aTag);
                }
                output.Content.AppendHtml(divTag.InnerHtml);
            }
        }
    }
}

In the code above we created a Tag Helper class called PageLinkTagHelper by inheriting from TagHelper.

public class PageLinkTagHelper : TagHelper

We also use the HtmlTargetElement Attribute to tell the Tag Helper to become active for any div element that has an html attribute named page-model.

[HtmlTargetElement("div", Attributes = "page-model")]
public class PageLinkTagHelper : TagHelper
{
   ...
}

We then use dependency injection to set a private variable called _urlHelperFactory to an IURLHelperFactory instance. We will use the IUrlHelperFactory instance to create an IURLHelper later in the code which will help us create the page links dynamically.

Next we lay out three class properties:
The first is named ViewContext of type ViewContext:

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext? ViewContext { get; set; }

A ViewContext is used during View execution to help us grab the HTTP context from within a view. Here we have used the HtmlAttributeNotBound attribute to tell ASP.Net Core that this property is not bound to an html attribute in the targeted div tag. In addition we mark the property with the ViewContext attribute to specify that it should be set with the current ViewContext when created.

The second property is called PageModel of type PagingInfo (our paging container class embedded in our ViewModel earlier). This property will be bound to an html attribute in the targeted div element named page-model.

public PagingInfo? PageModel { get; set; }

The third property is called PageAction of type string. This property will be bound to an html attribute in the targeted div element named page-action.

public string? PageAction { get; set; }

Next we override the Process method of the base TagHelper class which executes the tag helper.

public override void Process(TagHelperContext context,
    TagHelperOutput output)
{
    ...
}

The Process method takes in a TagHelperContext which contains information associated with execution of the current TagHelper, and a TagHelperOutput instance which is used to represent the output of a TagHelper.

Within the Process method we capture an instance of an IUrlHelper factory and assign it to a variable of type IUrlHelper named urlHelper.

IUrlHelper urlHelper
    = _urlHelperFactory.GetUrlHelper(ViewContext);

Here we use the IUrlHelperFactory's GetUrlHelper method and pass to it the ViewContext instance (our first class property laid out above).

Next we create a div tag by using a new TagBuilder class instance and pass to it the string “div”. This tag will serve as the div element in the final output html that acts as an html container for the page links.

TagBuilder divTag = new TagBuilder("div");

Finally, we use a for-loop to lay out the page links from Page One to the Total Page Count; Page n.

for (int i = 1; i < PageModel.TotalPageCount; i++)
{
    TagBuilder aTag = new TagBuilder("a");
    aTag.Attributes["href"] = urlHelper.Action(PageAction,
        new { pageNumber = i });
    aTag.InnerHtml.Append(i.ToString());
    divTag.InnerHtml.AppendHtml(aTag);
}

Within the body of the for-loop, we create an anchor tag for each page link needed again using an instance of the TagBuilder class.

TagBuilder aTag = new TagBuilder("a");

We then use our IUrlHelper factory instance’s Action method to create a URL for each page link needed. We pass to the urlHelper.Action method our PageAction property which will specify the action method for the current controller to use (bound to the targeted div element’s page-action attribute) and an object that contains route values; In this case, a single route value name being pageNumber and its value being the current integer value of i, the current iteration number.

aTag.Attributes["href"] = urlHelper.Action(PageAction,
    new { pageNumber = i });

Next, we append the integer i of the current iteration to the anchor tag’s inner html and append the anchor tag itself to the output results represented by the variable divTag we created earlier as our Tag Helper’s html div container.

aTag.InnerHtml.Append(i.ToString());
divTag.InnerHtml.AppendHtml(aTag);

Once we exit the for-loop, we append the divTag container html to the output content.

output.Content.AppendHtml(divTag.InnerHtml);

Register the custom Tag Helper

Earlier in this chapter we registered the built in Microsoft Tag Helpers in the ViewImports file. We can use the same file to register our custom Tag Helpers like the PageLink Tag Helper.

Modify the ViewImports file in the Views folder with the contents shown below in bold blue text.

FredsCars\Views\ViewImports.cshtml

@using FredsCars.Models
@using FredsCars.Models.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, FredsCars

In the code above we registered all custom Tag Helpers that can be found in the FredsCars namespace including its subfolder namespaces like FredsCars/TagHelpers.

We also added a new using statement for FredsCars.Models.ViewModels so we won’t have to include a using statement for ViewModel classes in every view.

Add a PageLinkTagHelper element to the View

In this section we can finally plug the custom PageLink Tag Helper into the View. Modify the Home Controller’s Index method with the code shown below in bold blue font.

FredsCars\Views\Home\Index.cshtml

@model VehiclesListViewModel

@{
    ViewData["Title"] = "Welcome";
}

<div class="container-fluid my-4 text-center">
    <h1>Welcome to Fred's Cars!</h1>
    Where you'll always find the right car, truck, or jeep.<br />
    Thank you for visiting our site!

    <div class="container-fluid mx-0 row"
        style="margin-top: 20px;">
        <!-- Categories -->
        <div class="col-4 col-md-3 col-lg-2"
             style="border-right: 2px solid black">
            <div class="d-grid gap-2 button-grid">
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-primary button">
                    <b>ALL</b></a>
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-outline-primary button">
                    <b>CARS</b></a>
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-outline-primary button">
                    <b>TRUCKS</b></a>
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-outline-primary button">
                    <b>JEEPS</b>
                </a>
            </div>
        </div>
        <!-- Results -->
        <div class="col">
            <h3 class="bg-dark text-success">ALL Results</h3>
            <table class="results table table-striped">
                <thead>
                    <tr>
                        <th>

                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.Vehicles[0].Status)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.Vehicles[0].Year)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.Vehicles[0].Make)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.Vehicles[0].Model)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.Vehicles[0].Color)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.Vehicles[0].Price)
                        </th>
                        <th>
                            @Html.DisplayNameFor(model => model.Vehicles[0].VehicleType)
                        </th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var v in Model.Vehicles)
                    {
                        <tr>
                            <td>
                                <img src="@v.ImagePath"
                                     class="result-image" />
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Status)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Year)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Make)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Model)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Color)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Price)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.VehicleType.Name)
                            </td>
                        </tr>
                    }
                </tbody>
             </table>

             <div id="page-links" page-model="@Model.PagingInfo"
                  page-action="Index">
             </div>
        </div>
    </div>  
</div>

In the code above for the Index View the first change we made was at the top of the file to change the Model type from just an IEnumerable of Vehicles to the new VehiclesList View Model.

@model VehiclesListViewModel

NOTE: In the VehiclesListViewModel class we have changed the Vehicles property from an IEnumerable<Vehicle> to a List<Vehicle> to make it easier to work with when laying out the table header column names based on the name of each property in a Vehicle class. More on this coming up.

The next change is where we lay out the table header column names. Now that the model is a VehiclesListViewModel class rather then an IEnumerable<Vehicle> we have to access the Vehicles property of the model to get each Vehicle property name for each header column name. We do this by accessing each property by index. For instance, the first index (0) returns the Status property of a Vehicle and as a parameter to the HTML helper’s DisplayNameFor method lays out the name of the property. Since LINQ expressions like the Lambda expression
model => model.Vehicles[0].Status are deferred, accessing a Vehicle by index in this way will not error out if the Vehicles property is null or empty.

<th>
    @Html.DisplayNameFor(model => model.Vehicles[0].Status)
</th>

The third small change comes where we lay out the results in the foreach loop. I just simply changed the variable name in the for loop from ‘item’ to ‘v’. So, we need to change the variable in all the Lambda expressions from item to v.

@foreach (var v in Model.Vehicles)
{
   ...
   <td>
    @Html.DisplayFor(modelItem => v.Status)
   </td>
   ...
}

And finally, the change we have all been waiting for and what the build up in this module has been all about, we insert a div element with the page-model (and page-action) html attributes so that our custom tag helper will recognize it and lay out the page links via the C# code and logic in the tag helper.

<div id="page-links" page-model="@Model.PagingInfo"
     page-action="Index">
</div>

The page-model html attribute will be set to the value of the PagingInfo class coming down from the controller’s Index method and fed back up to the PageLink tag helper’s PageModel property. The page-action html attribute is set to the literal string value “Index” and will be fed to the tag helper’s PageAction property.


If you restart the application and navigate to http://localhost:40080 you should get results similar to the following with the page links at the bottom.

If you hover over a link, you can see that the corresponding page number is appended to the URL in a querystring in the format
?pageNumber = N.


Let’s inspect the tag helper’s html in the web development tools. Right click one of the page links and select inspect in the browser. You’ll be taken to the Elements Tab in the web dev tools window and the page link will be highlighted. From there we can see the entire page links div html.

This shows us the crux of what a tag helper can do for us. A tag helper lays out and renders html based on the C# code and logic of the developer who wrote the tag helper.

Now click on one of the links in the browser.

In the screen shot above I have clicked on the third page link. It shows two results because there are twelve records with a page size of 5. So the third and last page show two results.

Style the Page Link buttons

Well we have everything working with the new page links but they do not look very nice at the moment. Let’s fix that now with some bootstrap styling.

Modify PageLinkTagHelper.cs with the code shown below.

FredsCars\TagHelpers\PageLinkTagHelper.cs

using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace FredsCars.TagHelpers
{
    [HtmlTargetElement("div", Attributes = "page-model")]
    public class PageLinkTagHelper : TagHelper
    {
        private IUrlHelperFactory _urlHelperFactory;

        public PageLinkTagHelper(IUrlHelperFactory urlHelperFactory)
        {
            _urlHelperFactory = urlHelperFactory;
        }

        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext? ViewContext { get; set; }
        
        public PagingInfo? PageModel { get; set; }

        public string? PageAction { get; set; }

        public bool PagingClassesEnabled { get; set; }
        public string? PagingBtnClass { get; set; }
        public string? PagingBtnClassNormal { get; set; }
        public string? PagingBtnClassSelected { get; set; }

        public override void Process(TagHelperContext context,
            TagHelperOutput output)
        {
            if (ViewContext != null && PageModel != null)
            {
                IUrlHelper urlHelper
                    = _urlHelperFactory.GetUrlHelper(ViewContext);
                TagBuilder divTag = new TagBuilder("div");
                for (int i = 1; i <= PageModel.TotalPageCount; i++)
                {
                    TagBuilder aTag = new TagBuilder("a");
                    aTag.Attributes["href"] = urlHelper.Action(PageAction,
                        new { pageNumber = i });
                    if (PagingClassesEnabled)
                    {
                        aTag.AddCssClass(PagingBtnClass ?? "");
                        aTag.AddCssClass(i == PageModel.PageIndex
                            ? PagingBtnClassSelected ?? "" : PagingBtnClassNormal ?? "");
                    }
                    aTag.InnerHtml.Append(i.ToString());
                    divTag.InnerHtml.AppendHtml(aTag);
                }
                output.Content.AppendHtml(divTag.InnerHtml);
            }
        }
    }
}

In the code above we only made some very small changes in two sections of the PageLinkTagHelper class but it will make the code much more flexible and reusable as far as styling goes.

In the first section we added four public class properties to dynamically set the styling and also decide whether or not to use dynamic styling at all from html attributes on the targeted div element.

public bool PagingClassesEnabled { get; set; }
public string PagingBtnClass { get; set; }
public string PagingBtnClassNormal { get; set; }
public string PagingBtnClassSelected { get; set; }

PagingClassEnabled will map to the targeted div element’s paging-class-enabled html attribute, PagingBtnClass will map to paging-btn-class, and so on.


  • PagingClassesEnabled: property specifies whether or not to use dynamic css styling.
  • PagingBtnClass: property specifies the CSS class to use for a paging button.
  • PagingBtnClassNormal: property specifies the normal CSS class to use for all unselected buttons.
  • PagingBtnClassSelected: property specifies the CSS class to use for the selected button.

In the second section we use an if block to dynamically apply the styles from the targeted div element’s html attributes that are mapped to the custom tag helper’s public button styling properties we have declared above.

for (int i = 1; i <= PageModel.TotalPageCount; i++)
{
    TagBuilder aTag = new TagBuilder("a");
    aTag.Attributes["href"] = urlHelper.Action(PageAction,
        new { pageNumber = i });
    if (PagingClassesEnabled)
    {
        aTag.AddCssClass(PagingBtnClass ?? "");
        aTag.AddCssClass(i == PageModel.PageIndex
            ? PagingBtnClassSelected ?? "" : PagingBtnClassNormal ?? "");
    }
    aTag.InnerHtml.Append(i.ToString());
    divTag.InnerHtml.AppendHtml(aTag);
}

This should be pretty self explanatory. But, one thing to note is the ternary operator used to dynamically apply normal or selected styling to a button.

aTag.AddCssClass(i == PageModel.PageIndex
    ? PagingBtnClassSelected ?? "" : PagingBtnClassNormal ?? "");

The ternary operator can be used like an if-then statement in a more concise manor.
(condition) ? (expression) else (expression)
This can be read as:

(if condition is true)   then (evaluate this expression) 
else (evaluate this expression)

Here, the code can be read as:
If (i == PageModel.PageIndex) then
PagingBtnClassSelected
else
PagingBtnClassNormal

In other words if i equals the current PageIndex then apply the PagingBtnClassSelected (btn-primary) CSS class to the button’s CSS otherwise apply PagingBtnClassNormal (btn-outline-dark).

Another thing to note is that since the PagingBtnClass styles are nullable strings meaning they may not be set, we also used the null coalescing operator twice int the if-then clause:

// if PagingingBtnClassSelected is null then use the value ""
//     or empty string
PagingBtnClassSelected ?? ""
// if PagingBtnClassNormal is null then use the value ""
//     or empty string
PagingBtnClassNormal ?? ""

Next, modify the paging div element in the Home Controller’s Index method.

FredsCars\Views\Home\Index.cshtml

... existing code ...
<div id="page-links"
     page-model="@Model.PagingInfo"
     page-action="Index"
     paging-classes-enabled="true"
     paging-btn-class="btn"
     paging-btn-class-normal="btn-outline-dark"
     paging-btn-class-selected="btn-primary"
     class="btn-group mt-3">
</div>
... existing code ...

Again, the code tweak above to the targeted div element should be self explanatory. The new html attributes map to the custom tag helper’s public class button properties we just set up. And, now we can reuse this custom tag helper in other parts of the application or even other applications and apply any unique styling to fit any layout.

Add a paging route

Right now the paging works just as it should. The button links in the button group point to URLs which include a querystring to specify the page number to go to.

Take the second button link for example. It’s URL looks like this:
http://localhost:40080/?pageNumber=2
Again, this does the job. But a more attractive URL might look like this:
http://localhost:40080/Page2

ASP.Net Core makes it easy to accomplish this through it’s routing system features. Let’s add a route in Program.cs to make this happen. Modify Program.cs with the following code.

FredsCars\FredsCars\Program.cs

... existing code ...
app.UseStaticFiles();

// Add custom routes
app.MapControllerRoute("paging",
    "Vehicles/Page{pageNumber}",
    new { Controller = "Home", action = "Index" });
/*** Add endpoints for contoller actions and
       the default route ***/
app.MapDefaultControllerRoute();
... existing code ...

In the code above we have added a new route to the application using the MapControllerRoute method of the WebApplication object (app). MapControllerRoute takes three parameters.

For the first parameter we named the route “paging”.
In the second parameter we defined the pattern an incoming URL should match to be directed to this route.
Vehicles/Page{pageNumber}
We used the third parameter to create a new object that contains default route values for route parameters.
new { Controller = "Home", action = "Index" }

In the URL the tag helper lays out for each button link, no controller was specified so the link goes back to the current controller, which in our case is Home. Then if you recall we used an IURLHelper’s Action method to set the action to Index and a route value named pageNumber to be tacked on to the URL as a querystring.

TagBuilder aTag = new TagBuilder("a");
aTag.Attributes["href"] = urlHelper.Action(PageAction,
    new { pageNumber = i });

Before adding the new route, this resulted in the original format of the URLs:
http://localhost:40080/?pageNumber=2

One of the features of the ASP.Net Core routing system is that not only are incoming requests directed to the appropriate route, but outgoing routes are also generated using route patterns in our custom defined routes.

Since our new route has default route values where controller = home and action = index, and the URLs we generate for the button links in the tag helper via the urlHelper.Action method match those values, and the custom route expects an incoming variable named pageNumber, and our generated button link URLs provide that variable with the same name in a querystring key/value pair, the ASP.Net Core routing system generates the button link URLs in the new custom route’s pattern:
http://localhost:40080/Vehicles/Page2

Doesn’t this look better?
Some people would call this a composite URL because it is more human readable.


Restart the application, navigate to http://localhost:40080 in the browser, and hover over a button link in the paging button group at the bottom of the screen.

In the above screenshot you can see that the button link URLs were generated in the pattern of the new route.

Click on one of the paging button links and you will be directed to that page number.

Fix the HomeController Tests

At this point I want to test our new component, the PageLinkTagHelper. But, before we do that let’s make sure all of our current tests are still working. If we perform a test run in Test Explorer on all of our tests, we see that both of our HomeController tests fail.

This is typical of unit testing. During development requirements change and we refactor our code. As the code changes, units of code and their corresponding unit tests we thought we had all squared away and tested sometimes fail. This is especially true in agile environments where the pace of development is rapid and fluid.

Let’s see if we can dig in to see what’s happening here.

The problem for the test Can_Access_VehicleList_FromVehicleRepo is that the Home controller’s Index method no longer returns an IEnumerable<Vehicle>. It now returns a VehiclesListViewModel.

So, we need to change the Act section’s statement from:

// Act
IEnumerable<Vehicle>? result =
    (await controller.Index()).ViewData.Model
        as IEnumerable<Vehicle>;

to:

// Act
VehiclesListViewModel? result =
    (await controller.Index()).ViewData.Model
        as VehiclesListViewModel;

We also need to change the first statement of the Assert section from:

// Assert
Vehicle[] vehicleArray = result?.ToArray()
    ?? Array.Empty<Vehicle>();

to:

Vehicle[] vehicleArray = result?.Vehicles.ToArray()
    ?? Array.Empty<Vehicle>();

At this point the Can_Access_VehicleList_FromVehicleRepo test should pass.


Now, let’s turn our attention to the Can_Page_Results test.

The fix for this test is the same as for the Can_Access_VehicleList_FromVehicleRepo test. But, remember to include the pageNumber parameter with a value of two in the Act section statement.

// Act
VehiclesListViewModel? result =
    (await controller.Index(2)).ViewData.Model
        as VehiclesListViewModel;

Here is the entire HomeControllerTests.cs file with the changes included.

FredsCars\FredsCars.Tests\Controllers\HomeControllerTests.cs

using FredsCars.Controllers;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using FredsCars.Models.ViewModels;
using MockQueryable;
using Moq;

namespace FredsCars.Tests.Controllers
{
    public class HomeControllerTests
    {
        [Fact]
        public async Task Can_Access_VehicleList_FromVehicleRepo()
        {
            // Arrange
            // 1 - create a List<T> with test items
            var vehicles = new List<Vehicle>
            {
                new Vehicle
                {
                    Id = 1,
                    Make = "Make1",
                    Model = "Model1",
                    VehicleType = new VehicleType
                    {
                        Id = 1,
                        Name = "Car"
                    }
                },
                new Vehicle
                {
                    Id = 2,
                    Make = "Make2",
                    Model = "Model2",
                    VehicleType = new VehicleType
                    {
                        Id = 1,
                        Name = "Car"
                    }
                },
                new Vehicle
                {
                    Id = 3,
                    Make = "Make3",
                    Model = "Model3",
                    VehicleType = new VehicleType
                    {
                        Id = 2,
                        Name = "Truck"
                    }
                },
                new Vehicle
                {
                    Id = 4,
                    Make = "Make4",
                    Model = "Model4",
                    VehicleType = new VehicleType
                    {
                        Id = 3,
                        Name = "Jeep"
                    }
                }
            };

            // 2 - build mock using MockQueryable.Moq extension 
            var mockVehiclesIQueryable = vehicles.BuildMock();

            // 3 - build mock IVehicleRepository
            Mock<IVehicleRepository> mockVehicleRepo =
                new Mock<IVehicleRepository>();
            mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);

            HomeController controller = new HomeController(mockVehicleRepo.Object);

            // Act
            VehiclesListViewModel? result =
                (await controller.Index()).ViewData.Model
                    as VehiclesListViewModel;

            // Assert
            Vehicle[] vehicleArray = result?.Vehicles.ToArray()
                ?? Array.Empty<Vehicle>();
            Assert.True(vehicleArray.Length == 4);
            int carCount = vehicleArray.Where(v => v.VehicleType.Name == "Car").Count();
            Assert.Equal(2, carCount);
            Assert.True(vehicleArray[2].Make == "Make3" &&
                vehicleArray[2].Model == "Model3");
            Assert.True(vehicleArray[3].Make == "Make4" &&
                vehicleArray[3].Model == "Model4");
        }

        [Fact]
        public async Task Can_Page_Results()
        {
            // Arrange
            // 1 - create a List<T> with test items
            var vehicles = new List<Vehicle>
            {
                new Vehicle
                {
                    Id = 1,
                    Make = "Make1",
                    Model = "Model1",
                    VehicleType = new VehicleType
                    {
                        Id = 1,
                        Name = "Car"
                    }
                },
                new Vehicle
                {
                    Id = 2,
                    Make = "Make2",
                    Model = "Model2",
                    VehicleType = new VehicleType
                    {
                        Id = 2,
                        Name = "Car"
                    }
                },
                new Vehicle
                {
                    Id = 3,
                    Make = "Make3",
                    Model = "Model3",
                    VehicleType = new VehicleType
                    {
                        Id = 3,
                        Name = "Truck"
                    }
                },
                new Vehicle
                {
                    Id = 4,
                    Make = "Make4",
                    Model = "Model4",
                    VehicleType = new VehicleType
                    {
                        Id = 4,
                        Name = "Jeep"
                    }
                },
                new Vehicle
                {
                    Id = 5,
                    Make = "Make5",
                    Model = "Model5",
                    VehicleType = new VehicleType
                    {
                        Id = 5,
                        Name = "Jeep"
                    }
                }
            };

            // 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension 
            var mockVehiclesIQueryable = vehicles.BuildMock();

            // 3 - build mock IVehicleRepository
            Mock<IVehicleRepository> mockVehicleRepo =
                new Mock<IVehicleRepository>();
            mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);

            HomeController controller =
                new HomeController(mockVehicleRepo.Object);
            controller.PageSize = 3;

            // Act
            VehiclesListViewModel? result =
                (await controller.Index(2)).ViewData.Model
                    as VehiclesListViewModel;

            // Assert
            Vehicle[] vehicleArray = result?.Vehicles.ToArray()
                ?? Array.Empty<Vehicle>();

            Assert.True(vehicleArray.Length == 2);
            Assert.Equal("Make4", vehicleArray[0].Make);
            Assert.Equal("Make5", vehicleArray[1].Make);
        }
    }
}

And now all of the HomeController Test should pass.


The Can_Page_Results test proves we can receive the right set of Vehicles corresponding to a page number. But we also need to prove the controller can send paging info correctly. Let’s add one more test called Can_Return_PagingInfo before we move on to testing the PageLinkTagHelper component.

Add the following test to the HomeControllerTests.cs file.

FredsCars\FredsCars.Tests\Controllers\HomeControllerTests.cs

... existing code ...        
        [Fact]
        public async Task Can_Return_PagingInfo()
        {
            // Arrange
            // 1 - create a List<T> with test items
            var vehicles = new List<Vehicle>
            {
                new Vehicle
                {
                    Id = 1,
                    Make = "Make1",
                    Model = "Model1",
                    VehicleType = new VehicleType
                    {
                        Id = 1,
                        Name = "Car"
                    }
                },
                new Vehicle
                {
                    Id = 2,
                    Make = "Make2",
                    Model = "Model2",
                    VehicleType = new VehicleType
                    {
                        Id = 2,
                        Name = "Car"
                    }
                },
                new Vehicle
                {
                    Id = 3,
                    Make = "Make3",
                    Model = "Model3",
                    VehicleType = new VehicleType
                    {
                        Id = 3,
                        Name = "Truck"
                    }
                },
                new Vehicle
                {
                    Id = 4,
                    Make = "Make4",
                    Model = "Model4",
                    VehicleType = new VehicleType
                    {
                        Id = 4,
                        Name = "Jeep"
                    }
                },
                new Vehicle
                {
                    Id = 5,
                    Make = "Make5",
                    Model = "Model5",
                    VehicleType = new VehicleType
                    {
                        Id = 5,
                        Name = "Jeep"
                    }
                }
            };

            // 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension 
            var mockVehiclesIQueryable = vehicles.BuildMock();

            // 3 - build mock IVehicleRepository
            Mock<IVehicleRepository> mockVehicleRepo =
                new Mock<IVehicleRepository>();
            mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);

            HomeController controller =
                new HomeController(mockVehicleRepo.Object);
            controller.PageSize = 3;

            // Act
            VehiclesListViewModel? result =
                (await controller.Index(2)).ViewData.Model
                    as VehiclesListViewModel;

            // Assert
            PagingInfo PagingInfo = result?.PagingInfo!;

            Assert.Equal(2, PagingInfo.PageIndex);
            Assert.Equal(3, PagingInfo.PageSize);
            Assert.Equal(5, PagingInfo.TotalItemCount);
            Assert.Equal(2, PagingInfo.TotalPageCount);
        }
    }
}

The buildup in the arrange and act sections are the same in the Can_Return_PagingInfo test as they are in Can_Page_Results. But the assert section is different because now we are testing that the properties of the PagingInfo class of the returned VehiclesListViewModel class are what we would expect from that build up. All of the tests should be in a passing state at this point.

Unit Test the PageLinkTagHelper component

One of the nice things about a custom tag link helper like the PageLinks tag helper is that we don’t have all of our C# paging logic embedded in the controller and the view. So the code is much cleaner and readable. The other benefit is that as a component the PageLinkTagHelper class is testable.

Add a new folder to the FredsCars.Tests project named TagHelpers. In the new TagHelpers folder add a class named PageLinkTagHelperTests.

FredsCars\FredsCars.Tests\TagHelpers\PageLinkTagHelperTests.cs

using FredsCars.Models.ViewModels;
using FredsCars.TagHelpers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Moq;

namespace FredsCars.Tests.TagHelpers
{
    public class PageLinkTagHelperTests
    {
        [Fact]
        public void Can_Generate_PageLinks()
        {
            // Arrange
            var urlHelper = new Mock<IUrlHelper>();
            urlHelper.SetupSequence(x =>
                x.Action(It.IsAny<UrlActionContext>()))
                    .Returns("Test/Page1")
                    .Returns("Test/Page2")
                    .Returns("Test/Page3");

            var urlHelperFactory = new Mock<IUrlHelperFactory>();
            urlHelperFactory.Setup(f =>
                f.GetUrlHelper(It.IsAny<ActionContext>()))
                    .Returns(urlHelper.Object);

            var viewContext = new Mock<ViewContext>();

            PageLinkTagHelper pageLinkTagHelper =
                new PageLinkTagHelper(urlHelperFactory.Object)
                {
                    ViewContext = viewContext.Object,
                    PageModel = new PagingInfo
                    {
                        PageIndex = 2,
                        TotalItemCount = 28,
                        PageSize = 10
                    },
                };

            TagHelperContext tagHelperCtx = new TagHelperContext(
                new TagHelperAttributeList(),
                new Dictionary<object, object>(), "");

            var content = new Mock<TagHelperContent>();
            TagHelperOutput tagHelperoutput = new TagHelperOutput("div",
                new TagHelperAttributeList(),
                (cache, encoder) => Task.FromResult(content.Object));

            // Act
            pageLinkTagHelper.Process(tagHelperCtx, tagHelperoutput);

            // Assert
            Assert.Equal(@"<a href=""Test/Page1"">1</a>"
                + @"<a href=""Test/Page2"">2</a>"
                + @"<a href=""Test/Page3"">3</a>",
                tagHelperoutput.Content.GetContent());
        }
    }
}

In the unit test code above, we test that the PageLink tag helper can generate page links. In the arrange section we setup all of the mocks and behaviors we will need to call the tag helper’s Process method.

First, we set up a mock for an IUrlHelper and use the Mock.SetupSequence method to define a sequence of URLs the IUrlHelper returns via its Action method.

// Arrange
var urlHelper = new Mock<IUrlHelper>();
urlHelper.SetupSequence(x =>
    x.Action(It.IsAny<UrlActionContext>()))
        .Returns("Test/Page1")
        .Returns("Test/Page2")
        .Returns("Test/Page3");

This is the first time we are seeing the SetupSequence method to define behavior of a member rather then SetupGet. Each time Action is called the next URL in the sequence is returned as we will see below.

We also specify that the Action member can take in any parameter value that is a UrlActionContext using the It.IsAny<T> method.

Next, we mock up an IUrlHelperFactory and define the behavior of its GetUrlHelper method again using It.IsAny to specify it can take in any ActionContext value as a parameter and it returns the mocked IUrlHelper’s object.

var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory.Setup(f =>
    f.GetUrlHelper(It.IsAny<ActionContext>()))
        .Returns(urlHelper.Object);

After setting up IURlHelperFactory and IUrlHelper we create a mocked ViewContext method and instantiate an instance of the PageLinkTagHelper class setting its ViewContext property to the mocked ViewContext and its PageModel property to an instance of the PagingInfo class with test values for it’s properties; PageIndex, TotalItemCount, and PageSize.

var viewContext = new Mock<ViewContext>();

PageLinkTagHelper pageLinkTagHelper =
    new PageLinkTagHelper(urlHelperFactory.Object)
    {
        ViewContext = viewContext.Object,
        PageModel = new PagingInfo
        {
            PageIndex = 2,
            TotalItemCount = 28,
            PageSize = 10
        },
    };

Finally, we need to create instances of TagHelperContext and TagHelperOutput to use as arguments when we call the Process method of the tag helper. We also have to mock a TagHelperContent object to use as an argument to the TagHelperOutput instance.

TagHelperContext tagHelperCtx = new TagHelperContext(
    new TagHelperAttributeList(),
    new Dictionary<object, object>(), "");

var content = new Mock<TagHelperContent>();
TagHelperOutput tagHelperoutput = new TagHelperOutput("div",
    new TagHelperAttributeList(),
    (cache, encoder) => Task.FromResult(content.Object));

In the act section we call the tag helper’s Process method feeding as arguments to its parameters the TagHelperContext and TagHelperOutput we created in the arrange section.

// Act
pageLinkTagHelper.Process(tagHelperCtx, tagHelperoutput);

And finally in the assert section, we test that the PageLink tag helper can indeed return the html for a series of of URLs as anchor links.

// Assert
Assert.Equal(@"<a href=""Test/Page1"">1</a>"
    + @"<a href=""Test/Page2"">2</a>"
    + @"<a href=""Test/Page3"">3</a>",
    tagHelperoutput.Content.GetContent());

What’s Next

In this module we created a custom tag helper to lay out the button links for paging. Along the way we used a View Model to pass information from the controller to the view that the view needs to render the PageLinks tag helper. We then learned how to register the tag helper and applied dynamic styling to it. We also learned how to create a custom route in ASP.Net Core.

In the next module we are going to add sorting by column header.

< Prev
Next >

Leave a ReplyCancel reply

Chapter 1: Static HTML – Designing the landing page.

  • Static HTML – Designing the landing page.
  • Let’s get started!
  • Mock your site with HTML
  • Make CSS easy with Bootstrap
  • Mock your content
  • Introducing JavaScript
  • JavaScript Code Improvements
  • Results Data
  • Images and the HTML Image Element.
  • Revisiting Reusability for CSS and JavaScript
  • Reuse for HTML: PART 1
  • Reuse for HTML: PART 2
  • Details Page – Using a Bootstrap Component
  • Creating Links
  • Chapter One Conclusion

Chapter 2: ASP.Net Core – Let’s talk Dynamic

  • Introduction to ASP.Net Core
  • What is .Net?
  • What is ASP.Net
  • Introduction to Entity Framework Core

Chapter 3: ASP.Net MVC Core – Models, Views, and Controllers [ASP.Net Core v9]

  • Introduction to ASP.Net Core MVC
  • Create the project: ASP.Net Core MVC
  • Explore the ASP.Net Core Empty Web Project Template
  • Configure the Application for MVC
  • Create a Controller: Home Controller
  • Create a View: Index View for the Home Controller
  • Install Bootstrap using Libman
  • Create the Layout template
  • Create the Model
  • Install EF Core & Create the Database
  • Seed the Database: Loading test data
  • DI (Dependency Injection): Display a List of Vehicles
  • Repository Pattern: The Vehicles Repo
  • Unit Test 1: Home Controller Can Use Vehicle Repository
  • Unit Test 2: Vehicle Repository Can Return List
  • Add the ImagePath Migration and Thumbnail images to results
  • Pagination: Create a Custom Tag Helper
  • Sorting
  • Category Filter
  • Partial View: Break out the vehicle results
  • View Component: Create dynamic category buttons
  • Create the Details page
  • Create the Create Page

Chapter 7: Using Server Side & Client Side technologies together. [ASP.Net Core v7 & Angular v15]

  • Intro to Full Stack Development
  • Fred’s Cars – Full Stack Development
  • Prepare the environment
  • Create the Visual Studio Solution
  • Add the ASP.Net Core Web API project
  • Add the Angular Project
  • Wire it up!
  • WeatherForecast: Understanding the basics
  • Vehicles API Controller: Mock Data
  • Vehicles Angular Component: Consuming Data
  • Routing and Navigation
  • Using a Component Library: Angular Material
  • Our first Angular Material Component: MatToolbar
  • Configuring for Saas: CSS with superpowers
  • Create the Header & Footer components
  • Displaying Results with MatTable
  • Loading: Using a Progress Spinner
  • MatTable: Client-Side Paging and Sorting
  • MatSidenav: Create a Search Sidebar
  • MatCheckbox: Category Search UI
  • Adding an image to the welcome page
  • Create the database with Entity Framework Core migrations
  • MatPaginator & PageEvent: Custom Server-Side Paging
  • Unit Testing: Custom Server-Side Paging
  • Repository Pattern: VehicleRepository
  • Unit Test: Paging in the Vehicles controller
  • Server-Side Sorting
  • Unit Tests: Sorting
  • Filter (Quick Search)
  • Unit Tests: Filter feature
  • Advanced Search: Categories
  • Unit Tests: Search by category
  • Progress Spinner: Final Fix

TOC

  • What were WebForms?
  • Enter MVC
    • Understanding MVC
    • Advantages of MVC
  • ASP.Net Core MVC – A total rewrite
  • ASP.Net Core 2 MVC – Here come Razor Pages
    • Understanding Razor Pages
  • ASP.Net Core 3 – Dropping the MVC reference
    • Understanding Blazor
  • Dropping the MVC reference
  • Hello .Net 5!
  • What’s Next? – Here comes .Net 6.

Recent Posts

  • Angular Commands Cheat Sheet
  • Installing Git for Windows
  • Installing Postman
  • Installing SQL Server 2022 Express
  • Installing Visual Studio 2022

Recent Comments

No comments to show.

Archives

  • November 2023
  • October 2023
  • June 2023
  • October 2021

Categories

  • Angular
  • ASP.Net
  • Environment Setup
  • See All
  • SQL Server
  • Visual Studio
  • Web API & Rest Services

WordPress Theme Editor

Copyright © 2025 Web Development School.

Powered by PressBook Blog WordPress theme