Custom Data Binding, Part 3

 

Michael Weinhardt
www.mikedub.net

March 15, 2004

Summary: In this third and final piece on custom data binding, we extend the sortable binding list developed in Part 2 of this series to include search functionality. Additionally, we create a custom search UI, FindStrip, in lieu of equivalent .NET Framework support. (13 printed pages)

Note This article targets the February 2005 CTP version of both the .NET Framework 2.0 and Visual Studio .NET, and provides a code sample written in both C# and Visual Basic .NET.

Download the winforms02152005_sample.msi file.

Where Were We?

In Part 1 of this series, we began the construction of the Chinese Pronunciation Game Data application to capture a list of Chinese-English data items. To capture that data, we created a custom type, Word:

public class Word {
  public string English { ... }
  public string Pinyin { ... }
  public Image Character { ... }
  public byte[] Pronunciation { ... }
}

We then utilized the BindingSource component's ability to transform an item data source, like Word, into a strongly-typed binding-capable list data source. BindingSource makes this happen internally through the use of a new generic .NET Framework class, BindingList<T>, which provides the framework for sorting and searching, although stops short of providing an implementation of either. In Part 2, we subsequently extended BindingList<T> with a sorting implementation to produce in a new, generic class, SortableBindingList<T>, which can be used to create a sortable list data source with a single line of code:

SortableBindingList<Word> source = new SortableBindingList<Word>();

Finally, we bound a DataGridView control to a BindingSource component whose data source was a SortableBindingList<T> instance. Once bound, the DataGridView brings native UI sorting support to bare, begun with a column header click and advertised with a sorting chevron that indicates sorted column and sort direction, as shown Figure 1.

Figure 1. Sortable DataGridView thanks to SortableBindingList<T>

Fundamentally, sorting is a useful way for users to locate discrete pieces of data that might be stored within a large list. Another technique in the same vein, searching, allows users to more easily hone in on a specific record. As with sorting, the data binding infrastructure provides the fundamental support, without an implementation. The rest of this article discusses how to leverage the infrastructure to provide a searchable list data source.

Implementing a Searchable List Data Source

As we saw in Part 2, BindingList<T> implements IBindingList, a well-known data binding interface used to expose a wide range of list-oriented data binding support. While BindingList<T> doesn't implement the sorting or searching members of IBindingList, it does implement enough of the interface to support list notifications, and configurations that specify whether list items can be added, updated, and deleted. Consequently, deriving from BindingList<T> is a good place to start building a new searchable list data source, SearchableSortableBindingList<T>:

public class SearchableSortableBindingList<T> : BindingList<T> { 
  ... 
  // Sorting implementation
  ...
  // Persistence implementation
  ...
}

As you can tell from the comments, SearchableSortableBindingList<T> also incorporates the sorting and persistence code we developed in the last installment. At runtime, we'll set the DataSource property of the BindingSource to a SearchableSortableBindingList<T> object that's loaded with game data from disk:

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

From a Search point of view, the two members of interest exposed by IBindingList are the SupportsSearching property and the Find method. SupportsSearching is a Boolean property that returns true if a list data source implements searching, or false otherwise. Find is called to subsequently execute a search. Both of these members are called from client code, most typically UI controls that provide search UI.

Unfortunately, SearchableSortableBindingList<T> can't go ahead and override the IBindingList members implemented by BindingList<T>, because they are neither virtual nor abstract. However, BindingList<T> exposes two additional members for this purpose: SupportsSearchingCore and FindCore. SupportsSearching and Find are still called from data bound controls, but the BindingList<T> implementations call down into their core namesake methods.

SupportsSearchingCore

The BindingList<T> base implementation of SupportsSearchingCore returns false, by default. Obviously, we'll need to override it to return true:

public class SearchableSortableBindingList<T> : BindingList<T> {
  ...
  protected override bool SupportsSearchingCore {
    get { return true; }
  }
  ...
}

FindCore

Client code should only call the IBindingList interface's Find method if SupportsSearch returns true. If this is the case, you'll need to have overridden your BindingList<T> derivation's FindCore method with a custom search algorithm, like the basic one shown here:

