Building GridView With ASP.NET AJAX Enabled Control with Script#

by mosessaur| 20 February 2008| 9 Comments

Introduction:
I was thinking of building ASP.NET AJAX Supported controls inorder to practice both ASP.NET AJAX extensions as well as Script# tool.

Actually I liked the work done by Matt Berseth regarding ASP.NET AJAX very much. So I decided to use his ideas, convert them with Script# and add extra view things. So 99% of this post credit returns to him, I will just show how to use Script# and work around few things [Demo].

Original Reference:
I picked a control Matt built as GridView Extender (Behavior).And he provided few posts about and here they are:

Simply the idea is to improve the look and feel of the simple GridView with extra styles. For example when hover over a grid cell/row or when click on a row. The control was built as AJAX Extender also known as Behavior that can be applied on a GridView.

Main differences:
I will implement exactly the same idea, but with few changes:

  • Will not use ASP.NET AJAX Toolkit, because I will use Script# to write C# code that will be compiled as JavaScript.
  • Will not build the control as Behavior, instead I'll inherit directly from existing GridView and add extra properties.
  • Will add new client feature "OnClientSelectedIndexChange". An event that will fire when you selet different row.

During my porting process of Matt's work. I faced few things that I couldn't do directly with Script#. However there are workarounds that I'll explore to show how to implement such things with Script#.

Existing Features:
The features that this control provided was:

  • Support styles for mouse hover and mouse click on each row (highlight the row).
  • Support styles for mouse hover and mouse click on each cell (highlight the cell).
  • Support styles for mouse hover and mouse click on whole column (highlight the whole column).

Additional Feature:
In addition I will provide:

  • Support for Client Side Selected Index Change when user moves from row to another. The client event will contain selected row data keys.

The idea: [View Demo]
The idea was very simple. Simply attached mouseover, mouseout and click event handlers to each cell during intialize process. And of course clearing all events upon disposing. So its just looping on rows then on reach row cell and create event handlers. Finally attached event handlers to each cell. Of course this is all client side process.

Client side handlers are responsible to switch between approperiate CSS classes. For example when mouse over a cell a RowHoverCssClass is applied to the row.

Implementation:
I assume how ASP.NET AJAX Enabled controls are made, if you wish to read a simple tutorial that shows you how, you can refer to a tutorial I wrote before "Creating Custom ASP.NET AJAX Client Controls" or to this one
"Adding Client Capability to a Web Server Control Using ASP.NET AJAX Extensions".

I started by creating the server side control. Simply the control inherits directly from System.Web.UI.WebControls.GridView Directly. And as this is considered an ASP.NET AJAX Enabled Control, so it should implement this Interface System.Web.UI.IScriptControl.
public class GridViewEx : System.Web.UI.WebControls.GridView, System.Web.UI.IScriptControl
GridViewEx control contains few properties (10 properties). 8 properties of them are related to CSS and they are:

  1. HeaderCellHoverCssClass (When mouseover on header cell)
  2. HeaderCellSelectCssClass (When click on header cell)
  3. CellHoverCssClass (When mouseover on data cell)
  4. CellSelectCssClass (When click on data cell)
  5. ColumnHoverCssClass (When mouseover a data cell the whole column but the header will apply this style)
  6. ColumnSelectCssClass (When click on data cell the whole column but the header will apply this style)
  7. RowHoverCssClass (When hover on any data cell the whole row will apply this style)
  8. RowSelectCssClass (When click on any data cell the whole row will apply this style)

There are 3 other properites but they already provided through the GridView control itself, RowStyle-CssClass, AlternatingRowStyle-CssClass and HeaderStyle-CssClass.

The other 2 remaing properties are:

  1. OnClientSelectedIndexChanged (Client Side handler method that will be invoked when selected row index change)
  2. RowSelectColumnIndex (Specify the column index that is considered selecting column of the row. Other wise clicking on any cell on a row will fire the OnClientSelectedIndexChange handler.)

Each property of the above has equivalent client side property to be used through client side control.
Bellow is sampel code for one of the CSS properties:

   1: public string RowHoverCssClass
   2: {
   3:     get
   4:     {
   5:         object obj = ViewState["RowHoverCssClass"];
   6:         return (obj != null) ? (string)obj : String.Empty;
   7:     }
   8:     set { ViewState["RowHoverCssClass"] = value; }
   9: }

