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:
- HeaderCellHoverCssClass (When mouseover on header cell)
- HeaderCellSelectCssClass (When click on header cell)
- CellHoverCssClass (When mouseover on data cell)
- CellSelectCssClass (When click on data cell)
- ColumnHoverCssClass (When mouseover a data cell the whole column but the header will apply this style)
- ColumnSelectCssClass (When click on data cell the whole column but the header will apply this style)
- RowHoverCssClass (When hover on any data cell the whole row will apply this style)
- 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:
- OnClientSelectedIndexChanged (Client Side handler method that will be invoked when selected row index change)
- 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)