public class SearchableSortableBindingList<T> : BindingList<T> {
  ...
  protected override int FindCore(
    PropertyDescriptor property, object key) {

    // Specify search columns
    if( property == null ) return -1;

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

    // Traverse list for value
    foreach( T item in items ) {

      // Test column search value
      string value = (string)property.GetValue(item);

      // If value is the search value, return the 
      // index of the data item
      if( (string)key == value ) return IndexOf(item);
    }
    return -1;
  }

This code checks to see if a search column was provided and, if so, scans the list of data items for the first data item that holds the desired value. If such a data item is found, its index is returned. Otherwise, -1 is returned to indicate the value wasn't found. Initiating the search and processing the returned index is a job normally performed by a search UI.

Implementing the Search UI

Unfortunately, DataGridView does not provide visual searching capability. Therefore, in order to use SearchableSortableBindingList<T>, we'll need to create our own with an eye to making it easy to use, reusable, and independent of any particular list data source implementation.

The FindStrip Control

Using native Windows Forms 2.0 support for tool strip controls, I decided to build a special type of tool strip, FindStrip, which is loosely based on the Microsoft Outlook 2003 Find tool strip. The FindStrip is shown in Figure 2.

Figure 2. Custom FindStrip

The two values essential to searching are a value to search for and a column to search in. FindStrip captures the former with the Search For text box and the latter using the Search In combo box, while the search itself is initiated by clicking the Find Now button.

To build the FindStrip control, I dropped a ToolStrip control onto a form and opened its Edit Items smart tag task to add a several tool strip items, including a tool strip label and text box for the Search For value, a tool strip label and combo box for the Search In column, and a tool strip button to click to begin the search. Each tool strip item was configured as necessary to produce what you see in Figure 2. To make it reusable, I took the code generated by the designer and redeployed it into a new class, FindStrip, which derives from ToolStrip:

public class FindStrip : System.Windows.Forms.ToolStrip {
  // Redeployed designer generated code
  ...
}

The RAD experience can only take you so far before you need to add the code to make it work which, in this case, means integrating with the SearchableSortableBindingList<T> and adding a few mod cons.

Acquiring a List Data Source Reference

Because it's the FindStrip control's job to orchestrate the search of bound list data source, the first addition to FindStrip will be a public read-write property that developers can set to the desired BindingSource:

public class FindStrip : ToolStrip {
  ...
  private BindingSource _bindingSource;
  ...
  public BindingSource BindingSource {
    get { return _bindingSource; }
    set { _bindingSource = value; }
  }
  ...
}

Instead of using a more traditional DataSource property, which would have been of type object, BindingSource is used primarily for simplicity; BindingSource wraps the complexity of managing a bound data source that we would otherwise have to write, thereby simplifying the overall effort. This property can be set programmatically, or declaratively using the Property Browser, as shown in Figure 3.

Figure 3. Declaratively selecting a BindingSource using the Property Browser

The BindingSource reference comes in handy not only for executing the search, but also for determining the columns against which the search will operate.

Before a search can commence, the user must obviously select a search column from the Search In combo box. That means it's our duty to populate the combo box with some appropriate values. From a data binding point of view, the names of the columns equate to the public properties implemented by the list data source's items. To get those, we can pull them from the BindingSource itself, from the GetItemProperties method implemented through its ITypedList interface. I put the list population code into a handler for the SearchInToolStripComboBox control's GotFocus event:

private void searchInToolStripComboBox_GotFocus(
  object sender, EventArgs e) {

  // Bail if no data source
  if( _bindingSource == null ) return;
  if( _bindingSource.DataSource == null ) return;

  this.searchInToolStripComboBox.Items.Clear();

  // Add column names to Search In list
  PropertyDescriptorCollection properties = 
    ((ITypedList)bindingSource).GetItemProperties(null);
  foreach( PropertyDescriptor property in properties ) {
    this.searchInToolStripComboBox.Items.Insert(0, property.Name);
  }

  // Select first column name in list, if column names were added
  if( this.searchInToolStripComboBox.Items.Count > 0 ) {
    this.searchInToolStripComboBox.SelectedIndex = 0;
  }
}

Whenever the Search In combo box drops down after a mouse click, this code clears its list items before loading it with one list item for each property of the data item, in this case the Word, before finally selecting the first list item. The populated combo box is shown in Figure 4.

Figure 4. List data source item properties displayed as columns to search in

Another benefit of using the GetListItemProperties method is that we pass in a data source object, not a specific type of data source, which means the code will work against any data source, thereby achieving our goal of independence.

Of course, both Character and Pronunciation columns are visual and consequently won't respond well to the textual search we've implemented. Because the new custom searching algorithm supports strings, one way to discern which columns would be to update the BindingSource property to check each property's type and, if string, add the property to the Search In list:

private void searchInToolStripComboBox_GotFocus(
  object sender, EventArgs e) {

  // Bail if no data source
  if( _bindingSource == null ) return;
  if( _bindingSource.DataSource == null ) return;

  this.searchInToolStripComboBox.Items.Clear();

  // Add column names to Search In list
  PropertyDescriptorCollection properties = 
    ((ITypedList)bindingSource).GetItemProperties(null);
  foreach( PropertyDescriptor property in properties ) {
    if( property.PropertyType == typeof(string) ) {
      this.searchInToolStripComboBox.Items.Insert(0, property.Name);
    }
  }

