Building Blazor pager component

I had presentation for local community about Blazor and as a side-product I built something useful. Blazor supports components that are a little bit similar to ones we have in React.js. I took my previous work from my blog posts Paging with Entity Framework Core and Building Pager view component and built simple but generic pager component for Blazor.

Source code available!. Please visit my GitHub repository gpeipman/BlazorDemo for full source code of my Blazor demo application.

Blazor books list

Goals

I had time enough to work on pager component and I stated some goals:

  • component must be generic and cannot depend on actual type of paged results,
  • component must be as independent as possible,
  • component must only notify parent of page change,
  • different from other samples try to keep code and presentation in separate files.

First two points were also covered with my previous works. Last two points were something to achieve on Blazor.

Blazor components

Blazor components are similar to what I’ve seen in React.js. They are added to page using simple syntax like here.

<Pager Result=@Books PageChanged=@PagerPageChanged />

As we can see then my pager simple and clean on page. Result and PageChanged are public properties of pager component. I will show later how they work.

Paged results

Before getting to pager component we need some additional mechanism to hold paged results. There are two important classes held in library that is shared between Blazor project and server back-end – PagedResultBase and PagedResult<T>.

public abstract class PagedResultBase
{
    public int CurrentPage { get; set; }
    public int PageCount { get; set; }
    public int PageSize { get; set; }
    public int RowCount { get; set; }

    public int FirstRowOnPage
    {

        get { return (CurrentPage - 1) * PageSize + 1; }
    }

    public int LastRowOnPage
    {
        get { return Math.Min(CurrentPage * PageSize, RowCount); }
    }
}

public class PagedResult<T> : PagedResultBase where T : class
{
    public IList<T> Results { get; set; }

    public PagedResult()
    {
        Results = new List<T>();
    }
}

PagedResultBase is used by pager and it only has the information about paging side of paged result. PagedResult<T> has additionally list of results returned from server.

Creating Blazor pager component

For pager component I use Blazor Page with backing model. You can find more information about this topic from my blog post Separating code and presentation of Blazor pages.

@inherits PagerModel

@if (Result != null)
{
    <div class="row">
        <div class="col-md-8 col-sm-8">
            @if (Result.PageCount > 1)
            {
                <ul class="pagination pull-right">
                    <li><button onclick="@(() => PagerButtonClicked(1))" class="btn">&laquo;</button></li>
                    @for (var i = StartIndex; i <= FinishIndex; i++)
                    {
                        var currentIndex = i;
                        @if (i == Result.CurrentPage)
                        {
                            <li><span class="btn">@i</span></li>
                        }
                        else
                        {
                            <li><button onclick="@(() => PagerButtonClicked(currentIndex))" class="btn">@i</button></li>
                        }
                    }
                    <li><button onclick="@(() => PagerButtonClicked(Result.PageCount))" class="btn">&raquo;</button></li>
                </ul>
            }
        </div>
    </div>
}

Although the mark-up side is simple there are few things to notice:

  • Null check – when component is loaded it’s possible there’s no data yet and Result property is null.
  • Button click events – there is no good syntax for event with parameters in Blazor yet.
  • currentIndex variable in for loop – the smell of JavaScript closures is here: we cannot use loop variable in Action because it is evaluated when button is clicked and it is clicked when loop is done and loop variable has value of FinishIndex. One trick to avoid this problem is to use local variable in loop.

StartIndex and FinishIndex are properties that tell to pager component from what page to start and to what page to go.

Now let’s add code to backing model.

public class PagerModel : BlazorComponent
{
    [Parameter]
    protected PagedResultBase Result { get; set; }

    [Parameter]
    protected Action<int> PageChanged { get; set; }

    protected int StartIndex { get; private set; } = 0;
    protected int FinishIndex { get; private set; } = 0;

    protected override void OnParametersSet()
    {
        StartIndex = Math.Max(Result.CurrentPage - 5, 1);
        FinishIndex = Math.Min(Result.CurrentPage + 5, Result.PageCount);

        base.OnParametersSet();
    }

    protected void PagerButtonClicked(int page)
    {
        PageChanged?.Invoke(page);
    }
}

The code here is pretty straightforward. We define Result and PageChanged as properties and decorate them with Blazor ParameterAttribute. This tells to tooling that it is possible to give values to these properties on page where component is put. PagerButtonClicked() method is called when user clicks some button of pager. This method fires PagedChanged event. Notice that the event can be zero if there are not delegates attached to it.

Changing page in list view

The page that has pager component listens to PageChanged event and loads new set of data from server when pager button is clicked. I don’t go too much to details in this point. Here is the event handler I’m using in my demo application.

protected void PagerPageChanged(int page)
{
    UriHelper.NavigateTo("/page/" + page);
}

When pager button is clicked then list page uses on of it’s routes where it can give current page index. When navigation happens then list gets new page index and loads the given page from server-side API.

Wrapping up

Blazor pager component built in this post follows good principles of components design. It has minimal public interface for calling pages and it only needs the data to draw out a pager. The component has no idea about how loading of page is implemented – it only fires PageChanged event and it is responsibility of calling page to react and load page of data from server. It also tuned out that we have to be careful when using parameterized click events because using loop counter in actions is not safe. Still we built up nice and simple pager component that is generic enough to use on all pages where data paging is needed.

Liked this post? Empower your friends by sharing it!

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    6 thoughts on “Building Blazor pager component

    • Pingback:The Morning Brew - Chris Alcock » The Morning Brew #2584

    • Pingback:Building a Blazor pager component - How to Code .NET

    • May 18, 2018 at 4:27 pm
      Permalink

      In the onclick in the view, you use a lambda like this:

      @(() => PagerButtonClicked(1))

      Is this actually necessary, or can you just call PagerButtonClicked directly, eg

      @(PagerButtonClicked(1))

      If not, why not?

    • May 18, 2018 at 7:44 pm
      Permalink

      Jon, the current syntax doesn’t support events with parameters. I found this kind of lambda as easiest workaround to this limitation. As Blazor is still very new and there is a lot of work going on, I am sure that for release version there will be something better for parameterized events.

    • November 22, 2018 at 5:27 pm
      Permalink

      Hello Sir,

      I just want to refer your source code and make my own Blazor pagination. Can I modify your source code and publish to other community with your permission? If you don’t agree I will not do it.

      Thanks in advance
      Sarathlal.S

    • November 22, 2018 at 7:05 pm
      Permalink

      Hi Sarathlal,
      With reference to my work (link to this blog post) you can do it.

    Leave a Reply

    Your email address will not be published. Required fields are marked *