Convert Web.UI ASP.NET Grid into a data entry spreadsheet

Posted Thu Mar 12, 2009 @ 3:19 PM

Using client templates and some JavaScript one can change Web.UI Grid's editing interface to behave more like a data entry spreadsheet.  I started from the code suggestions given in Q10111 - HOWTO: Create a Microsoft Excel spreadsheet-like Grid and added keystroke handling for adding new rows and moving up and down between editable cells (Up Arrow and Down Arrow to go up and down, Tab and Shift+Tab to go right and left).  I also added a delete template (a clickable X) so that users could easily remove rows. In my example I start with a blank Grid which is initially populated in the load client-event with a blank row.

ASPX:
<ComponentArt:Grid id="Grid1"
AllowTextSelection="true"
.... >
    <ClientEvents>
        <load eventhandler="Grid1_onLoad" />
    </ClientEvents>
    <Levels>
        <ComponentArt:GridLevel
        .... >
            <Columns>
                <ComponentArt:GridColumn DataCellClientTemplateId="DeleteCellTemplate" />
                <ComponentArt:GridColumn DataField="ProductName" DataCellClientTemplateId="EditCellTemplate" />
                <ComponentArt:GridColumn DataField="UnitPrice" DataCellClientTemplateId="EditCellTemplate" />
                <ComponentArt:GridColumn DataField="UnitsInStock" DataCellClientTemplateId="EditCellTemplate" />
            </Columns>
        </ComponentArt:GridLevel>
    </Levels>
    <ClientTemplates>
        <ComponentArt:ClientTemplate Id="EditCellTemplate">
            <input style="border-style:none;width:## DataItem.getCurrentMember().get_column().get_width() ##px;"
            id="textbox_## DataItem.get_index() ##_## DataItem.getCurrentMember().get_column().get_dataField() ##"
            type="text"
            value="## DataItem.getCurrentMember().get_value() ##"
            onfocus="this.select();editingTextboxId = this.id;editingClientId = '## DataItem.ClientId ##';
            editingDataField = '## DataItem.getCurrentMember().get_column().get_dataField() ##';
            editingIndexRow = ## DataItem.get_index() ##;"
            onKeyDown="return EditField_onKeyPress(event)"
            onBlur="saveCell('## DataItem.ClientId ##', '## DataItem.getCurrentMember().get_column().get_dataField() ##', this.value);" />
        </ComponentArt:ClientTemplate>
        <ComponentArt:ClientTemplate Id="DeleteCellTemplate">
            <a tabindex="-1"
            style="color:#f00;" 
            href="j__avascript:Grid1.deleteItem(Grid1.getItemFromClientId('## DataItem.get_clientId() ##'));">X</a>
        </ComponentArt:ClientTemplate>
    </ClientTemplates>

JS:
var editingIndexRow = -1;
var editingTextboxId = "";
var editingClientId = "";
var editingDataField = "";
var firstEditableDataField = "ProductName";

function Grid1_onLoad(sender, e)
{
	Grid1_addRow();
}
function saveCell(itemId, columnField, newValue)
{
	var row = Grid1.GetRowFromClientId(itemId);
	// Check if value was changed
	var oldValue = row.GetMember(columnField).Value;
	if (oldValue != newValue)
	{
	// Get column index for SetValue
		var col = 0;
		for (var i=0;i<Grid1.Table.Columns.length;i++)
		{
			if (Grid1.Table.Columns[i].DataField == columnField)
			{
				col = i;
				break;
			}
		}
		row.SetValue(col, newValue, true);
	}
	return true;
}
function Grid1_addRow()
{
	var col = 0;
	if (Grid1.get_table().getRowCount() > 0)
	{
		// cancel adding row if there is already a blank one
		var row = Grid1.get_table().getRow(Grid1.get_table().getRowCount()-1);
		for (var i=0;i<Grid1.Table.Columns.length;i++)
		{
			if (row.getMemberAt(i).get_text() != "")
			{
				Grid1.get_table().addRow();
				break;
			}
		}
	}
	else
	{
		Grid1.get_table().addRow();
	}
	setTimeout("setTextboxFocus();", 100);
}
function setTextboxFocus(datafield, index)
{
	if (typeof datafield == "undefined") datafield = firstEditableDataField;
	if (typeof index == "undefined") index = Grid1.get_recordCount() - Grid1.get_recordOffset() - 1;
	var elementId = "textbox_" + index + "_" + datafield;
	try
	{
		document.getElementById(elementId).focus();
	}
	catch (err)
	{
	}
}

