Export (0) Print
Expand All

Custom Data Binding, Part 2

 

Michael Weinhardt
www.mikedub.net

February 6, 2005

Summary: In part two, the custom data binding journey continues with a brief look at the role BindingList<T> plays in allowing BindingSource to convert a single type into a list data source. We also look at situations where relying on the BindingSource isn't enough, such as sorting and searching. Such features need you to go a step further and create your own BindingList<T> implementation with the additional support, which we also look at. To top it off, game data serialization is added to make life easier. (14 printed pages)

Note   This article targets the December 2004 CTP version of both the .NET Framework 2.0 and Visual Studio .NET.

Download the winforms02182005_sample.msi file.

Where Were We?

Recall the Chinese Pronunciation Game Data application we built in Part 1, shown in Figure 1, whose purpose was to capture data for use in a flashcard-style learning game.

Figure 1. Chinese Pronunciation Game Data entry application

We began with the creation of a custom Word type to capture our arbitrary game data:

public class Word {
  public Image Character { get; set; }
  public string English { get; set; }
  public string Pinyin { get; set; }
  public byte[] Pronunciation { get; set; }
}

Thanks to the data designers in Windows Forms 2.0, we were able to convert the Word type into a list data source, bind it to a DataGridView control, and add VCR-style navigation support without a writing a single line of data binding code.

Note   In retrospect, the Class Designer feature in Visual Studio .NET December CTP could have been used to visually create the Word type and avoid coding altogether.

To provide an editable list data source for any custom type, BindingSource relies on a small but very useful piece of technology known as BindingList<T>, located in the System.ComponentModel.Collections namespace.

BindingList<T>

BindingList<T> is a generic implementation of the IBindingList interface (from the System.ComponentModel namespace), the minimum interface required by the data binding infrastructure for list data sources to support editing. While IList is the minimum interface required to implement a binding-capable list data source, it doesn't provide editing capability, which is fine when bound to a non-editable control like ListBox. When bound to a control with full editing support like DataGridView, however, the ability to edit in a list data source is a must, as well as support for features like sorting, searching, indexing, and change notification. IBindingList derives from IList and extends to provide all of this support:

public interface IBindingList : IList, ICollection, IEnumerable {

  // Editing members
  bool AllowNew { get; } // Is adding new list data items supported?
  bool AllowEdit { get; } // Is editing list data items supports?
  bool AllowRemove { get; } // Is list data item removal supported?
  object AddNew(); // Adds and returns a new list data
  
  // Sorting members
  bool SupportsSorting { get; } // Supported?
  bool IsSorted { get; } // Is the list data source sorted?
  PropertyDescriptor SortProperty { get; } // Current sort column
  ListSortDirection SortDirection { get; } // Current sort direction
  void ApplySort(
    PropertyDescriptor property, 
    ListSortDirection direction); // Sort list by column/direction
  void RemoveSort(); // Revert to an unsorted state

  // Searching members
  bool SupportsSearching { get; } Supported?
  int Find(
    PropertyDescriptor property, 
    object key); // Find a list data item whose specified property 
                 // matches the provided key value
  
  // Indexing members
  void AddIndex(
    PropertyDescriptor property); // Add index to desired column
  void RemoveIndex(
    PropertyDescriptor property); // Remove index from desired column

  // Change notification members
  bool SupportsChangeNotification { get; } // Supported?
  event ListChangedEventHandler ListChanged; // Broadcast list change
}

BindingList<T> implements IBindingList using new .NET 2.0 generics support to allow any custom type to become a strongly typed IBindingList list data source. Strong typing is of particular benefit as it simplifies things for the databinding infrastructure, especially when it comes to inspecting a list data source for bindable properties.

Note   Discussion of generics falls outside the scope of this article, but I urge you to explore further, starting with http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvs05/html/csharp_generics.asp for a great introduction.

Although the code used by the BindingSource is a little more complex, the following example shows how simple the resulting BindingList<T> implementation makes this possible for the Word type:

BindingList<Word> words = new BindingList<Word>();

