Showing posts with label AJAX. Show all posts
Showing posts with label AJAX. Show all posts

Sunday, December 14, 2008

Simplify Inline Editing using GridView, GridViewControlEmbedder ASP.NET AJAX Extender.

I created another nice ASP.NET AJAX extender for GridView.

With it's help you can enable inline editing for Grids in web pages relatively faster, comparing to time needed to implement this feature from scratch using pure JavaScript.

For example having the following GridView on the page:

image

You can add an extender that allows editing of values of cells right in place:

image

Extender has inner elements named "GridViewControlEmbedderColumn", each of this elements enabled inline editing for one column in the GridView. ColumnIndex attribute points to client side cell index - for example 0 will point to "First Name" column. ControlType can have one of several values. For now extender supports two types of embedded controls for cells - "TextBox" and "ComboBox". CssClass attribute is optional and if you set it to some css class it will be assigned to embedded control when it's created.

Extender itself has several important properties and events.

HoverCssClass - set here the css class that will be assigned to cell's in GridView when they are hovered (hover css class will work only for cells having embedded control assigned to them).

TargetControlID - set here ID of target GridView control, this extender should extend.

RowIdAttributeName - set here name that will be used as name for client side attribute when GridView rows are rendered. It is important to understand that values that will be entered into each cells are identified using column index and row id - some data-driven value that will identify row uniquely. The ID itself can be set in event handler for OnGridViewControlEmbedderRowReady event (see below).

OnGridViewControlEmbedderRowReady - subscribe to this event to set row IDs initial cell values for each row and cells in it before GridView renders. Client side script has to know this values to correctly populate/accumulate values that will be later available on server side after postback.

For the sample markup showed above this event handler will look like this:

image 

Lets review this code in more details.

First line of code just gets to the DataItem property of the GridView Row, in the sample I use XmlDataSource and this code allows to get to the original XPathNavigator to be able to retrieve values from the datasource for that row. It's important to mention that this event is raised for each row of GridView in synch with OnRowDataBound event raised originally by GridView. So in this event handler you can do the same manipulation over row/cells as you could in OnRowDataBound event handler.

Now there are two properties of EventArgs object that need to be set. These are e.RowID and e.CellValues.

Set some unique value to e.RowID (type of string) - this will be used later to retrieve edited values in the grid.

Also set initial values for cells to e.CellValues collection (dictionary<int, string>) - they will be used on the client side initially on first inline editing operation for each cell.

 

The result of using three small code blocks showed above is show on this picture:

image

Vertical scroll appeared for our GridView thanks to another extender I created earlier (Cross Browser GridView Fixed Header Extender, ASP.NET AJAX)

As you see You can simply click any cell and edit it's value - extender allows to do it without going to the server - all this functionality is done on the client.

Now when you do postback (for example user clicks "Save" button), you have access to all values that user have edited in "Values" property of extender.

This property is of type "Dictionary<string, Dictionary<string, string>>" - here outer dictionary's keys are row ids and inner dictionary's keys are cell indexes.

For example using this code we can handle / validate / and process all cell values edited by the user:

image

so for example let's edit two rows in the grid an click "Submit":

image

 

Extender has nice API on the client side as well. It allows to control the process of cells editing, and allows to simplify really complex scenarios.

Please review GridViewControlEmbedder.js file and especially functions inside "Public methods" region.

Extender has two client side events, to which you can subscribe to control/validate editing on the client side:

beforeCellEdit - occurs before cell is entered for edit.

afterCellEdit - occurs before cell is left and it's edited value is committed to the client-side state.

You can subscribe to these events in javascript using simple syntax:

image

where c.ce in string - ClientID of the extender.

Extender passes custom event args passed to the event handlers .

Constructors look like this:

image

 

As you can see you can cancel entering or exiting the cell by setting cancel property of event args. You can analyze the information about currently edited cell, row, column index, inner text of cell, also you have a reference to the grid from there. There is also "callBack" property that can be used to perform some asynchronous operation before continuing cell edit. For example you can set cancel property of event agrs to true to prevent continuing of current operation (entering/exiting cell), issue request to the server, and only after getting response you can call e.get_callBack() property - which is pointer to the function in the extender that will complete the operation.

I also created second demo page that shows how to use client side events of the extender to use inline editing with ComboBoxes. Here is the screenshot:

