Sorting and Paging in Blazor using EF Core

Sorting and Paging in Blazor using EF Core

In my last post (CRUD using Blazor and Entity Framework Core), we discussed implementing CRUD using Entity Framework Core.  We also talked about Templated Components, Communication between components, etc.  In this post, we will see about implementing Sorting and Paging in Blazor using EF Core and Web API.  You will be familiar with the following topics after this post.

  • Call Web API from Blazor  
  • Implement Sorting and Paging using EF Core
  • Child components
  • Communication through EventCallback

I will use the sample application we have created in the previous post and extend it with the new functionalities we are going to talk about in this post.  I have also created a Web API project which will act as a data service. 

There are two sections to this post.

  • Implement Sorting and Paging in Web API using EF Core
  • Blazor Application Changes

In the first section, we will talk about implementing sorting and paging in a Web API using Entity Framework Core. The next section talks about consuming this API in a Blazor application to enable sorting and paging.

Okay, Let’s start.

Prerequisites

  • Visual Studio 2019
  • Install .NET Core 3.0
  • Install Blazor Templates

Implement Sorting and Paging in Web API using EF Core

I am going to create a Web API project and use the database created from the previous post.  We will see the following in detail.

  • Entity Framework Core Database First approach
  • Create an API Controller with actions using Entity Framework
  • Paginated List – Class to support Sorting and Paging
  • Data Access Service

Entity Framework Core Database First approach

I am going to use Entity Framework Database first approach to create the context and entity classes.  This approach is useful when you have an existing database. 

Run the following command from the Package Manager Console to create entity classes from the database.

PM> Scaffold-DbContext "Server=.\SQLExpress;Database=ToDo;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

The first part of the command is the usual connection string. The second is the provider name and I have mentioned Microsoft.EntityFrameworkCore.SqlServer for that. This is the provider for SQL Server. The last one -OutputDir specifies the folder where the entity classes will be generated in the Web API project.

You can move the connection string to a more secure place from the context class after the entities are created. For this post, I just moved the connection string to appsettings.

The following are the entities created when I execute the command in Package Manager Console.

EF Models created

Create an API Controller with actions using Entity Framework

Now we have created the DB Context and the entities. The next step is to create an API Controller. In the Create API controller wizard, make sure you select “API Controller with actions, using Entity Framework“. Then select “ToDo” as the model.

Add API Controller with Model

The newly created API will have the methods for basic CRUD actions.

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using BlazorDataService.Models;

namespace BlazorDataService.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ToDoController : ControllerBase
    {
        private readonly ApplicationDbContext _context;

        public ToDoController(ApplicationDbContext context)
        {
            _context = context;
        }

        // GET: api/ToDo
        [HttpGet]
        public async Task<ActionResult<IEnumerable<ToDo>>> GetToDoList()
        {
            return await _context.ToDoList.ToListAsync();
        }

        // GET: api/ToDo/5
        [HttpGet("{id}")]
        public async Task<ActionResult<ToDo>> GetToDo(int id)
        {
            var toDo = await _context.ToDoList.FindAsync(id);

            if (toDo == null)
            {
                return NotFound();
            }

            return toDo;
        }

        // PUT: api/ToDo/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutToDo(int id, ToDo toDo)
        {
            if (id != toDo.Id)
            {
                return BadRequest();
            }

            _context.Entry(toDo).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ToDoExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/ToDo
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPost]
        public async Task<ActionResult<ToDo>> PostToDo(ToDo toDo)
        {
            _context.ToDoList.Add(toDo);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetToDo", new { id = toDo.Id }, toDo);
        }

        // DELETE: api/ToDo/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<ToDo>> DeleteToDo(int id)
        {
            var toDo = await _context.ToDoList.FindAsync(id);
            if (toDo == null)
            {
                return NotFound();
            }

            _context.ToDoList.Remove(toDo);
            await _context.SaveChangesAsync();

            return toDo;
        }

        private bool ToDoExists(int id)
        {
            return _context.ToDoList.Any(e => e.Id == id);
        }
    }
}

Paginated List – Class to support Sorting and Paging

