ASP.NET Pagination View Component

Paging and filtering lists of data. If you build any sort of application then you're going to have to deal with both of these things.

Microsoft provides a pretty good tutorial on paging, sorting and filtering with Razor Pages and Entity Framework Core. If you're just getting started I highly suggest you go check it out.  I think that tutorial is a great start but when I started implementing it in a real life application I took it a bit further.

The End Goal

At the end of the day I wanted to be able to have a list page, actually several pages, that look like this.

Filtering, paging and eventually I would be adding sorting as well.  

IPaginatedList

The first step to implementing paging was to bring in a standard "IPaginatedList" interface and accompanying "PaginatedList<T>" class.

public interface IPaginatedList:IList
{
    int CurrentPage { get; }
    int TotalPages { get; }
    int TotalItems { get;  }
    bool HasPrevious { get; }
    bool HasNext { get; }
}
IPaginatedList.cs

The "IPaginatedList" class will be the key interface that the paging view component will use to generate the paging UI.

public class PaginatedList<T> : List<T>, IPaginatedList where T:class
{
    public PaginatedList(){}

    public PaginatedList(List<T> items, int count, int currentPage, int pageSize)
    {
        CurrentPage = currentPage;
        PageSize = pageSize;
        TotalItems = count;
        TotalPages = (int)Math.Ceiling(count / (double)pageSize);

        AddRange(items);
    }

    public int CurrentPage { get; private set; }
    public int PageSize { get; private set; }
    public int TotalPages { get; private set; }
    public int TotalItems { get; private set; }
    public bool HasPrevious => CurrentPage > 1;
    public bool HasNext => CurrentPage < TotalPages;
}
PaginatedList.cs

The "PaginatedList" class, along with some query extensions, will be used in our model code to interact with entity framework to form our queries.

Creating a Paging View Component

Once we have our paging interface defined we can create the pagination  view component.  View components are very similar to Razor Pages in that they have a .cshtml view page and .cshtml.cs model file.

In our view component we'll accept an IPaginatedList instance and create a view model to pass to the component UI.

public class PaginationViewComponent: ViewComponent
{
    public PaginationViewComponent() { }
    public IViewComponentResult Invoke(IPaginatedList values)
    {
        return View("Default", new PaginationViewModel(){List = values, , Dictionary<string,string> routeData = null});
    }
}

public class PaginationViewModel
{
    public IPaginatedList List { get; set; }
    public Dictionary<string, string> RouteData { get; set; }
}

The model class is fairly simple but the UI portion should make it clear why I wanted to make this into a view component, as opposed to just a code snippet I could copy and past where needed.

@model PagingExample.Pages.Components.Pagination.PaginationViewModel

<div class="d-flex justify-content-center">
  <nav aria-label="Pagination">
    <ul class="pagination pg-blue">
      @if (Model.List.HasPrevious)
      {
        <li class="page-item ">
          <a class="page-link" asp-all-route-data="Model.RouteData" asp-route-p="@(Model.List.CurrentPage - 1)" tabindex="-1" title="previous">
            <span>&laquo;</span>
          </a>
        </li>
      }

      @for (var i = 1; i <= Model.List.TotalPages; i++)
      {
        <li class="page-item @(i == Model.List.CurrentPage ? "active" : "")">
          <a asp-all-route-data="Model.RouteData" asp-route-p="@i" class="page-link">@i</a>
        </li>
      }

      @if (Model.List.HasNext)
      {
        <li class="page-item ">
          <a class="page-link" title="next" asp-all-route-data="Model.RouteData" asp-route-p="@(Model.List.CurrentPage + 1)">
            <span>&raquo;</span>
          </a>
        </li>
      }
    </ul>
  </nav>
</div>

I then insert this component in my razor pages with the following code.

<table>
 ...
</table>

<vc:pagination values="Model.Widgets" route-data="Model.LinkData"/>

This will give me the following UI into my pages.

asp-all-route-data

If you have looked at the Microsoft tutorial you'll notice that the page links contain a "asp-route-" value for each of the filtering, paging and sorting properties.

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>

