How DataBinder works

To use DataBinder control, simply drop onto an ASP.NET form as shown in Figure 1 (bottom). The control acts as an extender control to any existing controls on the form and adds a DataBindingItem property to the extended control in the Visual Studio 2008/2005 property editor. In Figure 1 you can see the DataBindingItem on DataBinder extend the txtCategory DropDownList control:

 

 
Figure 1 – The DataBinder control is an extender control that extends existing ASP.NET controls with data binding functionality.

 

The control sits on the form as a non-descript grey container (bottom of Figure 1) that is not rendered at runtime. It’s displayed on the form merely so you can set properties on the DataBinder control itself. Its properties allow you to configure how general behavior for data binding that is applied to any bound controls. As an extender control the DataBinder contains a collection of DataBindingItems that hold the configuration values for each of the extended controls.

 

Note that extender behavior is available only in the designer (or in Markup mode if the designer has at least been actived once). If you want to do everything in code you need to create DataBindingItem collection as part of the DataBinder control. The collection of items is maintained in the HTML Markup and once properties have been databound the control looks something like this:

<ww:DataBinder ID="DataBinder" runat="server" 
        DefaultBindingSource="this.Product.Entity">
        <DataBindingItems>
            <ww:DataBindingItem runat="server" BindingSource="this.Product.Entity" 
                BindingSourceMember="ProductName" ControlId="txtProductName" 
                IsRequired="True">
            </ww:DataBindingItem>
            <ww:DataBindingItem runat="server" BindingProperty="SelectedValue" 
                BindingSourceMember="CategoryID" ControlId="txtCategoryID" 
                BindingSource="this.Product.Entity">
            </ww:DataBindingItem>
            <ww:DataBindingItem runat="server" 
                ControlId="txtUnitPrice" BindingSourceMember="UnitPrice" DisplayFormat="{0:c}" >
            </ww:DataBindingItem>        

|




... more DataBindingItems here

        </DataBindingItems>
</ww:DataBinder> 


The collection of DataBindingItems establishes the individual bindings that are applied by the DataBinder. DataBinder extends existing controls, but nothing is extended until you explicitly set properties on the DataBindingItem extender property of any control on the form. In design mode the extender exposes a DataBindingItem property which is responsible for providing a mapping between a data item and a control property. Supported data items are plain old CLR objects and ADO.NET data objects such as ADO.NET DataRow fields (as in the screenshot) or any property of the Page class. It’s possible to bind something like this.Customer.Entity.CompanyName, where this refers to the Page object, which is used as the base object from which any binding expression originates.

BindingSource and BindingSourceMember

 

Binding is performed by using a BindingSource, which identifies a data item. The BindingSource consists of two properties – the BindingSource and the BindingSourceMember, where the BindingSource is a string property that describes the base object and the BindingSourceMember which maps the simple property, field or data field to bind to. Programmatic access can also use the BindingSourceObject property to provide a reference instead of a BindingSource string.

 

Assume that you want to bind to a DataRow object as in the example above. In order for the DataBinder to be able to bind to the DataRow, it needs to be accessible through a Page reference. So this.MyDataRow works as does this.Customer.DataRow where Customer is a business object that has a DataRow property. In the first case you’d bind with:

 

BindingSource: this.DataRow

BindingSourceMember: CompanyName

 

Where CompanyName is the name of the field to bind to. In the case of the business object with a DataRow you’d bind with:

 

BindingSource: this.Customer.DataRow

BindingSourceMember: CompanyName

 

You can also bind to a DataTables in the same way. When a DataTable or DataSet is used as the BindingSource object, DataBinder assumes the first DataRow for a table, or the first table and first DataRow for DataSet. You can also bind an ADO.NET object more directly:

 

BindingSource: this.MyDataSet.Tables["Products"]. Rows[0]

BindingSourceMember: CompanyName

 

Just remember that in order for the wwDataBinder to be able to bind, whatever object you’re binding to must be referenced through a property of the Page that is marked as protected or public. Public is preferred though, since public Reflection is supported in Medium Trust, where Reflection will fail on protected properties. Private properties are not supported for binding.

 

You can also bind to plain old CLR object (POCO) objects and business entities. For example, if you have a business object that lets you access the CompanyName with this.Customer.Entity.CompanyName you can express the BindingSource as:

 

BindingSource: Customer.Entity

BindingSourceMember: CompanyName

 

You can also bind directly to Page object members. If you have a DateTime RightNow property on the Page then binding looks like this:

 

BindingSource: this