function EditField_onKeyPress(e)
{
	if(!e) e = window.event;
	key = e.keyCode ? e.keyCode : e.which;
	if (key == 13) //enter
	{
		saveCell(editingClientId, editingDataField, document.getElementById(editingTextboxId).value);
		Grid1_addRow();
	}
	if (key == 38) //up
	{
		var index = editingIndexRow - 1;
		if (index > -1)
		{
			editingIndexRow = index;
			saveCell(editingClientId, editingDataField, document.getElementById(editingTextboxId).value);
			setTextboxFocus(editingDataField, index);
		}
	}
	if (key == 40) //down
	{
		var index = editingIndexRow + 1;
		if (index < Grid1.get_pageSize())
		{
			editingIndexRow = index;
			saveCell(editingClientId, editingDataField, document.getElementById(editingTextboxId).value);
			setTextboxFocus(editingDataField, index);
		}
	}
}

Here's a screenshot of what the Grid will look like:
Web.UI Grid using DataCell client templates
Download the code for this example (8.56 KB)

Notes:
i. I tested against Internet Explore 7, Firefox 3, Chrome, Safari and Opera. I set UseSubmitBehavior to false on the ASP Button so that a postback doesn't occur when the user hits Enter, but this doesn't seem to work in Chrome or Safari. The Grid functionality should otherwise be identical across these browsers.
ii. I save cell data whenever key navigation occurs since the Up/Down/Enter strokes won't raise the blur event.
iii. I had considered adding Left/Right Arrow navigation but that would require remembering which column is currently in focus, which would greatly complicate things so I opted not to.
iv. The "save changes" ASP Button simply loops through the Items collection on the server and outputs their values so that you can get an idea of how to store the edits.
v. I give a tabindex of -1 to the DeleteCellTemplate anchor so that it's skipped during Tab navigation.
vi. In the JS section above you'll need to remove the underscores from "j__avascript" as our blog software doesn't allow that string.

Posted to Hwan Hong by hwan

Posted on Thu Mar 12, 2009 @ 3:19 PM

Filed under: , ,

Comments

Posted on Thu Mar 12, 2009 @ 3:19 PM

What if I use asp:TextBox, asp:DropDownList? System does not display asp:TextBox and asp:DropDownList.

Posted on Thu Mar 12, 2009 @ 3:19 PM

This is a client-side solution, so I advise using client templates ie. JavaScript and HTML.  You could replace the DataCell client templates with server templates, but you'd still need to store the changes on the client and handle blur/focus.  On top of that, you'd have to callback/postback in order for the templates to redraw themselves and they wouldn't do this automatically: you'd need to use the ItemContentCreated server event to initialize each template instance.

Posted on Thu Mar 12, 2009 @ 3:19 PM

How can we copy paste to multiple columns and rows. When I copy from Excel multiple columns or rows they only get pasted to one celll in the grid.. alll the values. How can I go about copying pasting them in as many columns and rows, as selected from excel.

This is an urgent request, pls help

Thanks

-Sarah

Posted on Thu Mar 12, 2009 @ 3:19 PM

Sarah, copying and pasting multiple cells from Excel is not supported.  You could perhaps implement this by using a, say, separate text INPUT, and handling the user's pasted data from Excel ie. splitting the data up by tabs and newlines.

Posted on Thu Mar 12, 2009 @ 3:19 PM

Sweet thanks Hwan for your help on this

Incase someone needs help on that scanario. This is what I did to paste content from clipboard to the grid. Content on the clipboard came from the copy event from the spreadsheet or excel.

