Advertisement

#28 Starting Offset Pagination

In this article we will discover the parameters that we need in order to perform offset paging which we will get both from the user via the url and provided by us. Once we have the required information we will create a pager class that we will use to encapsulate the functionality that we need. Using the pager we will determine the cases when we would need to redirect the user and where to redirect them to as well as selecting the correct entities from the database based on the current page.

Code behind setup

  • WebUi
    • Pages
      • Offset-Paging.cshtml.cs

We are going to start by doing just a little bit of setup. Since we are going to be working on paging we are going to need a collection of data. We are going to use what we have done previously to mock a repository for us to use (a).

Offset-Paging.cshtml.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using WebUi.Features.Mocking;

namespace WebUi.Pages
{
    public class OffsetPagingModel : PageModel
    {
        private readonly OffsetPagingRepository _repository;

        public OffsetPagingModel()
        {
            var context = MockContextBuilder<OffsetPagingContext>.Start()
                .Add(20, x => x.Articles, x => new OffsetPagingArticle
                {
                    Id = x,
                    Title = $"Title {x}"
                })
                .Context;
            
            _repository = new OffsetPagingRepository(context);
        }

        public List<OffsetPagingArticle> Articles { get; private set; }

        public async Task OnGet()
        {
            Articles = await _repository.Articles.ToListAsync();
        }
    }

    public class OffsetPagingArticle
    {
        public int Id { get; set; }
        public string Title { get; set; }
    }

    public class OffsetPagingContext : DbContext
    {
        public DbSet<OffsetPagingArticle> Articles { get; set; }
    }

    public class OffsetPagingRepository : IDisposable
    {
        private readonly OffsetPagingContext _context;

        public OffsetPagingRepository(OffsetPagingContext context)
        {
            _context = context;
        }

        public DbSet<OffsetPagingArticle> Articles => _context.Articles;

        public void Dispose()
        {
            _context?.Dispose();
        }
    }
}
(a) Setting up the code behind for our work on paging.

View setup

  • WebUi
    • Pages
      • Offset-Paging.cshtml

Now that we have some data to work with we need to make sure everything is working the way it should be and that we can actually display the data (b)

Offset-Paging.cshtml

@page
@model OffsetPagingModel
@{
    ViewData["Title"] = "Offset Paging";
}

<div class="articles">
    @foreach (var article in Model.Articles)
    {
        <div class="article">
            <div class="article-title">@article.Title</div>
        </div>
    }
</div>
(b) Setting up our view to simply iterate over and display our test data.

Fundamentals of offset paging

  • WebUi
    • Pages
      • Offset-Paging.cshtml.cs

Next up we are going to modify our code behind so that we have the fundamentals in place to perform offset paging. In the end it really boils down to knowing two different pieces of information. The first would be the current page the user is on and the other is the number of items that we are showing per page (c). Obviously we will get the current page from a binding property and the number of items per page we will just set to a small number for now in this case we will set it to two.

Offset-Paging.cshtml.cs

...
using System.Linq;
...
using Microsoft.AspNetCore.Mvc;
...
namespace WebUi.Pages
{
    public class OffsetPagingModel : PageModel
    {
        ...
        [BindProperty(SupportsGet = true)] public int Pg { get; set; }
        ...
        public async Task<IActionResult> OnGet()
        {
            if (Pg <= 0)
            {
                return RedirectToPagePermanent("Offset-Paging", new { pg = 1 });
            }

            const int take = 2;
            Articles = await _repository.Articles
                .Skip((Pg - 1) * take)
                .Take(take)
                .ToListAsync();

            var total = await _repository.Articles.CountAsync();
            var lastPage = (int) Math.Ceiling(total / (double) take);

            if (Articles.Count == 0 && Pg != 1 && Pg > lastPage)
            {
                return RedirectToPage(new { pg = lastPage });
            }

            return Page();
        }
    }
    ...
}
(c) Modifying our code behind to perform the basics of offset paging.

Adding the page to our route

  • WebUi
    • Pages
      • Offset-Paging.cshtml

In order to have Asp.Net construct our route the way we want we just need to make a very small adjustment to the razor page. Although it will create a small complication I prefer to have what I would consider clean urls and to achieve this we need to update the page declaration (d).

Offset-Paging.cshtml

@page "page-{Pg:int}"
...
(d) Updating our page so that our urls are clean and display our current page the way we want it to.

Taking care of the complication

  • WebUi
    • Controllers
      • OffsetPagingController.cs

The complication that I was mentioning in the previous section arises from the fact that the way we are defining our url requires there to be a string containing 'page-' and the page number otherwise there will be no match. To deal with this we will create a controller that will redirect to page number one if the user does not specify it (e).

OffsetPagingController.cs

using Microsoft.AspNetCore.Mvc;

namespace WebUi.Controllers
{
    [Route("Offset-Paging")]
    public class OffsetPagingController : Controller
    {
        public IActionResult Index()
        {
            return RedirectToPage("/Offset-Paging", new {pg = 1});
        }
    }
}
(e) By adding a controller with an index route that redirects to our page with the page number of one we can handle when the page number is not included instead of just returning a 404.
Advertisement

Encapsulating the paging behaviour

  • WebUi
    • Features
      • Paging
        • OffsetPager.cs

Now that our paging is working we are going start to encapsulate the behaviour in a separate class so that we can easily reuse it elsewhere (f).

