Creating Grid View in ASP.NET Core with Custom searching

In this article, we are going to learn how to create a custom grid view with server-side paging, sorting and searching

  • Creating ASP.NET Core Application
  • Database Design
  • Adding Models
  • Installing X.PagedList.Mvc.Core NuGet package
  • Configuring Connection string in appsettings.json
  • Adding Interface IProduct
  • Installing NuGet package Microsoft.Data.SqlClient & Dapper
  • Adding ProductConcrete class
  • Adding Controller
  • Adding View Grid
  • Output

We are going to create ASP.NET Core Web Application for that we are going to choose ASP.NET Core Web Application template.

Creating ASP.NET Core Application

Next, we are going to set Project Name WebGridDemo and location and in last part, we are going to choose .Net Core framework and ASP.NET Core Version 3.0 as the framework for application and few advance settings for such as configuring https and enabling docker we are not going to enable docker settings for this project.

Now finally click on create button to create a project.
Project structure
The project structure generated according to the configuration.

Database Design

We are going to create a table with the name Products which have all products details which we are going to bind to grid view for displaying records.

Below is a Snapshot of Table Structure and Data in it.

Product’s Details

Note: – Table script, along with data, is available for download.

After creating the table next, we are going to add two Models.

Adding Models

Adding two models, one for getting products data from database and another for manage paging in Models folder.

  • Product
  • ProductPagingInfo

Code snippet Product Model

namespace WebGridDemo.Models
{
    public class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

Installing X.PagedList.Mvc.Core NuGet package

Before adding ProductPagingInfo model, we are going to adding X.PagedList.Mvc.Core package from NuGet’s.

What is X.PagedList.Mvc.Core?
PagedList is a library that enables you to easily take an IEnumerable/IQueryable, chop it up into “pages”, and grab a specific “page” by an index. PagedList.Mvc allows you to take that “page” and display a pager control that has links like “Previous”, “Next.”
Referend from: – https://github.com/dncuug/X.PagedList

We are going to use StaticPagedList class for paging which is in X.PagedList.Mvc.Core package.

Code snippet of ProductPagingInfo Model

using X.PagedList;

namespace WebGridDemo.Models
{
    public class ProductPagingInfo
    {
        public int? pageSize;
        public int sortBy;
        public string Search;
        public bool isAsc { get; set; }
        public StaticPagedList<ProductModel> Products { get; set; }
    }
}

Next, we are going set up a database connection for getting Products data.

Configuring Connection string in appsettings.json

Here in ASP.NET core, we don’t have web.config file which was there in the older version of ASP.NET and ASP.NET MVC here we have appsettings.json file where we are going to keep application-level settings, and this file is Json based.

appsettings.json file

In this file, we have added ConnectionStrings section inside that we have added DatabaseConnection key and other is value.

Notice: – here I am using SQL based authentication for database connection that why I have added User Id and password of SQL server.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DatabaseConnection": "Data Source=SAI-PC\\SQLEXPRESS; initial catalog=Sample2; user id=sa; password=Pass$123"
  }
}

Next, after adding the connection string for accessing it in the entire application, we are going to add a class with Name ShareConnectionString and set class as static inside that class we are going to add a static property named Value.

public static class ShareConnectionString
{
     public static string Value { get; set; }
}

After adding class Next, we are going to read the value of connection string from appsettings.json file.

var connection = Configuration.GetConnectionString("DatabaseConnection");

After reading the value of connection string we are going to assign value to static class

ShareConnectionString.Value = connection;

Code Snippet

public void ConfigureServices(IServiceCollection services)
 {
     //Getting Connection String from Database
     var connection = Configuration.GetConnectionString("DatabaseConnection");
     ShareConnectionString.Value = connection;
     services.AddControllersWithViews();
 }

After assigning value we can access connection string in the entire application.
Next, we are going to add a Repository folder and add Interface and Concrete class in it.

Adding Interface IProduct

using System.Collections.Generic;
using WebGridDemo.Models;

namespace WebGridDemo.Repository
{
    public interface IProduct
    {
        int GetProductsCount(string Search);
        List<ProductModel> ProductPagination(string Search, string orderBy, int? pageNumber, int pageSize);
    }
}

Installing NuGet package Microsoft.Data.SqlClient & Dapper

After adding interface Next, we are going to add a Concrete class ProductConcrete which inherit interface IProduct. Now to in ProductConcrete class, we are going to write logic to access database for accessing database we need two packages.

  1. Microsoft.Data.SqlClient
  2. Dapper

After installing the package next, we are going to create a stored procedure for getting products, and after that, we are going to write the logic for accessing this stored procedure.

