DataView for objects: Implementation Part V

Posted on November 5, 2006

Note: You can download the complete implementation here.

In my last post I described the implementation of the IBindingList interface in ObjectListView, my implementation of a view for collections of arbitrary objects.  The goal is to provide the same view capabilities for collections that DataView supports for DataTables.

In this post, we’ll move on to the final interface required for data binding our view, which is IBindingListView.

What’s missing?

At this point, we’ve implemented a number of interfaces that make ObjectListView act as a list-style collection: IEnumerableICollection, and IList.  We added specific data binding features with implementations of IBindingList and IRaiseItemChangedEvents.  By recognizing the presence of INotifyPropertyChanged and IEditableObject in our list items, we further extend the capabilities of our view in reporting item changes.

Although IBindingList adds the ability to sort the view, it only supports a single property.  Often, I’d like to be able to sort rows in a DataGridView by multiple columns.  For example, sorting a list of customers alphabetically by last name, and then first name within last name.  To support this, we need to be able to sort our view on multiple properties.

I might also want to filter the underlying list.  Filtering gives us the ability to present a selected subset of the underlying data to the bound control. To continue with my customer data example, perhaps I want to see the list of customers sorted by last name and first name who live in Oregon.

IBindingListView

IBindingListView adds multiple property sorting, and also filtering.  Here are the new methods of IBindingListView:

public void ApplySort(ListSortDescriptionCollection sorts);

public string Filter
{
    get;
    set;
}

public void RemoveFilter();

public ListSortDescriptionCollection SortDescriptions
{
    get;
}

public bool SupportsAdvancedSorting
{
    get;
}

public bool SupportsFiltering
{
    get;
}

The filter property provides a way to specify which list items should be included in the filtered subset of data presented to bound controls (or any other consumer of the view).

The MSDN documentation of the filter property indicates that the definition of the filter string is dependent on the data source implementation.  That means that we’re free to do whatever we want with the property.  I suspect that users of ObjectListView will expect the filter property to follow the conventions of the RowFilter property of DataView, however.  This property, like DataTable expression columns, uses a generalized SQL-like syntax to specify matches.  At it’s simplest, this would be

propertyName = value

Our implementation of Filter will start with this simplest of mechanisms:

public string Filter
{
  get
  {
    Lock();
    string s = filter;
    Unlock();
    return s;
  }
  set
  {
    Lock();
    try
    {
      if (value != this.filter)
      {
        if (!string.IsNullOrEmpty(value))
        {
          string[] filterParts = value.Split(new char[] { '=' });
          if (filterParts.Length != 2 || filterParts[0] == "" ||
              filterParts[1] == "")
            throw new ArgumentException("Filter string must be of the form 'propertyName=value'.", "Filter");

          // Trim whitespace from property name and value.
          string propertyName = filterParts[0].Trim();
          string propertyValue = filterParts[1].Trim();

          // Trim quoted values.
          if (propertyValue[0] == '\'')
          {
            if (propertyValue[propertyValue.Length - 1] != '\'')
              throw new ArgumentException("Unbalanced quotes in property value.", "Filter");
            propertyValue = propertyValue.Trim(new char[] { '\'' });
          }
          else if (propertyValue[0] == '"')
          {
            if (propertyValue[propertyValue.Length - 1] != '"')
              throw new ArgumentException("Unbalanced quotes in property value.", "Filter");
            propertyValue = propertyValue.Trim(new char[] { '"' });
          }

          // Is the property supported by the list item type?
          if (this.itemProperties == null)
            throw new InvalidOperationException("The list item type must be set first.");
          PropertyDescriptor property = null;
          foreach (PropertyDescriptor prop in this.itemProperties)
          {
            if (prop.Name == propertyName)
            {
              property = prop;
              break;
            }
          }
          if (property == null)
            throw new ArgumentException("The property '" + propertyName + "' is not a property of the list item type.", "Filter");

          if (string.Compare(propertyValue, "null", true) == 0)
            propertyValue = null;

          if (property.PropertyType == typeof(string))
          {
            StringComparerPredicate pred = new StringComparerPredicate(property, propertyValue, true);
            this.filterPredicate = new Predicate<object>(pred.Matches);
          }
          else
          {
            PropertyComparerPredicate pred = new PropertyComparerPredicate(property, propertyValue, true);
            this.filterPredicate = new Predicate<object>(pred.Matches);
          }
          this.filterProperty = property;
        }
        else
        {
          this.filterPredicate = null;
          this.filterProperty = null;
        }

        filter = value;

        ApplyFilter();
      }
    }
    finally
    {
      Unlock();
    }

    RaiseListChangedEvents();
  }
}

Once I establish the property and value represented by the filter string, I create an instance of PropertyComparerPredicate that will be used to match list items against the filter.  PropertyComparerPredicate just remembers the property descriptor and the value that represents the filter criteria.  I save this comparer object for later use in the ObjectListView field filterPredicate.  The filterPredicate is a Predicate<T> generic type, which represents any method that takes an instance of type T and returns true or false.  In our case, T is the list item type.  The method will return true if the list item meets the filter criteria, and false if it does not.

You’ll notice that I have a specialized comparer for string properties.  StringComparerPredicate interprets the target value as a string literal or a regular expression, so that wildcards can be used in the filter string.

The sorting support provided by IBindingListView allows multiple properties to be specified in the ApplySort() method.  Once set, the sort properties can be retrieved with the SortDescriptions property.

Like IBindingList, the features added by IBindingListView are “optional” in the sense that there are SupportsXYZ properties specifying whether the added features have a meaningful implementation.  For IBindingListView, we have SupportsAdvancedSorting and SupportsFiltering, which will always return true for ObjectListView, indicating that we do indeed fully implement IBindingListView.

Where are we?

We’ve examined all of the data binding interfaces needed to implement a DataView-style sorted and filtered view of arbitrary business objects.  I encourage you to look at the ObjectListView code for more insight into these interfaces and their requirements.  Hopefully, you’ll also have some ideas for improving the code.  Please let me know!

In the next (and final) installment, I’ll show some sample code that binds business objects to different WinForms controls using ObjectListView.


No Replies to "DataView for objects: Implementation Part V"


    Got something to say?

    Some html is OK