AspAlliance.com LogoASPAlliance: Articles, reviews, and samples for .NET Developers
URL:
http://aspalliance.com/articleViewer.aspx?aId=1532&pId=-1
Creating Custom Data Field Controls - Part 1
page
by Brian Mains
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 26298/ 27

Introduction

There are a few controls in the .NET Framework that support bulk amounts of data. All controls can contain some sort of data, but to show groups of data, to paginate or sort it, and perform some sort of analysis limits the number of control support to the GridView, DetailsView, DataList, and a few others. However, some of these controls are limited in their capability, meaning that out-of-the-box does not always cut it.

For the GridView, the TemplateField provides some flexibility. However, having to repeat templates in several places quickly challenges reusability. But, controls like the GridView allow for minor customizations by allowing the developer to reuse specific pieces within its grid, rather than having to recreate a custom control itself. This can be done simply by creating a new custom control that inherits from DataControlField.

So, the focus of this article will be to create extensibility by making the development of custom data fields easier, and show you how to do so. It is good to reflect on how this entire process works, which is the essential piece of this article.

Data Control Field Essentials

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 == nullreturn 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); 
}
Data Binding

When the control interface is created, an event handler is setup to respond to the data binding.  At this point, the control has not been bound and needs to be assigned a value. As you can see below, this method handles getting.

Listing 6

void control_DataBinding(object sender, EventArgs e) 
{
    if (sender is TableCell) 
    {
        TableCell cell = sender as TableCell;
        cell.Text = this.GetDataItemValue<string>(cell.NamingContainer, 
          this.DataField)); 
    }
    else
    {
        CheckBox box = control as CheckBox;
        if (!insertMode) 
            box.Checked = this.GetDataItemValue<bool>(box.NamingContainer,   
             this.DataField);
     } 
}

The GetDataItemValue is a method I illustrated above; it uses the naming container for the actual control, and performs the action to get the reference to the actual value, passing it into the Checked property. This assignment here is to the Checked property, but could also be an assignment to the Text property of a TextBox, the SelectedDate property of a Calendar, the SelectedValue property of a DropDownList, and many more. However, in readonly mode, the cell renders a readonly value, which uses the following method to render a more appropriate method.

Listing 7

protected virtual string GetReadOnlyValue(object initialValue) 
{
    if (initialValue != null)
        return initialValue.ToString(); 
    else
        return null; 
}

This does not seem very helpful, but it can be very helpful in allowing a custom data field to render a formatted read-only value that is appropriate. For instance, for a boolean field I created, I used it to change the text from True/False database values to the values that are stored in two property values within the control (so by setting the TrueString and FalseString properties of the custom data field, it translates the database boolean value into a more readable form in read-only mode).  In addition, for a numerical field I created, I can use this to prefix the text with $ and suffix it with .00, making it more meaningful to show whole dollar amounts.

A Custom Base Class

I am going to move onto a base class I created to help with some of the basic interactions in creating custom data control fields.

The first method overrides ExtractValuesFromCell, which does not have a lot of use in a base class. This is because the real meaning of this method is to extract the underlying values of the editing control and provide them to the data source, which none of this is known yet (exposed by the consumer). The following implementation simply determines if the rendering is in insert mode.

Listing 8

public override void ExtractValuesFromCell(IOrderedDictionary dictionary, 
  DataControlFieldCell cell, DataControlRowState rowState, bool includeReadOnly)
{
      //base.ExtractValuesFromCell(dictionary, cell, rowState, includeReadOnly);
      _insertMode = (rowState == DataControlRowState.Insert);
}

The next method that is helpful extracts an instance of a control from within the cell, making it easier to get a strongly-typed reference. It performs checking to make sure that the control really does exist at the specified cell index.

Listing 9

protected T ExtractControl<T>(TableCell cell, int controlIndex) where T : Control
{
      if (cell == null)
            throw new ArgumentNullException("cell");
      if (controlIndex < 0 || controlIndex >= cell.Controls.Count)
            throw new ArgumentOutOfRangeException("index", Errors.OUT_OF_RANGE);
 
      Control control = cell.Controls[controlIndex];
      if (control == null)
            throw new InvalidOperationException(Errors.CANT_EXTRACT_CONTROL);
 
      return (T)control;
}

The next method is very helpful in reflecting against the underlying data type, and pulling out the real value. Note that the DataBinder method is used, so the method does not use a new approach to extracting the values, but compacts the approach to retrieve the underlying value.

Listing 10

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 == nullreturn null;
      object value = null;
 
      if (!propertyName.Contains("."))
            //Get the value from the data item
            value = DataBinder.GetPropertyValue(dataItem, propertyName);
      else
      {
            value = dataItem;
            string[] propertyList = propertyName.Split('.');
 
            foreach (string propertyItem in propertyList)
            {
                  value = DataBinder.GetPropertyValue(value, propertyItem);
                  if (value == nullreturn null;
            }
      }
 
      return value;
}

The next method performs read-only formatting, making it easier to alter the read-only value that is rendered in the cell. Though in the base class there is not any meaning to this yet, it comes in handy later. For instance, for a numeric field I provided two properties called Prefix and Suffix, which append that text to the value itself. So, I could append a "$" and a ".00" to make a field look like a whole number monetary amount.

Listing 11

protected virtual string GetReadOnlyValue(object initialValue)
{
      if (initialValue != null)
            return initialValue.ToString();
      else
            return null;
}

For some reason, the data control field base class requires that the field be created through the CreateField method. Why not use reflection and handle this automatically? The following method does just that.

Listing 12

protected override DataControlField CreateField()
{
      return (DataControlField)Activator.CreateInstance(this.GetType());
}

In addition, the code that adds a data item to the underlying dictionary (which stores all the values that will be passed to the underlying data source) is simple code, but it helps not to have to repeat it over and over. Below is the code to check the dictionary for the existence of a dictionary entry by the name, and the appropriate method call to update it.

Listing 13

protected virtual void AddDictionaryEntry(IOrderedDictionary dictionary,
  string dataField, object value)
{
      if (dictionary == null)
            throw new ArgumentNullException("dictionary");
      if (string.IsNullOrEmpty(dataField))
            throw new ArgumentNullException("dataField");
 
      if (dictionary.Contains(dataField))
            dictionary[dataField] = value;
      else
            dictionary.Add(dataField, value);
}
Conclusion

Hopefully, you have understood that a data control field represents an interface for a field or fields in the underlying data source bound to a parent control (such as the GridView). This interface provides the plumbing to show an instance of the control in read, edit, or insert mode, creates the interface for the control based on the mode, and handles passing the edited/inserted value to the underlying data source.

The series of methods above reduce the amount of code required to create custom data fields.  However, there is still a lot of work to do. The next article shows you another level of abstraction that makes it even easier.

I am beginning to create custom data fields in my Nucleo framework and to develop a framework for this. I will be continually revising it to make the approach for creating custom data fields even easier. However, these are the basic functions that can be used in working with custom data fields.



©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-18 10:07:26 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search