Stored procedure GetProductCount_Search for getting product count along with search parameter

CREATE proc [dbo].[GetProductCount_Search]  
 @Search varchar(20) = null
as  
begin  
SELECT count(ProductId) FROM Products 
where Name like '%'+ Case when @Search is null then Name else @Search end+'%' 
end

Stored procedure ProductPagination_Search for paging, searching and sorting products

Paging logic is implemented in this procedure using keyword fetch and offset.
Search is implemented using the @search parameter if @search values are not null then it executes the Search query.

CREATE proc [dbo].[ProductPagination_Search]    
 @OrderBy varchar(20) = null,
 @Search varchar(20) = null,
 @PageNumber int,    
 @PageSize int    
as    
begin    
  
DECLARE @sqlCommand nvarchar(1000) 

if (@Search is null)
begin

SET @sqlCommand =   
'SELECT ProductId,    
Name,    
Price    
FROM Products ORDER BY '+ @OrderBy  +  
' OFFSET '+ CONVERT(varchar(10),@PageSize)  +'*'
+ '('+ CONVERT(varchar(10),@PageNumber) +'-' +'1) ROWS FETCH NEXT '
+ CONVERT(varchar(10),@PageSize) +' ROWS ONLY OPTION (RECOMPILE); ';   
print @sqlCommand
EXEC sp_executesql @sqlCommand;  
end
else
begin

SET @sqlCommand =   
'SELECT ProductId,    
Name,    
Price    
FROM Products where Name like ''%'+ @Search +'%'' ORDER BY '+ @OrderBy  +  
' OFFSET '+ CONVERT(varchar(10),@PageSize)  +'*'
+ '('+ CONVERT(varchar(10),@PageNumber) +'-' +'1) ROWS FETCH NEXT '
+ CONVERT(varchar(10),@PageSize) +' ROWS ONLY OPTION (RECOMPILE); ';   
print @sqlCommand
EXEC sp_executesql @sqlCommand;  
end
end  

After creating a procedure for getting product count and products next, we are going to add Class with name ProductConcrete and implement IProduct Interface. We are going to use dapper ORM to get data from database using a stored procedure which we have created.

Adding ProductConcrete class

using Dapper;
using Microsoft.Data.SqlClient;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using WebGridDemo.Models;

namespace WebGridDemo.Repository
{
    public class ProductConcrete : IProduct
    {
        public int GetProductsCount(string Search)
        {
            using (SqlConnection con = new SqlConnection(ShareConnectionString.Value))
            {
                var para = new DynamicParameters();
                para.Add("@Search", Search);
                var data = con.Query<int>("GetProductCount_Search", para, commandType: CommandType.StoredProcedure).FirstOrDefault();
                return data;
            }
        }

        public List<ProductModel> ProductPagination(string Search, string orderBy, int? pageNumber, int pageSize)
        {
            using (SqlConnection con = new SqlConnection(ShareConnectionString.Value))
            {
                var para = new DynamicParameters();
                para.Add("@orderBy", orderBy);
                para.Add("@PageNumber", pageNumber);
                para.Add("@PageSize", pageSize);
                para.Add("@Search", Search);
                var data = con.Query<ProductModel>("ProductPagination_Search", para, commandType: CommandType.StoredProcedure).ToList();
                return data;
            }
        }
    }
}

Next step after Creating a concrete class is to Register Both interface (IProduct) and Concrete (ProductConcrete) class as Transient service in ConfigureServices Method in Startup class.

Registering IProduct & ProductConcrete as Transient service

public void ConfigureServices(IServiceCollection services)
{
    var connection = Configuration.GetConnectionString("DatabaseConnection");
    ShareConnectionString.Value = connection;
    services.AddTransient<IProduct, ProductConcrete>();
    services.AddControllersWithViews();
}

Adding Controller

After registering Next, we are going to add Controller with name DemoController with grid action method which takes 4 parameters as input.

Search: – For Search Products by Name

Sortby: – Sortby by Column id.

isAsc: – is ascending and descending

Page: – page number of the current page.

We are using constructor injection for injecting the dependency, and then we are accessing GetProductsCount, ProductPagination method to get product count and product data and base on parameters (Search, sort by, isAsc, page)
Next, we create an instance of ProductPagingInfo class and assign values and send to view to render.

Code snippet of DemoController

using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Text.RegularExpressions;
using WebGridDemo.Models;
using WebGridDemo.Repository;
using X.PagedList;


namespace WebGridDemo.Controllers
{
    public class DemoController : Controller
    {
        IProduct _IProduct;
        public DemoController(IProduct IProduct)
        {
            _IProduct = IProduct;
        }