When bound to DataGridView, a BindingList<T> implementation provides support for list change notification, making it a good data binding infrastructure list data source, for example, using a well-known data binding mechanism for broadcasting list changes to bound clients. BindingList<T> actually does a whole bunch more to integrate into and data binding infrastructure, although a discussion would be non-trivial. However, feel free to start your own research at http://msdn2.microsoft.com/library/sywd766d.aspx.

BindingList<T>: What's Missing

DataGridView provides the visual mechanisms that allow users to add, update, and delete rows, ably supported by BindingList<T> list notification support, to provide synchronicity between control and list data source. BindingList<T> implements IBindingList, which is an interface that signals to the data binding infrastructure the level of service an implementation offers. However, no IBindingList implementation is under obligation to implement the entire interface and, as it turns out, BindingList<T> doesn't, with regard to sorting, searching and indexing. One reason for this could be that it would be quite difficult to implement genuinely generic sorting and searching algorithms, particularly as different types may have different properties that may or may not be sortable or searchable. Either way, this is an important consideration and may determine whether you can use BindingSource to automatically create and manage a list data source, as we've seen, or whether you need to write your own sorting, searching, or indexing code or some combination of all three. While any of these implementations could be built into or around the DataGridView itself, incorporating this logic into a custom IBindingList implementation means that subtleties and nuances derived from the custom type stored in a list data source are portable and reusable.

Applications like the Chinese Pronunciation Game Data manager are likely to display hundreds and thousands of rows of data and would definitely benefit from sorting and searching. The rest of this article focuses on building a sortable BindingList<T> implementation.

Creating a Sortable BindingList<T>

To implement sorting, we'll need to implement IBindingList and its sorting members as appropriate, including the following:

public interface IBindingList : IList, ICollection, IEnumerable {
  ...  
  // Sorting members
  bool SupportsSorting { get; }
  bool IsSorted { get; }
  PropertyDescriptor SortProperty { get; }
  ListSortDirection SortDirection { get; }
  void ApplySort(
    PropertyDescriptor property, 
    ListSortDirection direction);
  void RemoveSort();
  ...
}

While BindingList<T> doesn't provide sorting, it does provide the basic list data source support we'll need and is a great place to start rather than building a full IBindingList implementation from scratch. Although you could derive a type from BindingList<Word> to do so, the result would be specific to the Word type. Instead, we could take an extra step and create a generic sortable binding list:

public class SortableBindingList<T> : BindingList<T> {}

The result is a class that can be used to provide sorting to any type, including Word. Although BindingList<T> doesn't implement sorting, it does provide the hooks necessary for us to easily integrate our own.

The Core Members

These hooks are comprised of a set of properties and methods that you override, all of which take the name of the IBindingList member you'd like to implement and adding the "Core" suffix. The entire set of core sorting members is shown here:

public class BindingList<T> : IBindingList, ... {
  // Core sort methods
  protected virtual void ApplySortCore(PropertyDescriptor property, ListSortDirection direction);
  protected virtual void RemoveSortCore();

  // Core sort properties
  protected virtual bool SupportsSortingCore { get; }
  protected virtual bool IsSortedCore { get; }
  protected virtual ListSortDirection SortDirectionCore { get; }
  protected virtual PropertyDescriptor SortPropertyCore { get; }
}

In the BindingList<T> implementation, each IBindingList member that has a corresponding core member is simply a wrapper around the core member. For example, the SupportsSorting property calls the SupportsSortingCore method to do the real work:

public class BindingList<T> : IBindingList, ... {
  public bool SupportsSorting {
    get { return this.SupportsSortingCore; }
  }
  protected virtual bool SupportsSortingCore {
    get { return false; }
  }
}

As you can see, the SupportsSortingCore property of BindingList<T> returns false, indicating to the data binding world that sorting is not supported. To implement a complete sorting solution, we'll need to override each of the appropriate sorting methods, including IsSortedCore, SortPropertyCore, SortDirectionCore, ApplySortCore, RemoveSortCore, and SupportsSortingCore.

Do We Support Sorting?

As you've probably deduced by now, our custom list data source needs to broadcast its support for sorting by overriding SupportsSortingCore to return true, like so:

public class SortableBindingList<T> : BindingList<T> {}
  protected virtual bool SupportsSortingCore {
    get { return true; }
  }
}

Phew. That was easy. Of course, now that we talked the talk, we have to walk the walk and actually implement the sorting algorithm. Whenever I see the word algorithm, I usually freak out and load up my favorite game instead. If you are like me, please do whatever it is you do in these situations and report back ASAP.

Applying a Sort Algorithm

Implementing sorting requires overriding ApplySortCore, where the sorting brain-trust lives and executes. Specifically, it's the responsibility of ApplySortCore to simply run a sort algorithm, while the process is driven by bound controls like the DataGridView. Sorting on a DataGridView is initiated when a column header is left-mouse button clicked, and is a three-phase process. The first phase calculates the required sorting metadata, which is the column to sort and the order, which are converted to a PropertyDescriptor and ListSortDirection enumeration value respectively. The second phase sees DataGridView instructing the data source to sort itself with a call to IBindingList.ApplySort, passing in the required PropertyDescriptor and ListSortDirection arguments. When ApplySort returns, the DataGridView repaints itself to reflect the new sort order. The only job that the custom-binding list must perform is to take the sort arguments passed to ApplySort and, well, sort itself.

BindingList<T> actually derives from Collection<T> to provide a mechanism to manage one or more list data items. Collection<T> exposes an Items property that returns a reference to the List<T> that we need to perform the sort on. Conveniently, List<T> exposes a Sort method for this purpose. Using this method relies on utilizing an IComparer implementation to do the necessary sorting comparisons on a list data item-by-item basis. Types that implement IComparer know how to take two values and returns an integer value that specifies whether the first value was greater (returns 1), whether the first value was lower (returns -1), or whether both values were the same (returns 0). Sort has several overloads that each provides a different way to pass the IComparer you want to use:

public class List<T> : IList<T>, ... {
  ...
  // Use system-provided IComparer
  public void Sort();
  // Use custom IComparer<T> object
  public void Sort(IComparer<T> comparer);
  // Creates an internal IComparer shim that calls the Comparison<T> 
  // delegate to perform the sort
  public void Sort(Comparison<T> comparison);
  // Use custom IComparer<T> object to sort a portion of the list
  public void Sort(int index, int count, IComparer<T> comparer);
  ...
}

This article implements a custom PropertyComparer<T> class, which derives from IComparer<T>:

public class PropertyComparer<T> :

System.Collections.Generic.IComparer<T> {

// Constructor

  public PropertyComparer(
    PropertyDescriptor property, ListSortDirection direction) {...}
  // IComparer<T> interface

public int Compare(T xValue, T yValue) {...}

  public bool Equals(T xValue, T yValue) {...}
  public int GetHashCode(T obj) {...}
  ...
}

PropertyComparer builds on a comparison algorithm based built by Rockford Lhotka and turns it into a generic property comparer for any type.

Note   While a detailed analysis of the comparer is beyond the scope of this article, a brief discussion will suffice, although I urge you to read Rocky's great article and inspect the code sample.

PropertyComparer's constructor accepts two arguments, a PropertyDescriptor and a ListSortDirection enumeration value. These arguments are used during comparison; that is, when the Compare method is called. Compare itself accepts two type instances, retrieves the value of the property specified by the constructor's property argument on both instances, compares them, and returns the result.

Pulling it all together, we create a PropertyComparer instance in ApplySort, making sure to pass the constructor the appropriate arguments, then pass a reference to the PropertyComparer to List<T>.Sort, which calls PropertyComparer's Compare method as many times as it takes to sort the list by the specified property in the desired sort order:

public class SortableBindingList<T> : BindingList<T> {
  ...
  protected override void ApplySortCore(
    PropertyDescriptor property, ListSortDirection direction) {
    // Get list to sort
    List<T> items = this.Items as List<T>;

    // Apply and set the sort, if items to sort
    if( items != null ) {
      PropertyComparer<T> pc = 
        new PropertyComparer<T>(property, direction);
      items.Sort(pc);
    }

    // Let bound controls know they should refresh their views
    this.OnListChanged(
      new ListChangedEventArgs(ListChangedType.Reset, -1));
  }
}