image

Demo page demonstrates using of drop down lists for choosing options for each cells. what options will be shown for what cell or row is pretty controllable from javascript code. Here is the code snippet from this demo page:

image

Here I call web service for options, passing it column index, then I set cancel property of event args to true to prevent entering the cell. After response is get back from the server, I grab the reference to the select element (e.get_control()) and fill it with options containing information got from the server. Finally I call callBack provided again in event args to enter cell and allow user to edit it.

 

Finally here is a full source code for the control and demo pages. Download and use it,

Hope this helps.

 

P.S.

I am thinking about moving all my control's source code - I write about in this blog, to the Codeplex project. This version of controls library also contains some fixed and upgraded controls from earlier blog posts - HoverTooltip, GridViewFixedHeaderExtender. I think I will move the source to the Codeplex about next week. Also I will update all old blog posts to link to Codeplex project for controls suite download.

kick it on DotNetKicks.com

Monday, May 12, 2008

Loading and Executing JavaScript Files From JavaScript, ASP.NET AJAX

Download source code with samples for the article here.

Quite often we need to load js files dynamically right from javascript. With ASP.NET AJAX it is simple.

ASP.NET AJAX library has internal ScriptLoader class that can be used to load js files, specify callbacks that will be invoked when script is downloaded and ready, and execute functions inside newly loaded files.

I will not list all available methods from ScriptLoader class - you can see them in more details in AJAX source.

I will demonstrate how to load files and execute some functions in it.

ScriptLoader has static and instance members. To add script references(urls) and script blocks to the loader we have to create instance of the class:

image

To add one or more script references to the loader use queueScriptReference method:

image

the parameter is path to the script file.

And finally to load the file and insert link element to the body of the document use the following function:

image

 

Actually the loadScripts expects more parameters, but only first is required. The declaration of loadScripts function in AJAX library looks like this:

image

As you see the function can accept three callback functions that will be called when some condition is true: load was successful, load failed, or load was timed out.

 

Whole sample will look like:

image

Test.js file contains the following code:

image

First line simply executes alert function.

Second line is very important - this is how ScriptLoader knows when script is loaded to the end and calls success callBack.

If you don't add this line to the end of the file ScriptLoader will never know is or is not script executed (loaded) to the end.

Lets run the page:

 image

Script was loaded and executed!

 

Now lets try another example, where we will pass callback to the function to be able to call function inside dynamically loaded file, and be sure that it is loaded before we call anything in it.

The page will look like:

image

and js file:

image

As you see we are calling the function inside file in callback, this way we ensure it is called AFTER file was loaded.

Lets execute the page and first try to click second button:

image

Alert button says the script is not loaded yet - that's true. Now after we load the script (clicking on first button):

image

Is works.

 

You may be wondering why you need this. But suppose you are loading contents to the browser dynamically, for example using web service or page method call, and the content is using javascript. You can also load dynamically it's script file and execute anything you need there.

 

Hope this helps.

 

Technorati Tags:

 

kick it on DotNetKicks.com

Using AjaxRepeater Control, ASP.NET AJAX

You can download full source code with samples and controls for the article here.

Recently after reading blog post Ajax Templates by Nikhil Kothari I really liked the idea, the source code is great, and the control is huge help for AJAX development.

In this blog post I want to share what I learned after investigating sample more carefully, and in what scenarios I feel this control is doing it's job quite well.

I want also to clarify that I will discuss this control's client side API, not touching it's server side capabilities.

Also one thing to mention - I converted the source code from the sample to stand alone server control, and changed it's namespace to one used in my library. Please read carefully the header of the source for the control, it states the license and says you can freely use the source for commercial or not commercial software, there you can also find some limitations.

Also - I modified only few lines of original code in the override of DataBind method, I wanted the template to be constructed even if I have no server side DataSource property specified.

So what is AjaxRepeater control, and how to use it?

To create Template, just drag the control on the page and specify template inside it, for example this way:

image

Here "itemTemplate" is the id of the element inside repeater. And inner XML/HTML of this element is the template representing one data item. In this case the template that will be repeated many times inside single "UL" element is:

image

