Sunday, August 18, 2019

Adding Sorting To Paging In ASP.NET Core Razor Pages

Most sorting examples show how to pass the column header to C# code that uses a switch statement to build a LINQ query:
var query = context.SomeData;
switch(columnHeader)
{ 
    case "ID":
        query = query.SortBy(c => c.ID);
        break;
    case "FullName":
        query = query.SortBy(c => c.FullName);
        break;
    // etc
}    
This works but it suffers from 3 limitations:
  1. It can require a lot of code, especially if I also want to allow the user to specify the sort direction too.
  2. It is not reusable. I can't use this code block with a different data set because it is dependent on the columns in the result set.
  3. It is not particularly scalable. If I want to add or remove columns, I have to modify the switch statement which could result in the introduction of bugs.
A really neat solution to this is the Dynamic Linq library. It provides extension methods such as OrderBy that take string arguments instead of type-safe expressions. It means that I don't have to know the shape of the model that the OrderBymethod acts upon at design time. It is evaluated dynamically at runtime. This was originally released by Microsoft, but not as part of the .NET Framework. It has since been ported to .NET Core by a few people, one example of which can be found here: https://github.com/StefH/System.Linq.Dynamic.Core. I can install it using Nuget:
install-package System.Linq.Dynamic.Core
Or I can use the command line:
dotnet add package System.Linq.Dynamic.Core
Once it is available to the application, I add another method to the PersonService from the previous article. Here it is again in its entirety with its interface:
public interface IPersonService
{
Task<List<Person>> GetPaginatedResult(int currentPage, int pageSize);
Task<List<Person>> GetPaginatedResult(int currentPage, int pageSize, string sortBy);
Task<int> GetCount();
}
public class PersonService : IPersonService
{
private readonly IHostingEnvironment _hostingEnvironment;
public PersonService(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
}
public async Task<List<Person>> GetPaginatedResult(int currentPage, int pageSize)
{
var data = await GetData();
return data.OrderBy(d => d.Id).Skip((currentPage - 1) * pageSize).Take(pageSize).ToList();
}
public async Task<List<Person>> GetPaginatedResult(int currentPage, int pageSize, string sortBy)
{
var data = await GetData();
return data.AsQueryable().OrderBy(sortBy).Skip((currentPage - 1) * pageSize).Take(pageSize).ToList();
}
public async Task<int> GetCount()
{
var data = await GetData();
return data.Count;
}
private async Task<List<Person>> GetData()
{
var json = await File.ReadAllTextAsync(Path.Combine(_hostingEnvironment.ContentRootPath, "Data", "paging.json"));
return JsonConvert.DeserializeObject<List<Person>>(json);
}
}
view rawPersonService.cs hosted with ❤ by GitHub
The addition is the version of the GetPaginatedResult method that takes a string representing the column to sort by. I need to change the OnGetAsync method in the PageModel to call this method, and I need to add a property to the PageModel to represent the column to sort by. Here is the revised PageModel class:
public class PaginationModel : PageModel
{
private readonly IPersonService _personService;
public PaginationModel(IPersonService personService)
{
_personService = personService;
}
[BindProperty(SupportsGet = true)]
public int CurrentPage { get; set; }
[BindProperty(SupportsGet = true)]
public string SortBy { get; set; }
public int Count { get; set; }
public int PageSize { get; set; } = 10;
public int TotalPages => (int)Math.Ceiling(decimal.Divide(Count, PageSize));
public List<Models.Person> Data { get; set; }
public bool ShowPrevious => CurrentPage > 1;
public bool ShowNext => CurrentPage < TotalPages;
public bool ShowFirst => CurrentPage != 1;
public bool ShowLast => CurrentPage != TotalPages;
public async Task OnGetAsync()
{
Data = await _personService.GetPaginatedResult(CurrentPage, PageSize, SortBy);
Count = await _personService.GetCount();
}
}
view rawPagination.cshtml.cs hosted with ❤ by GitHub
One other change to note - I removed the default value for the CurrentPage property. I am going to move that to the page's route template instead, and add another parameter for sortby:
@page "{currentpage=1}/{sortby=Id}"
The only thing left to do now is to add column headers to the table with hyperlinks to specify the sort order, and to adjust the paging links. Here is the revised content page:
@page "{currentpage=1}/{sortby=Id}"
@model PaginationModel
<h2>Pagination Example</h2>
<table class="table table-striped">
<thead>
<tr>
<th><a asp-page="pagination" class="sort-link" asp-route-sortby="Id">Id</a></th>
<th><a asp-page="pagination" class="sort-link" asp-route-sortby="FullName">Full Name</a></th>
<th><a asp-page="pagination" class="sort-link" asp-route-sortby="Country">Country</a></th>
<th><a asp-page="pagination" class="sort-link" asp-route-sortby="Email">Email</a></th>
<th><a asp-page="pagination" class="sort-link" asp-route-sortby="CreatedAt">Created</a></th>
</tr>
</thead>
@foreach (var item in Model.Data)
{
<tr>
<td>@item.Id</td>
<td>@item.FullName</td>
<td>@item.Country</td>
<td>@item.Email</td>
<td>@item.CreatedAt</td>
</tr>
}
</table>
<div>
<ul class="pagination">
<li class="page-item @(!Model.ShowFirst? "disabled":"")" title="First">
<a asp-page="pagination" class="page-link" asp-all-route-data="@(new Dictionary<string, string>{ { "currentpage", "1" },{ "sortby", Model.SortBy }})">
<i class="fas fa-fast-backward"></i>
</a>
</li>
<li class="page-item @(!Model.ShowPrevious? "disabled":"")" title="Previous">
<a asp-page="pagination" asp-all-route-data="@(new Dictionary<string, string>{{ "currentpage", (Model.CurrentPage -1).ToString() },{ "sortby", Model.SortBy }})" class="page-link">
<i class="fas fa-step-backward"></i>
</a>
</li>
<li class="page-item @(!Model.ShowNext? "disabled":"")" title="Next">
<a asp-page="pagination" asp-all-route-data="@(new Dictionary<string, string>{{ "currentpage", (Model.CurrentPage + 1).ToString() },{ "sortby", Model.SortBy }})" class="page-link">
<i class="fas fa-step-forward"></i>
</a>
</li>
<li class="page-item @(!Model.ShowLast? "disabled":"")" title="Last">
<a asp-page="pagination" asp-all-route-data="@(new Dictionary<string, string>{{ "currentpage", Model.TotalPages.ToString() },{ "sortby", Model.SortBy }})" class="page-link">
<i class="fas fa-fast-forward"></i>
</a>
</li>
</ul>
</div>
view rawPagination.cshtml hosted with ❤ by GitHub
The thead element contains the column headers with the links. The links themselves are generated by anchor tag helpers. Each one has an asp-route-sortby attribute set to the value of the respective column. This will ensure that the correct value is provded to the RouteData dictionary, from where it will be bound to the SortBy property in the PageModel. The value for the currentpage parameter will be supplied from ambient route values.
Paging and Sorting in Razor Pages
The revised paging links at the bottom of the code include values for both the currentpage and the sortby parameters. The currentpage value is set explicitly, which has the effect of negating all subsequent ambient route values, so thesortby value must also be set explicitly.
Now when you click one of the sorting links, the paging links also include the sorting information:
Paging and Sorting in Razor Pages

No comments:

Post a Comment

How to register multiple implementations of the same interface in Asp.Net Core?

 Problem: I have services that are derived from the same interface. public interface IService { } public class ServiceA : IService { ...