Introduction
As I'm keen to learn new things, I thought I'd have a go at control extenders. For this extender, my inspiration was a GridView
control which holds records that have to be edited.
In the classic ASP.NET way, we would have a postback of the entire page, get the record returned, but this time entered in textfields to edit the data, post the entire page again to apply the changes and then receive the grid once more to have a consolidated set of data and no more textbox
es. This scenario gives us the same data travelling over the wire twice, plus the form to edit the data in goes over the wire twice as well.
The objective of this extender is to let the user edit the data directly in the grid and only send the affected record to the server, thus saving an awful lot of bandwidth.
Building Blocks
The ASP.NET AJAX Framework is set up in a way which allows us to write extenders for any control. Because the framework is extensible, we can create our own JavaScript objects to use when in edit mode.
All custom objects used in this extender are implemented in JavaScript only. The editable cells are a custom object (BM.Extenders.TableCell
) which injects HTML into the DOM object it is attached to.
Without further ado, I present you the code.
BM.Extenders.GridView Highlights
The GridView
extender hooks up to a GridView
and injects a button column at initialization. The buttons are hidden until their row switches to edit mode.
Also during initialization, an eventhandler is attached to the initializeRequest
of the PageRequestManager
. This eventhandler prevents updates (timer ticks, button clicks, ...) from occurring while a record is in editing mode.
Furthermore, we can define the indexes of the columns which should be editable and as such prevent the user from altering data he should not touch. We can also choose to hide some columns which hold values that are required for the update, but not useful for the user (the ID, fields used in the where
clause of our query, ...).
All that needs to be done is set some properties for alerts if needed (save successful, save failed, item changed, ...), the page method we use for the actual update in the database and – last but not least – initialize our TableCell
objects (see below).
The extender's initialization function (property accessors were removed for brevity):
BM.Extenders.GridViewBehavior.prototype = {
initialize : function() {
BM.Extenders.GridViewBehavior.callBaseMethod(this, 'initialize');
Sys.WebForms.PageRequestManager.getInstance().add_initializeRequest(onPostBack);
var element = this.get_element();
gridName = element.id;
initialise();
},
dispose : function() {
Sys.WebForms.PageRequestManager.getInstance().remove_initializeRequest
(onPostBack);
BM.Extenders.GridViewBehavior.callBaseMethod(this, 'dispose');
},
...
The initializeRequest
eventHandler:
function onPostBack(sender, args)
{
if(editing)
{
args.set_cancel(true);
}
}
We simply cancel the request when in editing mode.
BM.Extenders.TableCell Highlights
We need to have a way to know which cell we are editing. A cell can be uniquely identified by the row and column it is in. The TableCell
object has two properties: "row
" and "col
". During its initialization, an eventhandler is attached to the click
event and the properties are populated.
cell = $create(BM.Extenders.TableCell, {row: i, col: editableColumns[j]},
{click: edit}, null, rows[i].cells[editableColumns[j]]);
The eventhandler is quite simple. It gets passed a reference to the eventElement
, by which we can get hold of the properties. The first thing to do is check whether this is the first cell we're going to edit, a cell in the same row as the previous one or a cell in an entirely different row. For internal use, the row, column and old value are stored in an array. After this, the textbox
is added and buttons are shown.
function edit(eventElement)
{
var row = eventElement.get_row();
var col = eventElement.get_col();
if(!editing)
{
var cell = getCell(row, col);
setEditing(row, col);
makeEditable(cell);
}else{
if(evalEditingRow(row, col))
{
var cell = getCell(index[0], index[1]);
updateCell(cell);
index.Slide(1,2);
cell = getCell(row, col);
setEditing(row, col);
makeEditable(cell);
}
if(evalDifferentRow(row))
{
var cell = getCell(index[0], index[1]);
updateCell(cell);
var changed = evalCellChanged();
var proceed = false;
if(!changed || proceed)
{
resetValues();
index.Clear();
var cell = getCell(row, col);
setEditing(row, col);
makeEditable(cell);
}else{
}
}
}
}
The variable "index
" is an array to which I added a function Slide
to move the items back and create space and a function Clear
which clears the array.
Array.prototype.Slide = function(start, places)
{
for(i=start; i<=places; i++)
{
this.push(index[i]);
this[i] = "";
}
}
Basically, the current item is added at the end of the array (push) and emptied.
Array.prototype.Clear = function()
{
this.length = 0;
}
Other functions called in edit:
function getCell(row, col)
{
return $get(gridName).rows[row].cells[col];
}
function makeEditable(cell)
{
index[2] = cell.innerHTML;
cell.innerHTML = "<input type=\"text\" value=\"" + index[2] + "\"/>";
showButtons(index[0]);
}
function updateCell(cell)
{
var text = cell.getElementsByTagName("input")[0].value;
cell.innerHTML = text;
}
function setEditing(row, col)
{
index[0] = row;
index[1] = col;
editing = true;
}
function resetValues()
{
var row = index[0];
for(i=1; i<index.length; i+=2)
{
var cell = getCell(row, index[i]);
cell.innerHTML = index[i+1];
}
}
When sending the edited data, the function send
is called with a reference to the row.
function send(row)
{
var cell = getCell(index[0], index[1]);
updateCell(cell);
hideButtons();
removeEditing();
var row = $get(gridName).rows(row);
var arguments = new Array();
for(i=0; i<buttonColumn; i++)
{
arguments[i] = row.cells[i].innerHTML;
}
var path = window.location;
Sys.Net.WebServiceProxy.invoke(path, serviceMethod,
false, {args:arguments}, OnCallSaveComplete, OnCallSaveError);
}
First, the textbox
is removed after which the values (cell's text) can be put in an array. At last, a call to the AJAX frameworks Sys.Net.WebServiceProxy.invoke
method is made.
Methods called by send:
function removeEditing()
{
index.Clear();
editing = false;
}
function hideButtons()
{
var rows = $get(gridName).rows;
for(i=1; i<rows.length; i++)
{
rows[i].cells<buttoncolumn />.getElementsByTagName("span")[0].style.visibility = "hidden";
}
}
About Localization and Periodic Updates
To localize the text properties (thou shall address your users in their language), we can make use of resource files and add a meta:resourcekey
element to the server tag of the extender. This has been tested and worked very fine.
Periodic updates can be triggered by a timer for instance. Since the GridView
and its extender have to be in the same updatepanel
(luckily), the extender is initialized whenever the updatepanel
is refreshed.
Possible Improvements and Future Additions
I have already got some ideas to take this even further. I could add support for:
- masked
textbox
es (a popular AJAX extender) - regular expressions defined per column so the input can be validated on the user side when the
textbox
's blur event fires
History
- 2nd October, 2007: Initial version