Adding Simple Pagination to a Bootstrap Table in ASP.NET Core

I recently needed to add simple pagination to a Bootstrap table on a page in my ASP.NET Core project. I’m using MVC Razor Pages, but a similar approach will work with Razor Views.

My data set contains 270+ rows, and originally I was returning all 270+ rows to the client and displaying them in a table that scrolled well past the end of the page. I only want to show ten at a time with a simple paging control. Here’s what the final product will look like:

The following is a standard Bootstrap table generated from items in Model.Stocks from our page model:

<table class="table table-striped table-bordered table-sm table-responsive">
<thead>
    <tr>
        <th scope="col">Symbol</th>
        <th scope="col">Name</th>
        <th scope="col">Sector</th>
        <th scope="col">Price</th> 
    </tr> 
    </thead> 
    <tbody> 
        @foreach (var item in Model.Stocks) 
        { 
            <tr> 
                <th scope="row">@item.Symbol</th> 
                <td>@item.Name</td> 
                <td>@item.Sector</td> 
                <td>@item.Price</td> 
            </tr> 
        } 
    </tbody> 
</table>

Here is the page model:

public class DividendPicksModel : PageModel 
{ 
    private readonly IStockService _stockService;
    
    public DividendPicksModel(IStockService stockService)
    {
        _stockService = stockService ?? throw new ArgumentNullException(nameof(stockService));
    }
    
    public PaginatedList<StockViewModel> Stocks { get; private set; }
    
    public async Task<IActionResult> OnGetAsync(int? pageIndex, CancellationToken cancellation)
    {
        var stocks = await _stockService.GetCachedStockResources(cancellation) ?? new List<Stock>();
        Stocks = PaginatedList<StockViewModel>.Create(stocks.Select(s => new StockViewModel(s)), pageIndex ?? 1, 10, 5);
        return Page();
    }   
}

The page implements public async Task<IActionResult> OnGetAsync(int? pageIndex, CancellationToken cancellation)

This is responsible for handling GET requests for the page. There’s not too much to it: retrieve the data from a domain service, set the Stocks collection property on the page model, and return the page.

Note that the method takes int? pageIndex as a parameter which indicates which page of the table is being requested by the client. Not passing this parameter will cause the the first page of the table to be returned.

The interesting thing to note here is that the Stocks property is not a standard .NET collection. It’s a PaginatedList<Stock>. PaginatedList<T> derives from List<T> and represents a “page” of our data, and it exposes the properties needed to enable paging functionality for the collection on the view. Let’s take a look at it:

public class PaginatedList<T> : List<T> 
{ 
    private PaginatedList(
        List<T> items, int count, int pageIndex, int pageSize, int countOfPageIndexesToDisplay) 
    { 
        PageIndex = pageIndex; 
        TotalPages = (int)Math.Ceiling(count / (double)pageSize);

        SetPageIndexesToDisplay(pageIndex, countOfPageIndexesToDisplay); AddRange(items); 
    }

    public int PageIndex { get; }
    
    public int TotalPages { get; }
    
    public bool HasPreviousPage => (PageIndex > 1);
    
    public bool HasNextPage => (PageIndex < TotalPages);
    
    public List<PageIndex> PageIndexesToDisplay { get; private set; }
    
    public static PaginatedList<T> Create(
        IEnumerable<T> source, int pageIndex, int pageSize, int countOfPageIndexesToDisplay = 3)
    {
        var list = source.ToList();
        var count = list.Count;
        var items = list.Skip((pageIndex - 1) * pageSize)
                        .Take(pageSize).ToList();

        return new PaginatedList<T>(items, count, pageIndex, pageSize, countOfPageIndexesToDisplay);
    }
    