"itemTemplate" id should be used for every template container, even if you have multiply AjaxRepeater controls on the page. Repeater finds template container to add template items to it by this id, searching for it inside parent AjaxRepeater control, which in the browser is rendered to DIV or SPAN element, depending on server-side property "RenderMode" which can be set to "Inline" or "Block". So this markup:

image

will be rendered in the browser (with empty data source) this way:

image

and this markup:

image

will be rendered as:

image

 

On the client side the repeater is javascript object, deriving from Sys.UI.Control, and it can be accessed using $find shortcut, specifying the ClientID of the control as control's ID. For example we can use this code to access the object:

image

The most important functions that we can call on the object are:

set_data(value) - where value is the array of objects that is used to generate as many html items as are data items in the array. After calling this function, whole inner contents of item container will be cleaned and new items generated and added to the document under container element.

addDataItem(dataItem) - adds one item to the container under last one. dataItem here is the object that will be used to generate one item from template.

So we can use the following tricks:

to clear the container:

image

to bind repeater to data source:

image

to incrementally add items to the container:

image

You may be interested what is generated in the html markup, let's see the result of execution of last sample:

image

And visually it looks like this:

image

Ok, lets see the simplest sample page that shows how to create items dynamically:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Sample1.aspx.cs" Inherits="Sample1" %>
<%@ Register Assembly="Devarchive.Net" Namespace="Devarchive.Net" TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="SM">
    </asp:ScriptManager>

    <script type="text/javascript">
        function addItem(value) {
            var data = {text:value};
            $find("AjaxRepeater1").addDataItem(data);
        }
    </script>

    <div>
        <cc1:AjaxRepeater ID="AjaxRepeater1" runat="server" RenderMode="Block">
            <ul id="itemContainer">
                <li>{text}</li>
            </ul>
        </cc1:AjaxRepeater>
        <input type="text" id="txt" />
        <input type="button" value="Add List Item" onclick="addItem($get('txt').value);" />
    </div>
    </form>
</body>
</html>

 

As you see the code is really simple - when you enter some text in the text box and then click "Add List Item" button, new Item is generated and added to the container.

The screen shot shows what we get:

image

 

Ok, lets look into templates, and data that is used to built the template. It is really simple. In the samples above

Template:

image

is used to build item:

image

using this dataItem:

image

To build table rows for example,

Repeater markup:

image

can be used to build the table with rows:

image

using this code:

image

 

Ok, but who said we cannot use repeater to generate only one item, and this way use it as client side view rather then repeater?

This is simple, suppose we have the following view:

image

Here we can use the following code to rebind view with new data:

image

 

I want to say one more thing - js script for templating feature uses innerHTML to initially retrieve and parse the template in the browser, and this makes problems when we want to use templates inside style attributes of the elements.

for example when using the following template, script cleans out the style attribute inner contents, when accessing it using innerHTML:

image

And renders it without styles, so to fix this I used a little fix/amend to the original script, With this approach you should use "_style" attribute instead of "style". Then when templating script loads the template it just strips out the leading underscore and generates correct items.

 

Now ! I want to show you the scenario which is really easy to achieve with this control !

Suppose you want to bind data from the web service. For example you want to fetch data and bind it to the view or repeater. It is simple!

As we know web services (and page methods) have nice built in feature - when called from javascript they are serializing return objects into JSON - and therefore can be used to fetch data and set it to the repeater right from the browser.

The source code for the sample page demonstrating this is:

 

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="SM">
        <Services>
            <asp:ServiceReference Path="~/WebService.asmx" />
        </Services>
    </asp:ScriptManager>
    <script type="text/javascript">
        function bind() {
            WebService.GetData(function(result) {
                var rpt1 = $find("rpt1");
                rpt1.set_data(result);
            });            
        }
    </script>
    <div>
        <input type="button" onclick="bind();" value="Bind From Service" />
        <dn:AjaxRepeater runat="server" ID="rpt1">
        <table cellpadding="4" cellspacing="2" width="400px">
            <tbody id="itemContainer">
                <tr>
                    <td _style="{style1}">{field1}</td>
                    <td _style="{style2}">{field2}</td>
                    <td _style="{style3}">{field3}</td>
                    <td _style="{style4}">{field4}</td>
                </tr>
            </tbody>
        </table>
        </dn:AjaxRepeater>
    </div>
    </form>
</body>
</html>

 

WebService.cs :

 