I am going to add a new method in the API Controller to retrieve the paginated results and reuse the others for add, update and delete actions. To add sorting and paging abilities to the method, I will create a PaginatedList class that uses Skip and Take statements to filter data on the server instead of retrieving all the rows from the table.

I reuse the code for PaginatedList from Microsoft Docs and made some minor modifications to it.

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

namespace BlazorDataService.Services
{
    public class PaginatedList<T>
    {
        public PaginatedList()
        {

        }
        public int PageIndex { get; set; }
        public int TotalPages { get; set; }
        public List<T> Items { get; set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.Items = new List<T>();
            this.Items.AddRange(items);
        }

        public bool HasPreviousPage
        {
            get
            {
                return (PageIndex > 1);
            }
            set { };
        }

        public bool HasNextPage
        {
            get
            {
                return (PageIndex < TotalPages);
            }
            set { };
        }

        public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

The CreateAsync method takes Page number and size and applies the appropriate Skip and Take statements to IQueryable. When ToListAsync is called on the IQueryable, it will return a list containing data only for the requested page. The properties HasPreviousPage and HasNextPage can be used to enable or disable Previous and Next paging buttons.

We have the PaginatedList class to return the filtered data. The same class is also responsible for sorting. This sort parameter will be passed as string and I have used the following extension method of IQueryable to implement the sorting.

public static class QueryableExtensions
{
    public static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> query, string orderByMember, string direction)
    {
        var queryElementTypeParam = Expression.Parameter(typeof(T));
        var memberAccess = Expression.PropertyOrField(queryElementTypeParam, orderByMember);
        var keySelector = Expression.Lambda(memberAccess, queryElementTypeParam);

        var orderBy = Expression.Call(
            typeof(Queryable),
            direction == "ASC" ? "OrderBy" : "OrderByDescending",
            new Type[] { typeof(T), memberAccess.Type },
            query.Expression,
            Expression.Quote(keySelector));

        return query.Provider.CreateQuery<T>(orderBy);
    }
}

Data Access Service

Let’s create a service class that gets the data from the context. API Controller will call this service to retrieve the data. To keep this post simple, I have hardcoded the page size to 5. You can change the size or make it configurable by sending that as a parameter.

using BlazorDataService.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace BlazorDataService.Services
{
    public interface IToDoListService
    {
        Task<List<ToDo>> Get();
        Task<PaginatedList<ToDo>> GetList(int? pageNumber, string sortField, string sortOrder);
        Task<ToDo> Get(int id);
        Task<ToDo> Add(ToDo toDo);
        Task<ToDo> Update(ToDo toDo);
        Task<ToDo> Delete(int id);
    }
    public class ToDoListService : IToDoListService
    {
        private readonly ApplicationDbContext _context;

        public ToDoListService(ApplicationDbContext context)
        {
            _context = context;
        }
        public async Task<List<ToDo>> Get()
        {
            return await _context.ToDoList.ToListAsync();
        }

        public async Task<PaginatedList<ToDo>> GetList(int? pageNumber, string sortField, string sortOrder)
        {
            int pageSize = 5;
            var toDoList = _context.ToDoList.OrderByDynamic(sortField, sortOrder.ToUpper());
            return await PaginatedList<ToDo>.CreateAsync(toDoList.AsNoTracking(), pageNumber ?? 1, pageSize);
        }

        public async Task<ToDo> Get(int id)
        {
            var toDo = await _context.ToDoList.FindAsync(id);
            return toDo;
        }

        public async Task<ToDo> Add(ToDo toDo)
        {
            _context.ToDoList.Add(toDo);
            await _context.SaveChangesAsync();
            return toDo;
        }

        public async Task<ToDo> Update(ToDo toDo)
        {
            _context.Entry(toDo).State = EntityState.Modified;
            await _context.SaveChangesAsync();
            return toDo;
        }

        public async Task<ToDo> Delete(int id)
        {
            var toDo = await _context.ToDoList.FindAsync(id);
            _context.ToDoList.Remove(toDo);
            await _context.SaveChangesAsync();
            return toDo;
        }
    }
}

The GetList method accepts Page number, Sort Field, and Sort order and implements sorting first by calling the extension method OrderByDynamic. In the next line, it called the CreateAsync method of PaginatedList and returns PaginatedList<ToDo>. Now add a method in the API Controller to return the sorted and paginated results.

[HttpGet]
[Route("Getv2")]
public async Task<ActionResult<PaginatedList<ToDo>>> Get(int? pageNumber, string sortField, string sortOrder)
{
    var list = await _service.GetList(pageNumber, sortField, sortOrder);
    return list;
}

I get the following response when test the API with Postman

{
    "pageIndex": 2,
    "totalPages": 4,
    "items": [
        {
            "name": "Test Task A",
            "id": 2,
            "status": "In Progress",
            "dueDate": "2019-10-13T16:50:29.5359799"
        },
        {
            "name": "Test Task 9",
            "id": 9,
            "status": "In Progress",
            "dueDate": "2020-05-06T00:00:00"
        },
        {
            "name": "Test Task 8",
            "id": 13,
            "status": "In Progress",
            "dueDate": "2020-05-06T00:00:00"
        },
        {
            "name": "Test Task 7",
            "id": 7,
            "status": "In Progress",
            "dueDate": "2020-05-06T00:00:00"
        },
        {
            "name": "Test Task 6",
            "id": 12,
            "status": "In Progress",
            "dueDate": "2020-05-06T00:00:00"
        }
    ],
    "hasPreviousPage": true,
    "hasNextPage": true
}

pageIndex refers to the current page number and totalPages is the number of pages available for the criteria. items array contains the list of records fit the search criteria. hasPreviousPage and hasNextPage tell whether there is a previous or next page.

Okay, now we have created the API which returns the sorted and paged result. We have also tested the API through Postman. Let’s move to the next part of this post.

Blazor application Changes

We have implemented CRUD in Blazor using Entity Framework Core in the previous post. As mentioned earlier, We will use the same application for this post as well. We are going to do the following to implement sorting and paging in this post.

