Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / XHTML

Bulk Edit with GridView without xxxDataSource (SqlDataSource, ObjectDataSource, etc.)

4.35/5 (16 votes)
22 Jun 2008CPOL4 min read 8   7.4K  
How to implement bulk edit with GridView without xxxDataSource (SqlDataSource, ObjectDataSource, etc.)

Screenshot2.jpg

Introduction

The ASP.NET GridView is a very amazing control, and it makes it very easy to implement editing on multiple rows data. Some days ago, I needed to implement bulk editing on GridView. I searched on the Internet and I found a very good article from Matt Dotson at MSDN.

Unfortunately, he uses SQLDataSource and I prefer to bind grids using the property DataSource and the method Databind. GridView is not really well designed to be used without a xxxDataSource (SQLDataSource, ObjectDataSource, etc.), especially for paging, sorting, and editing.

This tutorial is intended to explain how to implement an extended version of GridView that allows to edit/update multiple rows without any xxxDataSource.

Background

The main idea of bulk edit is quite similar to the control implemented by Matt Dotson, but I had to change most of the code because the GridView works differently using manual binding. While overriding some methods in the GridView, I found problems using manual binding because some objects are not properly initialized. I used Reflector and I discovered that most of the code within GridView strictly relies on the DataSourceID.

The control implementation

The class BulkEditGridView inherits from GridView, and it add/overrides the followings properties/methods:

  • BulkEdit: Boolean property used to maintain the state about bulk edit. This property stores the value into the ViewState.
  • CreateRow (override): used in conjunction with BulkEdit to make all rows editable.
  • BulkUpdate: used to call the GridView's RowUpdating event for all rows.
  • GetOldValue and GetNewValue: used to get the old and new values.

Overriding the method CreateRow and using the BulkEdit property, I can make all rows editable:

C#
protected override GridViewRow CreateRow(int rowIndex, int dataSourceIndex, 
          DataControlRowType rowType, DataControlRowState rowState)
{
    GridViewRow row;

    if (this.BulkEdit)
        row = base.CreateRow(rowIndex, dataSourceIndex, 
              rowType, rowState | DataControlRowState.Edit);
    else
        row = base.CreateRow(rowIndex, dataSourceIndex, rowType, rowState);

    return row;
}

The method BulkUpdate calls the update on all rows using the GridView method, UpdateRow:

C#
public void BulkUpdate()
{
    foreach (GridViewRow row in this.Rows)
    {
        this.UpdateRow(row.RowIndex, false);
    }
}

The methods GetOldValue and GetNewValue (use Generics) have two overloads, one each for the Template columns and DataBound columns. The old values come from the values available in the control from the cell. The new values comes from the Request collection, and are converted using the private method ConvertValue:

C#
private T ConvertValue<T>(string strValue)
{
    object value = default(T);

    if (strValue != null)
    {
        if (typeof(T) == typeof(string))
        {
            value = strValue;
        }
        else if (typeof(T) == typeof(int))
        {
            value = Convert.ToInt32(strValue);
        }
        else if (typeof(T) == typeof(double))
        {
            value = Convert.ToDouble(strValue);
        }
        else if (typeof(T) == typeof(bool))
        {
            if (strValue.ToLower() == "on" || strValue.ToLower() == 
                        "true" || strValue.ToLower() == "1")
                value = true;
            else
                value = false;
        }
        else if (typeof(T) == typeof(DateTime))
        {
            value = Convert.ToDateTime(strValue);
        }
    }

    return (T)value;
}

public T GetOldValue<T>(int rowIndex, int cellIndex)
{
    BoundField bf = this.Columns[cellIndex] as BoundField;
    
    T retVal = default(T);

    if (bf != null)
    {
        if (bf.ReadOnly)
        {
            DataControlFieldCell cell = 
              this.Rows[rowIndex].Cells[cellIndex] as DataControlFieldCell;
            retVal = ConvertValue<T>(cell.Text);
        }
        else
        {
            Control ctrl = this.Rows[rowIndex].Cells[cellIndex].Controls[0];

            if (ctrl.GetType() == typeof(TextBox))
            {
                retVal = this.ConvertValue<T>(((TextBox)ctrl).Text);
            }
            else if (ctrl.GetType() == typeof(CheckBox))
            {
                retVal = this.ConvertValue<T>(((CheckBox)ctrl).Checked.ToString());
            }
            else if (ctrl.GetType() == typeof(DropDownList))
            {
                retVal = this.ConvertValue<T>(((DropDownList)ctrl).SelectedValue);
            }
        }
    }
    else
    {
        throw new ArgumentException("The cell selected is not a DataBoundControl!");
    }

    return retVal;
}

public T GetOldValue<T>(int rowIndex, string controlName)
{
    Control ctrl = this.Rows[rowIndex].FindControl(controlName);
    
    T retVal = default(T);

    if (ctrl != null)
    {
        if (ctrl.GetType() == typeof(TextBox))
        {
            retVal = this.ConvertValue<T>(((TextBox)ctrl).Text);
        }
        else if (ctrl.GetType() == typeof(CheckBox))
        {
            retVal = this.ConvertValue<T>(((CheckBox)ctrl).Checked.ToString());
        }
        else if (ctrl.GetType() == typeof(DropDownList))
        {
            retVal = this.ConvertValue<T>(((DropDownList)ctrl).SelectedValue);
        }
    }
    else
    {
        throw new ArgumentException("The controlName not found!");
    }

    return retVal;
}

private T GetNewValue<T>(string uniqueID)
{
    string strValue = this.Page.Request[uniqueID];
    return ConvertValue<T>(strValue);
}

