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