  • Make Http requests using IHttpClientFactory
  • Implement Sorting
  • Display Sort Indicator
  • Create the Pager Component
  • Implement Paging

Make HTTP requests using IHttpClientFactory

Blazor server apps call Web APIs using HttpClient instances typically created using IHttpClientFactory. An IHttpClientFactory can be registered and used to configure and Create HttpClient instances. This provides a centralized location for naming and configuring HttpClient instances. There are several ways IHttpClientFactory can be used in an app. This page explains all the methods in detail. I used Typed clients method to generate HttpClient. The configuration for a typed client can be specified during registration in Startup.ConfigureServices, rather than in the typed client’s constructor.

services.AddHttpClient<ApiService>(client =>
{
    client.BaseAddress = new Uri("http://localhost");
});

ApiService is the typed client here. The HttpClient is encapsulated inside the client. It accepts an HttpClient parameter in its constructor. The method GetPagedResult talks to the API we have created using the HttpClient and returns the result.

using BlazorApp.Data;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace BlazorApp.Services
{
    public class ApiService
    {
        public HttpClient _httpClient;

        public ApiService(HttpClient client)
        {
            _httpClient = client;
        }

        public async Task<PaginatedList<ToDo>> GetPagedResult(int? pageNumber, string sortField, string sortOrder)
        {
            var response = await _httpClient.GetAsync($"/BlazorDataService/ToDoList/Getv2?pageNumber={pageNumber}&sortField={sortField}&sortOrder={sortOrder}");
            response.EnsureSuccessStatusCode();

            using var responseStream = await response.Content.ReadAsStreamAsync();
            var result = await JsonSerializer.DeserializeAsync<PaginatedList<ToDo>>(responseStream, new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                PropertyNameCaseInsensitive = true,
            });
            return result;
        }
    }
}

Implement Sorting

Now we have IHttpClientFactory and the ApiService setup completed. I am going to copy ToDoList.razor into ToDoListv2.razor. Let’s use this component to implement sorting first. Other than sort field and sort order, GetPagedResult method also accepts page number. For the time being let’s pass 1 as the page number. The page size is already hardcoded as 5 in the ToDoListService. The following is the code we currently have for ToDoList component.

