Introduction
There are millions of articles written about how to add an insert line to the GridView
control. Anything that I got to work more or less suggests to add a FooterRow
with appropriate templates. Even that approach breaks apart if the data source is empty - then the GridView
just displays an empty line. Both of those problems are addressed in the proposed solution - a HeaderRow
is always shown and a FooterRow
is automatically populated with appropriate templates/input elements. Also, what sets this solution apart is that no special or additional setup is needed neither for the columns (the use of special types) nor for the footer row or for the data-source/SQL statement - you can use Visual Studio to configure this just like for the DetailsView
control.
Using the code
For anyone used to the GridView
, the usage is trivial - just drop and configure. The same design-time editor is used, so the GUI is identical. The only thing to make sure is that the data-source Insert
command is configured and the the EditFlag
(on the GridView
) is turned on. Alternatively, you can use VS to configure the GridView
and then just change tags to the InsertableGrid
.
The following additional events/properties were added to the InsertableGrid
(available through the Designer):
bool AllowInsert
- Defaults to true
. Once this property is cleared, it behaves just like the regular GridView
.
GridViewUpdatedEventHandler RowInserting
- The event is fired right before the row insertion. Fill up the default values here, if needed. Same as RowUpdating
(got a little lazy here and didn't implement a new delegate - rather reused the one from the update).GridViewUpdatedEventHandler RowInserted
- The event is fired just after the insert happens. Check and clear ExceptionHandled
(as needed) here, just like you would with RowUpdating
.
Additionally, the RowCreated
event will be generated for FooterRow
, allowing you to fill up some of the input fields with default values:
protected void InsertableGrid1_RowInserted(object sender, GridViewUpdatedEventArgs e) {
if (e.Exception != null) {
Label3.Text = e.Exception.Message;
e.ExceptionHandled = true;
}
}
protected void InsertableGrid1_RowCreated(object sender, GridViewRowEventArgs e) {
if (e.Row.RowType == DataControlRowType.Footer
&& e.Row.DataItem is DataRowView) {
DataRowView r = e.Row.DataItem as DataRowView;
r["schStart"] = DateTime.Now.ToShortDateString();
r["schEnd"] = DateTime.Now.AddDays(2).ToShortDateString();
}
}
Points of interest / Implementation details
In order to implement this functionality, the following tasks need to be accomplished:
- Make sure that the empty
GridView
still displays a header row. - Optionally, create a new
FooterRow
and fill it up with the same input elements specified for the Edit mode (EditTemplate
). - Create and process a new
Insert
command. Generate the appropriate event handlers. - Provide for two-way binding for the footer (insert) row so the data on the insert row can be easily initialized.
Make sure header/footer rows are still created
This proved to be quite a task. GridView
insists on creating a single cell table when there is no data. To address this issue, I had to override the protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
method and check the return code from the base class. If the return code is 0 (among all other), then it means that there is no data in the GridView
and I need to go and create a header/footer (insert) row. base.CreateRow
is used here to create the rows.
int ret = base.CreateChildControls(dataSource, dataBinding);
if (Columns.Count == 0 || DesignMode || !AllowInsert)
return ret;
DataControlField[] flds = new DataControlField[Columns.Count];
Columns.CopyTo(flds, 0);
if (ret == 0) {
Controls.Clear();
Table t = new Table();
Controls.Add(t);
GridViewRow r = CreateRow(-1, -1, DataControlRowType.Header,
DataControlRowState.Normal);
this.InitializeRow(r, flds);
t.Rows.Add(r);
gvFooterRow = CreateRow(-1, -1, DataControlRowType.Footer,
DataControlRowState.Insert);
this.InitializeRow(gvFooterRow, flds);
t.Rows.Add(gvFooterRow);
}
else
gvFooterRow = base.FooterRow;
Fill-up footer/insert row with edit elements
I thought it would be quite a chore to have to define all the same templates for the footer row as for the edit rows. That was what most of the solutions to this problem were offering. I thought it would make sense to automatically create a set of input elements on the footer row. So, right after the FooterRow
is identified/created, I iterate through all the columns and create the appropriate cells on the footer row.
for (int i = 0; i < Columns.Count; i++) {
DataControlFieldCell cell = (DataControlFieldCell)FooterRow.Cells[i];
DataControlField fld = Columns[i];
if (fld is CommandField) {
CommandField cf = (CommandField)fld;
CommandField ins = new CommandField();
ins.ButtonType = cf.ButtonType;
ins.InsertImageUrl = cf.InsertImageUrl;
ins.InsertText = cf.InsertText;
ins.CancelImageUrl = cf.CancelImageUrl;
ins.CancelText = cf.CancelText;
ins.InsertVisible = true;
ins.ShowInsertButton = true;
ins.Initialize(false, this);
ins.InitializeCell(cell, DataControlCellType.DataCell,
DataControlRowState.Insert, -1);
}
else {
fld.Initialize(base.AllowSorting, this);
fld.InitializeCell(cell, DataControlCellType.DataCell,
DataControlRowState.Edit | DataControlRowState.Insert, -1);
}
}
Allow for the two-way binding on the insert/footer row
One thing that I think is missing in the new DetailsView
data bound control is the ability to nicely initialize the insert data. Over here, I thought it would be nice to correct this error. So, I would create a dummy DataTable
and fill it up with the values for each column. Then, I call the OnRowCreated
function (raise the event) to allow the application to initialize the insert data (see InsertableGrid1_RowCreated
above). That is especially useful when you have a BoundField
where you can't easily identify the input controls. Of course, if you have templated columns, then you can use the Row.FindControl()
function since you would know the name of the control (just like you would with the DetailsView
control).
OrderedDictionary dict = new OrderedDictionary();
base.ExtractRowValues(dict, FooterRow, true, true);
DataTable tbl = new DataTable();
DataRow row = tbl.Rows.Add();
foreach (string k in dict.Keys) {
tbl.Columns.Add(new DataColumn(k));
row[k] = dict[k];
}
FooterRow.DataItem = new DataView(tbl)[0];
GridViewRowEventArgs args1 = new GridViewRowEventArgs(FooterRow);
this.OnRowCreated(args1);
FooterRow.DataBind();
this.OnRowDataBound(args1);
FooterRow.DataItem = null;
foreach (DataControlField f in Columns)
f.Initialize(this.AllowSorting, this);
Outstanding issues
- Can't put my finger on it, but somehow, during the design,
ItemTemplate
/EditTemplate
comes out in lower case. - Would like to change the designer to draw the grid with an insert line in it to provide for visual cue.
- The sample ASPX page works with the database on my local machine. You would need to either create the table or change the page. Once again, the configuration is identical to the
GridView
, so either drop and configure, or configure the GridView
and then just change tags.
History
- Apply footer styles to insert row - Changes made to set
ShowFooter
flags as long as AllowInsert
is set. This forces the GridView
to apply FooterStyle
to the insert row. - Allow page validation to cancel insert - Added check for
Page.IsValid
to the OnRowCommand
. Avoid insertion if failure. - Fixed crash with auto-increment columns - Fixed the initialization for the footer row binding to include auto-increment columns.
- Changed the
RowInserting
event to GridViewUpdateEventHandler
type - To prevent crash and to be more consistent. Note: this may affect compatibility with the prior version.