The rows of the GridView control have various styles to
them. A GridView row has a header and footer (optionally) that appear at the
top and bottom. Between them is the data rows, not taking into effect the paging
row, where the pager renders. Data rows can render in normal mode, as well as
show a separate style for the alternating row. The GridView can support in-line editing, which shows a completely different interface, and although not directly
supported, custom grids also support insertion (and the DetailsView does as
well). These are the various interactions to be aware of. Most specifically,
the data rows are the pieces you need to manage; headers and footers are
managed for you.
When a row is rendered or even selected in a GridView,
the cell being rendered is controlled by the DataControlField class that
exposes it. The underlying data source data is embedded in a table cell,
or a control that represents the interface. In order to use this data in a more
meaningful way, the ExtractValuesFromCell method can be used to get the value
from the underlying control, and pass it to an ordered dictionary (discussed in
the next article).
Listing 1
public override void ExtractValuesFromCell(IOrderedDictionary dictionary,
DataControlFieldCell cell, DataControlRowState rowState, bool includeReadOnly)
{
base.ExtractValuesFromCell(dictionary, cell, rowState, includeReadOnly);
string value = null;
if (cell.Controls.Count > 0)
value = ((TextBox)cell.Controls[0]).Text;
//DataField specifies the name of the property bound
if (dictionary.Contains(DataField))
dictionary[DataField] = value;
else
dictionary.Add(DataField, value);
}
The DataControlField base class handles a lot of the
plumbing of this process for you; however, to create the interface of a custom
data field, InitializeCell must be overridden to handle creating the user
interface and adding any controls to the table cell.
Listing 2
public override void InitializeCell(DataControlFieldCell cell,
DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
{
base.InitializeCell(cell, cellType, rowState, rowIndex);
Control control = null;
if (cellType == DataControlCellType.DataCell)
{
if (this.IsReadMode(rowState))
control = cell;
else
{
TextBox box = new TextBox();
box.Columns = 30;
cell.Controls.Add(box);
if (!string.IsNullOrEmpty(DataField))
control = cell.Controls[0];
}
if (control != null && this.Visible)
control.DataBinding += new EventHandler(control_DataBinding);
}
}
Remember that InitializeCell initializes the interface
of the cell and taps into data binding, whereas ExtractValuesFromCell extracts
the values from the cell or inner controls. You do not need to control how
this occurs (a benefit to this polymorphic approach); you only need to worry
about getting data in and out, and rendering the correct interface.
Next comes the issue of data access; the DataBinder's
GetPropertyValue method makes it handy to extract the actual value from the data
source, meaning that you do not need to perform that actual work or need to
determine whether it is a DataTable, DataSet, business object collection, or
something else that is enumerable. This method handles the data source inspection
for you, taking the name of the property or column, and returns the value
found (or an exception if that property does not exist). Note the call to
GetDataItem, which actually gets the reference to the targeted data item.
Listing 3
object dataItem = DataBinder.GetDataItem(cell.NamingContainer);
value = DataBinder.GetPropertyValue(dataItem, this.DataField);
For ease of use, I created a custom base class for data
control fields with a set of primitive methods to makeup the core functionality. The
following method makes it easier to extract a value from the underlying data
source. Let us take a look at the method below.
Listing 4
protected object GetDataItemValue(object container, string propertyName)
{
if (container == null || this.DesignMode) return null;
//Get the data item from the container
object dataItem = DataBinder.GetDataItem(container);
if (dataItem == null) return null;
return DataBinder.GetPropertyValue(dataItem, propertyName);
}
This method takes a naming container (often passed in
through tableCell.NamingContainer or control.NamingContainer) as a property,
which is used to reflect on the data item. Also required is the property
name to reflect against. The property value is a name of a property in a
business object, a name of a column in a data table, or some other
attribute that is used to retrieve the value from.
The last portion of the method above is meant to provide
some flexibility with this scenario. For instance, if a business object
employee has a property named Address (of type PersonalAddress), which has its
own properties, it would be helpful to provide a string Address.Line1 to
reference the first line of the address, rather than to have to use a TemplateField
and explicit casts. The solution to this is to break apart an expression
by the period, then reflect against each term individually. If a null is
returned for some reason, the trail ends and a null is returned; otherwise, the
final value is returned. This makes for a handy fix to a common problem.
The next task that must be discussed is data binding. Take
a look at the following partial implementation of InitializeCell, which sets up
the interface.
Listing 5
if (cellType == DataControlCellType.DataCell){
//IsReadMode determines if this is readonly mode (normal, alternate, or
//selected row, not editing or inserting)
if (this.IsReadMode(rowState))
control = cell;
else
{
//SetupEditControl returns an instance of the control that will
//render in insert/edit mode
cell.Controls.Add(this.SetupEditControl());
if (!string.IsNullOrEmpty(this.GetDataItemFieldName()))
control = cell.Controls[0];
}
if (control != null && this.Visible)
control.DataBinding += new EventHandler(control_DataBinding);
}