@page "/todolist"

@using BlazorApp.Data
@using BlazorApp.Services
@inject IToDoListService service
@inject IJSRuntime jsRuntime

<h1>To Do List</h1>

<p>This component demonstrates fetching data from Database.</p>

@if (toDoList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Task</th>
                <th>Status</th>
                <th>Due Date</th>
                <th>Edit</th>
                <th>Delete</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var toDoItem in toDoList)
            {
            <tr>
                <td>@toDoItem.Name</td>
                <td>@toDoItem.Status</td>
                <td>@toDoItem.DueDate.ToShortDateString()</td>
                <td><input type="button" class="btn btn-primary" @onclick="(() => PrepareForEdit(toDoItem))" data-toggle="modal" data-target="#taskModal" value="Edit" /></td>
                <td><input type="button" class="btn btn-danger" @onclick="(() => PrepareForDelete(toDoItem))" data-toggle="modal" data-target="#confirmDeleteModal" value="Delete" /></td>
            </tr>
            }
        </tbody>
    </table>
}
<div>
    <input type="button" data-toggle="modal" data-target="#taskModal" class="btn btn-primary" value="Add Task" @onclick="(() => InitializeTaskObject())" />
</div>

<ConfirmDialog OnClick="@Delete" />
<TaskDetail TaskObject=taskObject DataChanged="@DataChanged">
    <CustomHeader>@customHeader</CustomHeader>
</TaskDetail>

@code {
    List<ToDo> toDoList;
    ToDo taskObject = new ToDo();
    string customHeader = string.Empty;

    private void InitializeTaskObject()
    {
        taskObject = new ToDo();
        taskObject.DueDate = DateTime.Now;
        customHeader = "Add New Task";
    }

    private void PrepareForEdit(ToDo task)
    {
        customHeader = "Edit Task";
        taskObject = task;
    }

    private void PrepareForDelete(ToDo task)
    {
        taskObject = task;
    }

    protected override async Task OnInitializedAsync()
    {
        toDoList = await service.Get();
    }

    private async Task Delete()
    {
        var task = await service.Delete(taskObject.Id);
        await jsRuntime.InvokeAsync<object>("CloseModal", "confirmDeleteModal");
        toDoList = await service.Get();
        taskObject = new ToDo();
    }

    private async void DataChanged()
    {
        toDoList = await service.Get();
        StateHasChanged();
    }
}

Let”s create a method to accept the sort field and perform sorting. I have stored the current sort field and sort order in variables. If the sort field passed to the method is the same as the current sort field, just flip the current sort order. If the passed field is different from the current field, then sort order will be ascending. Once the sort order is derived, we will call the GetPagedResult method of ApiService.

PaginatedList<ToDo> paginatedList = new PaginatedList<ToDo>();
IEnumerable<ToDo> toDoList;

// Page and Sort data
int? pageNumber = 1;
string currentSortField = "Name";
string currentSortOrder = "Asc";

private async Task Sort(string sortField)
{
    if (sortField.Equals(currentSortField))
    {
        currentSortOrder = currentSortOrder.Equals("Asc") ? "Desc" : "Asc";
    }
    else
    {
        currentSortField = sortField;
        currentSortOrder = "Asc";
    }
    paginatedList = await service.GetPagedResult(pageNumber, currentSortField, currentSortOrder);
    toDoList = paginatedList.Items;
}

Now I have the method ready to do the sorting. Let’s wire up this method to the column headers. I bind the onclick event to the column headers.

<thead class="thead-light">
    <tr>
        <th><span @onclick="@(() => Sort("Name"))">Task</span></th>
        <th><span @onclick="@(() => Sort("Status"))">Status</span></th>
        <th><span @onclick="@(() => Sort("DueDate"))">Due Date</span></th>
        <th>Edit</th>
        <th>Delete</th>
    </tr>
</thead>

If you run the application now, you can see the results similar to this with sorting implemented. The sorting enabled column headers are clickable and on click of the header, the respective column will get sorted.

Sorting Implemented without sort indicator.