        public IActionResult Grid(string Search, int sortby, bool isAsc = true, int? page = 1)
        {
            string SearchValue = string.Empty;
            if (!string.IsNullOrEmpty(Search))
            {
                 SearchValue = Regex.Replace(Search, @"[^a-zA-Z0-9\s]", string.Empty);
            }

            if (page < 0)
            {
                page = 1;
            }

            ProductPagingInfo ProductPagingInfo = new ProductPagingInfo();
            var pageIndex = (page ?? 1) - 1;
            var pageSize = 5;

            string sortColumn;
            #region SortingColumn
            switch (sortby)
            {
                case 1:
                    if (isAsc)
                        sortColumn = "ProductId";
                    else
                        sortColumn = "ProductId Desc";
                    break;

                case 2:
                    if (isAsc)
                        sortColumn = "Name";
                    else
                        sortColumn = "Name Desc";
                    break;

                case 3:
                    if (isAsc)
                        sortColumn = "Price";
                    else
                        sortColumn = "Price Desc";
                    break;
                default:
                    sortColumn = "ProductId asc";
                    break;

            }
            #endregion

            int totalProductCount = _IProduct.GetProductsCount(SearchValue);
            var products = _IProduct.ProductPagination(SearchValue, sortColumn, page, pageSize).ToList();
            var productsPagedList = new StaticPagedList<ProductModel>(products, pageIndex + 1, pageSize, totalProductCount);
            ProductPagingInfo.Products = productsPagedList;
            ProductPagingInfo.pageSize = page;
            ProductPagingInfo.sortBy = sortby;
            ProductPagingInfo.isAsc = isAsc;
            ProductPagingInfo.Search = SearchValue;
            return View(ProductPagingInfo);

        }
    }
}

After adding controller next, we are going to Add View with name Grid view expects strongly type object ProductPagingInfo.

Adding View Grid
On View, we have ProductPagingInfo Model which has products collection which we are going to iterate to show product on-grid, and for displaying paging, we are using PagedListPager helper method of X.PagedList.Mvc.Core package.

We are also going to add Search Textbox for searching product Names with name Search and also with 2 buttons one for search and another for clearing search values.

Code snippet of Grid View

@{
    Layout = null;
}

@using X.PagedList.Mvc.Core;
@using X.PagedList;
@using X.PagedList.Mvc.Core.Common;
@model ProductPagingInfo

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/js/bootstrap.min.js"></script>

<style>

    .arrow {
        display: inline-block;
        height: 16px;
        vertical-align: middle;
        width: 16px;
    }

        .arrow.up {
            background-image: url('/images/arrow-up.png');
        }

        .arrow.down {
            background-image: url('/images/arrow-down.png');
        }
</style>

<div class="container">

    <h4>List of Products</h4>
    @using (Html.BeginForm("Grid", "Demo", FormMethod.Get, new { @id = "frmSearch" }))
    {
        <div class="row">

            <div class="col-md-4 col-sm-4 col-xs-12">
                @Html.TextBoxFor(m => m.Search, new { @class = "form-control" })
            </div>
            <div class="col-md-4 col-sm-4 col-xs-12">
                <input id="btnsubmit" class="btn btn-success" type="submit" value="Search" />
                <input id="btnclear" class="btn btn-success" type="button" value="Clear" />
            </div>
            <div class="col-md-4 col-sm-4 col-xs-12">
            </div>
        </div>
        <div class="row">
            <div class="col-lg-12" style="margin-top: 25px">
                <table class="table table-bordered">
                    <thead>
                        <tr sortby="@Model.sortBy" pagesize="@Model.pageSize" isAsc="@Model.isAsc" search="@Model.Search"></tr>
                    </thead>
                    <tbody>
                        @foreach (var product in Model.Products)
                        {
                            <tr>
                                <td>
                                    @product.ProductId
                                </td>
                                <td>
                                    @product.Name
                                </td>
                                <td>
                                    @product.Price
                                </td>
                            </tr>
                        }
                        <tr>
                            <td colspan="3">
                                @Html.PagedListPager((IPagedList)Model.Products,
             page => Url.Action("Grid", new
             {
                 page = page,
                 sortby = Model.sortBy,
                 Search =Model.Search
             }),
             PagedListRenderOptions.OnlyShowFivePagesAtATime)
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    }
</div>

<script type="text/javascript">
    $(document).ready(function () {
        $("#btnclear").click(function () {
            window.location.href = window.location.href.split('?')[0];
        });

        $("#Search").blur(function () {
            var value = $("#Search").val();
            value = value.trim();
            $("#Search").val(value);
        });
    });
