|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article describes an extended Updated: Several bugs have been fixed in the code. See the history for more information. BackgroundLet's face it, the Then, with a triumphant fanfare, along came ASP.NET 2 and its new flagship control the My only complaint is that there's no native support for inserting new records via the
The general technique for achieving this goes something like this:
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 It's been tough, mostly to do with figuring out how the Cosmetic ImprovementsResult SummaryQuite often I use a /// <summary>
/// Renders the contents of the control.
/// </summary>
/// <param name="writer">The <see cref="HtmlTextWriter"/> to write to.</param>
protected override void RenderContents(HtmlTextWriter writer)
{
if (this.ShowResultSummary && this.PageCount != 0)
{
// Create summary controls
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)
{
// Add summary to table at the top
this.Controls[0].Controls.AddAt(0, this.CreateSummaryRow(topSummaryControl));
// Add summary to table at the bottom
this.Controls[0].Controls.Add(this.CreateSummaryRow(bottomSummaryControl));
}
else
{
// Add summary control to top pager
if (this.TopPagerRow != null)
this.TopPagerRow.Cells[0].Controls.Add(topSummaryControl);
// Add summary control to bottom pager
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;
/// <summary>
/// Whether the results summary should be shown.
/// </summary>
[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; }
}
/// <summary>
/// The total number of rows in the data source.
/// </summary>
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 Sort IndicatorsSomething 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 /// <summary>
/// Initializes a row in the grid.
/// </summary>
/// <param name="row">The row to initialize.</param>
/// <param name="fields">The fields with which to initialize the row.</param>
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)
{
// Add sort indicator
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;
}
}
}
}
/// <summary>
/// Image that is displayed when <see cref="SortDirection"/> is ascending.
/// </summary>
[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; }
}
/// <summary>
/// Image that is displayed when <see cref="SortDirection"/> is descending.
/// </summary>
[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; }
}
Insert FunctionalityOkay, 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, /// <summary>
/// Fires before a row is inserted.
/// </summary>
[Category("Action")]
[Description("Fires before a row is inserted.")]
public event EventHandler<GridViewInsertEventArgs> RowInserting;
/// <summary>
/// Fires after a row has been inserted.
/// </summary>
[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. 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 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 /// <summary>
/// Creates the control's child controls.
/// </summary>
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)
{
// Create dummy table for inserting the first entry
Table tableControl = new Table();
if (this.ShowHeader)
{
// Create header
this._myHeaderRow = this.CreateRow(-1, -1, DataControlRowType.Header,
DataControlRowState.Normal);
this.InitializeRow(this._myHeaderRow, fields);
// Trigger events
GridViewRowEventArgs headerRowArgs =
new GridViewRowEventArgs(this._myHeaderRow);
this.OnRowCreated(headerRowArgs);
tableControl.Rows.Add(this._myHeaderRow);
if (dataBinding)
this.OnRowDataBound(headerRowArgs);
}
// Add insert row
this.Controls.Add(tableControl);
}
else
// Use generated header row
this._myHeaderRow = null;
// Create insertion row
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);
// Trigger events
GridViewRowEventArgs insertRowArgs =
new GridViewRowEventArgs(this._insertRow);
this.OnRowCreated(insertRowArgs);
// Add row to top of table, just below header
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 /// <summary>
/// Raises the <see cref="GridView.RowCommand"/> event.
/// </summary>
/// <param name="e">Event data.</param>
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")
{
// Perform validation if necessary
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)
{
// Get values
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)
{
// Get data source
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 That completes the Points of InterestI learned shedloads creating this control, mostly about the internal working of the History
| ||||||||||||||||||||