Display Sort Indicator

When you click on any of the sorting enabled columns, at first the records will get sorted in ascending order based on that column. If you click the same column again the records will then sorted in descending order. Okay, how do I know on which column the records are sorted and in which order? The solution is to display a sort indicator next to the column name in the header. Let’s create a method to check whether the records or sorted based on a column. This method checks whether a column is sorted. If yes, it checks on what order it is sorted and returns a sort indicator. I use Font Awesome icons to display the sort order.

private string SortIndicator(string sortField)
{
    if (sortField.Equals(currentSortField))
    {
        return currentSortOrder.Equals("Asc") ? "fa fa-sort-asc" : "fa fa-sort-desc";
    }
    return string.Empty;
}

Now we have to call this method in the header like this.

<thead class="thead-light">
    <tr>
        <th><span @onclick="@(() => Sort("Name"))">Task</span><i class="@(SortIndicator("Name"))"></i></th>
        <th><span @onclick="@(() => Sort("Status"))">Status</span><i class="@(SortIndicator("Status"))"></i></th>
        <th><span @onclick="@(() => Sort("DueDate"))">Due Date</span><i class="@(SortIndicator("DueDate"))"></i></th>
        <th>Edit</th>
        <th>Delete</th>
    </tr>
</thead>

Let’s run the application now. You will see an indicator next to the sorted column.

Sorting Implemented with Indicator

Create the Pager Component

Now we have implemented the Sorting. Let’s start with Paging. We will create a child component (Pager.razor) for the pager and pass the parameters to the component from ToDoList.

@if (TotalPages > 0)
{
<div class="float-left pager">
    <button type="button" class="btn btn-primary btn-sm" disabled="@((PageIndex == 1) ? "disabled" : null)"  @onclick="@(() => OnClick.InvokeAsync(1))">
        <i class="fa fa-angle-double-left" aria-hidden="true"></i>
    </button>
    <button type="button" class="btn btn-primary btn-sm" disabled="@(HasPreviousPage ? null : "disabled")"  @onclick="@(() => OnClick.InvokeAsync(PageIndex - 1))">
        <i class="fa fa-angle-left" aria-hidden="true"></i>
    </button>
    <span>@PageIndex</span>
    <button type="button" class="btn btn-primary btn-sm" disabled="@(HasNextPage ? null : "disabled")" @onclick="@(() => OnClick.InvokeAsync(PageIndex + 1))">
        <i class="fa fa-angle-right" aria-hidden="true"></i>
    </button>
    <button type="button" class="btn btn-primary btn-sm" disabled="@((PageIndex == TotalPages) ? "disabled" : null)" @onclick="@(() => OnClick.InvokeAsync(TotalPages))">
        <i class="fa fa-angle-double-right" aria-hidden="true"></i>
    </button>
</div>
}

<style>
    .pager button, .pager span{
        padding-left: 10px;
        padding-right:10px;
    }
</style>

@code {
    [Parameter]
    public int PageIndex { get; set; }

    [Parameter]
    public int TotalPages { get; set; }

    [Parameter]
    public bool HasPreviousPage { get; set; }

    [Parameter]
    public bool HasNextPage { get; set; }

    [Parameter]
    public EventCallback<int> OnClick { get; set; }

}

The pager will have 4 buttons to navigate. I have added code in the razor component to disable the pager buttons based on the following conditions.

  • “First” when the current page index is 1
  • “Previous” when HasPreviousPage is false
  • “Next” when HasNextPage is false
  • “Last” when the current page index is equal to the total count of pages

We will use EventCallbacks to trigger a function in the ToDoList.razor from Pager.razor. EventCallbacks are great for situations where you have nested components and you need a child component to trigger a parent components method upon a certain event. In this case, the pager component exposes an EventCallback<int> parameter, OnClick. Let’s see how to include the Pager component in the ToDoList component.

<Pager PageIndex=@paginatedList.PageIndex TotalPages=@paginatedList.TotalPages OnClick="PageIndexChanged"
       HasNextPage=@paginatedList.HasNextPage HasPreviousPage=@paginatedList.HasPreviousPage>
