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:

24 comments:

Anonymous said...

Doesn't work for me :(

I get the following error:

"Microsoft JScript runtime error: 'cells[...].style' is null or not an object"

My scenario:

I have a grid with dynamically created columns.

ASP.NET 2.0

regards.

Kirill Chilingarashvili said...

Please try to use the controls from this link: http://devarchive.net/downloads/Devarchive.Net_AjaxRepeaterSample.zip

this is latest code, As I remembered I improved some little points there.

Also if this does not help, please send me the code with issue to Administrator @ devarchive.net
I will look at it for sure.(please send only small part that makes an error)

Thanks

Anonymous said...

Actually, it appears to work at first - but my cell contents slide over to the right and arenot aligned perfectly under my headers - if I set a hard width for each of the cells, it works better for some o fthe cols/cells seem to "bleed" over into the adjacent cells/cols, etc.

Anonymous said...

The only way that I can get this to "work" is by setting my cols/cells with finite widths - also, the cell border disappears

Anonymous said...

Headers are not displayed if AutoGenerateColumns is set to "false". I have a grid with dynamically created columns.


Regards

Anonymous said...

It work but the width of header and grid not syn. Please check synwidth code. And it don't have footer

Anonymous said...

thank you very very much

sadettin çetin
türkiye

Anonymous said...

template field in error

sadettin çetin
türkiye

Anonymous said...

I want to use paging with this. How would you make the pager fixed as well?

Anonymous said...

Doing this with CSS seems to be a better deal all alltogether...

http://mattberseth.com/blog/2007/09/freezing_gridview_column_heade_1.html

Kirill Chilingarashvili said...

Not sure if Matt's solution works for all browsers,
I looked at this solution first but gave up after a while

Kirill

Anonymous said...

ok, i am a little slow here. how do i get the extender available in visual studio so that i can "drag it onto the design surface"?

Massoud12345 said...

Just drop the dll from the link Kirill posted to your Bin in your project.

Anonymous said...

I don't download the source code
please send to me,thank you,
emain:zb_zbzb#163.com

Anonymous said...

And horizontal scrollbar ?
Are you going to implement it?

Kirill Chilingarashvili said...

I don't think I have a time to implement horizontal scrollbar, sorry.

I think there is a lot of work to do to achieve that.

Best Regards,
Kirill

Komail Noori said...

Can we have 1st column fixed during horizontal scroll bar.

I need a Microsoft excel like functionality where we can fixed a column and other columns can scroll horizontally but the 1st or first two columns would be fixed.

can you plz help me in this regard.

Komail Noori
www.KomailNoori.com

Dhiren Mistry said...

Hi,

i am getting problem while runtime binding gridview datasource.

its not working while grid is bind after click on search button.

can any one give me solution ?

Dhiren Mistry said...

Hi,

i got solution. Problem is due to not getting "height" and "width" value of gridview render table in extender control javascript, while page is first time load and not binding grid datasource.

I had set default value of "height" and "width" in extender control javascript file.


i had extended control functionality by adding two client-side property like "GVHeight" and "GVWidth".

refer this link how to achive this
for simple extender control example

http://pietschsoft.com/post/2008/05/ASPNET_35_Create_AJAX_Extender_Controls_using_the_ExtenderControl_base_class.aspx

for more details contact send email to me dhiren4uk@gmail.com


thanks & regards,
dhiren mistry

Anonymous said...

Hello. And Bye.

Anonymous said...

Hi,

I have been trying to implement this functionality for the past few days and I had tried a lot of codes but yours worked like a charm...no changes were required...Thanks!!!

Anonymous said...

Hi,

I implemented this gridview extender. It works fine. But there is a problem when editing rows. I have AJAX enabled controls and they don't work...like my calender extendar no longer works. Do you know a fix for this?

Charlie said...

I have a question about your extender. I implemented it and it works great, if the grid has data. My GridView displays an EmptyDataTemplate sometimes, and after the EmptyDataTemplate is displayed, the 'locked' header effect no longer works if I rebind and the GridView has data. Do I need to call a function from the extender when I rebind?

Hansen Rio said...

Thanks for sharing this! I should try them in a minute!