Broadcasting a ListChange

After a sort, you need to let all bound controls know the list data source's order has changed and they should reflect the new order visually. As shown in the code sample above, notifying bound controls requires calling the OnListChanged method, inherited from BindingList<T>, and passing an appropriate ListChangedType enumeration value:

this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));

A value of Reset means the entire list is likely to have changed, which makes sense after a sort operation. ListChangedType (from the System.ComponentMode namespace) has several other values, shown here:

public enum ListChangedType {
  ItemAdded = 1, // A list item was added
  ItemChanged = 4, // A list item was changed
  ItemDeleted = 2, // A list item was deleted
  ItemMoved = 3, // A list item moved to a new list position
  PropertyDescriptorAdded = 5, // List schema change (new property)
  PropertyDescriptorChanged = 7, // List schema change (property changed)
  PropertyDescriptorDeleted = 6, // List schema change (property deleted)
  Reset = 0 // List-wide change
}

One thing you may have noticed is how the internals of BindingList<T> with respect to sorting, including Collection<T> and List<T>, are all generics, meaning strong-typing permeates the entire sort infrastructure and is a good thing from a performance point of view.

Are We Currently Sorted?

Once sorted, the onus is on an IBindingList implementation to report its current sort. IsSortedCore is the property we need to override to do so, allowing the data binding infrastructure to inspect our implementation's sorted state as necessary. This is easily achieved with a private state variable:

public class SortableBindingList<T> : BindingList<T> {
  private bool _isSorted;

  protected override void ApplySortCore(
    PropertyDescriptor property, ListSortDirection direction) {

    // Get list to sort
    List<T> items = this.Items as List<T>;

    // Apply and set the sort, if items to sort
    if( items != null ) {
      PropertyComparer<T> pc = new PropertyComparer<T>(property, direction);
      items.Sort(pc);
      isSorted = true;
    }
    else {
      isSorted = false;
    }

    // Let bound controls know they should refresh their views
    this.OnListChanged(
      new ListChangedEventArgs(ListChangedType.Reset, -1));
  }

  protected override bool IsSortedCore {
    get { return _isSorted; }
  }
}

Once implemented, ApplySortCore should be updated to change the implementation's sorted state to true upon sorting, as shown in the sample.

Removing the Sort

Likewise, when a sort is removed, the sort state must be set to false at the bare minimum. This logic needs to be implemented in the RemoveSortCore method, like so:

public class SortableBindingList<T> : BindingList<T> {
  private bool _isSorted;
  ...
  protected override void RemoveSortCore() {
    _isSorted = false;
  }
  ...
}

A real use for RemoveSort involves using a specialized sorted view instead of sorting directly against the underlying list data source, much like the DataView and DataTable model used by ADO.NET. Again, I urge you to read Rocky's article for more information.

Having overridden and implemented the necessary core sorting methods, we now have a sort-capable custom list data source that we need to bind to a DataGridView.

Using SortableBindingList<T>

Currently, our form as a DataGridView that's bound to a BindingSource component, itself bound to the Word type. Due to our work in the previous article, the DataGridView is also configured to display the appropriate columns at run-time, thanks especially to the bound BindingSource. All we need to do is to substitute SortableBindingList<T> as BindingSource's data source, for the Word type:

partial class MainForm : Form {
  private void MainForm_Load(object sender, EventArgs e) {
    ...
    SortableBindingList<Word> source = new SortableBindingList<Word>();
    this.wordBindingSource.DataSource = source;
  }
  ...
}

When the application now runs, the DataGridView is now sortable, shown in Figure 3.

Figure 2. Sortable DataGridView

The DataGridView presents the sorting results with the extra, expected UI accoutrement, such as the triangular sorting chevron.

It Pays to be Persistent

It's nice to have sorting, but it's nicer to actually be able to load and save data in a data management application. Persistence is a feature I left off in Part 1 but have incorporated now as a basic Save/Load implementation that utilizes file, gamedata.dat, provided in the project's bin\debug folder.

