Creating a Paged List in Umbraco using Razor Blog post

Umbraco

Note: This is an old post for Umbraco 4. In newer versions such as Umbraco 6 and 7 you should be using iPublishedContent and Partial Views and not DynamicNode and Macros.

In this blog post I'll show you how to display a list of Umbraco pages with some simple pagination using the new Razor scripting language added in the latest version of Umbraco. I'll be using Umbraco 4.7 as the basis of this post.

The Problem

Imagine, for instance, we have a section within our site with a lot of child pages you want to list (such as a News section). Your content tree might look like this:

Instead of listing them all in one long list it would be nice to add pagination to break-up the long list into manageable sections. Wouldn't it be preferable to show the following output instead?

Well, using Razor scripts you can achieve this goal quite easily! Plus it is much simpler to implement than it would be using an equivalent XSLT macro.

Getting Started

You can create a new Razor script in the Umbraco back-office section by going to the Developer section and right-clicking the Scripting Files folder. Call the file RazorListing and add the following basic script (this is using cshtml):

 
@inherits umbraco.MacroEngines.DynamicNodeContext
@{
   
int pageSize;// How many items per page
   
int page;// The page we are viewing

   
/* Set up parameters */
   
   
if (!int.TryParse(Parameter.PageSize,out pageSize))
   
{
        pageSize
=6;
   
}

   
if (!int.TryParse(Request.QueryString["page"],out page))
   
{
        page
=1;
   
}

   
/* This is your basic query to select the nodes you want */

   
var nodes = Model.Children.Where("Visible").OrderBy("displayDate desc");
   
   
int totalNodes = nodes.Count();
   
int totalPages = (int)Math.Ceiling((double)totalNodes /(double)pageSize);
   
   
/* Bounds checking */
   
   
if (page > totalPages)
   
{
        page
= totalPages;  
   
}
   
elseif (page <1)
   
{
        page
= 1;
   
}
}

<h2>Found @totalNodes results.Showing Page @page of @totalPages</h2>

<ul>
    @foreach (var item in nodes.Skip((page - 1) * pageSize).Take(pageSize))
    {
        <li><a href="@item.Url">@item.Name</
a>(@item.DisplayDate.ToShortDateString())</li>
   
}
</ul>

<ul class="paging">
    @for (int p = 1; p < totalPages + 1; p++)
    {
        string selected = (p == page) ? "selected" : String.Empty;
        <li class="@selected"><a href="?pa[email protected]" title="Go to page @p of results">@p</
a></li>
   
}
</ul>

How to Use

Once you have created your script you can add it as a Macro to your Umbraco template as usual. You can then add a PageSize parameter to it and supply whatever value you like your page size to be. This will give you something like this:

<umbraco:Macro Alias="RazorListing" PageSize="5" runat="server"></umbraco:Macro>

How The Script Works

The script is relatively simple. We have two integer variables called pageSize and page. The variable called pageSize is populated via a Parameter that passes the value through from a macro. Because macros pass everything as strings we first check it can be parsed as an Int and then perform the conversion. If, for some reason, it can't be converted we assign a default value of 6.

int pageSize;// How many items per page
int page;// The page we are viewing

/* Set up parameters */
   
if (!int.TryParse(Parameter.PageSize,out pageSize))
{
    pageSize
= 6;
}

We also populate our page variable by checking whether a page number has been passed in via the query string (if not we default to page 1).

if (!int.TryParse(Request.QueryString["page"], out page)) 
{
     page
= 1;
}

After this we perform our basic query against the Model (which represents the current page node) to retrieve all child pages that are visible which we then order by our custom displayDate in descending order (so we get the newest first). We store the result in a variable called nodes.

var nodes = Model.Children.Where("Visible").OrderBy("DisplayDate desc");

Note: Umbraco 4.7 has a bug that stops it ordering properly using "native" properties such as CreateDate or UpdateDate. If you try and use these you'll find ordering doesn't work. This should be fixed in 4.71.

We then count the number of pages returned by our LINQ-style query and store the value in an integer called totalNodes. We then work out how many pages of results there are by dividing totalNodes by our pageSize and rounding the results up. We also do a little check to ensure that the current page isn't out of bounds (in case someone has naughtily edited the query string).

var nodes =Model.Children.Where("Visible").OrderBy("DisplayDate desc");
   
int totalNodes = nodes.Count();
int totalPages = (int)Math.Ceiling((double)totalNodes /(double)pageSize);
   
/* Bounds checking */
   
if (page > totalPages)
{
    page
= totalPages;  
}
elseif (page <1)
{
    page
= 1;
}

We can then display the results by looping through the nodes using the Skip and Take IEnumerable extension methods to ensure we only iterate the current page of nodes.

<h2>Found @totalNodes results. Showing Page @page of @totalPages</h2>

<ul>
    @foreach (var item in nodes.Skip((page - 1) * pageSize).Take(pageSize))
    {
       
<li><a href="@item.Url">@item.Name</a> (@item.DisplayDate.ToShortDateString())</li>
    }
</ul>

Once we've done this we then can display some basic pagination using a simple for-loop to generate an unordered list with a link for each of the pages intotalPages. The link then passes in the page to jump to as part of the query string. We also check whether the page we are displaying is the current page and, if it is, give it a different CSS class called "selected".

<ul class="paging">
    @for (int p = 1; p < totalPages + 1; p++)
    {
        string selected = (p == page) ? "selected" : String.Empty;
       
<li class="@selected"><a href="[email protected]"title="Go to page @p of results">@p</a></li>
    }
</ul>

But We Can do Better...

