Introduction
The ASP.NET 2.0 Framework comes with the GridView
, a feature-rich ASP.NET server control to display, page through, and edit data on a webpage. Paging, selecting, sorting and editing of the GridView
is achieved by declaratively adding properties to the markup of the GridView
. This adds link buttons to the data rows of the GridView
. You have to click on one of the link buttons to delete or edit a row. One feature I missed is the ability to use these features via keyboard shortcuts.
To achieve that, I developed an ASP.NET 2.0 AJAX Extensions Extender Control for use with ASP.NET 2.0 GridView
on ASP.NET 2.0 AJAX enabled web pages.
What You Can Do with the Extender and How to Use It
You can use this extender control in existing ASP.NET 2.0 AJAX Extension application web pages which contain the ASP.NET 2.0 GridView
server control.
In the current version, the extender control must be placed at the same level in the control tree as the GridView
it belongs to.
In the current release, the extender provides the following navigation and editing features, and the following default keyboard shortcut scheme:
Feature | Feature Description | Default Shortcut Key |
Paging | If paging is enabled for the GridView , you have shortcuts to navigate to the next, previous, first, and last pages. You can navigate to first and last page by keyboard even if the link buttons for the first and last page aren't currently visible. |
- Next page: Right arrow key
- Previous page: Left arrow key
- First Page: ALT + Pos1
- Last Page: ALT + End
|
Selecting | If selecting is enabled and a row is preselected, you have shortcuts to navigate to the next and previous row. |
- Select next row: Down key
- Select previous row: Up key
|
Edit | If editing is enabled and a row is preselected, you have shortcuts to all editing features. |
- Switch selected row to edit mode: ALT + m key
- Cancel editing: ALT + q key
- Update changes: ALT + u key
|
Delete | If deleting is enabled and a row is preselected, you have shortcuts to delete that row. |
|
Sort | The GridView keyboard Extender supports sorting of columns now. To activate sorting on the GridView , you need to do the following:
- Set property
AutoGenerateColumns='False' of the GridView . - Add columns to
GridView Columns collection manually. The columns must be of type BoundField or a derived column type. - Set property
AllowSorting='true' of the GridView . - Set property
AllowSorting='true' (default) of the Extender . GridView column must have a value for property SortExpression .
The extender adds support for sorting on each column satisfying the prerequisities up to ten columns. |
- ALT+1 -ALT+0 - Repeat pressing the shortcut to toggle between ascending and descending sort order of the column.
ALT+1: 1. sortable column, ALT+0: 10. sortable column.
|
You don't need to display the corresponding link buttons in order to select, delete, sort and edit a data row by keyboard shortcuts.
This default keyboard scheme can easily be changed by changing the default key values in the extender class.
To configure the keyboard scheme for a GridView
, you can set the corresponding public key values XXXKey
to a value of the Keys
enumeration.
For paging, the extender works with both the numeric and the next/previous pager scheme. It works for all four existing GridView.PagerSettings.Mode
values. If the grid doesn't support paging at all, nothing happens when pressing the keyboard shortcuts.
Background
The solution is based on an ASP.NET 2.0 AJAX Extender WebControl. This makes it easy to change the keyboard scheme on server-side and minimize work in the client-side JavaScript.
The extender control works by simulating the corresponding postback commands of the GridView
's link buttons. So, the keyboard shortcuts are compatible to the default client/server based GridView
navigating and editing scheme, and the ViewState
of the GridView
remains in sync.
The article will not cover the general steps in developing Extenders and Behaviour controls, but will describe the solution specific details. For a comprehensive documentation on how to develop an ASP.NET 2.0 AJAX Extensions Extender Control, read the documentation on AJAX.ASP.NET, and particularly the article about developing an Extender control.
In a few words, to develop an Extender control which extends the client behavior of an ASP.NET server control on the client side, you must create an ASP.NET 3.5 AJAX Extender Control for the server side, and a corresponding JavaScript class, derived from the "Sys.UI.Behavior
" class, for client side handling. You need to register the JavaScript class in the server side Extender class so the script is emitted at runtime.
Server-side Code
In the following section, I will describe the important server side steps to build up the Extender
control. In the following snippet, you see the definition of the Extender
class on the server-side.
[assembly: System.Web.UI.WebResource("AjaxSamples.GridViewKeyBoardPagerBehavior.js",
"application/x-javascript")]
namespace AjaxSamples
{
[TargetControlType(typeof(System.Web.UI.WebControls.GridView))]
public class GridViewKeyBoardPagerExtender : System.Web.UI.ExtenderControl
{
...
The extender only makes sense in conjunction with a GridView
control. We can limit the use of the extender control to the target type GridView
by adding the TargetControlType
attribute to the class definition. A System.ArgumentNullException
is thrown when the control, targeted by the TargetControlID
property of the Extender control, is not of type GridView
.
As stated before, the extender works by triggering the postbacks of the GridView
. So, we need three properties for each action: the unique ID value of the GridView
control on the page, the key value of the keyboard which is intended to fire the postback, and the command argument of the corresponding action on the GridView
.
The following code snippet shows the server side properties for handling the delete event. All other events have analogous properties.
private string _delCmdArgument = string.Empty;
[Browsable(false)]
protected string DeleteCmdArgument
{
get { return _delCmdArgument; }
set { _delCmdArgument = value; }
}
private Keys _delKey = Keys.Delete;
[Browsable(false)]
protected string DeleteKeyCode
{
get { return Convert.ToInt32(_delKey).ToString(); }
}
[Browsable(true), DefaultValue(Keys.Delete)]
public Keys DeleteKey
{
get { return _delKey; }
set { _delKey = value; }
}
...
The property DeleteCmdArgument
holds the value for the command argument of the corresponding GridView
event. The actual value depends on the current state of the GridView
, and is determined later.
The public
property DeleteKey
is used for the key value so we can configure it declaratively, e.g., in a User Control. For attaching this key value to a corresponding key value on the client side, we use a property which gives back the string
representation of the ASCII key value of the selected public key enumeration value.
The key values are defined and enumeration found in the file KeyCodeEnums.cs.
We need the unique id of the GridView
on the client side to fire a postback on it.
[Browsable(false)]
protected string PostBackCtrlID
{
get { return Grid.UniqueID; }
}
The GridView
object the extender belongs to is determined by gaining a reference to the control with the ID value of the TargetControlID
of the extender control. The extender property TargetControlID
is the ID of that control the extender is associated with.
private GridView _grid;
[Browsable(false)]
protected GridView Grid
{
get
{
if (_grid == null)
{
_grid = Parent.FindControl(
TargetControlID) as System.Web.UI.WebControls.GridView;
if (_grid == null)
{
throw new NullReferenceException(string.Format(
"{0} is not of type GridView or the GridView is no initialized.",
TargetControlID));
}
}
return _grid;
}
}
In the OnPreRender
override, we set up the command arguments for postback of the GridView
. The current values of the command arguments depend on the current state of the GridView
, e.g., the index of the selected page or row.
protected override void OnPreRender(EventArgs e)
{
if (Grid.EditIndex > -1)
{
_editCancelCmdArgument = "Cancel$" + Grid.EditIndex.ToString();
_editUpdateCmdArgument = "Update$" + Grid.EditIndex.ToString();
}
else
{
if (Grid.SelectedIndex > -1)
{
_delCmdArgument = "Delete$" + Grid.SelectedIndex.ToString();
_editBeginCmdArgument = "Edit$" + Grid.SelectedIndex.ToString();
if (Grid.SelectedIndex == 0)
{
_prevSelectCmdArgument = String.Empty;
}
else
{
_prevSelectCmdArgument =
"Select$" + Convert.ToString(Grid.SelectedIndex - 1);
}
if ((Grid.SelectedIndex == Grid.PageSize - 1) || (
Grid.SelectedIndex == Grid.Rows.Count - 1))
{
_nextSelectCmdArgument = String.Empty;
}
else
{
_nextSelectCmdArgument =
"Select$" + Convert.ToString(Grid.SelectedIndex + 1);
}
}
#region Paging
switch (Grid.PagerSettings.Mode)
{
case PagerButtons.Numeric:
case PagerButtons.NumericFirstLast:
#region Prev/Next Paging Arguments
if (Grid.PageIndex == 0)
{
_prevPageCmdArgument = string.Empty;
}
else
{
_prevPageCmdArgument = "Page$" + Convert.ToString(Grid.PageIndex);
}
if (Grid.PageIndex + 2 > Grid.PageCount)
{
_nextPageCmdArgument = string.Empty;
}
else
{
_nextPageCmdArgument = "Page$" + Convert.ToString(Grid.PageIndex + 2);
}
#endregion
break;
case PagerButtons.NextPrevious:
case PagerButtons.NextPreviousFirstLast:
#region Prev/Next Paging
if (Grid.PageIndex == 0) {
_prevPageCmdArgument = String.Empty;
}
else
{
_prevPageCmdArgument = "Page$Prev";
}
if (Grid.PageIndex == Grid.PageCount - 1)
{
_nextPageCmdArgument = String.Empty;
}
else
{
_nextPageCmdArgument = "Page$Next";
}
#endregion
break;
}
#region First/Last Paging Settings
if (Grid.PageIndex == 0)
{
_firstPageCmdArgument = String.Empty;
}
else
{
_firstPageCmdArgument = "Page$First";
}
if (Grid.PageIndex == Grid.PageCount - 1)
{
_lastPageCmdArgument = String.Empty;
}
else
{
_lastPageCmdArgument = "Page$Last";
}
#endregion
#endregion
#region Sorting
if (_allowSorting
&& Grid.AllowSorting
&& (Grid.Columns != null)
&& (Grid.Columns.Count > 1))
{
int sortColumnCount = 0;
SortColumns = new List<string>();
for (int i = 0; i < Grid.Columns.Count; i++)
{
BoundField field = Grid.Columns[i] as BoundField;
if (field == null)
{
continue;
}
if ((String.IsNullOrEmpty(field.SortExpression)) ||
(String.IsNullOrEmpty(field.DataField)))
{
continue;
}
if (sortColumnCount == 9)
{
{
SortColumns.Add(field.DataField);
break;
}
}
else
{
SortColumns.Add(field.DataField);
sortColumnCount++;
}
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
SortColumnNames = serializer.Serialize(SortColumns);
}
#endregion
}
}
base.OnPreRender(e);
}
During postback, for security purposes, the ASP.NET runtime checks whether the control ID and the command argument is a valid combination that is allowed to fire postback events. For that, we need to register the GridView
's control ID and the command arguments the GridView
could post back. This is achieved by calls to the method RegisterForEventValidation
of the ClientScriptManager
class. This step must be done in the override of the Render
method of our extender control. After registering the combination of the ID and the command arguments, the extender is allowed to post back these commands of the GridView
.
protected override void Render(HtmlTextWriter writer)
{
ClientScriptManager csm = Page.ClientScript;
for (int i = 0; i < Grid.PageSize; i++)
{
csm.RegisterForEventValidation(Grid.UniqueID, "Select$" + i.ToString());
csm.RegisterForEventValidation(Grid.UniqueID, "Edit$" + i.ToString());
csm.RegisterForEventValidation(Grid.UniqueID, "Cancel$" + i.ToString());
csm.RegisterForEventValidation(Grid.UniqueID, "Delete$" + i.ToString());
...
To bridge the server side part of the extender with the client side part, you have to override the method GetScriptDescriptors
of the extender class. There you map the server properties with properties on the client side Behavior
class.
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(
Control targetControl)
{
ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor(
"AjaxSamples.GridViewKeyBoardPagerBehavior", targetControl.ClientID);
descriptor.AddProperty("_firstCmdArgument", this.FirstPageCmdArgument);
descriptor.AddProperty("_lastCmdArgument", this.LastPageCmdArgument);
...
return new ScriptDescriptor[] { descriptor };
}
In the method GetScriptReferences
override of the extender class, we register the client side script of the extender.
protected override IEnumerable<ScriptReference> GetScriptReferences()
{
ScriptReference reference = new ScriptReference();
reference.Name = "AjaxSamples.GridViewKeyBoardPagerBehavior.js";
reference.Assembly = "AjaxSamples";
return new ScriptReference[] { reference };
}
Now, we have finished the server side part of the extender control. The following part describes the important details on the client-side.
Client-side Code
As the first important step, we register a KeyDown
event handler in the initialization method of the behavior class.
AjaxSamples.GridViewKeyBoardPagerBehavior.prototype = {
initialize : function() {
AjaxSamples.GridViewKeyBoardPagerBehavior.callBaseMethod(this, 'initialize');
$addHandler(document, 'keydown', Function.createDelegate(this, this._onKeyDown));
},
dispose : function() {
AjaxSamples.GridViewKeyBoardPagerBehavior.callBaseMethod(this, 'dispose');
$clearHandlers(document);
},
Another thing we do here is deactivate any keyboard shortcuts of the browser that conflict with one of our keyboard shortcuts. In this case, we deactivate the "Add to Favorites" (ALT+D) of the Firefox browser that conflicts with our Save Changes shortcut when editing a row in the GridView
. We have to dispose the event handler to avoid memory leaks. We use the dispose
function for that.
The next interesting thing is the handler itself.
_onKeyDown : function(keyEvent) {
var cmdArgument = "";
if (keyEvent.altKey == true) {
if ((keyEvent.keyCode >= 48) || (keyEvent.keyCode <= 57)) {
var index = (keyEvent.keyCode == 48) ? 9 : keyEvent.keyCode - 49;
if ((index < this.get_sortColumns().length) && (index >= 0)) {
cmdArgument = "Sort$"; + this.get_sortColumns()[index];
}
}
if (keyEvent.keyCode == this._firstKeyCode) {
cmdArgument = this._firstCmdArgument;
}
if (keyEvent.keyCode == this._lastKeyCode) {
cmdArgument = this._lastCmdArgument;
}
...
if (keyEvent.keyCode == this._editUpdateKeyCode) {
window.onkeypress = function(keyEvent){return false;}
if (this._editUpdateCmdArgument != "") {
feedBack = confirm("Save Changes?");
if (feedBack) {
cmdArgument = this._editUpdateCmdArgument;
}
else {
cmdArgument = this._cancelUpdateCmdArgument;
}
}
}
if (cmdArgument != "") {
if (cmdArgument != "") {
keyEvent.stopPropagation();
keyEvent.preventDefault();
__doPostBack(this._postBackCtrlID, cmdArgument);
return;
}
}
...
In the handler, you can edit the code to adapt the behavior of the extender when pressing a key (combination) to your needs.
In the KeyDown
handler on the client side, we check if the pressed key (combination) is one of the keyboard shortcuts for the GridView
actions we defined on the server side. If this is the case, we call the __doPostBack
function with the ID of the GridView
and the command argument which fires the corresponding command on the server side. The __doPostBack
function is emitted by the ASP.NET framework to the HTML markup of the page. The purpose is for posting back (submitting the HTML form) as a response to a user action or another client side event which calls for server side handling.
Using the Code
To use the extender in your project, unzip the code and add it to your solution.
Now, you are ready to use the extender in your existing or upcoming projects. You can add it in the ASP.NET Toolbox if you use it regularly.
The following ASP.NET markup shows how the GridView
and the extender control coexist. One important property is the TargetControlID
. This is the ID of the control, in our case the GridView
control, the extender control adds client behavior to. TargetControlID
is a property of the System.Web.UI.Extender
class.
<%@ Register Assembly="AjaxSamples" Namespace="AjaxSamples" TagPrefix="cc1" %>
...
<asp:ScriptManager runat="server" ID="ScripManager1">
</asp:ScriptManager>
<asp:UpdatePanel runat="server" ID="UpdatePanel1" ChildrenAsTriggers="true"
UpdateMode="Conditional">
<ContentTemplate>
<div>
<asp:AccessDataSource ID="Northwind" runat="server"
DataFile="~/App_Data/Nwind.mdb"
SelectCommand="SELECT [CustomerID], [CompanyName], [ContactName],
[Address], [City], [Country] FROM [Customers]">
</asp:AccessDataSource>
<asp:GridView ID="GridView1"
runat="server"
DataSourceID="Northwind"
DataKeyNames="CustomerID"
AllowPaging="True"
AllowSorting="True"
AutoGenerateColumns="False" >
<Columns>
<asp:BoundField DataField="CustomerID"
HeaderText="Kunden ID" SortExpression="CustomerId" />
<asp:BoundField DataField="CompanyName"
HeaderText="Unternehmen" SortExpression="CompanyName" />
<asp:BoundField DataField="ContactTitle"
HeaderText="Titel" SortExpression="ContactTitle" />
<asp:BoundField DataField="ContactName"
HeaderText="Ansprechpartner" SortExpression="ContactName" />
<asp:BoundField DataField="Address"
HeaderText="Straße" SortExpression="Address" />
<asp:BoundField DataField="PostalCode"
HeaderText="PLZ" SortExpression="PostalCode" />
<asp:BoundField DataField="City" HeaderText="Ort" SortExpression="City" />
<asp:BoundField DataField="Country"
HeaderText="Land" SortExpression="Country" />
<asp:BoundField DataField="Region"
HeaderText="Region" SortExpression="Region" />
<asp:BoundField DataField="Phone"
HeaderText="Tel." SortExpression="Phone" />
<asp:BoundField DataField="Fax" HeaderText="Fax" SortExpression="Fax" />
</Columns>
</asp:GridView>
<cc1:GridViewKeyBoardPagerExtender NextKey="Up"
PreviousKey="Down" ID="GridViewKeyBoardPagerExtender1"
runat="server" TargetControlID="GridView1" />
</div>
</ContentTemplate>
</asp:UpdatePanel>
History
- 3 July 2008, Version 1.0.
- 20 July 2010, Version 2.0
- Use of ALT key instead of CTRL
- Add support for sorting