Click here to Skip to main content
15,879,535 members
Articles / Desktop Programming / Windows Forms
Article

A Helper Class to Bind a DataTable to a Class

Rate me:
Please Sign up or sign in to vote.
4.55/5 (22 votes)
9 Nov 2007CPOL11 min read 80.1K   1.1K   102   6
Binding to the DataTable without the System.Windows.Forms namespace.

Introduction

The .NET Framework provides several useful classes to support data binding. Unfortunately, these classes are associated with the System.Windows.Forms (SWF) namespace. This is because the usual goal of data binding is to associate a control with a (typically business layer) container. However, there are times when I've wanted to use data binding without pulling in the SWF namespace. Examples include console apps, unit tests, and services. The purpose of this article is to introduce a simple table binding helper that can be utilized without the SWF namespace.

What is Data Binding?

Data binding wires up property change events so that two or more classes can be automatically synchronized when (typically) a property value changes. Data binding is an elegant way of decoupling the presentation layer from the business layer, using events to move data across this boundary. Furthermore, the presentation layer and business layer don't need to know about each other if an intermediary (typically called a controller) is used to wire up the events between these two layers. Similarly, data binding can be used to decouple the data access layer's representation (perhaps in the form of a DataTable) with the business layer (perhaps wanting to map individual rows to class instances).

There are two terms used regarding data binding: simple and complex. Simple data binding is typically used with a container class and a control, where there is a one-to-one relationship between a property in the container and a property of the control. When the container's property value changes, the control's value (usually what's being displayed on the UI) is automatically updated. Similarly, when the control's editable property is changed, the container is updated.

Complex data binding refers to binding controls to a table. For this to work, the data binding mechanism needs to have the concept of a row cursor so that the values associated with a specific row update (in both directions) with the control that is displaying the values. And the row cursor often needs to be automatically updated as the user navigates a list-type control, such as a DataGridView. Hence the term "complex data binding". By watching events in the DataTable and events in the list-type control, UIs consisting of both list controls and discrete controls can be created which automatically track the selected row, handle UI updates when the table is changed, and so forth.

The History of Data Binding

Data binding in .NET 1.1 was initially implemented using reflection to detect events with the signature [propertyName]Changed. For example, a class with a Text property would implement a TextChanged event. The event handler is responsible for retrieving the Text property value and updating whatever object (and the corresponding property in that object) with the new value. Two way binding is achieved by also wiring up an associated event going the other direction.

The problem with this approach is that the event handler doesn't have any way of knowing which property was changed, which makes it difficult (but not impossible) to write a general purpose event handler. The .NET 2.0 Framework introduces the interface INotifyPropertyChanged which requires the implementer to supply a PropertyChanged event. The event signature is the standard "object, args" signature, in this case "args" is of the type PropertyChangedEventArgs, and the single property of this class is the property name. Now the event handler knows the name of the property associated with the event and a general purpose event handler can be easily written. Furthermore, it's easier to test whether a class provides property notification--you can test obj is INotifyPropertyChanged. Conversely, you don't know on "what" properties the class will provide notifications. You do with the .NET 1.1 style, using reflection (assuming that the xxxChanged event is actually correctly used by the associated "xxx" property).

The more "legacy" way of writing a general purpose event handler requires creating a helper object that retains the name of the property and implements the event sink so that the event handler has an instance of the helper object. Confused? The difference is this: with a class that implements INotifyPropertyChanged (and therefore the PropertyChanged event), the property name can be "forgotten" since the event contains the property name. With legacy implementations, you need a container class that retains the property name and also sinks the [propertyName]Changed event, so that the handler can acquire the property name given the instance of what is sinking the event. If you've optimized the code, you don't actually need the property name, you can acquire once and repeatedly use the PropertyInfo instance, but the concept is the same.

Following is a simple class (BindableDataElement) that illustrates the INotifyPropertyChanged interface. I use this class for the unit tests described later in this article. Notice how this class implements both the older style property changed event TextChanged and the newer style PropertyChanged event. Compare the differences. If you want to support legacy data binding in your container classes, you will need to implement both event styles.

C#
using System;
using System.ComponentModel;

namespace Clifton.Tools.Data
{
  public class BindableDataElement : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;
    public EventHandler TextChanged;
    protected string text;

    public string Text
    {
      get { return text; }
      set
      {
        if (text != value)
        {
          text = value;
          OnTextChanged();
        }
      }
    }

    public BindableDataElement()
    {
    }

    protected virtual void OnTextChanged()
    {
      if (TextChanged != null)
      {
        TextChanged(this, EventArgs.Empty);
      }

      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs("Text"));
      }
    }
  }
}