Note   Read Chris Sells's Document-Centric Management series for insightful coverage of the full complement of behavior a document-centric application should exhibit.

The interesting bits with respect to both the .NET Framework 2.0 and BindingList<T> are how simple the actual act of serializing and deserializing Word data was. While BindingList<T> is serializable, we are really interested in saving and loading the data items, which are found through the equally serializable Items property of BindingList<T>. Consequently, we can save and load the entire list of data items at once. I implemented Load and Save methods on SortableBindingList to make this portable:

public class SortableBindingList<T> : BindingList<T> {
  // NOTE: BindingList<T> is not serializable but List<T> is

  public void Save(string filename) {
    BinaryFormatter formatter = new BinaryFormatter();
    using( FileStream stream = 
      new FileStream(filename, FileMode.Create) ) {
      // Serialize data list items
      formatter.Serialize(stream, (List<T>)this.Items);
    }
  }

  public void Load(string filename) {
    this.ClearItems();
    if( File.Exists(filename) ) {
      BinaryFormatter formatter = new BinaryFormatter();
      using( FileStream stream = 
        new FileStream(filename, FileMode.Open) ) {
        // Deserialize data list items
        ((List<T>)this.Items).AddRange(
          (IEnumerable<T>)formatter.Deserialize(stream));
      }
    }

    // Let bound controls know they should refresh their views
    this.OnListChanged(
      new ListChangedEventArgs(ListChangedType.Reset, -1));
  }
}

The serialization is straightforward, but the deserialization (Load) requires a little extra. As a shortcut, the quickest way to load SortableBindingList's List<T> instance is to call its AddRange method, but required an IEnumerable<T> reference, which is returned by the BinaryFormatter's Deserialize method. Again, OnListChanged is called to signal a full list reset in response to the cleared and reloaded list data source.

Serialization logic only works on types that can be serialized, Generally, such types either are adorned with SerializableAttribute or implement ISerializable. The former is useful when only property values need to be serialized while the latter allows for a more complex serialization implementation. Since Word only requires its property values to be serialized, it was updated with SerializableAttribute:

[Serializable]
public class Word { ... }

On the other side, the SortableBindingList<Word> instance simply needs to be referenced to call both Load and Save, like so:

partial class MainForm : Form {
  ...
  private void MainForm_Load(object sender, EventArgs e) {
    ...
    // Open game data file
    SortableBindingList<Word> source = new SortableBindingList<Word>();
    source.Load(_filename);
    this.wordBindingSource.DataSource = source;
  }

  private void MainForm_FormClosing(
    object sender, FormClosingEventArgs e) {
    SortableBindingList<Word> list = 
      this.wordBindingSource.List as SortableBindingList<Word>;
    if( list != null ) {
      list.Save(_filename);
    }
  }
  ...
}

Now the Word-based list data source is loaded to and saved from, with DataGridView nicely updating itself.

Where Are We?

We saw how BindingSource relies on the generic BindingList<T> type to turn any type into an IBindingList implementation. That implementation, however, doesn't implement sorting, searching or indexing. It does, however, provide the hooks we need to do so through a set of core members that we override. We did just that for sorting, before finishing up with basic persistence.

I had intended to also implement searching but simply ran out of room this month. However, next time round I will not only show you how to build a basic searching implementation, but will also build a custom Find strip control that can easily slot into your UI and simplify the whole search process.

Acknowledgements

Thanks to Steve Lasker and Joe Stegman for continuing commentary. In particular, Joe spent some of his time helping me with a sticky beta issue and deftly turned a liability into an asset; neither SortableBindingList<T> nor PropertyComparer<T> would have been <T> without him. Equally, they wouldn't have been as useful to you.

References

Michael Weinhardt is currently working full-time on various .NET writing commitments that include co-authoring Windows Forms Programming in C#, 2nd Edition (Addison Wesley) with Chris Sells and writing this column. Michael loves .NET in general and Windows Forms specifically. He is also often accused of overrating the quality of 80s music, a period he believes to be the most progressive in modern history. Visit www.mikedub.net for further information.

Show:
© 2014 Microsoft