using System;
using System.Collections;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Collections.Generic;
using System.Web.Script.Services;

[WebService]
[ScriptService]
public class WebService : System.Web.Services.WebService
{
    Random r = new Random();

    [WebMethod]
    [ScriptMethod]
    public List<Data> GetData()
    {
        List<Data> data = new List<Data>();
        for (int i = 0; i < 5; i++)
        {
            Data d = new Data();
            d.field1 = "field1_" + i.ToString();
            d.field2 = "field2_" + i.ToString();
            d.field3 = "field3_" + i.ToString();
            d.field4 = "field4_" + i.ToString();
            d.style1 = "background-color:" + getRandomColor();
            d.style2 = "background-color:" + getRandomColor();
            d.style3 = "background-color:" + getRandomColor();
            d.style4 = "background-color:" + getRandomColor();
            data.Add(d);
        }
        return data;
    }

    private string getRandomColor()
    {
        string result = "#";
        for (int i = 0; i < 6; i++)
        {
            result += r.Next(0, 10).ToString();
        }
        return result;
    }

    public class Data
    {
        public string field1;
        public string field2;
        public string field3;
        public string field4;
        public string style1;
        public string style2;
        public string style3;
        public string style4;
    }
}
Is not this simple?
Here is the result page's screen shot:
 

image

 

Guys, we can use the control to build really cool dynamic contents using templates and web services !

 

Ok, but what about performance ? -

As I measured - on my computer (Athlon 64 4800+, 2 GB RAM DDR2) to fetch and build 5000 items in the repeater with markup used above it took only 13 seconds, this means 13/5000 = 0.0026 seconds for one template. Taking into account that performance degrade gracefully with growth of html markup size, these are really good figures. Besides this some time is spent to generate response on the server, and deserialize it on the client, all this is done in 13 seconds.

 

Now suppose scenarios when we have a lot of repeaters and views on the page - only templates are loaded first time, and all the data is bound later. This is really good approach very often - we can bind whole grids with sorting/paging this way, just need to specify sort direction and page number when calling web service. Or we can use master-detail relationship - where we select row in the table (master) which was bound using service, and in click event of the row bind view with corresponding data from service (details).

 

I hope this article will help you start with the control, And want to thank Nikhil for the great work.

 

Technorati Tags:

 

kick it on DotNetKicks.com

Saturday, May 3, 2008

Cross Browser GridView Fixed Header Extender, ASP.NET AJAX

I have created ASP.NET AJAX Extender control. It extends GridView control fixing it's header on the top while adding vertical scroll bar to it's contents.

The control also supports maintaining scroll position inside grid between postbacks.

The control works fine inside or outside of UpdatePanel.

 

To extend GridView, just drop extender onto the design surface and set its TargetControlID:

image

Width and height of scrollable area should be specified on GridView control itself:

image

And here is the result:

image

Now let's assign width 100% to the grid:

image

let's resize browser window:

image

As you see header cells are always above corresponding column.

 

I will place code for the control explaining main operations it does.

The steps it does are the following:

1) when extender is being initialized in the browser, Table element of the GridView is "shallow cloned" using clone javascript function, the cloning is done with it all attributes including style. The clone is placed before actual GridView in the document tree.

2) Header of original GridView is cloned and placed to the cloned table (see above)

3) Create a div element which will allow us scroll the contents of a table and place it inside new row in the cloned table

4) copy remaining rows from original GridView inside div element, also hide original GridView's header and remove height and width style elements from table element we are copying.

5) call function that iterates over all first row's cells in the contents table and sets corresponding widths to the header cells.

6) subscribe to "resize" event of window object, and in the handler do the same widths check for header

7) subscribe to "scroll" event of div element, every time div is scrolled store scroll position in hidden field registered by extender