Unfortunately, the .NET 2.0 control classes do not implement the INotifyPropertyChanged interface. However, for the purposes of this article, that doesn't matter because the point of this article is to implement table binding without using the System.Windows.Forms namespace (and hence controls).

Thinking about Data Binding

As stated earlier, data binding is typically thought of in the context of bridging the business and presentation layers. Even MSDN's documentation on data binding in .NET 3.0 propagates this concept: Data binding is the process that establishes a connection between the application UI and business logic. Of course, this is on the page discussing data binding in the context of WPF, which is how you'll see data binding always discussed--within the context of the UI.

However, data binding is useful in a non-UI context as well. It's a useful mechanism for associating two classes without either of them knowing about each other (similar to the observer pattern). It's also a useful mechanism for connecting the data access layer (DAL) and its more raw DataTable representation of the data with business layer objects where concrete classes are used as containers for table data.

The .NET Classes

The .NET Framework includes several useful classes for data binding. The premier class in .NET 2.0 is of course the BindingSource class, which implements ten interfaces to support complex binding via list management routines (navigation, sorting, filtering, updating) and currency management (row cursor, change notification, etc.). Whereas previously (in .NET 1.1), a control's DataSource property was initialized to a DataTable or DataView and the form's CurrencyManager (part of the BindingContext) had to be used to manage currency (the row cursor) of controls bound to tables, with the advent of .NET 2.0 the functions of data binding and currency management were exposed in the BindingSource class, which frankly made more sense. Unfortunately, the BindingSource class is still part of the System.Windows.Forms namespace.

In .NET 1.1, the BindingContext property is a member of the Form class, which manages the BindingContext collection class. Whether the data source is simple (single property) or complex (implementing IList or IBindingList) determines what object is returned by the BindingContext Item property--a PropertyManager or a CurrencyManager, respectively. It's a bit of a kludge, because the Position and Count properties of the PropertyManager class have no effect. As mentioned in the preceding paragraph, this approach (the Form managing BindingContext instances which gave you a reference to a base class from which PropertyManager and CurrencyManager were derived) is now essentially obsolete for complex binding--the BindingSource is a much better mechanism.

Fundamental to either implementation is the Binding class which maintains the actual binding between a control property and the property of an object or the current object in a list of objects. What's interesting about this class is that, besides using a property name, you can use a period-delimited navigation path. The navigation path helps you navigate data sets, data tables, class properties, and even navigate across relationships between tables. This magic happens by utilizing a combination of reflection, data set navigation, and currency management of each collection in the navigation path. The only rule is that the final property must resolve to a simple value property or a column in a collection managed by a currency manager.

The TableBindHelper Class

The class I'm going to present here is in no way a replacement to the .NET BindingSource class nor does it have the richness of the Binding class in terms of navigation path support. If you are binding complex types (DataTable, DataView, DataSet objects) to controls, I highly recommend the BindingSource class. What I'm presenting here is a small class that I need primarily to provide some limited complex binding in an environment where I'm not using the System.Windows.Forms namespace, namely unit testing of other components, console applications, and services. The work here extends my earlier article on Understand Data Binding and has some similarities to the article on Object Mapping - The Row Cursor.

Implementation

Management of Columns and Instance Properties

The TableBindHelper has four fields which are useful to understand, as they are the backbone of how the class works.

C#
protected DataTable table;
protected int rowIdx;
protected Dictionary<string, ColumnBinder> columnBinders;
protected Dictionary<PropertyBinding, ColumnBinder> propertyBinders;