Now I'll go with some code snippets from original JavaScript code wirtten by Matt and Script# code version I made.
Defining Property for RowHoverCssClass:

   1: MattBerseth.WebControls.AJAX.GridViewControl.GridViewControlBehavior = function(element) {
   2:     MattBerseth.WebControls.AJAX.GridViewControl.GridViewControlBehavior.initializeBase(this, [element]);
   3:     // Properties
   4:     this._rowHoverCssClass = null;
   5:     // Some code....
   6: }
   7: MattBerseth.WebControls.AJAX.GridViewControl.GridViewControlBehavior.prototype = {
   8: 
   9:     // Some Code....
  10:     get_RowHoverCssClass : function() {
  11:         return this._rowHoverCssClass;
  12:     },
  13:     set_RowHoverCssClass : function(value) {
  14:         this._rowHoverCssClass = value;
  15:     },....
  16: }

Now back to C#-Script# equivalent code:

   1: public class GridViewEx : Control
   2: {
   3:     //... Some Code
   4:     private string _rowHoverCssClass;
   5:     public string RowHoverCssClass
   6:     {
   7:         get { return _rowHoverCssClass; }
   8:         set { _rowHoverCssClass = value; }
   9:     }
  10:     //... Cest of Code
  11: }

Attaching handlers to cells is made in the initialize method as the following

   1: initialize : function() {
   2:     MattBerseth.WebControls.AJAX.GridViewControl.GridViewControlBehavior.callBaseMethod(this, 'initialize');
   3:     // get the elements
   4:     this._rows = this.get_element().getElementsByTagName("tr");
   5:     
   6:     if(this._rows) {
   7:         for(var i = 0; i < this._rows.length; i++) {
   8:             //  get the row
   9:             var row = this._rows[i];
  10:             
  11:             for(var j = 0; j < row.cells.length; j++) {
  12:                 var args = {rowIndex: i, cellIndex: j, behavior: this};
  13:                 var cell = row.cells[j]
  14:                 //  attach to the data cell events
  15:                 if(this._isDataRow(row)) {
  16:                     $addHandler(cell, 'mouseover', Function.createCallback(this._onDataCellOver, args));
  17:                     $addHandler(cell, 'mouseout', Function.createCallback(this._onDataCellOut, args));                    
  18:                     $addHandler(cell, 'click', Function.createCallback(this._onDataCellClick, args));                                        
  19:                 }
  20:                 else if(this._isHeaderRow(row)) {
  21:                     $addHandler(cell, 'mouseover', Function.createCallback(this._onHeaderCellOver, args));
  22:                     $addHandler(cell, 'mouseout', Function.createCallback(this._onHeaderCellOut, args));                   
  23:                 }
  24:             }            
  25:         }
  26:     }
  27: }

Back to C#-Script# version:

   1: public override void Initialize()
   2: {
   3:     this._rows = this.Element.GetElementsByTagName("tr");
   4:  
   5:     if (this._rows != null)
   6:     {
   7:         for (int i = 0; i < this._rows.Length; i++)
   8:         {
   9:             //get the row
  10:             TableRowElement row = (TableRowElement)this._rows[i];
  11:             
  12:             //loop through cells 
  13:             for (int j = 0; j < row.Cells.Length; j++)
  14:             {
  15:                 Dictionary args = new Dictionary("rowIndex", i, "cellIndex", j, "control", this);
  16:                 
  17:                 TableCellElement cell = (TableCellElement)row.Cells[j];
  18:  
  19:                 if (IsDataRow(row)) //Check if Data Row IsDataRow(row)
  20:                 {
  21:                     EventHandler dataCellOver = new EventHandler(OnDataCellOver);
  22:                     object dataCellOverCallback = Type.InvokeMethod(null, "Function.createCallback", dataCellOver, args);
  23:                     
  24:                     EventHandler dataCellOut = new EventHandler(OnDataCellOut);
  25:                     object dataCellOutCallback = Type.InvokeMethod(null, "Function.createCallback", dataCellOut, args);
  26:  
  27:                     DomEvent.AddHandler(cell, "mouseover", (DomEventHandler)dataCellOverCallback);
  28:                     DomEvent.AddHandler(cell, "mouseout", (DomEventHandler)dataCellOutCallback);
  29:                     EventHandler dataCellClick = new EventHandler(OnDataCellClick);
  30:                     object dataCellClickCallback = Type.InvokeMethod(null, "Function.createCallback", dataCellClick, args);
  31:                     if (this.RowSelectColumnIndex != null) 
  32:                     {
  33:                         if (j == Number.ParseInt(this.RowSelectColumnIndex))
  34:                         {
  35:                             DomEvent.AddHandler(cell, "click", (DomEventHandler)dataCellClickCallback);
  36:                         }
  37:                     }
  38:                     else
  39:                     {
  40:                         DomEvent.AddHandler(cell, "click", (DomEventHandler)dataCellClickCallback);
  41:                     }
  42:                 }
  43:                 else if (IsHeaderRow(row)) //Check if Header Row IsHeaderRow(row)
  44:                 {
  45:                     EventHandler headerCellOver = new EventHandler(OnHeaderCellOver);
  46:                     object headerCellOverCallback = Type.InvokeMethod(null, "Function.createCallback", headerCellOver, args);
  47:  
  48:                     EventHandler headerCellOut = new EventHandler(OnDataCellOut);
  49:                     object headerCellOutCallback = Type.InvokeMethod(null, "Function.createCallback", headerCellOut, args);
  50:  
  51:                     DomEvent.AddHandler(cell, "mouseover", (DomEventHandler)headerCellOverCallback);
  52:                     DomEvent.AddHandler(cell, "mouseout", (DomEventHandler)headerCellOutCallback);
  53:                 }
  54:             }
  55:         }
  56:     }
  57: }

