Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#4.0

Ajax dataGrid User Control by using Jquery

4.67/5 (3 votes)
5 Oct 2011CPOL3 min read 31.9K  
Ajax dataGrid user control by using Jquery

Introduction

In the first part, I describe how to implement Jquery Ajax call in the ASP.NET page. In this part, I will introduce the Ajax datagrid user control by using Jquery and the Ajax function in part one.

Background

The Ajax grid control actually is similar to the Microsoft grid control. It has two parts, the paging index and the grid. In the grid, there is 2 main parts, header and content. Headers are static and defined in resource file in server in most of the time, contents are dynamic from database query. Microsoft datagrid or gridview uses binding method to bind the result and render in server side. Our task is to bind to result and render in client side.

The reason why I didn't use Microsoft Ajax is because we want to make the Ajax lighter and more flexible. the Microsoft Ajax will still bind and render the grid in server side, that consumes more resource to build and format the grid and also transfer more redundant data like HTML tag, style and static command names or links. My idea is that we return datatable like JSON format result to client, let client bind the result with predefined schema, render table in client side, so all binding, rendering, styling and customization jobs will be done in client so that makes traffic and server lighter.

Demo Part by Part

To make it work, the page needs to inherit from the Jpage which I demo in the part one, because it will fire Ajax call, and also uses an Ajax datagrid which I will demo underneath. The grid looks like this:

demo_size1600x1200-small.jpg

Let me describe the Ajax dataGrid part by part.

Part 1: In ascx File

ASP.NET
<%@ Control Language="c#" AutoEventWireup="false" Codebehind="JgridCtl.ascx.cs" 
    Inherits="YourNameSpace.UserControls.JgridCtl" 
    TargetSchema="<a href="http://schemas.microsoft.com/intellisense/ie5"%">
    http://schemas.microsoft.com/intellisense/ie5"%</a>>
<div>
   <span id="pageIdx"></span>
   <span id="loading" class="hide">  
   <span class="ProgressText">loading......</span></span><br/>
   <asp:PlaceHolder ID="JgridHolder" runat="server"></asp:PlaceHolder>
</div>

It is pretty simple, it includes an HTML span which holds paging index content, an HTML span which holds waiting message and a PlaceHolder which holds the grid.

Part 2: In Code Behind, ascx.cs File

There are two goals in C#:

  1. Loading a template into the PlaceHolder, the template will hold the basic grid structure and header; the idea is like <asp:TemplateColumn>. Because the structure and the header depend on different pages.
  2. Initialize data for JavaScript and load the Ajax datagrid JS file.

The code is shown below (read the comments):

C#
namespace YourNameSpace.UserControls{
public class JgridCtl : System.Web.UI.UserControl
{
    #region WebControls
    protected PlaceHolder JgridHolder;
    private ITemplate _template;
    #endregion

    #region Properties
    //set a template to hold HTML table for header
    [PersistenceMode(PersistenceMode.InnerProperty),
    TemplateContainer(typeof(TemplateControl))]
    public ITemplate Template {
    get { this.EnsureChildControls(); return _template; }
    set { this.EnsureChildControls(); _template = value; }
    }
    
    //your JS file path
    public string PathToJgridPageScript {
    get { return Page.ResolveUrl("~/YourPath/JgridCtl.js") + "?version=" + 
        Config.Version; }
    }
    #endregion

    #region Web Form Designer generated code
    override protected void OnInit(EventArgs e) {
    InitializeComponent();
    //load the template into the placeHolder
    if (_template != null) {
    _template.InstantiateIn(JgridHolder);
    }
    base.OnInit(e);
    }
    ...........
    #endregion
    
    #region Events
    private void Page_Load(object sender, System.EventArgs e) {
    //initialize javascript data
    if (!Page.IsPostBack) {
    StringBuilder sb = new StringBuilder();
    sb.Append("var Jgrid={};");
    sb.Append(string.Format("Jgrid.Pager='{0}';", 
        "Your resource string that construct the index"));
    sb.Append(string.Format("Jgrid.NoRsult='{0}';", 
        "Your resource string that show when no query result"));
    Page.ClientScript.RegisterClientScriptBlock(this.GetType(), 
        "JgridCtl_Block", sb.ToString(), true);
    Page.ClientScript.RegisterClientScriptInclude(this.GetType(), "JgridCtl", 
        PathToJgridPageScript);
    }
    }
    #endregion