public T GetNewValue<T>(int rowIndex, int cellIndex)
{
    BoundField bf = this.Columns[cellIndex] as BoundField;

    if (bf != null)
    {
        if (bf.ReadOnly)
        {
            DataControlFieldCell cell = 
              this.Rows[rowIndex].Cells[cellIndex] as DataControlFieldCell;
            return ConvertValue<T>(cell.Text);
        }
        else
        {
            string uniqueID = this.Rows[rowIndex].Cells[cellIndex].Controls[0].UniqueID;
            return this.GetNewValue<T>(uniqueID);
        }
    }
    else
    {
        throw new ArgumentException("The cell selected is not a DataBoundControl!");
    }
}

public T GetNewValue<T>(int rowIndex, string controlName)
{
    string uniqueID = this.Rows[rowIndex].FindControl(controlName).UniqueID;
    return this.GetNewValue<T>(uniqueID);
}

Using the control

In the sample web project, I used a single table (tblProducts) in a SQLite database. I used SQLite because it is very powerful, small, fast, and because my free hosting doesn't have support for database. SQLite is a file based database with most of the features available in modern RDBMSs. It supports a large subset of SQL, Views, Triggers, etc. For additional details, refer to this link.

SQLite is an open-source project created in C language, but there exists a specific ADO.NET provider that makes it very easy to use it with .NET. You can download it from here.

The web project is composed of the following files:

  1. Default.aspx: The page containing the example to use BulkEditGridView.
  2. Progress.gif: Used within the UpdateProgress control (ASP.NET AJAX Framework).
  3. ProductEntity.cs: A simple class with properties containing the data for a single product (used to bind the BulkEditGridView manually).
  4. DataAccessLayer.cs: A Singleton class used to access the database. It retrieves the list of ProductEntity instances and saves the new data.

The project uses the ASP.NET AJAX framework (UpdatePanel) to make it all more nice and fast, but it's not required for the control. The Default.aspx page contains one instance of BulkEditGridView and three buttons (Edit, Update, and Cancel) used to change the state of the grid and to update the data. The grid implements paging using the built-in paging mechanism (I know it's not very smart, but it's very easy to implement! :) ). The most important part of the code is the following:

C#
private void RefreshButtons(bool editMode)
{
    if (editMode)
    {
        this.btnEdit.Visible = false;
        this.btnUpdate.Visible = true;
        this.btnCancel.Visible = true;
    }
    else
    {
        this.btnEdit.Visible = true;
        this.btnUpdate.Visible = false;
        this.btnCancel.Visible = false;
    }
}

protected void btnEdit_Click(object sender, EventArgs e)
{
    this.grdProducts.BulkEdit = true;
    RefreshGrid();
    RefreshButtons(true);
}

protected void btnUpdate_Click(object sender, EventArgs e)
{
    this.grdProducts.BulkEdit = true;
    RefreshGrid();

    this.grdProducts.BulkUpdate();

    this.grdProducts.BulkEdit = false;
    RefreshGrid();
    RefreshButtons(false);
}
    
protected void btnCancel_Click(object sender, EventArgs e)
{
    this.grdProducts.BulkEdit = false;
    RefreshGrid();
    RefreshButtons(false);
}

protected void grdProducts_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
    int productID = this.grdProducts.GetNewValue<int>(e.RowIndex, 0);
    string productName = this.grdProducts.GetNewValue<string>(e.RowIndex, 
                                                       "txtProductName");
    double unitPrice = this.grdProducts.GetNewValue<double>(e.RowIndex, 2);
    bool discontinued = this.grdProducts.GetNewValue<bool>(e.RowIndex, 3);

    string oldProductName = this.grdProducts.GetOldValue<string>(e.RowIndex, 
                                                          "txtProductName");
    double oldUnitPrice = this.grdProducts.GetOldValue<double>(e.RowIndex, 2);
    bool oldDiscontinued = this.grdProducts.GetOldValue<bool>(e.RowIndex, 3);

    if (productName != oldProductName || unitPrice != 
             oldUnitPrice || discontinued != oldDiscontinued)
    {
        List<SQLiteParameter> parameters = new List<SQLiteParameter>();
        parameters.Add(new SQLiteParameter("@ProductID", productID));
        parameters.Add(new SQLiteParameter("@ProductName", productName));
        parameters.Add(new SQLiteParameter("@UnitPrice", unitPrice));
        parameters.Add(new SQLiteParameter("@Discontinued", discontinued));
        parameters.Add(new SQLiteParameter("@LastChange", DateTime.Now));

        DataAccessLayer.Instance.ExecuteQuery(
                        @"Update tblProducts set ProductName=@ProductName," + 
                        @" UnitPrice=@UnitPrice, Discontinued=@Discontinued, " + 
                        @"LastChange=@LastChange where ProductID=@ProductID", 
                        parameters.ToArray());
    }

In the Click event of the Edit button, I set the BulkEdit property of the grid to true, I bind the data, and I make the Edit button hidden and the Update and Cancel buttons visible. This action make all rows of grid in edit mode. The Click event on Update restores the edit mode for the grid (this is necessary to restore the control state), and the I call the BulkUpdate method of the grid. This method calls the RowUpdating event for each row in the grid. In RowUpdating, I use the methods GetOldValue and GetNewValue to compare the differences and save only the changed row. Both methods have two overloads to get the data from a simple DataBound column or from a Template column.

Points of Interest

Probably, this solution is not the best one because I have to manually compare the changes in the rows, but I haven't found a generic way to solve this problem. Any suggestions are welcome! :)

History

  • 15 June 2008 - First release.

License

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