#28 Starting Offset Pagination
Saturday, April 27, 2019
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.
Parts
- Part 29: Offset Pager Urls
- Part 28: Offset Pager Start
- Part 27: Mock Context Builder
- Part 26: Mock Repository
- Part 25: Mock Async
- Part 24: Picture Tag Helper
- Part 23: Img DPR Tag Helper
- Part 22: Img Responsive Tag Helper
- Part 21: Img Optimized Display
- Part 20: Img Optimization
- Part 19: Img Lazy Loading
- Part 18: Img Responsive
- Part 17: Bottom Nav
- Part 16: Main Nav Cookie
- Part 15: Main Nav Mobile
- Part 14: Main Nav Search
- Part 13: Main Nav Auth
- Part 12: Main Nav Anchors
- Part 11: Main Nav Logo
- Part 10: Search Results
- Part 9: Search Manager
- Part 8: Search Start
- Part 7: Seeding the Database
- Part 6: Domain Database
- Part 5: Emailing Exceptions
- Part 4: Mailkit
- Part 3: View Renderer
- Part 2: Upgrade to 2.1
- Part 1: Quick Start
Code behind setup
- WebUi
- Pages
- Offset-Paging.cshtml.cs
- Pages
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();
}
}
}
View setup
- WebUi
- Pages
- Offset-Paging.cshtml
- Pages
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>
Fundamentals of offset paging
- WebUi
- Pages
- Offset-Paging.cshtml.cs
- Pages
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();
}
}
...
}
Adding the page to our route
- WebUi
- Pages
- Offset-Paging.cshtml
- Pages
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}"
...
Taking care of the complication
- WebUi
- Controllers
- OffsetPagingController.cs
- Controllers
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});
}
}
}
Encapsulating the paging behaviour
- WebUi
- Features
- Paging
- OffsetPager.cs
- Paging
- Features
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();
}
}
}
Initial use of the pager
- WebUi
- Pages
- Offset-Paging.cshtml.cs
- Pages
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();
}
}
...
}
Determining if we need to redirect
- WebUi
- Features
- Paging
- OffsetPager.cs
- Paging
- Features
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);
}
}
}
Updating the code behind for redirecting
- WebUi
- Pages
- Offset-Paging.cshtml.cs
- Pages
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();
}
}
...
}
Combining things
- WebUi
- Features
- Paging
- OffsetPager.cs
- Paging
- Features
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);
}
...
}
}
Back to the code behind
- WebUi
- Pages
- Offset-Paging.cshtml.cs
- Pages
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();
}
}
...
}