    private void SetPageIndexesToDisplay(int pageIndex, int countOfPageIndexesToDisplay)
    {
        PageIndexesToDisplay = new List<PageIndex>();  
  
        if (pageIndex > TotalPages - countOfPageIndexesToDisplay + Math.Floor(countOfPageIndexesToDisplay / 2.0m))
        {
            for (var i = Math.Max(TotalPages - countOfPageIndexesToDisplay + 1, 1); i <= TotalPages; i++)
            {
                PageIndexesToDisplay.Add(new PageIndex(i, i == pageIndex));
            }
        }
        else if (pageIndex < (countOfPageIndexesToDisplay + 1) - Math.Floor(countOfPageIndexesToDisplay / 2.0m))
        {
            for (var i = 1; i <= Math.Min(countOfPageIndexesToDisplay, TotalPages); i++)
            {
                PageIndexesToDisplay.Add(new PageIndex(i, i == pageIndex));
            }
        }
        else
        {
            var startIndex = pageIndex - (int)Math.Floor(countOfPageIndexesToDisplay / 2.0m);
            for (var i = startIndex; i <= startIndex + countOfPageIndexesToDisplay - 1; i++)
            {
                PageIndexesToDisplay.Add(new PageIndex(i, i == pageIndex));
            }
        }
    }  
} 

We create a PaginatedList by calling into the static Create method and passing it the collection that will be paged, the index of the page we need to display, the page size, and the number of page indexes we want to display in the pagination control at a time (in the screenshot at the beginning of the post it’s five).

PaginatedList exposes several properties about the page of data that it represents:

  • PageIndex
  • TotalPages
  • HasPreviousPage
  • HasNextPage
  • PageIndexesToDisplay

… and, of course, since it derives from List<T>, we have access to the list of items on our page.

The most complex part of PaginatedList<T> is SetPageIndexesToDisplay(..), which builds the collection of page indices to display in the Bootstrap pagination control based on the current page index and the total count of indices to display. PageIndex is a simple POCO that contains the index number and a boolean indicating whether that page index is the active (displayed page:

public class PageIndex 
{ 
    public PageIndex(int index, bool isActive) 
    { 
        Index = index; 
        IsActive = isActive; 
    }

    public int Index { get; }
    
    public bool IsActive { get; }
    

} 

The final piece to pull this all together is the actual pagination control on the view. Immediately below the table in our HTML we have the following:

@{
    var prevDisabled = !Model.Stocks.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Stocks.HasNextPage ? "disabled" : "";
}
<nav aria-label="Pagination" class="col-12">
    <ul class="pagination justify-content-end">
        <li class="page-item @prevDisabled">
            <a class="page-link" 
                asp-page="./MyPage" 
                asp-route-pageIndex="@(Model.Stocks.PageIndex - 1)"
                aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
                <span class="sr-only">Previous</span>
            </a>
        </li>
        @foreach (var i in Model.Stocks.PageIndexesToDisplay)
        {
            var activeClass = i.IsActive ? "active" : "";
            
            <li class="page-item @activeClass"><a class="page-link"  
                                        asp-page="./DividendPicks"
                                        asp-route-pageIndex="@(i.Index)">@i.Index</a></li>
        }
        <li class="page-item @nextDisabled">
            <a class="page-link" 
                asp-page="./MyPage"
                asp-route-pageIndex="@(Model.Stocks.PageIndex + 1)"
                aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
                <span class="sr-only">Next</span>
            </a>
        </li>
    </ul>
</nav>

Here we use the properties exposed by our PaginatedList<Stock> to control how the pagination control is rendered and functions. We enable/disable the previous/next buttons based on the HasPreviousPage and HasNextPage properties, respectively. We build the numeric page index buttons from the PageIndexesToDisplay collection, and we set the link appropriately for the given index.

Clicking one of the links in the pagination control requests the page again passing the given page index and the PaginatedList<Stock> is rebuilt for the requested page.

Wrapping Up

We’ve explored here a simple, no-frills way to add pagination to a Bootstrap table. There are a couple of improvements that could be made:

  • Interacting with the pagination control causes a refresh of the entire page. This could be modified to execute a little Javascript to hit an endpoint that returns the new page of data and update the table and pagination control in-place without reloading the whole page.
  • This implementation retrieves the entire data set from the service and then applies paging within our app service. This is fine for my purposes since my dataset is small, but for larger datasets I would push the execution of the paging (skip/take operators) to the database. (If using Entity Framework, this could be as simple as deferring query execution until after the Skip and Take are applied in PaginatedList<T>.Create(..).)

I may consider these enhancements in the future, but for now this implementation suits my needs just fine.