8) if we already have scroll position stored - just set it to the div position (this step is important after postback - when we need to restore scroll position.

 

Ok here is the code for GridViewFixedHeaderExtender.cs file:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Web;
using System.Security.Permissions;

namespace Devarchive.Net
{
    [
        AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal),
        AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal),
        Designer("Devarchive.Net.SimpleDesigner, Devarchive.Net"),
        ToolboxData("<{0}:GridViewFixedHeaderExtender runat=server></{0}:GridViewFixedHeaderExtender>"),
        TargetControlType(typeof(GridView))
    ]
    public class GridViewFixedHeaderExtender : ExtenderControl
    {

        #region Overrides
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
        }

        protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
        {
            if (TargetControl == null || !TargetControl.Visible || TargetControl.Rows.Count == 0)
            {
                TargetControl.Height = Unit.Empty;
                yield break;
            }
            ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor("Devarchive.Net.GridViewFixedHeaderExtender", targetControl.ClientID);
            descriptor.AddProperty("scrollField", HiddenFieldID);
            yield return descriptor;
        }

        protected override IEnumerable<ScriptReference> GetScriptReferences()
        {
            if (TargetControl == null || !TargetControl.Visible || TargetControl.Rows.Count == 0)
            {
                TargetControl.Height = Unit.Empty;
                yield break;
            }
            yield return new ScriptReference("Devarchive.Net.GridViewFixedHeaderExtender.js", this.GetType().Assembly.FullName);
        }

        protected override void Render(HtmlTextWriter writer)
        {
            ScriptManager.RegisterHiddenField(
                this,
                HiddenFieldID,
                LastScroll.ToString()
                );
            base.Render(writer);
        }

        private GridView TargetControl
        {
            get
            {
                GridView result = this.NamingContainer.FindControl(TargetControlID) as GridView;
                return result;
            }
        }

        private int LastScroll
        {
            get
            {
                int result = 0;
                if (Page.Request[HiddenFieldID] != null)
                {
                    int.TryParse(Page.Request[HiddenFieldID], out result);
                }
                return result;
            }
        }

        private string HiddenFieldID
        {
            get
            {
                return String.Format("{0}_GVFHE_Scroll", ClientID);
            }
        }

        #endregion
    }
}

-----------------------------

And here is js file listing:

-----------------------------

 

/// <reference name="MicrosoftAjax.debug.js" />
/// <reference name="MicrosoftAjaxTimer.debug.js" />
/// <reference name="MicrosoftAjaxWebForms.debug.js" />

Type.registerNamespace("Devarchive.Net");

Devarchive.Net.GridViewFixedHeaderExtender = function(element) {
    Devarchive.Net.GridViewFixedHeaderExtender.initializeBase(this, [element]);
    this._documentResizeDelegate = null;
    this._lock = false;
    this._mainTableID = null;
    this._innerTableID = null;
    this._divChild = null;
    
    this._scrollField = 0;
}