This works for a specific pages requirements but I wanted to create a reusable paging component and not every usage may support sorting or some usages may require additional parameters (like a page with a more advanced search form).

Fortunately the ASP.NET Core anchor tag helper supports a property called "asp-all-route-data".  This property accepts a dictionary of name/value pairs that are translated to the equivalent "asp-route-{param}" when generating the link.  

Using the parameter the pagination component only has to know about the paging specific parameters and can let the users of the component take care of passing in other parameters as needed.

That means that the anchor tags in our component can look like this.

<a asp-all-route-data="Model.RouteData" asp-route-p="@i" class="page-link">@i</a>

In the page model I created a "LinkData" property that can be setup with link values that the view can pass on to the component.

public Dictionary<string, string> LinkData =>
    new Dictionary<string, string>()
    {
    	{"isActive", IsActive},
        {"filter", Filter},
        {"sortBy", SortBy}
    };

The Page Model

To support paging and filtering in our page model I needed to create properties for the filter value and the current page.  I wanted all the values to be added to the page query string so I could have passed the values in to the "OnGetAsync" method but I chose instead to use bindable properties on my page model that supports get binding.  So instead of a parameter to the get method I have a filter property like this.

[BindProperty(SupportsGet = true)]
public string Filter { get; set; }

One issue I ran into when creating my paging parameters is that I wanted to use a query string parameter called "page" to indicate the current page.  I thought it would end up with a nice url like "https://mysite.com/widgets?page=3&filter=cog". Unfortunately, ASP.NET kept trying to treat the "page" name  as a key word when generating links.  So I had to fall back to using a property called "PageIndex" with a name of "p" in the query string.

[BindProperty(SupportsGet = true, Name = "p")]
public int PageIndex { get; set; } = 1;

Database Paging and Filtering

Entity Framework Core makes it pretty easy to page and filter database queries.  

With the addition of some "IQueryable" extension methods I was able to trim my page model get method down to the following.

 public async Task<IActionResult> OnGetAsync()
 {
     Widgets = await _dbContext.Widgets.Filter(Filter).OrderBy(x => x.Name).ToPagedList(PageIndex, AppConfig.PageSize);

    return Page();
}

The "ToPagedList" extensions methods encapsulate two database calls. Once to get a total count of available items and one to retrieve the page of requested items using the "Skip" and "Take" methods.

public static class PaginationExtensions
    {
        public static async Task<PaginatedList<T>> ToPagedList<T>(this IQueryable<T> source, int page = 1, int pageSize = AppConfig.PageSize) where T:class
        {
            var count = await source.CountAsync();
            var items = await source.Skip( (page - 1) * pageSize).Take(pageSize).ToListAsync();
            
            return new PaginatedList<T>(items, count, page, pageSize);
        }

        public static async Task<PaginatedList<TResult>> ToPagedList<T, TResult>(this IQueryable<T> source, 
            Func<T, TResult> selectFunc, 
            int page = 1, 
            int pageSize = AppConfig.PageSize)where TResult:class where T:class
        {
            var count = await source.CountAsync();
            var items = await source.Skip( (page - 1) * pageSize).Take(pageSize).ToListAsync();

            return new PaginatedList<TResult>(items.Select(selectFunc).ToList(), count, page, pageSize);
        }
    }

For filtering values I created a similar extension method on my model class for applying the desired query constraints.

public static class WidgetQueryExtensions{
    
    public static IQueryable<Widget> Filter(this IQueryable<Widget> query, string filter){
        if(string.IsNullOrWhiteSpace(filter)){
            return query;
        }

        filter = filter.ToLower();

        return query.Where(x => 
            x.Name.ToLower().Contains(filter)
            || x.PartNumber.ToLower().Contains(filter)
            || x.Description.ToLower().Contains(filter)
        );
    }
}

Project Source

You can find an example project with the component and queries over on GitHub: https://github.com/hansonio/post_pagingexample

Other Scenarios

  • Add Sorting by column headers? Do it in reusable way?
  • Create a more involved filter form

Learn More