The first two are obvious--the DataTable instance to which we are providing binding services, and a field to manage the "currency"--the current row. (An amusing story about the term "currency"--when I first looked at the .NET binding classes and saw properties dealing with "currency", I couldn't for the life of me understand why there were properties dealing with monetary values.)

It's the two Dictionary collections that are really of interest. The first, columnBinders, maintains a mapping of columns, by name, bound to properties in a class. The second, propertyBinders, is a map of properties in an instance that are mapped to columns in the table.

The AddColumnBinder method adds entries to these two dictionaries so that the TableBindHelper can update the value appropriately when a change is made to the DataTable or to the instance property.

C#
public void AddColumnBinder(string columnName, object dest, 
                string propertyName, bool useLegacyChangeEvent)
{
  ColumnBinder cb = new ColumnBinder(this, columnName, dest, propertyName);
  columnBinders[columnName] = cb;
  PropertyBinding db = new PropertyBinding(dest, propertyName);
  propertyBinders[db]=cb;

  if ( (dest is INotifyPropertyChanged) && (!useLegacyChangeEvent) )
  {
    // Create a generic property watcher.
    CreatePropertyWatcher(dest, propertyName);
  }
  else
  {
    // Create the event sink in the container that knows about the 
    // the property name.
    cb.CreatePropertyWatcher();
  }

  if (rowIdx < table.Rows.Count)
  {
    object val = table.Rows[rowIdx][cb.ColumnName];
    UpdateTargetWithValue(cb, val);
  }
}

The first four lines set up the entries in the dictionary. For testing purposes, I decided to implement both .NET 1.1 and 2.0 binding support, and to allow the programmer to force legacy event hooking.

Lastly, if there's a valid row index, the instance property is updated with the value in the column for that row.

DataTable Events Hooks

In the constructor:

C#
public TableBindHelper(DataTable table)
{
  this.table = table;
  columnBinders = new Dictionary<string, ColumnBinder>();
  propertyBinders = new Dictionary<PropertyBinding, ColumnBinder>();
  table.ColumnChanged += new DataColumnChangeEventHandler(OnColumnChanged);
  table.RowDeleted += new DataRowChangeEventHandler(OnRowDeleted);
  table.RowChanged += new DataRowChangeEventHandler(OnRowChanged);
}

There are three DataTable events that are hooked. The first, ColumnChanged, is the most obvious, as it fires when a column value has changed:

C#
protected void OnColumnChanged(object sender, DataColumnChangeEventArgs e)
{
  ColumnBinder cb = null;
  bool ret = columnBinders.TryGetValue(e.Column.ColumnName, out cb);

  if (ret)
  {
    UpdateTargetWithValue(cb, e.ProposedValue);
  }
}

If the column has been bound to an instance property, the property is updated.

The other two events, RowDeleted and RowChanged, are responsible for synchronizing the instance with all the bound columns when the row index changes as a result of inserting or deleting a row.

C#
protected void OnRowDeleted(object sender, DataRowChangeEventArgs e)
{
  if (rowIdx >= table.Rows.Count)
  {
    // Can result in rowIdx set to -1.
    rowIdx = table.Rows.Count - 1;
  }

  if (rowIdx >= 0)
  {
    UpdateAllDestinationObjects();
  }
}

protected void OnRowChanged(object sender, DataRowChangeEventArgs e)
{
  if (e.Action == DataRowAction.Add)
  {
    RowIndex = FindRow(e.Row);
  }
}

These two methods are "dumb" in that they don't attempt to optimize the update of the instance properties by determining whether the current row index is impacted by the insert or delete operation.

When the Column Value Changes

Above, the OnColumnChanged event handler calls UpdateTargetWithValue:

C#
protected void UpdateTargetWithValue(ColumnBinder cb, object val)
{
  if ( (val == null) || (val==DBNull.Value) )
  {
    // TODO: We need a more sophisticated way of:
    // 1: does the target handle null/DBNull.Value itself?
    // 2: specifying the default value associated with a property.
    val = String.Empty;
  }

  cb.PropertyInfo.SetValue(cb.Object, val, null);
}

The problem is always, what to do with a null or DBNull.Value? I suppose I should have made this method virtual so that you could override the behavior of this method, but I figure, you'd probably just fix it here in the code. Unfortunately, "fix it" probably results in different implementations for different people. Certainly setting val to an empty string is not going to make a numeric property happy when the type converter tries to convert an empty string to a number!

So, add implementation here as you require. This also involves the decision regarding nullable types--if your class uses nullable types, you can avoid the annoying question of what to set the property to when the column value is DBNull.Value.

When the Instance Property Value Changes

Going the other direction is easier because an object type is used as the storage mechanism for columns:

C#
protected void UpdateTablePropertyValue(ColumnBinder cb)
{
  if (rowIdx < table.Rows.Count)
  {
    object val = cb.PropertyInfo.GetValue(cb.Object, null);

    if (val == null)
    {
      val = DBNull.Value;
    }

    table.Rows[rowIdx][cb.ColumnName] = val;
  }
}

And you'll note here that I'm explicitly setting a null value to DBNull.Value, so it's suitable for transactions to the database.

Unit Tests

I have several unit tests that validate both legacy and the new .NET 2.0 binding processes. The unit tests also do some minimal testing to verify that row changes are properly handled. And of course, the unit tests provide good usage examples, so I will show the entire suite of unit tests here. Notice I don't show you the whole TableBindHelper class, oh no, the unit tests are far more important! By the way, these aren't NUnit tests--these run with my unit test engine, but converting them to NUnit tests wouldn't be difficult.

C#
using System;
using System.Data;

using Clifton.Tools.Data;

using Vts.UnitTest;

namespace UnitTests
{
  [TestFixture]
  public class TableBindHelperTests
  {
    protected DataTable table;
    protected BindableDataElement bdeLastName;
    protected BindableDataElement bdeFirstName;
    protected TableBindHelper tableBindHelper;

    [SetUp]
    public void Setup()
    {
      table = new DataTable();
      table.Columns.Add(new DataColumn("LastName", typeof(string)));
      table.Columns.Add(new DataColumn("FirstName", typeof(string)));
      DataRow row = table.NewRow();
      row["LastName"] = "A";
      row["FirstName"] = "B";
      table.Rows.Add(row);
      row = table.NewRow();
      row["LastName"] = "C";
      row["FirstName"] = "D";
      table.Rows.Add(row);
      bdeLastName = new BindableDataElement();
      bdeFirstName = new BindableDataElement();
      tableBindHelper = new TableBindHelper(table);
    }

    [Test]
    public void DataIsUpdatedTest()
    {
      tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
      tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
      table.Rows[0]["LastName"] = "AA";
      table.Rows[1]["FirstName"] = "BB";
      Assertion.Assert(bdeLastName.Text == "AA", 
              "Destination object did not get updated.");
      Assertion.Assert(bdeFirstName.Text == "BB", 
              "Destination object did not get updated.");
    }

    [Test]
    public void DataIsUpdatedLegacyTest()
    {
      tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text", true);
      tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text", true);
      table.Rows[0]["LastName"] = "AA";
      table.Rows[1]["FirstName"] = "BB";
      Assertion.Assert(bdeLastName.Text == "AA", 
               "Destination object did not get updated.");
      Assertion.Assert(bdeFirstName.Text == "BB", 
               "Destination object did not get updated.");
    }

    [Test]
    public void RowIsUpdatedTest()
    {
      tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
      tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
      bdeLastName.Text = "AA";
      bdeFirstName.Text = "BB";
      Assertion.Assert(table.Rows[0]["LastName"].ToString()== "AA", 
                "Table did not get updated.");
      Assertion.Assert(table.Rows[0]["FirstName"].ToString() == "BB", 
                "Table did not get updated.");
    }

    [Test]
    public void RowCursorChangeTest()
    {
      tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
      tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
      tableBindHelper.RowIndex = 1;
      Assertion.Assert(bdeLastName.Text == "C", 
                "Destination object did not get updated.");
      Assertion.Assert(bdeFirstName.Text == "D", 
                "Destination object did not get updated.");
    }

    [Test]
    public void InitialValueTest()
    {
      tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
      tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
      Assertion.Assert(bdeLastName.Text == "A", 
                "Destination object did not get updated.");
      Assertion.Assert(bdeFirstName.Text == "B", 
                "Destination object did not get updated.");
    }

    [Test]
    public void DeleteRowTest()
    {
      tableBindHelper.AddColumnBinder("LastName", bdeLastName, "Text");
      tableBindHelper.AddColumnBinder("FirstName", bdeFirstName, "Text");
      tableBindHelper.RowIndex = 1;
      table.Rows[1].Delete();
      Assertion.Assert(bdeLastName.Text == "A", 
                "Destination object did not get updated.");
      Assertion.Assert(bdeFirstName.Text == "B", 
                "Destination object did not get updated.");
    }
  }
}

Conclusion

I hope this article is useful as a starting point for people interested in data binding independent of the System.Windows.Forms namespace. While it is by no means comprehensive, it should be a good foundation for a rigorous implementation, but hopefully it will be useful as is for lightweight binding of classes to the DataTable. It seems that every year or so I end up revisiting data binding for one reason or another. It's a very useful technique, and it's very "rich" in capability, fitting into different architectures and working well with imperative and declarative code alike.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey2-Mar-12 22:57
professionalManoj Kumar Choubey2-Mar-12 22:57 
GeneralUpdate Controls .NET Pin
Michael L Perry9-Apr-08 16:44
Michael L Perry9-Apr-08 16:44 
GeneralCheck Genius Binder into codeplex Pin
jmptrader10-Dec-07 11:01
professionaljmptrader10-Dec-07 11:01 
GeneralWell done Pin
Josh Smith13-Nov-07 16:41
Josh Smith13-Nov-07 16:41 
GeneralRe: Well done Pin
Marc Clifton14-Nov-07 7:50
mvaMarc Clifton14-Nov-07 7:50 
GeneralFive! Pin
pleykov9-Nov-07 21:49
pleykov9-Nov-07 21:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.