    #region Helper Methods
    //load column headers
    internal void ColLocalize(string id, string resource) {
    try {
        Label tmpTxt;
        tmpTxt = (Label)this.FindControl(id);
        tmpTxt.Text = resource;
    }catch(Exception){}
    }
#endregion
}
}

Part 3: JS File

Be patient while reading the code and comments, it is not big though (less than 4K).

C#
//TB and schema will be defined in page level, all these variables are 
//essential and enforce to check in the code.
//TB is html table id, schema is the string array that must be matched with both 
//table columns and JSON return headers (they are datatable headers or
//anonymous type properties in C# result)
var TB=schema=currentPg=currentPerPage=param=null;
//all these 3 functions must be defined in page level.
var pageFuns=['JgridReady','getParameter','customTable'];

//html methods, just a short cut to construct html.
function td(C){return '<td>'+C+'</td>';}
function pageLink(Idx,C){return(' '+link('javascript:goPage('+Idx+')',C,Idx));}
function link(L,C,N){
    return('<a class="HyperLink" name="'+N+'" href="'+L+'">'+C+'</a>');}

//page methods
//remember the pageheader control in part one, when page is loaded,
//$(document).ready will forward the call to page level pageReady();
//we check these 3 essential functions whether are defined in page level 
//here and fire another page level JgridReady().
function pageReady(){
var missing=[];
//check whether those 3 page functions above were defined in page level.
$.each(pageFuns,function(){if(!window[this]){missing.push(this);}})
if(missing.length>0){alert('missing function: '+missing.join());return;}
//call page level ready function.
JgridReady();
if(TB==null||schema==null){alert(
    'Variable TB or schema is not initialized in JgridReady().');}
}

//a search click function must attach in page level searching button, 
//call getParameter() to construct parameter object
function OnClickSearch(){
//call page level method to construct parameter.
getParameter();
if(param==null||currentPerPage==null){alert(
    'Variable param or currentPerPage is not initialized in getParameter().');return;}
goPage(1);
}

//when page index click, fire Ajax call for query.
function goPage(pgNum){
//disable page index link and other controls to prevent user keep sending request.
enableCtls(false);
enablePgLinks(false);
currentPg=pgNum;
param.Page=pgNum;
//call server 'getList' method and return to callback method, so in the 
//page level code behind, it must provide a 'protected getList()' method for ajax call, 
//or you can improve it by setting the method name in variable to make it dynamic. 
//callAjax() is define in Jpage in <a href="/KB/ajax/AjaxJquery.aspx">part one</a>.
callAjax('getList',param,'getListCallBack');
}

function getListCallBack(result){
//in the result, it has 2 parts, the total amount of the result and the result list
//convert the JSON result list to an Object array match with its header.
var list=jstrToObjAry(result.list);
//construct the paging index link.
$('#pageIdx').html(rowCountHeader(result.total,currentPg,
    list.length)+getPageLink(result.total));
//clear the old content before render
$('#'+TB).find('tr:gt(0)').remove().end().hide();
//build a plain table that match the result with the schema which 
//defined in the page level
generateTable(TB,schema,list);
//formatting and styling the table
formatTable(TB);
//customize the table, do exact work that you want, like add command
//links in every row.
try{customTable(TB);}catch(e){}
enableCtls(true);
}

//enable or disable the control when Ajax call back or Ajax call processing
function enableCtls(F){
$('#loading').toggleClass('hide',F);
$('#btnSearch').attr('disabled',!F);
}

//enable or disable the paging index when Ajax call back or Ajax call processing
function enablePgLinks(F){$('#linkWrapper a').each(function(){(F)?$(this).attr(
    'href','javascript:goPage('+$(this).attr('name')+')'):$(this).attr('href','#');});}
//format table, usually set some columns invisible(like id)
function formatTable(tbId){$('#'+tbId).find(' tr:first .hideCol').each(
   function(){$('#'+tbId+' tbody td:nth-child('+($(
   this).index()+1)+')').hide();}).end().show();}

//general methods
//this is optional, it gets some control value to construct parameter
function getSort(V){return (Val('select[id*=ddlSortExpr]')==V)?Val(
    'select[id*=ddlSortOrder]'):'';}

function Val(selector){return $(selector).val();}

//get column index, the id is define in your page level, html table template.
function tdIdx(id){return $('#'+id).index();}

//build index header information
function rowCountHeader(total,pgNum,rowNum){
if(total<=0){return ("<span>"+Jgrid.NoRsult+"</span>");}
var startIndex=(pgNum-1)*currentPerPage+1;
return Jgrid.Pager.replace('{0}',startIndex).replace('{1}',
   startIndex+rowNum-1).replace('{2}',total);
}