  // Select first column name in list, if column names were added
  if( this.searchInToolStripComboBox.Items.Count > 0 ) {
    this.searchInToolStripComboBox.SelectedIndex = 0;
  }
}

This tweak has the effect of reducing the list two contain two entries, as shown in Figure 5.

Figure 5. FindStrip with valid searchable columns

Now the user can provide a Search For and Search In value, they'll initiate a search by pressing the Find Now button:

public class FindStrip : ToolStrip {
  ...
  // Start find if Find Now button clicked
  private void findNowToolStripButton_Click(object sender, EventArgs e) {
    this.Find();
  }
  ...
}

It's the Find method that executes the search, so let's look at how it works.

The first thing that Find needs to do is determine whether or not the list data source actually supports searching which, as we saw, requires inspecting the SupportsSearch property using an IBindingList implementation. BindingSource actually implements IBindingList itself, primarily as a helper implementation to simplify the code we'd need to write to deal directly with underlying data source's IBindingList implementation. Subsequently, Find can make the SupportsSearch check like so:

public class FindStrip : ToolStrip {
  ...
  private void Find() {
    // Don't search if the underlying IBindingList implementation
    // doesn't support searching
    if( !((IBindingList)_bindingSource).SupportsSorting ) return;
    ...
  }
  ...
}

If search is supported, a call to the Find method of the list data source's IBindingList interface is required. Find expects two arguments: a PropertyDescriptor that represents the column to search (really, the property of the list data item that can be represented as a column in the DataGridView) and an object that is the value to search for. While we can pull the object value directly from the FindStrip control's Search For text box, we need to convert the Search In combo box's selected value to a PropertyDescriptor. This can be achieved using the ListBindingHelper again:

public class FindStrip : ToolStrip {
  ...
  private void Find() {
    // Don't search if nothing specified to look for
    string find = this.findToolStripTextBox.Text;
    if( string.IsNullOrEmpty(find) ) return;

    // Don't search of a column isn't specified to search in
    string findIn = this.searchInToolStripComboBox.Text;
    if( string.IsNullOrEmpty(findIn) ) return;
    
    // Get the PropertyDescriptor
    PropertyDescriptorCollection properties =
      ((ITypedList)_bindingSource).GetItemProperties(null);
    PropertyDescriptor property = properties[findIn];

    // Find a value in a column
    int index = _bindingSource.Find(property, find);
    ...
  }
  ...
}

This code validates both Search For and Search In values before grabbing the PropertyDescriptor for the specified Search In column. Armed with both Search For and Search In values, Find is then invoked on the BindingSource, which forwards the call to the list data source's Find method if it implements IBindingList. In our example, this means a call to the Find method that we implemented earlier on SearchableSortableBindingList<T>. The returned index value can be used to select the row in the DataGridView that corresponds to the found list data source item. Returning that value to developers is the trick.

Note In Windows Forms 2.0 Beta 2.0, the BindingSource component's Find method will be changed to accept a string property name value, instead of a property descriptor as it does now. Consequently, this client code will become much smaller.

Processing the Search Results

To keep it simple, I created a delegate, ItemFoundEventHandler, that is used by FindStrip to implement the ItemFound event. ItemFoundEventHandler expects a custom event argument object that stores the index of the found item, ItemFoundEventArgs. The delegate, the event arguments and the FindStrip event are all shown here:

public class ItemFoundEventArgs : EventArgs {
  private int _index;
  public ItemFoundEventArgs(int index) { _index = index; }
  public int Index { get { return _index; } }
}

public delegate void ItemFoundEventHandler(
  object sender, ItemFoundEventArgs e);
    
public class FindStrip : ToolStrip {
  ...
  public event ItemFoundEventHandler ItemFound;
  protected virtual void OnItemFound(ItemFoundEventArgs e) {
    // Report find results
    if( ItemFound != null ) ItemFound(this, e);
  }
  ...
}    

The FindStrip control's Find method calls OnItemFound when a search completes, which fires the ItemFound event and passes an ItemFoundEventArgs instance that captures the returned index:

public class FindStrip : ToolStrip {
  ...
  private void Find() {
    ...
    this.OnItemFound(new ItemFoundEventArgs(index));
  }
  ...
}

On the form side, handling the event is simply a case of adding an event handler for it using the Property Browser, and processing the index accordingly:

partial class MainForm : Form {
  ...
  private void findStrip_ItemFound(object sender, ItemFoundEventArgs e) {
    // If value found, select row
    if( e.Index >= 0 ) {
      this.wordDataGridView.ClearSelection();
      this.wordDataGridView.Rows[e.Index].Selected = true;

      // Change current list data source item
      // (to ensure currency across all controls
      // bound to this BindingSource)
      this.wordBindingSource.Position = e.Index;
    }
  }
}

In this case, the code simply selects the row in the DataGridView control that corresponds to the returned index, as shown in Figure 6.

Figure 6. Search results selected in DataGridView

The code also ensures the currently selected record is broadcast to all other bound controls, so they have the opportunity to update themselves as appropriate. Of course, you could use more elaborate and informative techniques for relaying search results to the user and this implementation provides a basic framework that you can easily extend as needed.

Where Are We?

In this final installment in the series, we created a new list data source that incorporates the sorting and persistence code we built in the previous installment and extends it with a searching capability. Specifically, we overrode the SupportsSearchCore and FindCore members of the base BindingList<T> class. Because the DataGridView control doesn't provide a UI to support searching, we also built a custom reusable FindStrip control to fill the gap. The key feature of FindStrip are that that it will operate against any list data source that supports searching, and is relatively simple to configure and use.

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.