BindingSourceMember: RightNow

 

You can even bind to native form properties. For example, if you want to bind to the Page.Title property that controls the Browser's title bar you can use:

 

BindingSource: this

BindingSourceMember: Title

 

Any BindingSource implicitly uses this or me to signify the Page object, but unless you are using the Page itself as the binding source they are optional and don’t need to be specified. A BindingSource is relative to a top level container object – typically the ASP.NET Page object – but you can supply the top level container as part of the DataBind and Unbind methods as a parameter. This is useful if you want to use the wwDataBinder in a user control where the top level container is the UserControl rather than the page.

 

BindingSourceMembers are always simple types like ints, strings, DateTime, bool etc. They can also be Enum values which are translated into strings and work for two-way binding.

 

The job of a BindingSource is to establish a binding between a data item or property and the control property that the value is bound to. This mapping is used both for binding to a control property and from a control property back into the data item or property. Inbound binding is established by calling the wwDataBinder.DataBind method, unbinding by calling wwDataBinder.Unbind. The process is deterministic in that you explicitly call these methods from your code, which provides a lot of flexibility about how binding is handled.

 

BindingProperty - the Property on the Control to bind

The DataBindingItem exposes additional properties that are associated with the data binding process, as shown in Figure 1. The BindingProperty determines the field of the control you are binding to – the default is Text, but if you’re binding to a CheckBox you likely bind to Checked or if you’re binding to a list control to SelectedValue. A new DataBindingItem will default to the control's default property depending on the type of control that is used.

 

DisplayFormat - Formatting display Content

You can use standard IFormatProvider format strings to specify how to display any bound values on inbound binding. So you can use "{0:c}" for currency values, or "{0:d}" to display a date, or "{0:f}%" to display a percentage for example. The DataBinder will use the format expression for inbound binding when the value is displayed in the specified BindingProperty of the control.

 

Watch out for DisplayFormats that can't Round Trip
Keep in mind that if you do non-standard formatting of text that you use in two-way binding you may not be able to bind back. The DataBinder uses the loosest parsing formats for dates and numbers - which catches things like currency formats, thousand delimiters, and different date delimiters and orders etc. - but if you use a custom format it may not be recognized on an .Unbind() operation. If you're using two-way binding stick to the simplest formats possible or else use one-way binding with manual unbinding after fixing up user input.

 

BindingMode - Two-way, One-way or no Binding

Data binding can be one-way, two-way or none. Generally input controls like textboxes generally use two-way binding, display controls like labels use one-way binding. Sometimes you want to explicitly bind controls, but not unbind them, such as scenarios where you might have to do special checks on input values and One-way binding can be useful for that. None can be useful if you want a control to participate in binding error management, but you don’t want to bind the control to a value. It can also be useful if you want to turn on databinding conditionally through code.  In the example form above the item selection drop down at the top might use a binding mode of none. It’s not bound to data since it’s used only to select the active item, yet I still want to be able to display a binding error next to it when a specific customer cannot be selected or updated.

 

Binding Errors 

Speaking of binding errors, the DataBinder control automatically manages both binding and unbinding errors. When a control cannot be bound or unbound, the control generates a BindingError in the BindingErrors collection. No exception fires. You can query this collection for individual binding errors and retrieve a summary of errors with the ToHtml method. By default each control with an error also displays an error icon next to it. Figure 2 shows an example of the form with a few binding errors displayed on it.

 

Figure 2 – Binding errors can be automatically displayed next any control that have errors during unbinding and summary with links to each field is automatically generated. Clicking the link highlights and sets focus to the control with the error.

 

BindingErrors can either be automatically generated by the control during the binding or unbinding process or can be manually added to the BindingErrors collection from your own code. Automatic BindingErrors are generated when an input value cannot be converted back to the bound data item or property usually due to a data conversion error. For example, the Reorder Expected date in Figure 2 is an invalid date and this error is automatically captured by the control because the date conversion fails. Each DataBindingItem also has a IsRequired property which not surprisingly requires that the field is not left blank. If empty a binding error is automatically generated and added to BindingErrors.

 

The other two errors on this form are ‘manual’ errors. The Dairy Products error is handled through a ValidateControl event that is hooked up to the wwDataBinder. The event fires for each control bound and the event handler and allows for checking each control after it has been unbound. If you decide a value is not valid in your code, you can set the BindingItem’s BindingErrorMessage programmatically and return false to let the binder know that this DataBindingItem is in error and needs to display on the error list.

 