//build paging index
function getPageLink(total){
var idx1=(0!=currentPg%10)?currentPg-currentPg%10+1:(currentPg-10)+1;
var idxLast=Math.ceil(total/currentPerPage);
var A=[];
A.push('<span id="linkWrapper">');
if(1<idx1){A.push(pageLink((idx1-1),"..."));}
for(var i=idx1;i<idx1+10&&i<=idxLast;i++){if(i==currentPg){
    A.push(' '+i);}else{A.push(pageLink(i,i));}}
if((idx1+10)<=idxLast){A.push(pageLink((idx1+10),'...'));}
A.push('</span>');
return A.join('');
}

//generic methods
//to create a plain table that map the result to the schema, 
//if schema field is not found in the result, it will create 
//a cell with undefine content, then you can edit it in customTable 
//method in your page level after the grid is rendered.
function generateTable(tbId,colSchema,dataAry){
var A=[];
for(var i=0;i<dataAry.length;i++){
A.push('<tr '+((i&1)?'':'class="NormalGridItem"')+' 
    onmouseover="style.backgroundColor=\'#aaaaff\';" 
    onmouseout="style.backgroundColor=\'\';" >');
//insert cell by order that define in your schema and match with the result.
$.each(colSchema,function(n){A.push(td(dataAry[i][colSchema[n].toString()]));});
A.push('</tr>');
}
$(A.join('')).insertAfter('#'+tbId+' tr:first');
}

//construct an object array which maps your schema and return 
//result header, if the schema field is not found in result, it will be nothing.
function jstrToObjAry(jsonStr){
var jAry=JSON.parse(jsonStr);
var oAry=[];
for(var i=1;i<jAry.length;i++){oAry.push(new objType(jAry,i));}
return oAry;
}
function objType(ary,n){for(var x=0;x<ary[0].length;x++){
    this[ary[0][x].toString()]=ary[n][x];}}

How It Works in the Page

As I mentioned on the top, the page must be Jpage and use this control.

In aspx page, it should look like this normally:

ASP.NET
<%@ Register TagPrefix="uc1" TagName="JgridCtl" Src="../UserControls/JgridCtl.ascx" %>
......
<script type="text/javascript" src="<%= PathToPageAjaxScript%>"></script>
<script type="text/javascript">
var pgData=<%=pageData%>;
var res=<%=resource%>;
</script>
......
<form id="Form1" method="post" defaultfocus="btnSearch" runat="server">
.......
<uc1:JgridCtl ID="JgridCtl1" runat="server">
<template>
<table id="tbLocation" class="simpleTable" 
    style="display:none;width:100%;margin-top:3px">
<tr class="NormalGridHeader">
<td id="idxCol"></td>
<td class="hideCol" id="IdCol">Id</td>
<td><asp:label id="ColCompany" runat="server"></asp:label></td>
<td><asp:label id="ColLocation" runat="server"></asp:label></td>
<td><asp:label id="ColType" runat="server"></asp:label></td>
<td><asp:label id="ColContact" runat="server"></asp:label></td>
<td><asp:label id="ColAddress" runat="server"></asp:label></td>
<td></td>
</tr>
</table>
</template>
</uc1:JgridCtl>
.......
</form>

As you see, page loads the Jgrid control and defines your table template with column headers.

In aspx.cs

C#
//inherit from Jpage
public class Locations : Jpage
{
    .....
    //initial data for JavaScript
    #region Properties
    public string pageData { get; private set; }
    public string resource { get; private set; }
    public string PathToPageAjaxScript { get { 
        return Page.ResolveUrl("../YourPath/Locations.js") + 
        "?version=" + Config.Version; } }
    #endregion
    .....
    
    private void Page_Load(object sender, System.EventArgs e)
    {
        //usually remove the viewstate to improve performance
        Page.EnableViewState=false;
        .....
        if ( !Page.IsPostBack )
        {
        ......
        //initial resource for javascript
        resource = serializer.Serialize(new{Edit = GetResource("[Edit]"),
            Del = GetResource("[Delete]")});
        //initial other variable for javascript
        pageData = serializer.Serialize(new{
        companyId=m_CompanyID,
        editLink = this.Request.ApplicationPath + 
            "/YourPath/EditLocation.aspx?{0}&RetPath=" + 
            Global.GetEscapedUrl(this.Request.RawUrl),
        });
        }
        ....
    }

    #region Ajax methods
    //the Ajax methods must be protected with return as part one demo
    protected object getList() {
        var result = YourLogic.Locations.Lookup(ParamId("Type"),
            Param("Zip"), Param("Phone"), Param("Contact"), ParamId("PerPage"), 
            ParamId("Page"));
        //construct the result set, filter result from the collection type by 
        //LINC and convert it to JSON format string.
        //the anonymous type properties must map with the schema which defined
        //in the JavaScript.
        //after the serialize, the result looks like 
        //[["ID","Name","Company".....],["195","mylocation","my company"...],[....],.....]
        var list = result.OfType<YourLogic.Location>().Select(i => new {
            i.ID, i.Name, i.Company,i.TypeDesc,i.Contact,
            Address=i.Address.FullAddressText }).ToList().ToDataTable().ToJsonString();
        //return an anonymous type with result list and amount number
        return new { total = result.VirtualRows, list };
    }
}

ToDataTable() and ToJsonString() are extension methods you need:

C#
public static DataTable ToDataTable<T>(this IList<T> data) {
    PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
    DataTable table = new DataTable();
    foreach (PropertyDescriptor prop in properties)
        table.Columns.Add(prop.Name, 
            (prop.PropertyType.IsGenericType && 
            prop.PropertyType.GetGenericTypeDefinition() == 
            typeof(Nullable<>)) ? Nullable.GetUnderlyingType(
            prop.PropertyType) : prop.PropertyType);

        foreach (T item in data) {
        DataRow row = table.NewRow();
        foreach (PropertyDescriptor prop in properties)
        row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
        table.Rows.Add(row);
        }
    return table;
}
//we use Newtonsoft library here, you can write your own.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public static string ToJsonString(this DataTable table)
{
    JArray jsItems = new JArray();
    // first row should be a Header containing field names
    JArray jsItem = new JArray();
    for (int i = 0; i < table.Columns.Count; i++)
        jsItem.Add(table.Columns[i].ColumnName);
        jsItems.Add(jsItem);
        for (int r = 0; r < table.Rows.Count; r++) {
            jsItem = new JArray();
            for (int c = 0; c < table.Columns.Count; c++)
            jsItem.Add(table.Rows[r][c].ToString());
            jsItems.Add(jsItem);
    }
    return jsItems.ToString(Formatting.None);
}

In page JS File

C#
var cmdContent=null;
//page level ready function
function JgridReady(){
//html table id in the template
TB='tbLocation';
//the schema need match the order in the grid and the result header
schema=['Idx','ID','Company','Name','TypeDesc','Contact','Address'];
cmdContent=td(link('#',res.Edit,'Edit'));
}

//construct the parameter
function getParameter(){
currentPerPage=parseInt(Val('select[id*=ctlRows]'));
param={Type:Val('select[id^=ctlType]'),Status:Val('select[id^=ddlActive]'),
    Resource:Val('#txtResource'),ComId:pgData.companyId,Company:Val(
    '#txtCompany'),Phone:Val('#txtPhone'),Zip:Val('#txtZip'),Loc:Val(
    '#txtLoc'),Contact:Val('#txtContact'),PerPage:currentPerPage};
}

//customize the table after it was created
function customTable(tbId){
var startRow=(currentPg-1)*currentPerPage+1;
var idxIdx=tdIdx('idxCol');
var idIdx=tdIdx('IdCol');
var rows=$('#'+TB+' tr:gt(0)');
var R=null;
for(var i=0;i<rows.length;i++){
R=rows[i];
//add the row index in every row, actually you can make it in the user control
R.cells[idxIdx].innerHTML=startRow+i;
//add a link command in every row
$(R).append(cmdContent.replace('#',pgData.editLink.replace('{0}',
    'LocId='+R.cells[idIdx].innerHTML+'&CompanyId='+pgData.companyId)));
/*
//add a command dynamically in every row, if command link name 'Delete',
//it will create javascript call tbLocationDelete(row) dynamically.
$(R).append(cmdContent).find('a:last').click(function(e){e.preventDefault();
    var funcName=TB+$(this).attr('name');window[funcName]($(this).closest('tr'))
;});
*/
}
}

Now, you can see that it is easy to implement this grid, 3 steps only:

  1. Import it in aspx and define a template with HTML table for structure and header.
  2. Create protected Ajax methods with return in code behind.
  3. Assign table id, schema in JS file, provide a getParameter() to construct parameters and customTable() method to customize table.

You can improve this solution as you like, it will be more powerful. In the next part, I will provide another solution to export your Ajax grid data to Excel file.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)