Multi-row Editing in the ASP.NET DataGrid...
This articles demonstrates how you can have multiple datagrid rows available for editing at one time, not just the one row you are normally limited to.
By: John KilgoShajahan Kakkattil ("Shaji")
Date: August 28, 2003 Download the code. Printer Friendly VersionIntroduction
The ASP.NET DataGrid is a vital part of .Net web programming and is the highly discussed control among news groups and discussion forums. No wonder Microsoft has a newsgroup (dotnet.framework.aspnet.datgridcontrol) solely devoted for the datagrid!
You can use the datagrid to display as well as to edit data. The default in-place editing is a cool feature of the datagrid but it is restricted to edit and update one row at a time by enabling the "EditItemIndex" property and by handling EditCommand, UpdateCommand and CancelCommand to intiate edit, update and cancel editing respectively. Obviously there are limitations to enable windows form or excel style editing as far as the web application development is concerned.
However, in real life scenarios we may face situations where we really need to enable multi row editing, but without losing the easiness of coding with asp.net server controls.
This tutorial discusses a technique to enable the datagrid for multi row editing.
Scenario
Suppose that we have a web form which allows candidates to update their resume details, something like a wizard which collects the relevant information in each step. One of the intermediate steps is say, entering the work experience details of the candidate. This is a set of line entries in which the candidate can enter any number of rows which he needs, and can come back and edit it on the fly.
We are using a data grid to do this with multi row editing facility.
The technique
Before jumping to the implementation details of this scenario, let me put up how we are achieving this for those who are so curious. As mentioned earlier, we cannot use the default "EditItemIndex" property here because it can be set only to a single row. If it set to multiple rows the last row will override the others and only the last row will be in edit mode.
Here, we use "template columns" for all the editable columns as in the normal inplace editing. But the difference is we don't use the "EditItemTemplate", instead the "ItemTemplate" itself is used for item editing. Appropriate edit controls (textbox, combo box, check box, etc.) will be placed in the "ItemTemplate" of the template column. Then bind the datagrid with the datasource and display the data in the template columns. So, upon displaying the grid in the browser all rows will be rendered with editing controls and the data can be entered/edited and posted back to the server.
Things are straight forward up to this point, but how do we get back the edited data and merge with the data set? That is DataGrid.DataBind set the data to the datagrid, but we need to get back the data from the grid to the dataset after the user edited the data. Yes we need the reverse process of the DataGrid.DataBind…...maybe DataGrid.ReverseBind! But unfortunately datagrid does not support anything as such.
If there is nothing built-in, then we build up! But it is easy, loop thru each rows and collect the data back. Here is the code snippet that implements "ReverseBind" for the application which we are going to build.
Public Sub ReverseBind()
'This function is the heart of this application.
'It reads each grid rows,get the edited data
'and serialize back to the dataset.
'Yes the reverse process of "datagrid.databind", hence the name.
'The grid row
Dim gridrow As DataGridItem
'Typed data set row
Dim datarow As Dataset1.WorkExperianceDataRow
'Loop thru each grid row and synchronize the edited data
'with the corresponding data set.
'In this demo the typed data set has a one to one mapping
'with the templated columns for the sake of simplicity.
For Each gridrow In DataGrid1.Items
'"DatsetIndex" property of the gridrow gives the
'refernce to the datrow used for binding
datarow = _dataSet.WorkExperianceData(gridrow.DataSetIndex)
'get the data from grid row element and update the column in the 'data row
datarow.ExperiancePeriod = CType(gridrow.FindControl("ExperiancePeriod"), TextBox).Text
'(Note:the hardcoded control names is not a good programming 'practice, better to use
'a constant instead.)
'reverse bind the other fields
datarow.TotalYears = CShort(CType(gridrow.FindControl("TotalYears"), TextBox).Text)
datarow.CompanyName = CType(gridrow.FindControl("CompanyName"), TextBox).Text
datarow.JobDescription = CType(gridrow.FindControl("JobDescription"), TextBox).Text
'Update the data tabe with new values.
_dataSet.WorkExperianceData(gridrow.DataSetIndex).ItemArray = datarow.ItemArray
Next
End Sub
Please note that the datagrid in question is DataGrid1 and the DataSource is "Dataset1.WorkExperianceData" which is a typed dataset table with 4 columns (ExperiancePeriod, TotalYears, CompanyName, JobDescription). Also, the datagrid has template columns to edit these fields respectively.
The key point here is getting the reference of the dataset row used for binding each row, then updating the datarow with the edited data. In the above mentioned code snippet it is done by the following code
datarow = _dataSet.WorkExperianceData(gridrow.DataSetIndex)
Where, "_dataSet" is a form level variable which holds the typed dataset with work experience data.
Demo application
Let us have a look at how this is implemented in this demo application. It demonstrates the multi row data entry for the "work experience" data entry scenario. When the form first loads the grid shows a set of rows in edit mode with some initialization data. The existing data can be edited and deleted; also new rows can be added. You can add multiple rows at a time by specifying the number of rows to add. Also the paging is enabled, so during editing you can navigate thru the pages before making the final update by clicking the "Update" button.
Controls
Following are the main server controls used in the application. "DataGrid1" is used for entering and editing the data. The definition of DataGrid1 is as follows:
<asp:datagrid id="DataGrid1" runat="server" AutoGenerateColumns="False" PageSize="5" AllowPaging="True">
<Columns>
<asp:TemplateColumn HeaderText="Period">
<ItemTemplate>
<asp:TextBox id="ExperiancePeriod" runat="server"></asp:TextBox>
</ItemTemplate>
</asp:TemplateColumn>
<asp:TemplateColumn HeaderText="No of Years">
<ItemTemplate>
<asp:TextBox id="TotalYears" runat="server" Width="70px"></asp:TextBox>
<asp:CompareValidator id="CompareValidator1" runat="server" Type="Integer"
Operator="DataTypeCheck" ControlToValidate="TotalYears"
ErrorMessage="No of Years - Numeric value expected">!
</asp:CompareValidator>
</ItemTemplate>
</asp:TemplateColumn>
<asp:TemplateColumn HeaderText="Name of the Company">
<ItemTemplate>
<asp:TextBox id="CompanyName" runat="server" Width="256px"></asp:TextBox>
</ItemTemplate>
</asp:TemplateColumn>
<asp:TemplateColumn HeaderText="Job Description">
<ItemTemplate>
<asp:TextBox id="JobDescription" runat="server" Width="224px"></asp:TextBox>
</ItemTemplate>
</asp:TemplateColumn>
<asp:TemplateColumn HeaderText="Delete">
<ItemTemplate>
<asp:LinkButton id="DeleteRow" runat="server">X</asp:LinkButton>
</ItemTemplate>
</asp:TemplateColumn>
</Columns>
<PagerStyle NextPageText="Next" PrevPageText="Previous"
HorizontalAlign="Center" Mode="NumericPages">
</PagerStyle>
</asp:datagrid>
"DataSet1" is a typed dataset having a table with 4 fields.
[Note:Headers omitted for simplicity]
<xs:element name="Dataset1" msdata:IsDataSet="true">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="WorkExperianceData">
<xs:complexType>
<xs:sequence>
<xs:element name="ExperiancePeriod" type="xs:string" minOccurs="0" />
<xs:element name="TotalYears" type="xs:short" minOccurs="0" />
<xs:element name="CompanyName" type="xs:string" minOccurs="0" />
<xs:element name="JobDescription" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
Other Controls
AddRows; Link button to add additional rows
Save; Command button. The data is not saved to anywhere instead it is displayed in "DataGrid2" in read-only mode
DataGrid2; datagrid to show the edited data.
The important methods and event handlers are explained below. For the full listing please refer to the source code of the application provided with this tutorial.
Page_Load Event
Initialize the data and bind the grid if the page is loading for the first time. We are using a stub method "InitializeData" for loading the data with some values for the first time. On each post back the data is binding back to the data set from the grid by calling "ReverseBind()".
Private Sub Page_Load(ByVal ………..) Handles MyBase.Load
'Put user code to initialize the page here
If Not IsPostBack Then
'Load the data for the first time
InitializeData()
BindGrid()
Else
'Get the edited data, and populate back to the data holder
' (dataset)
ReverseBind()
End If
End Sub
InitializeData
Loading some start up data.
Private Sub InitializeData()
'Initializing the data for the first time.
'This is just a "stub" to load some startup data for demo purpose only.
'Normally the data will be loaded from data provider (database) or it can be empty initially.
With _dataSet.WorkExperianceData
.AddWorkExperianceDataRow("1/Jan/1988 to 31/Dec/2000", 3, "IBM", "Bulding IBM Mainframe systems")
etc……
etc……
End With
End Sub
LoadViewState / SaveViewState
The edited data collected from the grid is stored in the dataset, and the dataset is persisted in a viewstate variable called "data". Again, you can implement your own logic to persist the data, but using the viewstate is ideal in most situations, provided that the dataset is not so huge. For the database programmers, if the underlying dataset is huge then it is better to use a custom dataset to collect required data from the user, then load it back to the dataset representing your database tables before making a database update.
To enable the viewstate variable as our temporary data store the "LoadViewstate" and "SaveViewState" are overridden as follows.
Protected Overrides Sub LoadViewState(ByVal savedState As Object)
MyBase.LoadViewState(savedState)
'Get the dataset from the viewstate. This is the data before last editing
If (Not Me.ViewState("Data") Is Nothing) Then
_dataSet = CType(Me.ViewState("Data"), Dataset1)
End If
If (Not Me.ViewState("LastEditedPage") Is Nothing) Then
'Just a variable to keep the last edited page index for the purpose of this application
_lastEditedPage = CType(Me.ViewState("LastEditedPage"), Integer)
End If
End Sub
Protected Overrides Function SaveViewState() As Object
'The datset used for editing needs to be persisted across postbacks.
'The performance will be affected if the dataset is huge in size.Please
'refer the tutorial for more details
Me.ViewState("Data") = _dataSet
Me.ViewState("LastEditedPage") = _lastEditedPage
Return (MyBase.SaveViewState())
End Function
Addrow_click
Adding new rows to the application. This is the event handler of command button "AddRow".
Private Sub AddRow_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles AddRow.Click
'Add the number of rows specified by "RowCount" to grid
If (IsNumeric(RowCount.Text)) Then
Dim i As Integer
For i = 1 To CInt(RowCount.Text)
'Adding a blank record to the dataset.
_dataSet.WorkExperianceData.AddWorkExperianceDataRow("", 0, "", "")
'In our case the typed datset has only a few fileds, so that it is
'easy to pass the default data as arguments. If there are many
'fields (that is the case usually in real life scenarios)
'instatiate a data row and load initialization data one by one. Then
'plug it into the data table.
'You can even pass blank row with out initialization data, but in
'that case you need to handle "DBNull" situation during grid row
'binding either by checking "IsDBNull" or by
'setting default value property for the field in the the dataset.
Next
End If
'Rebind the grid to show the newly added row(s).
BindGrid()
End Sub
DataGrid1_ItemDataBound
Nothing special, simply populating the templated controls with data during grid bind.
Private Sub DataGrid1_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DataGridItemEventArgs) Handles DataGrid1.ItemDataBound
'This event handler is executed once per each row of the datagrid
'while binding the dataset records.
'The templated controls get populated with data here from
'the "workexperiance" data table
If (e.Item.ItemType = ListItemType.Item Or e.Item.ItemType = ListItemType.AlternatingItem) Then
Dim datarow As Dataset1.WorkExperianceDataRow
datarow = CType(CType(e.Item.DataItem, DataRowView).Row, Dataset1.WorkExperianceDataRow)
CType(e.Item.FindControl("ExperiancePeriod"), TextBox).Text = datarow.ExperiancePeriod
CType(e.Item.FindControl("TotalYears"), TextBox).Text = datarow.TotalYears.ToString
CType(e.Item.FindControl("CompanyName"), TextBox).Text = datarow.CompanyName
CType(e.Item.FindControl("JobDescription"), TextBox).Text = datarow.JobDescription
End If
End Sub
There are couple of other methods also to complete the functionality of the application but are not worth mentioning here. See the "WebForm1.aspx" for a complete code listing.
Conclusion
This is not a ready made solution suitable for all multi-row data editing requirements, but the technique can be adopted for many situations. I think there are only minor changes required for Microsoft to enable multi-row editing in the grid control. For instance, by providing a property called "EnableEditing" at the row level, and if it is true that row should be rendered using "ItemEditTemplate". Also the Update and cancel Edit commands should be provided in the grid level, not in the row level. Upon postback itemUpdate event should be raised for each row which had the "EnableEditing" set to true. The data can be read back from the grid at this point, the exact reverse process of ItemDataBound event handler. Let me see if I can mange to customize the grid to enable this, I will surely get back here if can.
You may run the program here.
You may download the code here.