Introduction
This article describes an extended GridView
ASP.NET control which adds insert functionality that can be used in a similar manner to the existing edit and delete functionality. It also looks at the internal working of the GridView
control and identifies some useful methods for extension.
Updated: Several bugs have been fixed in the code. See the history for more information.
Background
Let's face it, the DataGrid
was poor. It proudly strutted around, claiming to do everything you could ever want to do when displaying tabular data and that it was going to bring about world peace and a cure for cancer. Okay, so I made the last two up, but it was never really that good at what it was supposed to do. I hated the way it teased you with properties like AllowPaging
and AllowSorting
only to leave you out in the cold as you had to manually wire up all the paging and sorting plumbing yourself. Thanks a bunch.
Then, with a triumphant fanfare, along came ASP.NET 2 and its new flagship control the GridView
. Having long since consigned the DataGrid
to the bin, instead hand-coding slick Repeaters
and DataLists
, I was sceptical about trying out the latest bloatware. But now, like a crazed born-again zealot, I've fallen for the GridView
and all the easy-peasy declarative joy it brings. Combine the GridView
with an ObjectDataSource
and you can do all your paging, sorting, searching, editing and deleting without writing any code. And that's not a marketing "no code" that really means "just one or two methods", but a full-on serious "no code".
My only complaint is that there's no native support for inserting new records via the GridView
. This is something that I've done quite a lot in the past, especially in those ever-thrilling list maintenance pages. The technique in the past has been to add a blank dummy row to the top of the GridView
and allow users to insert via that row. Here's how it's supposed to look.
The general technique for achieving this goes something like this:
- Get your data
- Jimmy it around by inserting a blank record at the very start (i.e.: index 0)
- Bind the jimmied data to the grid
- Jimmy the page size as the first page needs to show the insert row
- Do some more jimmying as rows are bound, fiddling with the command buttons
Now I've got nothing against people called Jimmy, Jimbo, Jim or James but that's a lot of faff and palaver to do over and over again on all these wonderfully exciting list maintenance pages that I should really be doing with my eyes shut. So what I've done is extend the GridView
to support insertion like this. I've tried to mimic its modus operandi for editing a row, so it should be straightforward to use and directly support its relationship with data sources.
It's been tough, mostly to do with figuring out how the GridView
actually works under the hood, and I couldn't have done it without the most excellent .NET disassembly tool Reflector which I can't recommend highly enough if you're getting into control development. But I got there and present to you (almost) everything you could ever want to do with a GridView
, but were afraid to ask for.
Cosmetic Improvements
Result Summary
Quite often I use a GridView
to display search results, which means I'm always putting phrases like "Results 1-10 out of 50" in literal controls which I've got to remember to show and hide all the time an oh my it's a bore, so I've added a "summary row" which displays this information automatically. The summary inserts itself just above the header, as that's how I usually make my grids look, but could go anywhere you liked.
protected override void RenderContents(HtmlTextWriter writer)
{
if (this.ShowResultSummary && this.PageCount != 0)
{
int firstResultIndex = this.PageIndex * this.PageSize;
HtmlGenericControl topSummaryControl = new HtmlGenericControl("div");
topSummaryControl.Style.Add("float", "left");
topSummaryControl.InnerHtml = string.Format("<b>Records:</b> {0} to {1} of {2}",
firstResultIndex + 1, firstResultIndex +
this.Rows.Count, this.DataSourceCount);
HtmlGenericControl bottomSummaryControl = new HtmlGenericControl("div");
bottomSummaryControl.Style.Add("float", "left");
bottomSummaryControl.InnerHtml = topSummaryControl.InnerHtml;
if (this.PageCount == 1)
{
this.Controls[0].Controls.AddAt(0, this.CreateSummaryRow(topSummaryControl));
this.Controls[0].Controls.Add(this.CreateSummaryRow(bottomSummaryControl));
}
else
{
if (this.TopPagerRow != null)
this.TopPagerRow.Cells[0].Controls.Add(topSummaryControl);
if (this.BottomPagerRow!= null)
this.BottomPagerRow.Cells[0].Controls.Add(bottomSummaryControl);
}
}
base.RenderContents(writer);
}
private TableRow CreateSummaryRow(Control summaryControl)
{
TableRow summaryRow = new TableRow();
TableCell summaryCell = new TableCell();
summaryCell.ColumnSpan = this.HeaderRow.Cells.Count;
summaryRow.Cells.Add(summaryCell);
summaryCell.Controls.Add(summaryControl);
return summaryRow;
}
private int _dataSourceCount;
[DefaultValue(false)]
[Category("Appearance")]
[Description("Whether the results summary should be shown.")]
public bool ShowResultSummary
{
get
{
if (this.ViewState["ShowResultSummary"] == null)
return false;
else
return (bool)this.ViewState["ShowResultSummary"];
}
set { this.ViewState["ShowResultSummary"] = value; }
}
public int DataSourceCount
{
get
{
if (this.Rows.Count == 0)
return 0;
else if (this.AllowPaging)
return this._dataSourceCount;
else
return this.Rows.Count;
}
}
The couple of properties allow the summary row to be turned on and off at will and provide a way to get hold of the total number of records in the data source that was bound to the GridView
, the absence of which always irritated me in the past. The value is taken from the InitializePager
method (which I've omitted here, but you'll find it in the demo project) and is a very useful method and worthy of an article all of its own.
Sort Indicators
Something else the absence of which has always baffled me is column sort indicators. Two new properties allow you to set the ascending and descending images. If you're hardcore you might like to embed the supplied images as Web resources and use these as defaults. The images are injected into the appropriate column in the header row when it is initialized by the InitializeRow
method.
protected override void InitializeRow(GridViewRow row, DataControlField[] fields)
{
base.InitializeRow(row, fields);
if (row.RowType == DataControlRowType.Header && this.AscendingImageUrl != null)
{
for (int i = 0; i < fields.Length; i++)
{
if (this.SortExpression.Length > 0 && fields[i].SortExpression ==
this.SortExpression)
{
Image sortIndicator = new Image();
sortIndicator.ImageUrl =
this.SortDirection == SortDirection.Ascending ?
this.AscendingImageUrl : this.DescendingImageUrl;
sortIndicator.Style.Add(HtmlTextWriterStyle.VerticalAlign, "TextTop");
row.Cells[i].Controls.Add(sortIndicator);
break;
}
}
}
}
[Editor(typeof(ImageUrlEditor), typeof(UITypeEditor))]
[Description("Image that is displayed when SortDirection is ascending.")]
[Category("Appearance")]
public string AscendingImageUrl
{
get { return this.ViewState["AscendingImageUrl"] as string; }
set { this.ViewState["AscendingImageUrl"] = value; }
}
[Editor(typeof(ImageUrlEditor), typeof(UITypeEditor))]
[Description("Image that is displayed when SortDirection is descending.")]
[Category("Appearance")]
public string DescendingImageUrl
{
get { return this.ViewState["DescendingImageUrl"] as string; }
set { this.ViewState["DescendingImageUrl"] = value; }
}
InitializeRow
is another interesting method, providing a means to perform extra tasks when each row is initialized. You could think of it as an internal OnRowCreated
, but with greater access to how the row is constructed.
Insert Functionality
Okay, I've teased you enough with the cosmetic stuff, here's the real meat.
When implementing this functionality, I wanted to support as much of the existing functionality in the Framework as possible, especially when working with data sources and data binding to the grid. I also wanted to mimic the existing interface as much as possible to keep things consistent, so the first thing I did was introduce two new events, RowInserting
and RowInserted
, which would fire just prior to and just after the actual insertion takes place just as with the RowUpdating
and RowUpdated
events. I also created two custom EventArg
classes, GridViewInsertEventArgs
and GridViewInsertedEventArgs
to accompany these events, again following the row update pattern.
[Category("Action")]
[Description("Fires before a row is inserted.")]
public event EventHandler<GridViewInsertEventArgs> RowInserting;
[Category("Action")]
[Description("Fires after a row has been inserted.")]
public event EventHandler<GridViewInsertedEventArgs> RowInserted;
I also added a few more properties to make the grid as flexible as possible. AllowInserting
allows users to enable or disable the insert functionality altogether for times when the grid is used in a read-only or update-only mode. InsertRowActive
controls the default state of the insert row and if true
requires the user to click a "New" button to switch the insert row into its edit state.
With these properties in place, the next thing to do is worry about actually creating the insert row. Previously the tactic was to add a dummy row to the first page of results, but this played havoc with your Rows
collection and mucked up paging, so I went for the CreateChildControls
method which ASP.NET calls when the control is being created on the server and takes care of the creation of all child controls within the grid, taking into account your data source, pagination settings and whatnot. All I needed to do was use a couple of the helper methods, CreateRow
and CreateColumns
, to create my insert row and the cells within it and I was away. With the row in hand all I needed to do was add it to the grid's table and I was done.
One complication arose: when there are no rows, by default the grid doesn't render anything, so I have to create a dummy table if the grid is empty. I also added some extra checks to the InitializeRow
method I'd already overridden to ensure the insert button only appeared on the insert row and that we didn't have anything daft on the insert row, like a delete button. I've omitted that code in this article for brevity.
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
int controlsCreated = base.CreateChildControls(dataSource, dataBinding);
if (this.DisplayInsertRow)
{
ICollection cols = this.CreateColumns(null, false);
DataControlField[] fields = new DataControlField[cols.Count];
cols.CopyTo(fields, 0);
if (this.Controls.Count == 0)
{
Table tableControl = new Table();
if (this.ShowHeader)
{
this._myHeaderRow = this.CreateRow(-1, -1, DataControlRowType.Header,
DataControlRowState.Normal);
this.InitializeRow(this._myHeaderRow, fields);
GridViewRowEventArgs headerRowArgs =
new GridViewRowEventArgs(this._myHeaderRow);
this.OnRowCreated(headerRowArgs);
tableControl.Rows.Add(this._myHeaderRow);
if (dataBinding)
this.OnRowDataBound(headerRowArgs);
}
this.Controls.Add(tableControl);
}
else
this._myHeaderRow = null;
this._insertRow = this.CreateRow(-1, -1, DataControlRowType.DataRow,
this.InsertRowActive ? DataControlRowState.Insert :
DataControlRowState.Normal);
this._insertRow.ControlStyle.MergeWith(this.AlternatingRowStyle);
this.InitializeRow(this._insertRow, fields);
GridViewRowEventArgs insertRowArgs =
new GridViewRowEventArgs(this._insertRow);
this.OnRowCreated(insertRowArgs);
this.Controls[0].Controls.AddAt
(this.Controls[0].Controls.IndexOf(this.HeaderRow) + 1, this._insertRow);
if (dataBinding)
this.OnRowDataBound(insertRowArgs);
}
return controlsCreated;
}
Okay, so I'm not quite done. The final piece to the puzzle is the code to actually perform the insert. We do this by overriding the OnRowCommand
method and acting on our events. When the user clicks the "New" button, we must cancel any edits and when starting any edits we show the "New" button - these two effectively act as a toggle so that the user is either inserting a row or editing a row but never both at the same time. When they hit the "Insert" button we do our best to pull the values out of the insert row and raise the RowInserting
event. If the grid is connected to a data source, we call its insert method so the complete suite of CRUD operations can be achieved with zero plumbing.
protected override void OnRowCommand(GridViewCommandEventArgs e)
{
base.OnRowCommand(e);
if (e.CommandName == "New")
{
this.InsertRowActive = true;
this.EditIndex = -1;
this.RequiresDataBinding = true;
}
else if (e.CommandName == "Edit")
this.InsertRowActive = false;
else if (e.CommandName == "Insert")
{
bool doInsert = true;
IButtonControl button = e.CommandSource as IButtonControl;
if (button != null)
{
if (button.CausesValidation)
{
this.Page.Validate(button.ValidationGroup);
doInsert = this.Page.IsValid;
}
}
if (doInsert)
{
this._insertValues = new OrderedDictionary();
this.ExtractRowValues(this._insertValues, this._insertRow, true, false);
GridViewInsertEventArgs insertArgs =
new GridViewInsertEventArgs(this._insertRow, this._insertValues);
this.OnRowInserting(insertArgs);
if (!insertArgs.Cancel && this.IsBoundUsingDataSourceID)
{
DataSourceView data = this.GetData();
data.Insert(this._insertValues, this.HandleInsertCallback);
}
}
}
}
private IOrderedDictionary _insertValues;
private bool HandleInsertCallback(int affectedRows, Exception ex)
{
GridViewInsertedEventArgs e = new GridViewInsertedEventArgs(this._insertValues, ex);
this.OnRowInserted(e);
if (ex != null && !e.ExceptionHandled)
return false;
this.RequiresDataBinding = true;
return true;
}
The rather nifty DataSourceView
performs an asynchronous insert so if your database is slow the rest of the page gets a chance to render while it's executing. As with most asynchronous operations we have a callback which in this case calls the RowInserted
method and provides the same mechanism for handling exceptions as the update and delete operations do.
That completes the ExtendedGridView
class which can be dropped onto any page and used just like a GridView
but provides a slick way of using the grid to maintain tabular data. If you've used a GridView
before to do updates and deletes, you should have no problem using the ExtendedGridView
to perform inserts. The same trade-off applies as always with the GridView
: if you're happy with basic functionality and use BoundColumns
throughout you can do everything without writing any code, but if you start using TemplateColumns
to customize things then you must do a little more tweaking yourself. Even so, I believe this component will save you time and headaches.
Points of Interest
I learned shedloads creating this control, mostly about the internal working of the GridView
control. There are a number of interesting methods exposed to classes extending the GridView
, including InitializePager
, InitializeRow
, CreateRow
and CreateColumns
. This is also a perfect example of how extending a control can save you time when implementing the same functionality in several places. Get extending, people!
- 27-Aug-2007: First release
- 27-Oct-2007: Second release including bugfixes
- Fixed
null
reference exception when using PagerSettings
other than TopAndBottom
- Fixed "Collection was modified" bug when placing the
new
button in a TemplateField
- Validators in the insert row now fire automatically
- Modifying
AllowInserting
or InsertRowActive
programmatically will result in the grid re-binding automatically