Creating a GridView with Resizable Column Headers
I was recently reading Dan Wahlin's excellent 'How To' for creating a GridView extender control. While I was going through his code samples, I thought it might be interesting to create another GridView extender that allows you to resize the column widths of the GridView by clicking and dragging the header cell borders. Before I went down the path of actually implementing the extender control, I wanted to get a feel for what the client side code for handling this might look like. So I quick created a sample application where I added this functionality to the Google Analytics GridView I blogged about in an earlier post.
** Because this sample is a proof of concept, I only tested in IE7. If I move the code to an extender control I will go back and test it with other browsers.
Before I get into the implementation details, here is a quick screen shot of the grid. Unfortunately the mouse cursor didn't come across in the screen shot, but when it is placed over the cell borders in the header row, it displays the east/west pointing arrow.
Adding this behavior to the existing GridView required implementing a handful of JavaScript functions. My approach for implementing this features was to do the following:
- Handle each of the header cells's mousemove events to determine when the user has the cursor placed roughly over the cell's right hand border. If a resize is already in processes then I use this event to determine what the new width of the column should be
- Handle each of the header cell's mousedown events to determine when the resizing begins
- Handle the document's mouseup event. When this occurs I make sure that the resize has stopped
- Handle the document's selectstart event. I cancel this event if a resize is currently in executing. Doing this make's sure that the header cell's text isn't highlighted when I am resizing the cell. I learned this trick from the AjaxControlToolkit's ResizeableControlExtender. Here is a screen shot of what this would look like if you don't cancel this event:
pageLoad Event Handler
First, I added the pageLoad JavaScript function to my page. The ASP.NET AJAX framework will call this method for you so you will not have to worry about wiring it up. I use this method to add event handlers to the mousemove and mousedown events for the table's header cells. Additionally I add handlers for the document level mouseup and selectstart events as follows ...
// true when a header is currently being resized var _isResizing; // a reference to the header column that is being resized var _element; // an array of all of the tables header cells var _ths; function pageLoad(args){ // get all of the th elements from the gridview _ths = $get('gvCustomers').getElementsByTagName('TH'); // if the grid has at least one th element if(_ths.length > 1){ for(i = 0; i < _ths.length; i++){ // determine the widths _ths[i].style.width = Sys.UI.DomElement.getBounds(_ths[i]).width + 'px'; // attach the mousemove and mousedown events if(i < _ths.length - 1){ $addHandler(_ths[i], 'mousemove', _onMouseMove); $addHandler(_ths[i], 'mousedown', _onMouseDown); } } // add a global mouseup handler $addHandler(document, 'mouseup', _onMouseUp); // add a global selectstart handler $addHandler(document, 'selectstart', _onSelectStart); } }
TH's mousemove Event
Next, I add logic to the mousemove event handler. This event will fire everytime the mouse position changes while the cursor is over any of the header cells. The code in this handler first checks to see if we are currently resizing a cell. If we are, we do a little math to figure out what the new width of the column being resized needs to be. If a resize is not currently taking place, then I check to see how close the mouse cursor is to the cells right hand border. If it is within 2 pixels I display the east/west cursor so the user knows they can resize the cell. Here is the JavaScript code for this handler ...
function _onMouseMove(args){ if(_isResizing){ // determine the new width of the header var bounds = Sys.UI.DomElement.getBounds(_element); var width = args.clientX - bounds.x; // we set the minimum width to 1 px, so make // sure it is at least this before bothering to // calculate the new width if(width > 1){ // get the next th element so we can adjust its size as well var nextColumn = _element.nextSibling; var nextColumnWidth; if(width < _toNumber(_element.style.width)){ // make the next column bigger nextColumnWidth = _toNumber(nextColumn.style.width) + _toNumber(_element.style.width) - width; } else if(width > _toNumber(_element.style.width)){ // make the next column smaller nextColumnWidth = _toNumber(nextColumn.style.width) - (width - _toNumber(_element.style.width)); } // we also don't want to shrink this width to less than one pixel, // so make sure of this before resizing ... if(nextColumnWidth > 1){ _element.style.width = width + 'px'; nextColumn.style.width = nextColumnWidth + 'px'; } } } else{ // get the bounds of the element. If the mouse cursor is within // 2px of the border, display the e-cursor -> cursor:e-resize var bounds = Sys.UI.DomElement.getBounds(args.target); if(Math.abs((bounds.x + bounds.width) - (args.clientX)) <= 2) { args.target.style.cursor = 'e-resize'; } else{ args.target.style.cursor = ''; } } }
TH's mousedown Event
The mousedown event handler if very simple. If checks if the cursor is style is 'e-resize'. If so it sets the _isResizing bit to true so the other handlers know that a resize is currently taking place. It also grabs the header cell that is being resized. Here is the code ...
function _onMouseDown(args){ // if the user clicks the mouse button while // the cursor is in the resize position, it means // they want to start resizing. Set _isResizing to true // and grab the th element that is being resized if(args.target.style.cursor == 'e-resize') { _isResizing = true; _element = args.target; } }
document's mouseup Event
Next, I added code for the mouseup event. This is a handler is setup at the document level so no matter where the user lets go of the button this logic will be executed. This handler does 2 things: resets the _isResizing and _element values back to there unitialized state and resets the header cells cursor style back to its initial state. Here is the code for this ...
function _onMouseUp(args){ // the user let go of the mouse - so // they are done resizing the header. Reset // everything back if(_isResizing){ // set back to default values _isResizing = false; _element = null; // make sure the cursor is set back to default for(i = 0; i < _ths.length; i++){ _ths[i].style.cursor = ''; } } }
document's selectstart Event
Finally, the last handler. Like I mentioned earlier, I added this one to prevent the header cell's text from highlighting as I am resizing the cells. All this function does is cancel's the event if a resize is currently executing. Here is the code for this ...
function _onSelectStart(args){ // Don't allow selection during drag if(_isResizing){ args.preventDefault(); return false; } }
That's it. Enjoy!
Comments
I am using gridview control and i want to resize the column header of gridview. Please suggest me , where should i call pageLoad(args) frunction in my code. My site is not ajax enabled. pleae reply me
Awesome! It would be really cool if when you double-clicked on the dividers it automatically sized it perfectly, as Excel does. Great post, thanks for sharing!
Nice example. You could add an array to keep the column widths that gets filled in the first time through the pageLoad function and is updated by the mousemove function. Those values would then be used to reset the columns sizes in a subsequent page load to maintain the widths of the columns between pages.
Josh, if you are using IE, you could add the dblclick event processing along with the mousemove and mousedown on each TH to calculate the width necessary for autosizing the column by creating a text range, moving it to each cell in that column, getting the boundingWidth and getting the maximum width for the column entries and then setting the width with that value.
Another great post. Thank you.
Cheers,http://www.bloggingdeveloper.com/
hi,
_ths = $get(GVIBS).getElementsByTagName(TH);
if i put alert above this line it works but if i put alert after belowe this line it doesnt work.So i am not able to resize column in my gridview.So what should I do now?
Thanx
In Firefox, resizing will still select text of another column if your mouse is over its text.
Mike,
FireFox doesnt implement onselectstart, for FireFox and other browsers you can use CSS browser directives like:
-moz-user-select: none;
-khtml-user-select: none;
Hope this helps.
-Q
For some instances, it may be a good practice to set the CellPadding property of the gridview to some values between 15 and 20 in order to avoid the overlapping of the HeaderText and the sorting arrow gifs.
Thanks for your good stuff, I have you live demo and try to resize columns but when I click column to sort data, it does not persist the column size
Thanks for this great article. It gave me the simple solution to implement keyboard navigation on the grid view.
Add a key board handler and a mouse click handler as follows;
var _trs;
var curSelRow;
var currow=0;
function _onKeyDown(ev){
//
var k = ev.keyCode ? ev.keyCode : ev.rawEvent.keyCode;
if (k === Sys.UI.Key.down){
var nextrow=currow +1;
if (nextrow >(_trs.length-1) ){
nextrow=_trs.length-1;
}
var trnext=_trs[nextrow];
trnext.focus();
_selrow(trnext);
}
if(k === Sys.UI.Key.up){
var nextrow=currow -1;
if (nextrow
nextrow=1
}
var trnext=_trs[nextrow];
trnext.focus();
_selrow(trnext);
}
}
Hook the Mouse keyboard event as shown below in the page script
function pageLoad(args){
// get all of the tr elements from the gridview
_trs=$get(GridView1).getElementsByTagName(tr);
if(_trs.length > 1){
for(i = 1; i
// attach Keydown Keyup
if(i
$addHandler(_trs[i], keydown, _onKeyDown);
}
}
}
}
Hook the Mouse click in Row created event as shown below in the code behind of the Aspx page
protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
e.Row.Attributes.Add("onclick", "_selrow(this)");
}
}
I can submit the working sample that I modified using your Column downloaded sample which has the implemented feature.
Great article!
I just added some treat to the code, cause my grid is only rendered after a post back.
try { _ths = $get(grvBusca).getElementsByTagName(TH); } catch (e) { return; }
hi matt
your code works great
but i have an issue.my table is something as below
[table cellspacing="0" rules="all" border="1" id="gvCustomers" style="border-collapse:collapse;"]
[tr]
[th align="left" colspan="10"][table cellpadding="0" cellspacing="0" border="0" width="100%"]
[tr]
[td width="50%" align="left" class=""][span id="gvCustomers_ctl01_lblRecordSummary"]You have currently 0 item(s) defined.[/span][/td]
[td align="right" class=""][a id="gvCustomers_ctl01_btnExportExcel" href="javascript:__doPostBack(gvCustomers$ctl01$btnExportExcel,)"]ExportToExcel[/a][/td]
[/tr]
[/table]
[/th]
[/tr][tr]
[/tr][tr]
[th scope="col"]rollno[/th][th scope="col"]fname[/th][th scope="col"]lname[/th][th scope="col"]sex[/th][th scope="col"]class[/th][th scope="col"]subject1[/th][th scope="col"]subject2[/th][th scope="col"]subject3[/th][th scope="col"]phno[/th][th scope="col"]email[/th]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]0[/td][td] [/td][td] [/td][td] [/td][td] [/td][td] [/td][td] [/td][td] [/td][td] [/td][td] [/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]1[/td][td]nitin[/td][td]bajaj[/td][td]male[/td][td]10[/td][td]Physics[/td][td]Chemistry[/td][td]Maths[/td][td]1234[/td][td]a@b.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]2[/td][td]deepesh[/td][td]jain[/td][td]female[/td][td]11[/td][td]Physics[/td][td]Chemistry[/td][td] [/td][td]246457[/td][td]c@d.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]3[/td][td]ajay[/td][td]singh[/td][td]male[/td][td]12[/td][td]Physics[/td][td]Chemistry[/td][td] [/td][td]515474[/td][td]c@d.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]4[/td][td]sulabh[/td][td]gupta[/td][td]male[/td][td] [/td][td]Physics[/td][td]Chemistry[/td][td] [/td][td]246457[/td][td]a@b.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]5[/td][td]abhinav[/td][td]mehra[/td][td]male[/td][td]10[/td][td] [/td][td]Chemistry[/td][td]Maths[/td][td]543125[/td][td]a@b.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]6[/td][td]nihit[/td][td]bajaj[/td][td]male[/td][td]12[/td][td]Physics[/td][td]Chemistry[/td][td] [/td][td]4573757[/td][td]c@d.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]8[/td][td]parul[/td][td]mahajan[/td][td]female[/td][td]11[/td][td]Physics[/td][td]Chemistry[/td][td] [/td][td]3577[/td][td]a@b.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]9[/td][td]anshul[/td][td]mittal[/td][td]male[/td][td]11[/td][td]Physics[/td][td]Chemistry[/td][td] [/td][td]457437[/td][td]c@d.com[/td]
[/tr][tr onmouseover="this.style.backgroundColor = ;" onmouseout="this.style.backgroundColor = ;"]
[td]10[/td][td]nitin[/td][td]singh[/td][td]male[/td][td]10[/td][td]Physics[/td][td] [/td][td]Maths[/td][td]52436[/td][td]c@d.com[/td]
[/tr]
[/table]
i want to resize the columns of the base tablewhile not altering the nested table.hw do i go about doing it?
thanx
I made a Control Extender from this code. You can download the code here if you want:
http://blog.lavablast.com/post/ASPNET-GridView-column-resizing.aspx
Thanks Matt the code is working but the problem is the column is not being compressed its only extending in the grid view control can u help me
Hi Matt,
Its very good.
will it work without ajax ?
If yes where should i call pageload method.
Thanks very much
Excellent, this is extremely useful and interesting. I have always used CSS to make the columns fixed in size to stop the jumping around effect; but this blends a deeper more usable interface for the user.
First Thanks for the grate code.
I would like to know how to create an extender control with resizable columns with the above code example. Thanks in advance for your response.
Hi
indeed this is great but i am using Ajaxtoolkit in my project and if i am placing the after ajaxtoolkits script manager it is giving me error
if i put asp:scriptmanager then in name space ambiguity is occuring does any one has any idea about this , i really need this.
Thanks and Regards
Parth
Very useful code it was the solution I looking for. thanks
this is really a simple solution for most cases.
but the demo page is not working. maybe you should add the pageLoad to body.onload
Thank you for your work.
I am trying to implement it using a Javascript file
so I can call this function for all the GridViews on different pages
How do I accomplsh this
Page.RegisterClientScriptBlock("MyScript", @"");
Page.ClientScript.RegisterStartupScript(Type.GetType("System.String"), "addScript", "MyPageLoad('ctl00_ContentPlaceHolder1_GridView2');", true);
How to maintain widht between subsequent page load
Hi, very helpful post. I noticed one small problem: resizing doesn't work if the browser window scrollbars are in non-zero position. I think you need to consider the scrollbar position, as in:
var width = document.body.scrollLeft + args.clientX - bounds.x;
Hi Matt,
It's very good.
it is helped me very lot..
I need one help..
i need to provide column and header freezing along with resizing...
Can any body help me on this?
Thanks and Regards
Murali.MD