The Pagination Model
The foundation of a reusable pagination is a well-structured model. I use the ever so straightforward name... PaginationModel
. The goal here is designed to be generic, accommodating any type of content that requires pagination. Therefore, the primary things we need to take into consideration are:
- How many items per page? -
PageSize
, - How many pages? -
TotalPages
- Which page are we on? -
CurrentPage
- How do I navigate through my pages? Well, probably by knowing where in the site we are, so the
Url
- What items are we displaying on this page? -
Items
This last piece - the Items
- is particularly important because we're using IEnumerable<T>
to make this property generic. This means that when we create our model, we need to know what T is. So our model has to be named PaginationModel<T>
so we can give it our generic T
and get out the items we want, no matter what they are.
public class PaginationModel<T>
{
public int PageSize { get; set; }
public int TotalPages { get; set; }
public int CurrentPage { get; set; }
public string? Url { get; set; }
public IEnumerable? Items { get; set; }
}
Our Pagination Services
Because I'm writing this for modern versions of Umbraco and its use of dependency injection, it's important to set up both an interface and a class for our pagination service. Therefore, we start with IPaginationService
and follow with its implementation, PaginationService
.
This is where the meat of the functionality happens, and while there is only one method, you're going to see a lot of <T>
here because that is what is holding our articles, events, videos, or whatever list we're paginating!
public interface IPaginationService
{
PaginationModel<T> Paginate<T>(IEnumerable<T> items, string url, int page, int pageSize);
}
After that it's a bit of skip and take logic that handles the pagination and returns the items meant to display on this page only in our PaginationMode<T>
.
public class PaginationService : IPaginationService
{
public PaginationModel<T> Paginate<T>(IEnumerable<T> items, string url, int page, int pageSize)
{
//get the total pages for pagination
var totalPages = (int)Math.Ceiling((double)items.Count() / (double)pageSize);
// Set the pagination start and end based on the total pages
if (page >= totalPages)
{
page = totalPages;
}
else if (page < 1)
{
page = 1;
}
// Skip the current page minus one and take the amount we want, that's our pagination!
var paginatedItems = items.Skip((page - 1) * pageSize).Take(pageSize);
return new PaginationModel<T>()
{
PageSize = pageSize,
CurrentPage = page,
TotalPages = totalPages,
Url = url,
Items = paginatedItems
};
}
}
And, of course, the final step is registering the service in our IUmbracoBuilder
. I usually have an UmbracoBuilderExtensions
file that handles all of this, but you should use whatever your standard process is. In the location where you register your services, you'll want to add this line:
builder.Services.AddSingleton<IPaginationService, PaginationService>();
For more information on how to add services to Umbraco's dependency injection, you can check the Umbraco docs! This is the explicit technique that I am referencing.
Rendering the Data
There are two general ways we might want to view our pagination - in a JSON response from an API call for an application solution, or in a view model to render straight into the front-end. Both of these are zippy to implement.
For this particular example, I am going to show how it works with an Article
class that I have built with ModelsBuilder in Umbraco - but remember, this is generic! It means you can use it with any class you want - only how you get your initial list of items to paginate will be different depending on your implementation.
Option 1: An API Response
For accessing your paginated items through an API, create a Controller that inherits from UmbracoApiController
to hold the logic you're going to pass into your service.
For the purposes of this example, I am going to assume you are deciding on the front-end how many articles you want to have as well as what page you are on and what your current URL is. That means you will need to pass your parameters into the call in GetArticles(string url, int pageSize, int page)
.
It's also important to note that because I am not using the content API that was introduced in v12, I have made a small ArticleSnippet
model that I don't display directly in the article but reference in the code. This pulls in only the title, URL, summary, and an image URL for the article. You can use whatever you want in little models like this!
public class ArticlesApiController : UmbracoApiController
{
private readonly IUmbracoContextAccessor _umbracoContext;
private readonly IPaginationService _paginationService;
public ArticlesApiController(IUmbracoContextAccessor umbracoContext, IPaginationService paginationService)
{
_umbracoContext = umbracoContext;
_paginationService = paginationService;
}
[HttpGet]
public PaginationModel<ArticleSnippet> GetArticles(string url, int pageSize, int page)
{
if (_umbracoContext.TryGetUmbracoContext(out var ctx))
{
// Use your favorite way of getting your list of articles in Umbraco
var umbracoArticles = ctx.Content?.GetByXPath("//articlesLanding/article").OfType<Article>();
if (umbracoArticles != null)
{
// Rendering the entire article outside of the content api in v12+ isn't great
// we'll make a smaller model that gives us only what we need
var articleSnippets = umbracoArticles.Select(article => new ArticleSnippet
{
Title = article.Name,
Url = article.Url(),
Summary = article.Summary,
Image = article.Image != null ? article.Image.GetCropUrl(600, 200) : null
});
// We pass our snippets into the pagination, not the original articles from Umbraco
var paginatedArticles = _paginationService.Paginate(articleSnippets, url, page, pageSize);
return paginatedArticles;
}
}
return new PaginationModel<ArticleSnippet>();
}
}
And now you can call it with a GET post using something like /umbraco/api/ArticlesApi/GetArticles?url=/blog&page=1&pageSize=12
.
Option 2: A View Component and Razor View
If you want to use your pagination straight in a view, then the best way to do that is to pass what you need in through a ViewComponent
.
The ViewComponent Class
In this example, my PaginatedArticlesViewComponent
serves as the bridge between the service layer and the presentation layer instead of an API Controller that serves up the data. However, just like the previous example, it utilizes the IPaginationService
to fetch paginated content based on my list of items, the current page, specified page size.
I also use a little bit of sneaky HttpContext reading to get parameters from the querystring because even though it may not remain accurate, I think being able to bookmark and link directly to pages in a paginated list is very useful!
One thing you'll notice is that even though I call the pagination, I don't actually pass in T
. Because my articles are already strongly typed and T
is the same throughout the service, when I pass them in, it already knows what the type is.
public class PaginatedArticlesViewComponent : ViewComponent
{
private readonly IHttpContextAccessor _contextAccessor;
private readonly IPaginationService _paginationService;
public PaginatedArticlesViewComponent(IHttpContextAccessor contextAccessor, IPaginationService paginationService)
{
_contextAccessor = contextAccessor;
_paginationService = paginationService;
}
public IViewComponentResult Invoke(IEnumerable<Article> articles, string url, int pageSize)
{
var ctx = _contextAccessor.GetRequiredHttpContext();
if(ctx != null)
{
// We get the querystring here from the request
var query = ctx.Request.Query;
// Find the page variable if it exists
var page = query["page"].ToString();
// If it doesn't exist, we set it to 1; but it will default to 1 when we paginate anyway. This is just so we can make it an int.
var pageId = page.IsNullOrWhiteSpace() ? 1 : int.Parse(page);
// Here we pass our articles, url, the page id, and the page size into the service, then return the nicely paginated package.
var model = _paginationService.GetArticlesWithPagination(articles, url, pageId, pageSize);
return View("/Views/Partials/ViewComponents/PaginatedArticles.cshtml", model);
}
return null;
}
}
This component is invoked within the Razor view, passing in the necessary parameters such as the list of articles, the current URL, and the desired page size - this is because I like to allow my front-end colleagues to control the more display dynamics of it, and often because I am passing in things I am already receiving from the Umbraco context. No reason to reinvent the wheel!
The Razor View
Finally, the Razor view ArticlesLanding.cshtml
and the partial view PaginatedArticles.cshtml
render the paginated content and the pagination controls, respectively. We use the main view to invoke the PaginatedArticlesViewComponent
with the relevant parameters because it gives our front-enders control and flexibility without having to ask us to change variables whenever they need an adjustment.
Then, the partial view iterates over the paginated items and displays the pagination controls based on the PaginationModel
.
@* ArticlesLanding.cshtml *@
@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.ArticlesLanding>
@using ContentModels = Client.Core.Models.Generated;
@using Client.Core.ViewComponents;
@{
Layout = "Main.cshtml";
}
<section class="comp articles-section">
<div class="container-lg">
@* THE ARTICLES ARE CALLED HERE *@
@(await Component.InvokeAsync<PaginatedArticlesViewComponent>(new
{
articles = Model.Children?.OfType<ContentModels.Article>(),
url = Model.Url(),
pageSize = 12
}))
</div>
</section>
@* PaginatedArticles.cshtml *@
@inherits UmbracoViewPage<PaginationModel<ContentModels.Article>>
@using Client.Core.Models
@using ContentModels = Client.Core.Models.Generated;
@if (Model.Items != null && Model.Items.Any())
{
<div class="row">
@foreach (var articleCard in Model.Items.OfType<ContentModels.Article>())
{
<div>
@* Article Stuff Here *@
@articleCard.Name
</div>
}
</div>
<ul class="pagination">
@if(Model.TotalPages > 1)
{
for (int i = 1; i <= Model.TotalPages; i++)
{
var activeClass = Model.CurrentPage == i ? " active" : string.Empty;
<li><a href="@Model.Url?page=@i" class="pagination-link@(activeClass)">@i </a></li>
}
}
</ul>
}