Devarchive.Net.GridViewFixedHeaderExtender.prototype = {
    // Overrides
    //#region
    initialize: function() {
        Devarchive.Net.GridViewFixedHeaderExtender.callBaseMethod(this, 'initialize');
        this.initGrid();
    },
    dispose: function() {        
        //Add custom dispose actions here
        $removeHandler(window, "resize", this._documentResizeDelegate);
        if(this._divChild) {
            $clearHandlers(this._divChild);
        }
        Devarchive.Net.GridViewFixedHeaderExtender.callBaseMethod(this, 'dispose');
    },
    //#endregion
    
    // Properties
    //#region
    get_scrollField : function() {
        return this._scrollField;
    },
    
    set_scrollField : function(value) {
        if (this._scrollField !== value) {
            this._scrollField = value;
            this.raisePropertyChanged('scrollField');
        }
    },
    //#endregion
    
    // Methods
    //#region
    getLastScroll : function() {
        var result = 0;
        var hf = $get(this._scrollField);
        if(hf) {
            result = parseInt(hf.value);
            if(!result) result = 0;
        }
        return result;
    },
    setLastScroll : function(value) {
        var hf = $get(this._scrollField);
        if(hf) {
            hf.value = value;
        }
    },
    initGrid : function() {
        // create deep clone of target grid
        var target = this.get_element();
        var clone = target.cloneNode(true);
        
        // get desired height of inner scrollable area
        var height = target.style.height;
        var width = target.style.width;
        
        var mainTable = target.cloneNode(false);
        mainTable.id = String.format("outer_{0}", target.id);
        target.parentNode.insertBefore(mainTable, target);


        var mainHead = document.createElement("thead");
        mainTable.appendChild(mainHead);
        var mainBody = document.createElement("tbody");
        mainTable.appendChild(mainBody);
        
        // Clone original header
        var header = target.rows[0].cloneNode(true);
        mainHead.appendChild(header);

        // add scrollable area mainTable
        var secondRow = document.createElement("tr");
        mainBody.appendChild(secondRow);
        var mainTd = document.createElement("td");
        secondRow.appendChild(mainTd)
        this.setAttribute(mainTd, "colspan", target.rows[0].cells.length);
        this.setAttribute(mainTd, "align", "left");
        this.setAttribute(mainTd, "valign", "top");
        var divChild = document.createElement("div");
        mainTd.appendChild(divChild);
        divChild.style.width = width;
        divChild.style.height = height;
        $addHandler(divChild, "scroll", Function.createDelegate(this, this.syncScroll));
        divChild.style.overflow = "auto";
        divChild.style.overflowX = "hidden";
        divChild.style.overflowY = "scroll";
        this._divChild = divChild;
        
        // now remove old grid from document and insert new clone into the place
        target.parentNode.removeChild(target);
        divChild.appendChild(clone);
        
        // assign extender related data to clone
        clone._behaviors = target._behaviors;
        clone.GridViewFixedHeaderExtender = target.GridViewFixedHeaderExtender;
        
        // correct styles
        var attributes = [];
        for(var i = 0; i < clone.attributes.length; i++) {
            var attr = clone.attributes.item(i);
            var value = attr.value.trim().toLowerCase();
            if(value != "cellpadding" && value != "cellspacing") {
                Array.add(attributes, attr);
            }
        }
        Array.forEach(attributes, this.deleteAttribute, clone);
        clone.deleteRow(clone.rows[0]);
        clone.border = "0";
        clone.style.borderWidth = "0px";
        clone.style.width = "100%";
        clone.style.height = "";
        mainTable.style.height = "";
        target.style.height = "";
        
        // correct widths of header columns and subscribe to document resize event:
        this._mainTableID = mainTable.id;
        this._innerTableID = clone.id;
        this._documentResizeDelegate = Function.createDelegate(
            this,
            this.syncWidths
        );
        this._documentResizeDelegate.call();
        // Attach to window's resize event to resize header cells when inner cells change their size
        $addHandler(window, "resize", this._documentResizeDelegate);  
        // Restore scroll position from last time  
        divChild.scrollTop = this.getLastScroll();
    },
    
    setAttribute : function(element, attribute, value) {
        var namedItem = document.createAttribute(attribute);
        namedItem.value = value;
        element.attributes.setNamedItem(namedItem);
    },
    deleteAttribute : function(attribute, index, attributes) {
        this.removeAttribute(attribute);
    },
    syncScroll : function(args) {
        if(this._divChild) {
            this.setLastScroll(this._divChild.scrollTop);
        }
    },
    syncWidths : function(args) {
        if(!this._lock) {
            this._lock = true;
            var mainTable = $get(this._mainTableID);
            var innerCellPadding = mainTable.cellPadding;
            var header = mainTable.rows[0];
            var innerTable = $get(this._innerTableID);
            var originalRow = innerTable.rows[0];
            var headerWidth = Sys.UI.DomElement.getBounds(header).width;
            var originalRowWidth = Sys.UI.DomElement.getBounds(originalRow).width;
            var diff = headerWidth - originalRowWidth - innerCellPadding * 2;
            if (originalRow && header) {
                for(var i = 0; i < originalRow.cells.length; i++) {
                    var bounds = Sys.UI.DomElement.getBounds(originalRow.cells[i]);
                    var x = bounds.width;
                    if(i == originalRow.cells.length-1) {
                        x = x + diff - innerCellPadding * 2;
                    } else {
                        x = x - innerCellPadding;
                    }
                    header.cells[i].style.width = x + "px";
                }
            }
            this._lock = false;
        }
    }
    //#endregion
}
Devarchive.Net.GridViewFixedHeaderExtender.registerClass('Devarchive.Net.GridViewFixedHeaderExtender', Sys.UI.Behavior);
 
You can download source code for the Devarchive.Net toolkit including this control here.
To the moment toolkit contains four controls:
StateBag control
HoverTooltip control
Timer control
GridViewFixedHeaderExtender control
 

StateBag control in the toolkit is little modified to support scenarios when you might need to use it inside UpdatePanel control.

 

Hope this helps.

Technorati Tags: