Getting your data out of the data controls
After you've created a layout with a data control of your choice and eventually bound them to some datasource, you almost always want to get that data back out of them(and why wouldn't you, its your data). One thing you'll learn about developing asp.net webpages is that its like petting a porcupine (yikes). If you go with the flow, you probably won't get stuck, but the moment you try to go against the grain you end up with a hand full of thorns.
Most of the data controls have events ending in ing and ed e.g. RowUpdating, RowUpdated etc. In most of the event handlers of the ing events it is easy to get the values from the event args.
private void OnGridViewRowUpdating(object sender, GridViewUpdateEventArgs e) {
IDictionary keys = e.Keys;
IDictionary newValues = e.NewValues;
IDictionary oldValues = e.OldValues;
}
Forgive me for the variation in my code formatting I'm trying to find the right one.
If we look at this event we can see the GridView nicely packages for us the new values, old values and the keys for the updating row. Unfortunately these dictionaries are only filled out if you are bound to a datasource control :(. So that means when you bind to some raw collection and hook up to the DataSource property then call databind, if you try to handle the delete or update events these dictionaries are going to be empty. Right now you must be asking yourself how can you get those dictionaries filled out just as if you were bound to a Datasource control? The good news is you CAN do it(this is what this blog post is all about right?).
Going Hunting in the Control Tree
This is probably one of the worst things you can do. From the time you write code that depends on the immediate layout of your page then your asking for trouble. I often see people on the forums writing code like this:
((TextBox)GridView1.Rows[e.RowIndex].Cells[2].Controls[1]).Text
Seeing stuff like that makes my spine tingle (and not in a good way). DO NOT write code like this!
FindControl
FindControl is a very powerful method on Control that allows you to search for a nested control, NOT synonymous to DOM function getElementById. I Often see people abusing find control and not understand that it is NOT recursive by default and complain when code like this
Throws a null reference exception. My advice is use Use FindControl as a last resort.
2-way DataBinding
2-way Databinding is a cool feature in ASP.NET 2.0 which allows the user to write some special syntax to bind against property values they would like to extract from a control.
There is alot of magic going on behind the scenes (which I will blog about in a separate post), but you do not need to know how it works to use it, e.g
<asp:GridView runat="server" ID="GridView1">
<Columns>
<asp:TemplateField>
<EditItemTemplate>
<asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox>
</EditItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
When you write the <%# Bind("ProductName") #> expression then the value of the Text property is pushed into a dictionary which the data control can retrieve later. So how does GridView/FormView/DetailsView/ListView get these values? Each data control has a method which is responsible for populating a dictionary of name value pairs from field name to value. Here is a mapping of data control to method used to extract values:
GridView => protected ExtractRowValues
FormView => protected ExtractRowValues
DetailsView = > protected ExtractRowValues
ListView => public ExtractItemValues
As we can see with the exception of ListView these oh so useful methods are protected, that means we can't call them from our code if we are using these built in controls. What can we do to surface these methods:
- Derive new controls that expose ExtractRowValues through a public method
- Use private reflection to call the protected method (yikes)
- Stick all of your interesting fields in DataKeyNames, then use the DataKeys[rowIndex].Values[fieldName] (and watch your ViewState grow :( )
- Do Nothing :)
To rectify some of this I've written a little helper for the GridView (and the other controls if you demand) that basically duplicates the functionality of ExtractRowValues method.
public static IDictionary GetValues(GridViewRow row) {
IOrderedDictionary values = new OrderedDictionary();
foreach (DataControlFieldCell cell in row.Cells) {
if (cell.Visible) {
// Extract values from the cell
cell.ContainingField.ExtractValuesFromCell(values, cell, row.RowState, true);
}
}
return values;
}
The method itself is pretty simple. It iterates over the control collection of the GridViewRow and calls ExtractValuesFromCell on each cell which puts values into the dictionary.
Uses
Most data controls support writing custom commands and handling some Command event(RowCommand for GridView). This method would come in handy if you needed to get the values out of the GridView for some custom command you wanted to execute.
If you can, use 2 way databinding, if your control doesn't have a useful property to bind against and you need to do some more logic to get the right value then use FindControl.
Edit:
I've updated the code to loop over the Cells collection instead of the Controls collection (which is alot cleaner). Also there is no real compelling reason to return a strongly typed IDictionary<string, object>, so I just return the IDictionary instead.
DetailsView:
Reader daveh551 needed some help adapting the code to DetailsView so here it is:
public static IDictionary GetValues(DetailsView detailsView) {
IOrderedDictionary values = new OrderedDictionary();
foreach (DetailsViewRow row in detailsView.Rows) {
// Only look at Data Rows
if (row.RowType != DataControlRowType.DataRow) {
continue;
}
// Assume the first cell is a header cell
DataControlFieldCell dataCell = (DataControlFieldCell)row.Cells[0];
// If we are showing the header for this row then the data is in the adjacent cell
if (dataCell.ContainingField.ShowHeader) {
dataCell = (DataControlFieldCell)row.Cells[1];
}
dataCell.ContainingField.ExtractValuesFromCell(values, dataCell, row.RowState, true);
}
return values;
}
FormView:
public static IDictionary GetValues(FormView formView) {
IOrderedDictionary fieldValues = new OrderedDictionary();
ExtractValuesFromBindableControls(fieldValues, formView);
IBindableTemplate itemTemplate = null;
if (formView.CurrentMode == FormViewMode.ReadOnly && formView.ItemTemplate != null) {
itemTemplate = formView.ItemTemplate as IBindableTemplate;
}
else if (formView.CurrentMode == FormViewMode.Edit && formView.EditItemTemplate != null) {
itemTemplate = formView.EditItemTemplate as IBindableTemplate;
}
else if (formView.CurrentMode == FormViewMode.Insert && formView.InsertItemTemplate != null) {
itemTemplate = formView.InsertItemTemplate as IBindableTemplate;
}
if (itemTemplate != null) {
foreach (DictionaryEntry entry in itemTemplate.ExtractValues(formView)) {
fieldValues[entry.Key] = entry.Value;
}
}
return fieldValues;
}
private static void ExtractValuesFromBindableControls(IOrderedDictionary values, Control container) {
IBindableControl control = container as IBindableControl;
if (control != null) {
control.ExtractValues(values);
}
foreach (Control childControl in container.Controls) {
ExtractValuesFromBindableControls(values, childControl);
}
}
Let me know if you find any bugs.
Hope this helps
UPDATE: I've had some requests to add more data controls and to convert the code to VB etc. So I decided to instead put the code in an assembly and make available for download here.