In the client template id of EditCellTemplate add onpaste="fnPaste()"

After that add this javascript code

function fnPaste() {

       event.returnValue = false;      

       alert(window.clipboardData.getData("Text"));

       var DatafromClipBoard = window.clipboardData.getData("Text");

       var SplitArrayNewLine = DatafromClipBoard.split("\n");

       for (var i = 0; i < SplitArrayNewLine.length; i++)

           {

               var NewLine = SplitArrayNewLine[i];            

               var SplitArrayTabs = NewLine.split("\t");            

               var row = Grid1.get_table().getRow(Grid1.get_table().getRowCount() - 1);

               for (var k = 0; k < SplitArrayTabs.length; k++) {

                   var CellContent = SplitArrayTabs[k];                                    

                   row.SetValue(k+1, CellContent, true);

               }

               Grid1_addRow();        

           }      

   }

Posted on Thu Mar 12, 2009 @ 3:19 PM

Hi Hwan

I am using your code for a user to input data in a spreadsheet way. Now I want ot be able to add css style to the rows based on teh entry that the user makes. Do you have a suggestion for that. Like if a user puts a character in a colum that should only take integers.. I want to make that row Red. What is your suggestion, how can I further develop your code to do that as I want it to turn entire row red right away and stay like that untill the user fixes that row. How to add css style, I cant use conditional formatting 'cause that only works when the data is loaded, here user is adding data in a sheet.

Pls help

Thanks

-Sarah

Posted on Thu Mar 12, 2009 @ 3:19 PM

Nice idea, but ~painfully slow~ on IE.  Have there been any updates?

Posted on Thu Mar 12, 2009 @ 3:19 PM

I actually see similar performance between FireFox 3.6.3 and IE 8.0.7600.  Have you tried reducing the column and/or row count (PageSize) shown?

You may want to start a forum thread for the issue, so that we can more easily exchange code and have a dedicated page for discussion.

Posted on Thu Mar 12, 2009 @ 3:19 PM

Clamont ..Why do you think it will be slow on IE, since these are client side formatting changes only?

Hwan, yes I started a form thread on that.. and was able to resolve it by firing an event upon item change. So yes its been resolved.

www.componentart.com/.../99914.aspx

But I would definately like to know why would it affect performance in browsers?

Thanks

-Sarah

Posted on Thu Mar 12, 2009 @ 3:19 PM

I just came back to this page and am happy to see people are using this forum... even though each reply shows a data of Thu Mar 12, 2009 @ 3:19 PM ... I think this is still an active discussion.

I'll re-run my performance test on IE8 and will create a forum discussion if I see any performance issues.

Posted on Thu Mar 12, 2009 @ 3:19 PM

Here's a different approach to getting an Excel-like interface from Web.UI DataGrid, which uses its native editing functionality combined with the ItemClick client event.

Javascript:

var rowBeingEditedId = '';

function Grid1_onItemClick(sender, e)

{

 if (rowBeingEditedId != e.get_item().get_clientId())

 {

   // save changes

   sender.editComplete();

   // edit new row

   sender.edit(e.get_item());

 }

 rowBeingEditedId = e.get_item().get_clientId();

}

ASPX:

<ComponentArt:DataGrid id="Grid1"

 EditOnClickSelectedItem="false"

 ...

 <ClientEvents>

   <itemClick eventhandler="Grid1_onItemClick" />

It's based on the "Editing - Client Mode" example:

aspnetajax.componentart.com/.../WebForm1.aspx

You'd probably want to add some CSS styling to the textboxes and such to improve the experience, but the performance should be considerably better than the templated version given in the blog post.

Hope this helps!

Posted on Thu Mar 12, 2009 @ 3:19 PM

Dear ComponentArt,

why don't you release an official demo for the spreadsheet feature ?

Like many others, I would like a stable and efficient spreadsheet-like entry component for AJAX and for Silverlight as well.

I don't ask for a "google docs", but something in that direction would be great !

Anonymous comments are not allowed. Click here to log in or create an account.