This is nice, but there were a couple of niggling problems that bugged me about this. First off, using the algorithm that uses Skip() and Take() to get the current page of results is a little clunky. Wouldn't it be better if it was simplified to a method we could call?

Secondly, the code that generates the paging is all mixed in with the listing. But what if we wanted to have paging elsewhere? Do we really want to copy and paste this code around? That's not very DRY now, is it?

So How Can We Improve Things?

Well, the first thing we can do is create a custom extension method that helps us grab the "page" of results we want. The Umbraco blog on Razor points us in the correct direction. The quickest way to create an extension method is simply to add a static class file to your App_Code folder (you can always create a proper class library project later). What I did was create a C# class called RazorExtensions.cs in App_Code that looked like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using umbraco.MacroEngines;

namespace Diplo
{
   
public static class RazorExtensions
   
{
       
public static DynamicNodeList Paged(this DynamicNodeList nodes,int page,int pageSize)
       
{
           
return new DynamicNodeList(nodes.Items.Skip((page -1)* pageSize).Take(pageSize));
       
}
   
}
}

As you can see this method "extends" the DynamicNodeList that Umbraco returns and performs the skip and take operation using a more fluid style.

To solve the second problem we can use something called a Razor Helper file that Microsoft introduced as part of the Razor scripting engine. These work just as well with Umbraco as they do in ASP.NET MVC and WebMatrix. You also can create these in the App_Code via Visual Studio. Just right-click yourApp_Code folder and select Add File and add a new Razor Helper. I just created a file called DiploHelpers which created a DiploHelpers.cshtml file for me. Into this file I created this simple paging helper method:

@helper GeneratePaging(int currentPage,int totalPages)
{
   
if (totalPages > 1)
   
{
       
<ul class="paging">
       
@for (int p = 1; p < totalPages + 1; p++)
       
{
           
string selected =(p == currentPage)?"selected":String.Empty;
           
<li class="@selected">
               
<a href="[email protected]" title="Go to page @p of results">@p</a>
            </
li>
       
}
       
</ul>
   
}
}

As you can see this takes our former paging code and encapsulates it within a helper that can be re-used elsewhere. If, at a later date, you want to make this simple method a little more complex (e.g. add previous and next links) then you only need to alter the code in one place.

Putting it All Together

I'll show you below how you can use these helpers in a new version of the paging script that replaces the last part (that generates the pages list and displays pagination) with the following code:

<ul>
    @foreach (var item in nodes.Paged(page, pageSize))
    {
       
<li><a href="@item.Url">@item.Name</a> (@item.DisplayDate.ToShortDateString())</li>
    }
</ul>

@DiploHelpers.GeneratePaging(page, totalPages)

As you can see we call the Paged extension method against our nodes and include the GeneratePaging script via the @DiploHelpers directive (which matches the name of our .cshtml script in App_Code). Note how we can pass in variables to both extension methods and Razor helpers.

So there you go - paging in Umbraco using Razor. Simples!


9 Comments


Greg McMullen Avatar

So, my question is how can we improve this even further?

Instead of displaying (in my case) 39+ pages. How can we attempt to only show the CURRENT page and 2 or 3 on either side PLUS first and last if somewhere in the middle.

Almost like a Take First / Take Last. Take Current +/- 3 on either side of current.

Any suggestions here?


Dan Diplo Avatar

I'll give you a clue about using "windowed" paging...

Here's a bit of an algorithm to get you started:

int range = window * 2;
int start = (currentPage - window) < 0 ? (currentPage - window) : 1;
int end = start + range;
if (end < totalPages)
{
    end = totalPages;
    start = (end - range) > 0 ? (end - range) : 1;
}

That will give you a range of pages between the start and end. The rest you can work out yourself :)


Anthony Avatar

Hi Dan,

Thanks again for a great blogpost. I just implemented your solution in my Umbraco 4.11.8 instance and it just works fine.

I just wonder, if it can't be made even a bit more DRY by abstracting away this code:

int pageSize; // How many items per page pageSize = 5; int page;

if (!int.TryParse(Request.QueryString["page"], out page)) { page = 1; }
var nodes = Model.Children.Where("Visible").OrderBy("createDate desc");
int totalNodes = nodes.Count(); int totalPages = (int)Math.Ceiling((double)totalNodes / (double)pageSize);
if (page > totalPages) { page = totalPages; } else if (page < 1) { page = 1; }

As for now, this code is in every .cshtml script that implements paging.

Isn't there a way to move it away to a Razor Helper?

greetings,

Anthony


Dan Diplo Avatar

@Anthony - you are totally correct it can be made more DRY - in fact, this is how I use it. The code you see there is really just a simplified version of what I now use - just to get people started.

So what I do is create a class called Paging into which I pass the collection of Nodes that need paginating. This class then has properties and methods that do all the calculations - so I have a properties like int CurrentPage, int TotalPages, int TotalNodes and DynamicNodeList CurrentNodes (which returns just the current page of nodes).

I then pass an instance of this class to the razor helper that renders the pagination as an unordered list.

Hope that gives you some ideas!


Amir Avatar

How could you read if the current page is the last page and add a next button?


Dan Diplo Avatar

Hi Amir. If CurrentPage is less than TotalPages then you know you can show a next button. Likewise if CurrentPage is greater than 1 then you know you can show a previous button.


Mark Downie Avatar

Hi,

This is a great post. Very clear, thank you.

Thanks, Mark


Scott L Avatar

great post


Gavin Avatar

Thank you for this.

Just fill in the form and click Submit. But note all comments are moderated, so spare the viagra spam!

Tip: You can use Markdown syntax within comments.