I want to focus on something here. In JavaScript you noticed this line:

   1: $addHandler(cell, 'mouseover', Function.createCallback(this._onDataCellOver, args));

In Script# I couldn't find equavlient to Function.createCallback but I've made a workaround. First thing I declared a delegate that takes 2 paramerts object and Dictionary (that will work as JSON). The declaration of this delegate was outside my control class as Script# do not support nested types:

   1: public delegate void EventHandler(object e, Dictionary args);

Then in my Script# Initialize overrided method I did the following:

   1: EventHandler dataCellOver = new EventHandler(OnDataCellOver);
   2: object dataCellOverCallback = Type.InvokeMethod(null, "Function.createCallback", dataCellOver, args);
   3: DomEvent.AddHandler(cell, "mouseover", (DomEventHandler)dataCellOverCallback);

OnDataCellOver is the event handler for mouseover event. And because I cannot find anything similar to Function.createCallback directly in Script# I used Type.InvokeMethod. This method gives me the ability to invoke global methods in JavaScript. So I used it to invoke Function.createCallback and passed the parameters as shown in the code.

The args parameter is JSON object. In Script# you can use Dictionary to to represents JSON objects. as the following

   1: Dictionary args = new Dictionary("rowIndex", i, "cellIndex", j, "control", this);

This line of code will produce something similar to this in JavaScript:

   1: var args = {rowIndex: i, cellIndex: j, control: this};

The logic of the event handler is very simple you can look at it in the sample code.

Now I will explore how to add custom event to your client control using Script#. The following shows how to declare an event in your client control:

   1: public event EventHandler SelectedIndexChanged
   2: {
   3:     add { this.Events.AddHandler("selectedIndexChanged", value); }
   4:     remove { this.Events.RemoveHandler("selectedIndexChanged", value); }
   5: }

This event is fired when the user changes his selected row. So it fires on click event of a row or a specific cell that is marked as select column and. This version has a bug, that even if the row is selected and you click on it again it will fire that SelectedIndexChanged Event:

   1: //Original Handler for Cell Click
   2: private void OnDataCellClick(object e, Dictionary args)
   3: {
   4:     //...some code
   5:     //Prepare to invoke client SelectedIndexChange
   6:     int rowIndex = (int)args["rowIndex"]-1;// Zero Index is for header row.            
   7:     Dictionary dataKey = null;            
   8:     if(_dataKeys != null)
   9:         dataKey = (Dictionary)_dataKeys[rowIndex];
  10:  
  11:     Dictionary selIndxChngArgs = new Dictionary("rowIndex", (int)args["rowIndex"] - 1, "dataKey", dataKey);
  12:     //Raise Custom Event SelectedIndexChange
  13:     if(rowIndex == )
  14:     {
  15:         EventHandler handler = (EventHandler)this.Events.GetHandler(eventName);
  16:         if (handler != null)
  17:         {
  18:             handler(this, e);
  19:         }    
  20:     }
  21: }