</Pager>

Implement Paging

Now we have to define the PageIndexChanged event in the ToDoList.razor.

public async void PageIndexChanged(int newPageNumber)
{
    if (newPageNumber < 1 || newPageNumber > paginatedList.TotalPages)
    {
        return;
    }
    pageNumber = newPageNumber;
    paginatedList = await service.GetPagedResult(pageNumber, currentSortField, currentSortOrder);
    toDoList = paginatedList.Items;
    StateHasChanged();
}

The parent component has registered its ClickHandler method with the child component. When the pager button is clicked, the method in the ToDoList component is invoked with the page number from the child. Due to the automatic call to PageIndexChanged, the page is refreshed with the new data. If you run the application at this point, the following screen will be displayed with the pager component. As we are on the first page, the “First” and “Previous” buttons are disabled.

Paging Implemented

Other Functionalities

We have seen the implementation of Sorting and Pagin by calling the Web API from Blazor component. We used only the Get method for this function. I have also implemented other methods to provide the add, edit and delete functionality. I have added the following methods to ApiService for the same.

public async Task<ToDo> Delete(int id)
{
    var response = await _httpClient.DeleteAsync($"/BlazorDataService/ToDoList/{id}");
    response.EnsureSuccessStatusCode();

    using var responseStream = await response.Content.ReadAsStreamAsync();
    return await JsonSerializer.DeserializeAsync<ToDo>(responseStream);
}

public async Task<ToDo> Add(ToDo task)
{
    var stringData = JsonSerializer.Serialize(task);
    var httpContent = new StringContent(stringData, System.Text.Encoding.UTF8, "application/json");
    var response = await _httpClient.PostAsync("/BlazorDataService/ToDoList", httpContent);
    response.EnsureSuccessStatusCode();

    using var responseStream = await response.Content.ReadAsStreamAsync();
    return await JsonSerializer.DeserializeAsync<ToDo>(responseStream);
}

public async Task<string> Update(ToDo task)
{
    var stringData = JsonSerializer.Serialize(task);
    var httpContent = new StringContent(stringData, System.Text.Encoding.UTF8, "application/json");
    var response = await _httpClient.PutAsync($"/BlazorDataService/ToDoList/{task.Id}", httpContent);
    response.EnsureSuccessStatusCode();

    string jsonResponse = await response.Content.ReadAsStringAsync();
    return jsonResponse;
}

The final version of ToDoListv2.razor after implemented all the functionalities is like below.

@page "/todolistv2"

@using BlazorApp.Data
@using BlazorApp.Services

@inject ApiService service
@inject IJSRuntime jsRuntime

<h1>To Do List</h1>

<style>
    .thead-light span {
        cursor: pointer;
    }

    .thead-light i {
        color: darkblue;
        padding: 5px;
    }
</style>

<p>This component demonstrates fetching data from Database.</p>

@if (toDoList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead class="thead-light">
            <tr>
                <th><span @onclick="@(() => Sort("Name"))">Task</span><i class="@(SortIndicator("Name"))"></i></th>
                <th><span @onclick="@(() => Sort("Status"))">Status</span><i class="@(SortIndicator("Status"))"></i></th>
                <th><span @onclick="@(() => Sort("DueDate"))">Due Date</span><i class="@(SortIndicator("DueDate"))"></i></th>
                <th>Edit</th>
                <th>Delete</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var toDoItem in toDoList)
            {
                <tr>
                    <td>@toDoItem.Name</td>
                    <td>@toDoItem.Status</td>
                    <td>@toDoItem.DueDate.ToShortDateString()</td>
                    <td><input type="button" class="btn btn-primary" @onclick="(() => PrepareForEdit(toDoItem))" data-toggle="modal" data-target="#taskModal" value="Edit" /></td>
                    <td><input type="button" class="btn btn-danger" @onclick="(() => PrepareForDelete(toDoItem))" data-toggle="modal" data-target="#confirmDeleteModal" value="Delete" /></td>
                </tr>
            }
        </tbody>
    </table>
}
<div>
    <Pager PageIndex=@paginatedList.PageIndex TotalPages=@paginatedList.TotalPages OnClick="PageIndexChanged"
           HasNextPage=@paginatedList.HasNextPage HasPreviousPage=@paginatedList.HasPreviousPage>
    </Pager>

    <div class="float-right">
        <input type="button" data-toggle="modal" data-target="#taskModal" class="btn btn-primary" value="Add Task" @onclick="(() => InitializeTaskObject())" />
    </div>
