Click here to Skip to main content
15,860,861 members
Articles / Web Development / ASP.NET
Article

Extended GridView with Insert Functionality

Rate me:
Please Sign up or sign in to vote.
4.96/5 (35 votes)
26 Oct 2007CPOL8 min read 305.4K   3.7K   130   101
An extended GridView that adds inserting to its capabilities plus a number of other enhancements

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.

Screenshot - ExtendedGridView.gif

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.

C#
/// <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 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.

C#
/// <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; }
}

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.

C#
/// <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. 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.

C#
/// <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 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.

C#
/// <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 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!

History

  • 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionRe-activate InsertRow after editing another row Pin
Cultx7-Oct-12 9:56
Cultx7-Oct-12 9:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.