Sorting, Filtering (Search Box) And Paging In .NET Core 2 Razor Pages

In this blog, I will provide information on how to sort and search inside a Razor Page.

In order to see how to retrieve data from SQL Server in .NET Core Razor Pages using Entity Framework, you can visit my previous blog

Below are the software/concepts used in this article.

  1. Visual Studio 2019
  2. .Net Core 2.0
  3. Razor Pages
  4. C# Language

Open your project in Visual Studio 2019

In my case, I am opening the earlier created project where Razor pages are present.

Sorting, Filtering (Search Box) and Paging in NET Core 2 Razor Pages

Open the Index page (Razor page)where the data is present. In my example, I am opening Index.cshtml under “Customers” folder, as my customer data is displayed on this page.

Add Sorting to the Razor Page

We can add a sorting feature to all the columns present on the Index.cshtml page (Razor Page). In my example, I have the below-mentioned columns where I require sorting to happen.

CustomerID (Hidden on the Data page), LastName, FirstName and Email

Add properties in the Index.cshtml.cs page for sorting the columns. In my example: I have 4 columns, so I will create 4 properties.
  1. public class IndexModel : PageModel  
  2. {  
  3.     private readonly ScaffModels.TestingCLRContext _context;  
  4.   
  5.     public IndexModel(ScaffModels.TestingCLRContext context)  
  6.     {  
  7.         _context = context;  
  8.     }  
  9.   
  10.     public string CustIDSort { getset; }  
  11.     public string LNameSort { getset; }  
  12.     public string FNameSort { getset; }  
  13.     public string EmailSort { getset; }  
User should be able to sort the column, once he clicks on the column heading displayed on the page. To achieve this we would place the column heading inside an anchor tag. In my example: I have placed 3 columns inside anchor tag, as (CustomerID) column is hidden for me.
  1. @{  
  2.     ViewData["Title"] = "Index";  
  3. }  
  4.   
  5. <h2>Index</h2>  
  6. <p>  
  7.     <a asp-page="Create">Create New</a>  
  8. </p>  
  9.   
  10. <table class="table">  
  11.     <thead>  
  12.         <tr>  
  13.             <th>  
  14.                 <a asp-page="./Index" asp-route-sortOrder="@Model.FNameSort">@Html.DisplayNameFor(model => model.Customers[0].FirstName)</a>  
  15.             </th>  
  16.             <th>  
  17.                 <a asp-page="./Index" asp-route-sortOrder="@Model.LNameSort">@Html.DisplayNameFor(model => model.Customers[0].LastName)</a>  
  18.             </th>  
  19.             <th>  
  20.                 <a asp-page="./Index" asp-route-sortOrder="@Model.EmailSort">@Html.DisplayNameFor(model => model.Customers[0].Email)</a>  
  21.             </th>  
  22.             <th></th>  
  23.         </tr>  
  24.     </thead>  
  25.     <tbody>  
  26. @foreach (var item in Model.Customers)  
Since the column heading would be clickable and would send the column name to sort as Query string in the URL, we need to capture it and process it further. To achieve this, update the existing “OnGetAsync” method in Index.cshtml.cs page.
  1. public async Task OnGetAsync(string sortOrder)  
  2. {  
  3.     CustIDSort = String.IsNullOrEmpty(sortOrder) ? "Cust_ID" : "";  
  4.     LNameSort = sortOrder == "LName_Asc_Sort" ? " LName_Desc_Sort" : " LName_Asc_Sort";  
  5.     FNameSort = sortOrder == "FName_Asc_Sort" ? " FName_Desc_Sort" : " FName_Asc_Sort";  
  6.     EmailSort = sortOrder == "Email_Asc_Sort" ? " Email_Desc_Sort" : " Email_Asc_Sort";  
  7.   
  8.     IQueryable<ScaffModels.Customers> cust = from s in _context.Customers select s;  
  9.   
  10.     switch (sortOrder)  
  11.     {  
  12.         case "Cust_ID":  
  13.             cust = cust.OrderByDescending(s => s.CustomerId);  
  14.             break;  
  15.         case " LName_Asc_Sort":  
  16.             cust = cust.OrderBy(s => s.LastName);  
  17.             break;  
  18.         case " LName_Desc_Sort":  
  19.             cust = cust. OrderByDescending (s => s.LastName);  
  20.             break;  
  21.        case " FName_Asc_Sort":  
  22.             cust = cust.OrderBy(s => s.FirstName);  
  23.             break;  
  24.         case " FName_Desc_Sort":  
  25.             cust = cust. OrderByDescending (s => s.FirstName);  
  26.             break;  
  27.         case " Email_Asc_Sort":  
  28.             cust = cust.OrderBy(s => s.Email);  
  29.             break;  
  30.         case " Email_Desc_Sort":  
  31.             cust = cust. OrderByDescending (s => s.Email);  
  32.             break;  
  33.         default:  
  34.             cust = cust.OrderBy(s => s.CustomerId);  
  35.             break;  
  36.     }  
  37.     Customers = await cust.AsNoTracking().ToListAsync();  
  38. }  
Test the files by right clicking on the Index file and opening it with browser. Then click on the column headings

Add Filtering (Search Box) to the Razor Page

We can allow filtering by putting the text box for a user to search his data based on any present on the Index.cshtml page (Razor Page).

The user should be able to search the column, once he enters the search term in the search box and clicks on the button on the page. To achieve this we would update Index.cshtml page. In my example: I am allowing user to search on LastName & FirstName only.
  1. @{  
  2.     ViewData["Title"] = "Index";  
  3. }  
  4.   
  5. <h2>Index</h2>  
  6.   
  7. <p>  
  8.     <a asp-page="Create">Create New</a>  
  9. </p>  
  10.   
  11. <form asp-page="./Index" method="get">  
  12.     <div class="form-actions no-color">  
  13.         <p>  
  14.             Find by First or Last name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />  
  15.             <input type="submit" value="Search" class="btn btn-default" /> ||  
  16.             <a asp-page="./Index">Back to full List</a>  
  17.         </p>  
  18.     </div>  
  19. </form>  
  20.   
  21. <table class="table">  
Once user clicks on the button to search, column name to filter would be sent as Query string in the URL. We need to capture it and process it further. To achieve this, update the existing “OnGetAsync” method in Index.cshtml.cs page.
  1. public IndexModel(ScaffModels.TestingCLRContext context)  
  2. {  
  3.    _context = context;  
  4. }  
  5.   
  6. public string CustIDSort { getset; }  
  7. public string LNameSort { getset; }  
  8. public string FNameSort { getset; }  
  9. public string EmailSort { getset; }  
  10. public string CurrentFilter { getset; }  
  11.   
  12. public async Task OnGetAsync(string sortOrder, string searchString)  
  13. {  
  14.     CustIDSort = String.IsNullOrEmpty(sortOrder) ? "Cust_ID" : "";  
  15.     LNameSort = sortOrder == "LName_Asc_Sort" ? " LName_Desc_Sort" : " LName_Asc_Sort";  
  16.     FNameSort = sortOrder == "FName_Asc_Sort" ? " FName_Desc_Sort" : " FName_Asc_Sort";  
  17.     EmailSort = sortOrder == "Email_Asc_Sort" ? " Email_Desc_Sort" : " Email_Asc_Sort";  
  18.     CurrentFilter = searchString;  
  19.   
  20. IQueryable<ScaffModels.Customers> cust = from s in _context.Customers select s;  
  21.   
  22.     if (!String.IsNullOrEmpty(searchString))  
  23.         cust = cust.Where(s => s.LastName.Contains(searchString) || s.FirstName.Contains(searchString));  
  24.   
  25.     switch (sortOrder)  
  26.     {  
Test the files by right clicking on the Index file and opening it with the browser. Then click on the button to search the LastName,
 
Sorting, Filtering (Search Box) and Paging in NET Core 2 Razor Pages
 
Sorting, Filtering (Search Box) and Paging in NET Core 2 Razor Pages

Add Paging to the Razor Page

We can add a paging feature on the Index.cshtml page (Razor Page), so that performance of the page is improved and load time of the page is reduced. We are using “PaginatedList” class to support paging. This class uses “Skip” and “Take” statements to filter data on the server instead of retrieving all rows of the table.

Create PaginatedList.cs class in the project with the below code,
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5. using Microsoft.EntityFrameworkCore;  
  6.   
  7. namespace Core.Test  
  8. {  
  9.     public class PaginatedList<T> : List<T>  
  10.     {  
  11.         public int PageIndex { getprivate set; }  
  12.         public int TotalPages { getprivate set; }  
  13.   
  14.         public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)  
  15.         {  
  16.             PageIndex = pageIndex;  
  17.             TotalPages = (int)Math.Ceiling(count / (double)pageSize);  
  18.             this.AddRange(items);  
  19.         }  
  20.   
  21.         public bool HasPreviousPage  
  22.         {  
  23.             get   { return (PageIndex > 1); }  
  24.         }  
  25.   
  26.         public bool HasNextPage  
  27.         {  
  28.             get { return (PageIndex < TotalPages); }  
  29.         }  
  30.   
  31.         public static async Task<PaginatedList<T>> CreateAsync(  
  32.             IQueryable<T> source, int pageIndex, int pageSize)  
  33.         {  
  34.             var count = await source.CountAsync();  
  35.             var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();  
  36.             return new PaginatedList<T>(items, count, pageIndex, pageSize);  
  37.         }  
  38.     }  
  39. }  
Now we need to understand if any filter was already applied to the page, when next or previous page was clicked. Also we need to understand whether the user is already coming from a previous page. To achieve this, update the existing “OnGetAsync” method in Index.cshtml.cs page.
  1. public string CustIDSort { getset; }  
  2. public string LNameSort { getset; }  
  3. public string FNameSort { getset; }  
  4. public string EmailSort { getset; }  
  5. public string CurrentFilter { getset; }  
  6. public string CurrentSort { getset; }  
  7.   
  8. public PaginatedList<ScaffModels.Customers> Customers { getset; }  
  9.   
  10. public async Task OnGetAsync(string sortOrder, string searchString, string currentFilter, int? pageIndex)  
  11. {  
  12.     CurrentSort = sortOrder;  
  13.     CustIDSort = String.IsNullOrEmpty(sortOrder) ? "Cust_ID" : "";  
  14.     LNameSort = sortOrder == "LName_Asc_Sort" ? " LName_Desc_Sort" : " LName_Asc_Sort";  
  15.     FNameSort = sortOrder == "FName_Asc_Sort" ? " FName_Desc_Sort" : " FName_Asc_Sort";  
  16.     EmailSort = sortOrder == "Email_Asc_Sort" ? " Email_Desc_Sort" : " Email_Asc_Sort";  
  17.     if (searchString != null)  
  18.         pageIndex = 1;  
  19.     Else  
  20.         searchString = currentFilter;  
  21.     CurrentFilter = searchString;  
  22.     IQueryable<ScaffModels.Customers> cust = from s in _context.Customers select s;  
  23.   
  24.     if (!String.IsNullOrEmpty(searchString))  
  25.         cust = cust.Where(s => s.LastName.Contains(searchString) || s.FirstName.Contains(searchString));  
  26.   
  27.     switch (sortOrder) ………  
  28.   
  29.     int pageSize = 3;  
  30. //Customers = await cust.AsNoTracking().ToListAsync();  
  31.     Customers = await PaginatedList< ScaffModels.Customers>.CreateAsync(  
  32.         cust.AsNoTracking(), pageIndex ?? 1, pageSize);  
  33. }  
Add paging related buttons in the page for user to navigate to previous/next pages. To achieve this, update Index.cshtml page,
  1. <form asp-page="./Index" method="get">  
  2.     <div class="form-actions no-color">  
  3.         <p>  
  4.             Find by First or Last name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />  
  5.             <input type="submit" value="Search" class="btn btn-default" /> ||  
  6.             <a asp-page="./Index">Back to full List</a>  
  7.         </p>  
  8.     </div>  
  9. </form>  
  10.   
  11. <table class="table">  
  12. <thead>  
  13.         <tr>  
  14.             <th>  
  15.                 <a asp-page="./Index" asp-route-sortOrder="@Model.FNameSort" asp-route-currentFilter="@Model.CurrentFilter">@Html.DisplayNameFor(model => model.Customers[0].FirstName)</a>  
  16.             </th>  
  17.             <th>  
  18.                 <a asp-page="./Index" asp-route-sortOrder="@Model.LNameSort" asp-route-currentFilter="@Model.CurrentFilter">@Html.DisplayNameFor(model => model.Customers[0].LastName)</a>  
  19.             </th>  
  20.             <th>  
  21.                 <a asp-page="./Index" asp-route-sortOrder="@Model.EmailSort" asp-route-currentFilter="@Model.CurrentFilter">@Html.DisplayNameFor(model => model.Customers[0].Email)</a>  
  22.             </th>  
  23.             <th></th>  
  24.         </tr>  
  25.     </thead>  
  26.     <tbody>  
  27.         @foreach (var item in Model.Customers)  
  28. {  
  29.             <tr>  
  30.                 <td>  
  31.                     @Html.DisplayFor(modelItem => item.FirstName)  
  32.                 </td>  
  33.                 <td>  
  34.                     @Html.DisplayFor(modelItem => item.LastName)  
  35.                 </td>  
  36.                 <td>  
  37.                     @Html.DisplayFor(modelItem => item.Email)  
  38.                 </td>  
  39.                 <td>  
  40.                     <a asp-page="./Edit" asp-route-id="@item.CustomerId">Edit</a> |  
  41.                     <a asp-page="./Details" asp-route-id="@item. CustomerId">Details</a> |  
  42.                     <a asp-page="./Delete" asp-route-id="@item. CustomerId">Delete</a>  
  43.                 </td>  
  44.             </tr>  
  45.         }  
  46.     </tbody>  
  47. </table>  
  48.   
  49. @{  
  50.     var prevDisabled = !Model.Customers.HasPreviousPage ? "disabled" : "";  
  51.     var nextDisabled = !Model.Customers.HasNextPage ? "disabled" : "";  
  52. }  
  53.   
  54. <a asp-page="./Index" asp-route-sortOrder="@Model.CurrentSort" asp-route-pageIndex="@(Model.Customers.PageIndex - 1)" asp-route-currentFilter="@Model.CurrentFilter" class="btn btn-default @prevDisabled">Previous</a>  
  55. <a asp-page="./Index" asp-route-sortOrder="@Model.CurrentSort" asp-route-pageIndex="@(Model.Customers.PageIndex + 1)" asp-route-currentFilter="@Model.CurrentFilter" class="btn btn-default @nextDisabled">Next</a>  
Test the files by right clicking on the Index file and opening it with the browser. Then click on the next and prev buttons to browse to pages,
 
Sorting, Filtering (Search Box) and Paging in NET Core 2 Razor Pages

That is it. I hope you have learned something new from this blog and will utilize this in your work.