OffsetPager.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace WebUi.Features.Paging
{
    public class OffsetPager
    {
        public OffsetPager()
        {
            Take = 24;
        }

        public int LastPage => (int)Math.Ceiling(TotalCount / (double)Take);
        public int Take { get; set; }
        public int TotalCount { get; private set; }

        public async Task<List<T>> GetPageEntitiesAsync<T>(int page, IQueryable<T> queryable)
        {
            TotalCount = await queryable.CountAsync();

            return await queryable
                .Skip((page - 1) * Take)
                .Take(Take)
                .ToListAsync();
        }
    }
}
(f) The beginning of our offset pager class will take care of setting the total count and retrieving the appropriate data from the database.

Initial use of the pager

  • WebUi
    • Pages
      • Offset-Paging.cshtml.cs

We have defined our new pager and now it is time to make use of it (g). Again we are making the number of elements that we take artificially low just to make sure we have several page.

Offset-Paging.cshtml.cs

...
using WebUi.Features.Paging;

namespace WebUi.Pages
{
    public class OffsetPagingModel : PageModel
    {
        ...
        public OffsetPagingModel()
        {
            ...
            Pager = new OffsetPager
            {
                Take = 2
            };
        }
        ...
        public OffsetPager Pager { get; }

        public async Task<IActionResult> OnGet()
        {
            if (Pg <= 0)
            {
                return RedirectToPagePermanent("Offset-Paging", new { pg = 1 });
            }

            Articles = await Pager.GetPageEntitiesAsync(Pg, _repository.Articles);

            if (Articles.Count == 0 && Pg != 1 && Pg > Pager.LastPage)
            {
                return RedirectToPage(new { pg = Pager.LastPage });
            }

            return Page();
        }
    }
    ...
}
(g) Updating our code behind to make use of our pager.

Determining if we need to redirect

  • WebUi
    • Features
      • Paging
        • OffsetPager.cs

Each time we add paging to a page we are going to need to check if the page the use has requested is out of bounds and redirect to another page if needed. Of course this means we would benefit from encapsulating this ability as well (h).

OffsetPager.cs

...
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
...
namespace WebUi.Features.Paging
{
    public class OffsetPager
    {
        private readonly PageModel _model;
        private readonly string _pageName;

        public OffsetPager(PageModel model, string pageName)
        {
            _model = model;
            _pageName = pageName;

            ...
        }
        ...
        public (bool redirect, RedirectToPageResult result) RedirectToFirstPage(int page)
        {
            return page <= 0
                ? (true, _model.RedirectToPagePermanent(_pageName, new { pg = 1 }))
                : (false, null);
        }

        public (bool redirect, RedirectToPageResult result) RedirectToLastPage<T>(
            int page, IList<T> entities)
        {
            return entities.Count == 0 && page != 1 && page > LastPage
                ? (true, _model.RedirectToPage(new { pg = LastPage }))
                : (false, null);
        }
    }
}
(h) Adding the ability for our pager to let us know if we need to redirect to the last page or not.

Updating the code behind for redirecting

  • WebUi
    • Pages
      • Offset-Paging.cshtml.cs

Time to use our new redirecting capabilities (g).

Offset-Paging.cshtml.cs

...

namespace WebUi.Pages
{
    public class OffsetPagingModel : PageModel
    {
        ...
        public OffsetPagingModel()
        {
            ...
            Pager = new OffsetPager(this, "Offset-Paging")
            {
                Take = 2
            };
        }
        ...
        public async Task<IActionResult> OnGet()
        {
            var (redirectFirst, redirectFirstResult) = Pager.RedirectToFirstPage(Pg);

            if (redirectFirst)
            {
                return redirectFirstResult;
            }

            Articles = await Pager.GetPageEntitiesAsync(Pg, _repository.Articles);

            var (redirectLast, redirectLastResult) = Pager.RedirectToLastPage(Pg, Articles);

            if (redirectLast)
            {
                return redirectLastResult;
            }

            return Page();
        }
    }
    ...
}
(g) Now we will let the pager tell us if we need to redirect and provide the redirect result.

Combining things

  • WebUi
    • Features
      • Paging
        • OffsetPager.cs

The last thing we are going to do for now is make it easier for us to do both retrieve the articles and determine if we need to redirect to the last page all in one step (i).

OffsetPager.cs

...
namespace WebUi.Features.Paging
{
    public class OffsetPager
    {
        ...
        public async Task<(bool redirect, RedirectToPageResult result, List<T> entities)> CreateAsync<T>(
            int page, IQueryable<T> queryable)
        {
            var (redirectFirst, redirectFirstResult) = RedirectToFirstPage(page);
            if (redirectFirst)
            {
                return (true, redirectFirstResult, null);
            }

            var entities = await GetPageEntitiesAsync(page, queryable);

            var (redirectLast, redirectLastResult) = RedirectToLastPage(page, entities);

            return (redirectLast, redirectLastResult, entities);
        }
        ...
    }
}
(i) Adding a single method that will perform all of the functionality that we currently need the pager to perform.

Back to the code behind

  • WebUi
    • Pages
      • Offset-Paging.cshtml.cs

Now we just need to make one last update to our code behind to take advantage of the method we just created (j).

Offset-Paging.cshtml.cs

...
namespace WebUi.Pages
{
    public class OffsetPagingModel : PageModel
    {
        ...
        public async Task<IActionResult> OnGet()
        {
            var (redirect, redirectResult, entities) = await Pager.CreateAsync(
                Pg, _repository.Articles);

            if (redirect)
            {
                return redirectResult;
            }

            Articles = entities;

            return Page();
        }
    }
    ...
}
(j) Now a single method call will take care of letting us know if we need to redirect and if we do where to redirect to and if not it will also provide the page of entities that we need.
Exciton Interactive LLC
Advertisement