</script>

After we have completed with adding View Grid next, we are going to create tag helper for a header for proving sorting feature.

Adding TagHelper

Before creating tag helper, we are going to add new New folder with name CustomTagHelper in this folder we are going to add a class with name SorterTagHelper which will inherit TagHelper class and override Process method where we are going to write logic for generating header and showing arrows of sorting. In SorterTagHelper class, we are also injecting dependency of IUrlHelperFactory interface, which we are going to use for generating action link.

The SorterTagHelper takes four Attributes as input (isAsc, sortby, pagesize, search).

In Process method we know we have fix header to generate for that I have created a list of string which contains header names. We have 3 fix headers means we are going to iterate 3 times.

First, we are going to add single <tr> table row inside that we are going to add <th>table header inside that table header <th> we are going to add action link which contains sorting and paging and toggle logic of ascending and descending for display arrows on headers.

Arrows of sorting are applied using CSS and images of arrows are stored in wwwroot -> images.

Code snippet of Tag helpers

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Collections.Generic;

namespace WebGridDemo.CustomTagHelper
{
    [HtmlTargetElement("tr", Attributes = "isAsc,sortby,pagesize,search")]
    public class SorterTagHelper : TagHelper
    {
        private const int V = 0;
        private IUrlHelperFactory urlHelperFactory;
        public SorterTagHelper(IUrlHelperFactory helperFactory)
        {
            urlHelperFactory = helperFactory;
        }

        #region Input Attributes
        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }

        [HtmlAttributeName("isAsc")]
        public bool isAsc { get; set; }

        [HtmlAttributeName("sortby")]
        public int sortby { get; set; }

        [HtmlAttributeName("pagesize")]
        public int? pagesize { get; set; }

        [HtmlAttributeName("search")]
        public string search { get; set; }
        #endregion

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);

            List<string> li = new List<string>()
            {
                "ID","Name","Price"
            };

            TagBuilder tr = new TagBuilder("tr");
            int headerid = V;
            for (int row = 1; row <= 3; row++)
            {
                TagBuilder th = new TagBuilder("th");
                TagBuilder tag = new TagBuilder("a");
                var togglesort = (row == sortby ? (!isAsc).ToString() : "true");

                tag.Attributes["href"] = urlHelper.Action("Grid", "Demo", new { page = pagesize, sortby = row, isAsc = togglesort , Search = search });
                tag.InnerHtml.Append(li[headerid]);

                if (sortby != 0)
                {
                    if (row == sortby)
                    {
                        TagBuilder tagspan = new TagBuilder("span");
                        tagspan.AddCssClass($"arrow {(isAsc ? "up" : "down")}");
                        th.InnerHtml.AppendHtml(tagspan);
                    }
                }
                th.InnerHtml.AppendHtml(tag);
                tr.InnerHtml.AppendHtml(th);
                headerid += 1;
            }
            output.Content.AppendHtml(tr.InnerHtml);
        }
    }
}

After creating SorterTagHelper next, we use it on View.

<thead>
    <tr sortby="@Model.sortBy" pagesize="@Model.pageSize" isAsc="@Model.isAsc" search="@Model.Search"></tr>
</thead>

As SorterTagHelper takes 4 parameters as input, we need to pass these parameters to render header.

<table class="table table-bordered">
                    <thead>
                        <tr sortby="@Model.sortBy" pagesize="@Model.pageSize" isAsc="@Model.isAsc" search="@Model.Search"></tr>
                    </thead>
                    <tbody>
                        @foreach (var product in Model.Products)
                        {
                            <tr>
                                <td>
                                    @product.ProductId
                                </td>
                                <td>
                                    @product.Name
                                </td>
                                <td>
                                    @product.Price
                                </td>
                            </tr>
                        }
                        <tr>
                            <td colspan="3">
                                @Html.PagedListPager((IPagedList)Model.Products,
             page => Url.Action("Grid", new
             {
                 page = page,
                 sortby = Model.sortBy,
                 Search =Model.Search
             }),
             PagedListRenderOptions.OnlyShowFivePagesAtATime)
                            </td>
                        </tr>
                    </tbody>
                </table>

After adding SorterTagHelper on view next, we are going to save this run application to Test and see how grid view and Search, sorting, paging features work.

Output

By Saineshwar Bageri

I am Microsoft MVP | C# Corner MVP | Code Project MVP | FULL STACK .NET Developer and working on .Net Web Technology (Asp.net, Asp.net Core,.Net Core, C#, Sqlserver, MVC, Windows, Console Application, javascript, jquery, json, ORM Dapper) and also a freelance developer.