How To: Create an ASP.NET AJAX Toolkit Extender Control to Extend a GridView
In a previous post I showed examples of how CSS and JavaScript code could be used to freeze the header row of a GridView control and add scrolling capabilities for Internet Explorer and Firefox. Here’s an example of a GridView with a frozen header:
In this post I’m going to discuss how to package up CSS and JavaScript files into a custom control that can be used on the server-side to extend the GridView. Doing this allows you to continue using the standard GridView that ships with ASP.NET 2.0 in applications while extending it’s functionality through a custom extender control.
There are multiple ways to go about creating an AJAX control extender. You can use native classes available in the ASP.NET AJAX Extensions to build a control that would get the job done. However, doing so requires that you write custom code to map server-side properties to client-side properties which isn’t hard. Additional details on how to do this can be found in the ASP.NET 2.0 Professional AJAX book Matt Gibbs and I co-authored. You can download the code from the book here if you’d like to see the sample code (see the chapter 11 code).
To simplify things as much as possible, I’m going to discuss how to build a custom control that is based upon the ASP.NET AJAX Control Toolkit available from http://www.codeplex/com/AtlasControlToolkit (that’s not a typo, they never changed the Atlas name to AJAX in the URL for some reason). The ASP.NET AJAX Control Toolkit provides a base class named ExtenderControlBase along with several attribute classes that minimize the amount of code that has to be written create a control extender. Here's the steps I went through to make the control.
1. Create a new Class Library Project in VS.NET
Create a class library project in VS.NET. Add the AjaxControlToolkit.dll file to the bin folder and add a reference to the assembly using the "Add Reference" dialog. I also added references to the System.Web and System.Web.Extensions assemblies.
2. Create the Behavior Script that the Extender Control will Output.
Add a new JavaScript file into the project (I named mine GridViewHeaderBehavior.js). Here's the entire script I used for the frozen header extender control. I started out by writing JavaScript in a page to get the code working properly first and then converted the code to the "prototype design pattern" as shown next. This leverages functionality in the ASP.NET AJAX and ASP.NET AJAX Toolkit script libraries.
Type.registerNamespace('WahlinControlToolkit'); WahlinControlToolkit.GridViewHeaderBehavior = function(element) { /// <summary> /// The GridViewHeaderBehavior is used to fix a GridView control header and make the
/// control scrollable /// </summary> /// <param name="element" type="Sys.UI.DomElement"> /// The GridView element this behavior is associated with /// </param> WahlinControlToolkit.GridViewHeaderBehavior.initializeBase(this, [element]); this._WrapperDivCssClass = null; } WahlinControlToolkit.GridViewHeaderBehavior.prototype = { initialize : function() { /// <summary> /// Initialize the behavior /// </summary> WahlinControlToolkit.GridViewHeaderBehavior.callBaseMethod(this, 'initialize'); var element = this.get_element(); this._FreezeGridViewHeader(); }, dispose : function() { /// <summary> /// Dispose the behavior /// </summary> var element = this.get_element(); WahlinControlToolkit.GridViewHeaderBehavior.callBaseMethod(this, 'dispose'); }, _FreezeGridViewHeader : function () { var grid = this.get_element(); if (grid != 'undefined') { grid.style.visibility = 'hidden'; var div = null; if (grid.parentNode != 'undefined') { div = grid.parentNode; if (div.tagName == 'DIV') { div.className = this._WrapperDivCssClass; div.style.overflow = 'auto'; } } var tags = grid.getElementsByTagName('TBODY'); if (tags != 'undefined') { var tbody = tags[0]; var trs = tbody.getElementsByTagName('TR'); var headerHeight = 8; if (trs != 'undefined') { headerHeight += trs[0].offsetHeight; var headTR = tbody.removeChild(trs[0]); var head = document.createElement('THEAD'); head.appendChild(headTR); grid.insertBefore(head, grid.firstChild); } tbody.style.height = (div.offsetHeight - headerHeight) + 'px'; tbody.style.overflowX = 'hidden'; tbody.overflow = 'auto'; tbody.overflowX = 'hidden'; } grid.style.visibility = 'visible'; } }, get_WrapperDivCssClass : function() { return this._WrapperDivCssClass; }, set_WrapperDivCssClass : function(value) { this._WrapperDivCssClass = value; this.raisePropertyChanged('WrapperDivCssClass'); } } WahlinControlToolkit.GridViewHeaderBehavior.registerClass
('WahlinControlToolkit.GridViewHeaderBehavior', AjaxControlToolkit.BehaviorBase);
3. Mark the Script as an Embedded Resource in VS.NET
Right-click on the JavaScript file in the Solution Explorer and select Properties from the menu. Change the Build Action property to "Embedded Resource". This will embed the script into the extender control assembly so that you don't have to worry about deploying it as a standalone file.
4. Add the Control Extender Class into the Project
Add a new class into the project (I named mine GridViewHeaderExtender.cs). The class imports the AjaxControlToolkit namespace and derives from ExtenderControlBase which is a class provided by the ASP.NET AJAX Toolkit assembly (AjaxControlToolkit.dll). You can have the control automatically output the script mentioned in the previous steps to the client by using the WebResource attribute along with the ClientScriptResource attribute as shown next. The control (named GridViewHeaderExtender) exposes a single property named WrapperDivCssClass that represents the CSS class that will be applied to the div that wraps the GridView control.
The control also overrides OnPreRender() in order to dynamically output two CSS classes into the page that are needed to help freeze the GridView header row. This is done by calling the CreateStyleRule() method which relies on two custom style classes to generate the necessary CSS data. The complete code for the GridViewHeaderExtender is shown next:
using System; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Web.UI; using System.ComponentModel; using System.ComponentModel.Design; using AjaxControlToolkit; [assembly: WebResource("WahlinControlToolkit.GridViewHeaderBehavior.js", "text/javascript")] namespace WahlinControlToolkit { [TargetControlType(typeof(GridView))] [ClientScriptResource("WahlinControlToolkit.GridViewHeaderBehavior",
"WahlinControlToolkit.GridViewHeaderBehavior.js")] [ToolboxItem("System.Web.UI.Design.WebControlToolboxItem,
System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")] public class GridViewHeaderExtender : ExtenderControlBase { [DefaultValue("")] [Description("CSS class to apply to the GridView wrapper div element. Example of a
wrapper div style: .WrapperDiv {width:800px;height:400px;border: 1px solid black;}")] [ExtenderControlProperty] [ClientPropertyName("WrapperDivCssClass")] public string WrapperDivCssClass { get { return GetPropertyValue<string>("WrapperDivCssClass", string.Empty); } set { SetPropertyValue<string>("WrapperDivCssClass", value); } } protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); if (String.IsNullOrEmpty(this.WrapperDivCssClass)) { throw new ApplicationException("WrapperDivCssClass property value must be defined for the GridViewHeaderExtender control. Here's an example of a wrapper div style that should be defined in the page and referenced with the WrapperDivCssClass property: .WrapperDiv {width:800px;height:400px;border: 1px solid black;}"); } if (this.Enabled && !this.Page.Items.Contains("GridViewHeaderStyles")) { this.Page.Items["GridViewHeaderStyles"] = true; this.Page.Header.StyleSheet.CreateStyleRule(new GridViewThCssClass(),
null, "." + this.WrapperDivCssClass + " TH"); this.Page.Header.StyleSheet.CreateStyleRule(new GridViewTrCssClass(),
null, "." + this.WrapperDivCssClass + " TR"); } } private class GridViewTrCssClass : Style { protected override void FillStyleAttributes(CssStyleCollection attributes,
IUrlResolutionService urlResolver) { base.FillStyleAttributes(attributes, urlResolver); attributes[HtmlTextWriterStyle.Height] = "0px"; } } private class GridViewThCssClass : Style { protected override void FillStyleAttributes(CssStyleCollection attributes,
IUrlResolutionService urlResolver) { base.FillStyleAttributes(attributes, urlResolver); attributes[HtmlTextWriterStyle.Position] = "relative"; } } } }
5. Create a Website and Reference the Extender Control Assembly
Once the extender control is compiled and the assembly is ready to use, you need to reference it in the page that needs to extend a GridView (or you can define it in web.config if you'd like all pages to use it). This is done by adding the following code into the page:
<%@ Register Assembly="WahlinControlToolkit" Namespace="WahlinControlToolkit"
TagPrefix="toolkit" %>
6. Define the Wrapper Div CSS Class Used by the Extender Control
Once the extender control has been referenced you'll need to define the CSS class that will be used to wrap the GridView control (add borders around it if desired, set height and width, etc.) in the page or in an external CSS stylesheet:
<style type="text/css"> .WrapperDiv { width:800px;height:400px;border: 1px solid black; } </style>
7. Use the Extender Control in the Page to Extend the GridView Control and Freeze the Header Row
Add the extender control into to the page:
<toolkit:GridViewHeaderExtender ID="gvExtender" runat="server" TargetControlID="GridView1" WrapperDivCssClass="WrapperDiv"/>
And there you have it! The hardest part of building an extender control is getting the script to do what you want. After you get the script working properly, the actual extender control class is pretty simple to write since you can leverage existing functionality in the ASP.NET AJAX Toolkit. I'm considering making a video that covers these concepts so if I get some free time I'll post it here (no promises though :-)).
All of the code discussed in this post can be downloaded here. If you enhance it in some way, fix any bugs that are discovered, or add additional functionality please let me know about it and I'll get the code base updated with your changes.