The following code shows how to attache this event to your control from ASPX:

   1: <cc1:GridViewEx ID="gv" runat="server" AutoGenerateColumns="False" AllowSorting="True"
   2:     DataKeyNames="ProductID" DataSourceID="SqlDataSource1" AllowPaging="True"
   3:     CssClass="igoogle igoogle-night"
   4:     RowSelectCssClass="row-select" 
   5:     RowHoverCssClass="row-over" 
   6:     ColumnSelectCssClass="column-select"
   7:     OnClientSelectedIndexChange="gv_OnClientSelectedIndexChange"
   8:     RowSelectColumnIndex="2">
   9:     <RowStyle CssClass="data-row" />
  10:     <AlternatingRowStyle CssClass="alt-data-row" />
  11:     <HeaderStyle CssClass="header-row" />
  12:     <Columns>
  13:         <asp:BoundField DataField="ProductID" HeaderText="ProductID" InsertVisible="False"
  14:             ReadOnly="True" SortExpression="ProductID" />
  15:         <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" />
  16:         <asp:TemplateField>
  17:             <itemtemplate>
  18:                 <asp:HyperLink id="lnkSelect" runat="server" Text="Select" NavigateUrl="javascript:void(0);"/>
  19:             </itemtemplate>
  20:             <itemstyle horizontalalign="Center" />
  21:         </asp:TemplateField>
  22:     </Columns>
  23: </cc1:GridViewEx>
   1: <script type="text/javascript">
   2:     function gv_OnClientSelectedIndexChange(s,e)
   3:     {
   4:         alert("RowIndex:"+e.rowIndex +", ProductID:" + e.dataKey.ProductID);
   5:     }
   6: </script>

Note that I've specified DataKeyNames in the GridViewEx Declaration.
Well that was all for this post. To have a closer look at the code, you can download the full project with Script# project.

Download the code (25.63 kb) 

kick it on DotNetKicks.com

Comments (9) -

hesam
hesam on 2/29/2008 12:01 AM hi
tanks a lot for this object
now how i can selected row with arrow key (up,down) and press enter on this row selected
then come back me rowIndex and DataKeyNames value
mosessaur
mosessaur Egypt on 2/29/2008 12:33 AM Actually I didn't implement this feature, didn't think of it! but good idea, I'll try to make an update as soon as I get free.
But from your inquiry, you want to move on rows using keyboard arrow keys (up & down), then confirm selection using enter key! this is special requirement, as the record will be ready selected and fire selected index changed event. I might add another event handler for enter key.
keep tuned
hesam
hesam on 3/1/2008 7:45 AM hi mosessaur
tanks a lot
i wait for new object you !!
and that better is client side
plzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz  quikly
so tanks a lot
mosessaur
mosessaur Egypt on 3/1/2008 6:27 PM Hesam, I'll do my best, very loaded these days, with tons of workload.
But I liked the idea and wish to implemented myself. Thank you for the hint.
hesam
hesam on 3/1/2008 10:48 PM hi mosessaur
thanks a lot
plzzzzz quikly new object
yasin
yasin on 4/2/2008 9:31 AM Thanks for the great article.
A note Instead of :

object obj = ViewState["RowHoverCssClass"];
return (obj != null) ? (string)obj : String.Empty;

you can write

return (string)(ViewState["RowHoverCssClass"] ?? String.Empty);

mosessaur
mosessaur on 4/4/2008 4:21 PM Well yes Yasin, I guess I'm an old fashioned C# guy; no matter they improve the language to ease its use, I found myself without feeling return to old fashion syntax Laughing . Anyway, both works fine, but your code reduces number of code lines so it is better
SandhyaBhavni
SandhyaBhavni India on 5/8/2008 12:02 AM Hello Mosessaur,

Relly Gud JOb.But while applying pagination and sorting functionality it is not applying the stylesheets whatever i applied before.

Thanx
Sandhya


mosessaur
mosessaur United States on 5/8/2008 6:19 AM That is supposed to be a bug in the demo itself. I mean if you apply the style sheet to the gird and do paging or sorting it should work normally. It is just the demo that gives you options to change the style. And I'm not handling that correctly in the demo.
Comments are closed