The final error is a purely programmatic error which is added by using wwDataBinder.AddBindingError() and specifying the error message and Control instance or ID in the Page code. This allows assigning of an arbitrary error message to a control that is data bound and makes the control show up both with the icon as well as in the error listing.

 

All of this provides for automatic and fully programmatic binding error generation so there’s lots of flexibility on how errors and error icons are displayed. The contol also automatically picks up any Validator controls on the page and picks up their error messages and control ids and incorporates them into the binding error mechanism.

 

If a control has an error an icon is displayed next to it by default. The icon display is optional and you can turn off the warning icons for the DataBinder as a whole, or you can change the location and format for each of the individual DataBindingItems through the ErrorMessageLocation property. By default a WebResource is used pointing to an embedded image resource in the compiled control assembly or you can override the image explicitly with the ErrorIconUrl property on wwDataBinder.

 

The assembly also includes a wwErrorDisplay control, which is used to display the error summary in Figure 2. This control is optional, but I like to use it for consistency. In the example above I simply have assigned the BindingErrors.ToHtml method’s string output to the text property. The control also has easy message display methos like ShowError and ShowMessage which display one line messages with an icon – a nice consistent way to display messages on a Page.

 

You’ll notice that error summary has links for each error displayed. These links jump to the individual control that is in error when clicked and causes that control to be focused and highlighted to make it easier to see and fix errors.

Hooking up the control in Code

Let’s take a look at the code required to run the form shown in Figures 1 and 2. The pattern used with the wwDataBinder control works something like this:

 

To bind:

  • Hook up control bindings, mapping data to control properties
  • Load your data into a page level object reference(s)
  • Call the DataBind method to bind the control on the form

 

To unbind

  • Make sure the bound data object(s) is available and loaded
  • Call the Unbind method to unbind the control values into the data
  • Check for BindingErrors
  • Handle any custom server side validation
  • Display any binding and validation errors on failures
  • Otherwise save the data

 

The example above uses a few business objects that load internal DataSet objects to keep the sample code short and concise, so this code isn’t littered with SQL syntax. The business objects return their data in DataSets which is convenient for the examples here because we can bind the controls directly to these data items. Figure 3 shows the full page code for the sample.

 

Binding Controls

To bind you typically use Page_Load() and only bind when Page.IsPostBack is false since you don't want to overwrite any values the user enters. The basic concept is to load the data you want to bind or unbind to, then call databind. In simplest terms the code should do something like this:

 

this.ProductEntity = this.Product.Load(pkToLoad);
if (!this.IsPostBack)
   this.DataBinder.DataBind();
 

where the DataBinder would bind to the this.ProductEntity properties.

 

Realistically there's usually a bit more work to do, like parsing IDs and dealing with new records etc. In the example shown above in the Figure 1 and 2, the form loads an entity object (Product.Entity) and binds the values to the controls (code truncated for brevity). Here's the code:

 

