Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

EditableGridView

2.36/5 (4 votes)
2 Oct 2007CPOL4 min read 1   635  
An article on a GridView extender which enables in-place editing of the records
Screenshot - EditableGridView.gif

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 textboxes. 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):

C#
BM.Extenders.GridViewBehavior.prototype = {
    initialize : function() {
        BM.Extenders.GridViewBehavior.callBaseMethod(this, 'initialize');
        //hook up to the initialization of the request
        Sys.WebForms.PageRequestManager.getInstance().add_initializeRequest(onPostBack); 
        var element = this.get_element();
        gridName = element.id;
        initialise();
    },

    dispose : function() {
        //remove the event handler
        Sys.WebForms.PageRequestManager.getInstance().remove_initializeRequest
								(onPostBack);
        BM.Extenders.GridViewBehavior.callBaseMethod(this, 'dispose');
    },
    ...

The initializeRequest eventHandler:

C#
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.

C#
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.

C#
function edit(eventElement)
{
    var row = eventElement.get_row();
    var col = eventElement.get_col();
    //first cell to edit
    if(!editing)
    {
        var cell = getCell(row, col);
        setEditing(row, col);
        makeEditable(cell);
    }else{
        //determine which cell was previously being edited
        if(evalEditingRow(row, col))
        {
            //another cell in the same row
            //remove textbox from previously edited cell
            var cell = getCell(index[0], index[1]);
            updateCell(cell);
            //push the items back to make room
            index.Slide(1,2);
            cell = getCell(row, col);
            setEditing(row, col);
            makeEditable(cell);
        }
        if(evalDifferentRow(row))
        {
            //click raised on different row
            var cell = getCell(index[0], index[1]);
            //remove textbox
            updateCell(cell);
            var changed = evalCellChanged();
            var proceed = false;
            //notification removed for brevity
            if(!changed || proceed)
            {
                resetValues();
                index.Clear();
                var cell = getCell(row, col);
                setEditing(row, col);
                makeEditable(cell);
            }else{
                //put textbox back
            }
        }
    }
}

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.

C#
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.

C#
Array.prototype.Clear = function()
{
    this.length = 0;
}

Other functions called in edit:

C#
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.

C#
function send(row)
{
    //remove textbox
    var cell = getCell(index[0], index[1]);
    updateCell(cell);
    hideButtons();
    removeEditing();
    //load values from grid
    var row = $get(gridName).rows(row);
    var arguments = new Array();
    for(i=0; i<buttonColumn; i++)
    {
        arguments[i] = row.cells[i].innerHTML;
    }
    //Call server side function
    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:

C#
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 textboxes (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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)