Real World GridView: Bulk Editing

GridView is a tremendously useful control in ASP.NET 2.0. Despite being much improved from the DataGrid of the ASP.NET 1.x era, GridView still lacks some features which always seem to be requested. This is a multipart post on how to customize GridView to add some of this missing functionality. I'll post the full source code at the end of the series. Hopefully this will be useful to everyone.

One of the most needed improvements is the ability to edit multiple rows at once. By default, GridView allows users to modify only one row at a time, and requires the user to postback many times to switch between editable rows. Here is a quick look at the "Out of the Box" GridView.

This is not the greatest user experience, so we'll start by allowing the user to modify all of the rows before saving. To begin, we will create a custom control which inherits from GridView. This will allow us to override and change some of the behavior we don't like. One of the first things we want to change is how the rows get created.

   public class BulkEditGridView : System.Web.UI.WebControls.GridView
{
protected override GridViewRow CreateRow(int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState)
{
return base.CreateRow(rowIndex, dataSourceIndex, rowType, rowState | DataControlRowState.Edit);
}
}

What we have done here is make sure that every row which gets created has a rowstate of Edit, meaning it's editable. Since the row is now editable, the columns that are NOT read-only will show up as textboxes (for boundfields) or what ever their EditItemTemplate is. Here is a look at our grid.

Wow! Looks great ... are we done yet ... um ... not quite. That would be great if that was all we had to do, but unfortunately there is a bit more. Notice that there is no way to save our changes, which might be a <sarcasm>minor</sarcasm> problem for our user. In ASP.NET 1.X, we might have just added a Save() function and made developers add code to the code-behind file to call our function. Well, this is now a 2.0 world, and we are going to create a great design time experience. We'll allow developers to set the SaveButtonID on our grid, and we'll take care of setting up all the event handlers.

   [IDReferenceProperty(typeof(Control))]
public string SaveButtonID
{
get
{
string val = (string)this.ViewState["SaveButtonID"];
if (val == null)
{
return string.Empty;
}
return val;
}
set
{
this.ViewState["SaveButtonID"] = value;
}
}

The IDReferenceProperty tells Visual Studio that this property contains the ID of another property on the page. This way you get a nice dropdown of control ids in the Properties Window. It's just nice "fit and finish" that makes our control that much more developer friendly.

   protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);

      //Attach an event handler to the save button.
if (false == string.IsNullOrEmpty(this.SaveButtonID))
{
Control btn = RecursiveFindControl(this.NamingContainer, this.SaveButtonID);
if (null != btn)
{
if (btn is Button)
{
((Button)btn).Click += new EventHandler(SaveClicked);
}
else if (btn is LinkButton)
{
((LinkButton)btn).Click += new EventHandler(SaveClicked);
}
else if (btn is ImageButton)
{
((ImageButton)btn).Click += new ImageClickEventHandler(SaveClicked);
}

            //add more button types here.
}
}
}

   private void SaveClicked(object sender, EventArgs e)
{
this.Save();
this.DataBind();
}

The developer doesn't have to write any C# to get the save button to save data!! Thats a great developer experience, and the kind of standard we should hold ourselves to. Of course, we want to be flexible and we will expose the Save() method publically so developers CAN access this programatically if they want. We have added support for Buttons, LinkButtons, and ImageButtons, but there could be more controls you want to include. The click event of all these controls is routed to the same SaveClicked handler which calls Save(). Pretty simple. 

On a Tangent: You might wonder what the RecursiveFindControl function is. Why can't we just use FindControl? Well, FindControl() works great when we are not using MasterPages, but if our grid is in a different ContentSection than the Button, FindControl() won't find the control we are looking for. FindControl() is scoped to the NamingContainer that our control exists in. To work around this, we recusively climb the tree of NamingContainers to find our button.

And now on to actually saving. One thing we want to do, is make sure that we don't save rows that have not changed. Well, how do we know if a row changes? We have to watch for it to change.

   protected override void InitializeRow(GridViewRow row, DataControlField[] fields)
{
base.InitializeRow(row, fields);
foreach (DataControlFieldCell cell in row.Cells)
{
if (cell.Controls.Count > 0)
{
AddChangedHandlers(cell.Controls);
}
}
}

   private void AddChangedHandlers(ControlCollection controls)
{
foreach (Control ctrl in controls)
{
if (ctrl is TextBox)
{
((TextBox)ctrl).TextChanged += new EventHandler(this.HandleRowChanged);
}
else if (ctrl is CheckBox)
{
((CheckBox)ctrl).CheckedChanged += new EventHandler(this.HandleRowChanged);
}
else if (ctrl is DropDownList)
{
((DropDownList)ctrl).SelectedIndexChanged += new EventHandler(this.HandleRowChanged);
}
}
}

   void HandleRowChanged(object sender, EventArgs args)
{
GridViewRow row = ((Control) sender).NamingContainer as GridViewRow;
if (null != row && !dirtyRows.Contains(row.RowIndex))
{
dirtyRows.Add(row.RowIndex);
}
}

   private List<int> dirtyRows = new List<int>();

Adding XxxxxChanged handlers to the controls allows us to know when a control in a row changed values. If any value in the row changes, we mark the row as "dirty". Like with the buttons, I have added some of the most common input controls. You may wish to add additional ones. Additionally, TemplateFields might have nested controls, so you might want to recursively look for controls. 

I'm sure that you have already figured out what our save method will look like. Just loop through the dirty rows and save them.

   public void Save()
{
foreach (int row in dirtyRows)
{
this.UpdateRow(row, false);
}

      dirtyRows.Clear();
}

Using the GridView's UpdateRow() function allows our save to behave just like the "Out of the Box" save. An associated DataSourceControl will be used to update the data, the RowUpdated event will fire, and everything else the basic save does. Finally, here is the ASPX.

   <asp:Button runat="server" ID="SaveButton" Text="Save Data" />
<blog:BulkEditGridView runat="server" id="EditableGrid" DataSourceID="AdventureWorks" AutoGenerateColumns="False" DataKeyNames="LocationID" SaveButtonID="SaveButton" >
<Columns>
<asp:BoundField DataField="LocationID" HeaderText="LocationID" InsertVisible="False" ReadOnly="True" SortExpression="LocationID" />
<asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
<asp:BoundField DataField="Availability" HeaderText="Availability" SortExpression="Availability" />
<asp:BoundField DataField="CostRate" HeaderText="CostRate" SortExpression="CostRate" />
</Columns>
</blog:BulkEditGridView>

I wrote this to give you a basic understanding of how to create a bulk edit GridView. Of course there are many enhancements you could add to this. Perhaps you only want some of the rows to be editable, not all of them. Or, you want the grid to not be editable until the user clicks an "edit everything" button. Maybe you would like all of the changes to complete in a transaction. And probably you want this to do something I haven't even thought of (I would like to know if you come up with something though). I've added some of these enhancements for customers on projects, and most of them are not that difficult to implement.

 

NOTE: These comments are the authors own thoughts, and do not represent an official Microsoft recommendation. This post is for educational purposes only.
   

UPDATE: Thanks everyone for the feedback!! Based on several of your comments, I have incorporated an inline insert into the grid (still very beta). Also I have attached the source code as a zip for those people who can't see the Got Dot Net workspace. I can't promise I'll keep the attachment up to date, but the workspace will always have the latest.

UPDATE (JAN 2007): Updated source code is now available on CodePlex, and it should be easier to download than it was on GotDotNet.
https://www.codeplex.com/ASPNetRealWorldContr

RealWorldGrids.zip