protected void Page_Load(object sender, EventArgs e)
{
    string strId = Request.Params["Id"];
 
    if (strId == null || strId == "-1"// Temporary New ID
    {
        if (this.Product.NewEntity() == null)
        {
            this.ErrorDisplay.ShowError("Unable to create a new Product.");
            return;
        }
    }
    else
    {
        int.TryParse(strId, out this.ProductID);
        if (this.Product.Load(this.ProductID) == null)
        {
            this.ErrorDisplay.ShowError("Unable to load Product. Invalid Id.");
            return;
        }
    }
 
    // bind only on first load or if the product is changed
    if (!this.IsPostBack || this.IsProductChange)
        this.DataBinder.DataBind();
 
    // Manually bind the PK field control ALWAYS - it's ReadOnly and so doesn't post back
    this.DataBinder.GetDataBindingItem(this.txtPk).DataBind();
}
 

This code basically gets an ID to look up an entity using a business object. The business object loads the entity into Product.Entity, which is the default binding object on the DataBinder control on the page. Most of the code deals with parsing the id provided and loading up the entity or if no id is provided loading up a new entity. 

 

DataBinding is then performed only when not on a postback (ie. first visit) or when the user changes the product to display. The actual binding is done with DataBinder.DataBind() which binds all the values to their assigned controls. If an error occurs during binding due to an invalid value that can't be converted or assigned to the control property an error icon is displayed next to the control.

 

Note that typically you can bind all controls assigned to DataBinder automatically with the DataBinder.DataBind() method. DataBinder.DataBind() walks through all items in the DataBindingItems of the DataBinder and binds them except those that are marked as BindingMode.None. There are some situations where you ALWAYS want to bind fields as above with the txtPk field which in the form is ReadOnly and so needs to be updated on every hit. If you don't use ViewState (as I do most of the time) any non-postback controls like labels also have to be rebound on every hit and you can do this by using the DataBinder.GetDataBindingItem(<controlInstance>) to manually bind - or unbind a control.

Unbinding Controls

Unbinding typically occurs when a Save operation occurs. In the example in Figure 2, clicking the save button fires the unbinding operation. To unbind you simply call the this.DataBinder.Unbind() method which takes the values from the controls and stuffs them back into the underlying data source.

 

Important: When unbinding make sure your DataSource is loaded properly

In order to bind back properly you need to make sure that the DataSource - in this case the Product.Entity object - is in the proper state. This means when you save you want to have the correct entity for the currently edited record loaded. This is why Page_Load() above always loads up an entity rather than doing it just before the DataBind() although that's optional. Where you load your underlying data source is irrelevant - all that matters is that when you call DataBind() or Unbind() that the object is in the correct state to display itself or be updated with the input data.

Since the Entity is loaded up in Page_Load the save button code simply can unbind, check for binding errors and then save. Here's what the save code looks like:

 

protected void btnSave_Click(object sender, EventArgs e)
{
    // unbind back into the underlying data source: Product.Entity for most fields
    this.DataBinder.Unbind();
 
    // check for binding errors and display if there's a problem
    if (this.DataBinder.BindingErrors.Count > 0)
    {
        this.ErrorDisplay.Text = this.DataBinder.BindingErrors.ToHtml();
        return;
    }
 
    // validate the business object - check product entity for rule violations
    if (!this.Product.Validate())
    {
        this.DataBinder.AddValidationErrorsToBindingErrors(this.Product.ValidationErrors);
        this.ErrorDisplay.Text = this.DataBinder.BindingErrors.ToHtml();
        return;
    }
 
    bool isNew = false;
    if (this.Product.Entity.ProductID < 1)
        isNew = true;
 
    if (!this.Product.Save())
    {
        this.ErrorDisplay.ShowError("Couldn't save Product:<br/>" + this.Product.ErrorMessage);
        return;
    }
 
    this.ErrorDisplay.ShowMessage("Product information has been saved.");
 
    if (isNew)
    {
        // rebind the product list
        this.GetProductList();
 
        // select the new product 
        this.txtProductID.SelectedValue = this.Product.Entity.ProductID.ToString();
    } 
}
 

The save code starts by simply calling DataBinder.Unbind() since the Product.Entity is already loaded with the correct data to be updated based on the ID loaded in Page_Load(). Unbind() attempts to map the text input to the underlying bound properties of the BindingSource.

 

When errors occur during unbinding they are stored in the BindingErrors collection which you can check for errors by checking its count. If the count is greater than 0 you have errors and you can use BindingErrors.ToHtml() method to create an HTML list with links to each of the fields that are in error.

 

Next, it's a good idea to also check the business object and its validation rules. If there are errors in the validation, you can optionally add those validation errors to the binding errors. DataBinder.AddValidationErrorsToBindingErrors() accepts an IList of objects that have Message and ControlID properties (ControlID is optional). Alternately you can manually add BindingErrors to the BindingErrors collection from your own error capture mechanism.

 

Binding to DataRows

The example above binds to a CLR object - in this case a LINQ to SQL generated entity object which is common. You can also bind to ADO.NET objects in a similar fashion. The DataBinder understands certain ADO.NET types and can use them for binding. If in the example above we had used a DataSet to retrieve the Product row we could set up data binding as follows:

 

BindingSource: this.ProductDataRow

BindingSourceMember: UnitPrice

 

As with objects its vital that the DataRow you are binding to is accessible through the form. Above ProductDataRow needs to be a property of the form, or of a business object that is a member of the form (ie. this.Product.DataRow).

 

You can also bind using long hand syntax through a DataSet:

 

BindingSource: this.NwDataSet.Tables["Product"].Rows[0]

BindingSourceMember: UnitPrice

 

or maybe more simply:

 

BindingSource: this.NwDataSet.Tables["Product"]

BindingSourceMember: UnitPrice

 

which actually does the same thing. When you bind to a DataTable the assumption is that you are binding to the first DataRow. If you bind to a DataSet, you are binding to the first DataTable and the first DataRow within.

 

Otherwise the behavior of DataRow binding and object binding is pretty much identical.


© West Wind Technologies, 1996-2016 • Updated: 11/26/09
Comment or report problem with topic