</div>

<ConfirmDialog OnClick="@Delete" />
<TaskDetailv2 TaskObject=taskObject DataChanged="@DataChanged">
    <CustomHeader>@customHeader</CustomHeader>
</TaskDetailv2>

@code {
    PaginatedList<ToDo> paginatedList = new PaginatedList<ToDo>();
    IEnumerable<ToDo> toDoList;
    ToDo taskObject = new ToDo();
    string customHeader = string.Empty;


    // Page and Sort data
    int? pageNumber = 1;
    string currentSortField = "Name";
    string currentSortOrder = "Asc";

    private void InitializeTaskObject()
    {
        taskObject = new ToDo();
        taskObject.DueDate = DateTime.Now;
        customHeader = "Add New Task";
    }

    private void PrepareForEdit(ToDo task)
    {
        customHeader = "Edit Task";
        taskObject = task;
    }

    private async Task Sort(string sortField)
    {
        if (sortField.Equals(currentSortField))
        {
            currentSortOrder = currentSortOrder.Equals("Asc") ? "Desc" : "Asc";
        }
        else
        {
            currentSortField = sortField;
            currentSortOrder = "Asc";
        }
        await GetData();
    }

    private string SortIndicator(string sortField)
    {
        if (sortField.Equals(currentSortField))
        {
            return currentSortOrder.Equals("Asc") ? "fa fa-sort-asc" : "fa fa-sort-desc";
        }
        return string.Empty;
    }

    private void PrepareForDelete(ToDo task)
    {
        taskObject = task;
    }

    protected override async Task OnInitializedAsync()
    {
        await GetData();
    }

    private async Task GetData()
    {
        paginatedList = await service.GetPagedResult(pageNumber, currentSortField, currentSortOrder);
        toDoList = paginatedList.Items;
    }

    private async Task Delete()
    {
        var task = await service.Delete(taskObject.Id);
        await jsRuntime.InvokeAsync<object>("CloseModal", "confirmDeleteModal");
        await GetData();
        taskObject = new ToDo();
    }

    private async void DataChanged()
    {
        await GetData();
        StateHasChanged();
    }

    public async void PageIndexChanged(int newPageNumber)
    {
        if (newPageNumber < 1 || newPageNumber > paginatedList.TotalPages)
        {
            return;
        }
        pageNumber = newPageNumber;
        await GetData();
        StateHasChanged();
    }
}

Summary

This post tried to explain about implementing Sorting and Paging using EF Core with Blazor.  We also discussed calling Web API from Blazor, Child Components, and EventCallbacks.

Code for Blazor App and the Web API are available in Github

Further reading & References

This Post Has 4 Comments

  1. Philip the Duck

    Really good stuff, Senthil, thank you. I noticed the GitHub code is missing the BlazorDataService project, although anyone working through this post should be able to created it for themselves.

    Keep up the good work!

    1. Admin

      Thanks, Philip for your comment. I missed the BlazorDataService project. It is available now.

  2. Encarta

    Thanx Admin for adding the post on Sorting and Paging. You ve provided very clarified insight on these capabilities.

    I believe you will add the Filtering capabilities and also add the ability to export the data in the View to Microsoft Excel.

    Thanx for your guidance.

  3. Encarta

    Hello Admin.

    Please I hope you you taken note of my comment (as repeated below) after you added the sorting and paging capabilites to you CRUD operations with Blazor.

    [[[[[[*******Thanx Admin for adding the post on Sorting and Paging. You ve provided very clarified insight on these capabilities.

    I believe you will add the Filtering capabilities and also add the ability to export the data in the View to Microsoft Excel.

    Thanx for your guidance.*******]]]]]]]

Leave a Reply