Jesse Johnston
{ ironic tagline here }

DataView for Objects: Final Cut

Sunday, 19 November 2006 21:25 by jesse

 

Note: You can download the complete implementation here.

In my last post I described the implementation of IBindingListView interface in ObjectListView, my implementation of a view for collections of arbitrary objects.  This was the last interface needed to support binding to controls such as the DataGridView.

In this post, I'll show some sample code that demonstrates the use of ObjectListView.

The Demo

I'm going to display data from the Microsoft sample Northwind database in a ComboBox and a DataGridView, showing how to filter and sort with ObjectListView.  Here's the main window:

 

After installing the Northwind database (available here), alter the configuration file Demo.exe.config to reflect your computer name (in place of "DADBOX").  Alternatively, you can modify the connection string in the text box of the demo program main window.

Run demo.exe, set the connection string if needed, and press the Get Data button.  This causes two lists to be populated from the database.  For the demo, I've created two classes, Customer and Order, which do nothing but hold the data from a row in the corresponding database table.  The two lists I'm populating are a List<Customer> and List<Order>.  Then, I create two instances of ObjectListView, binding one to the customers list, and one to the orders list:

private void buttonGetData_Click(object sender, EventArgs e)
{
    Database db = new Database();
    db.ConnectionString = this.textBoxConnectionString.Text;
 
    viewCompanies = new ObjectListView(db.GetCustomers());
    viewOrders = new ObjectListView(db.GetOrders());
 
    this.comboBoxCustomers.DataSource = viewCompanies;
    this.comboBoxCustomers.DisplayMember = "Company";
    this.comboBoxCustomers.ValueMember = "Id";
 
    this.dataGridView.AutoGenerateColumns = false;
    this.dataGridView.DataSource = viewOrders;
 
    this.textBoxFilter.Text = "";
    this.textBoxFilter.Enabled = true;
}

Finally, I set the data source of the company ComboBox to the customers ObjectListView, specifying that the Company property of each Customer will be displayed, and that the Id property will be returned from ComboBox.SelectedValue.  The data source of the grid is set to the orders ObjectListView.  Each grid column is bound through it's DataPropertyName property to a different property of Order.  The column binding code is in designer-generated code.

Now the ComboBox contains a list of all of the customers.  When we select a customer, the orders ObjectListView is filtered to present only the orders for the selected customer:

private void comboBoxCustomers_SelectedValueChanged(object sender, EventArgs e)
{
    if (this.comboBoxCustomers.SelectedValue == null)
        this.viewOrders.Filter = "CustomerId=null";
    else
        this.viewOrders.Filter = "CustomerId='" + this.comboBoxCustomers.SelectedValue.ToString() + "'";
}

Note the usage of CustomerId=null above, to indicate that the CustomerId property of Order should be compared to null when no customer is selected.

Once the grid is populated with orders, you can click on the column headers to sort the orders.  This is done behind the scenes by DataGridView, which delegates to the IBindingList.ApplySort implementation of ObjectListView.

You can also filter the companies displayed in the ComboBox by entering text in the filter TextBox.  As the text is changed, the customers ObjectListView filter is updated:

private void textBoxFilter_TextChanged(object sender, EventArgs e)
{
    this.viewCompanies.Filter = "Company=" + this.textBoxFilter.Text + "*";
}

That's all there is to it!  We've bound lists of arbitrary objects to both a DataGridView and a ComboBox, sorting and filtering our views of the lists without changing the lists themselves, in just a few lines of code.

What's ahead

There's a few things I know want to add to ObjectListView, and I'd like your feedback on other enhancements that you'd like.  Please email me and let me know how you're using ObjectListView and what you think should be added or changed.  In the meantime, I plan on adding:

  • More complex filter expressions (!=, <, >, LIKE, NOT, etc).
  • Support for code-based filter predicates (provide a callback method rather than a filter string expression).
  • BeginUpdate() and EndUpdate() methods to suppress ListChanged events while doing a large number of list insertions/deletions.
  • User-provided comparer for custom sorting.

I've enjoyed building ObjectListView - I hope you find it useful!

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Off to DevConnections

Sunday, 5 November 2006 00:37 by jesse

And not a moment too soon - the rain has come to Portland.  My tentative schedule:

Monday

  • Registration
  • Keynote
  • Schmoozing

Tuesday

  • Vista and Office 2007
  • Top 10 ways to light up your apps on Vista
  • Window Vista Tips and Tricks
  • Intro to .NET 3.0
  • Vista for Managed Developers

Wednesday

  • Basics of WPF
  • Multithreading in .NET 2.0
  • Layout and Navigation in WinForms
  • Generics
  • Too close to call - Writing Reliable Code vs. Workflow-Driven apps

Thursday

  • Implementing a DAL with the Dataset designer
  • Transactions
  • Presentation Technologies II
  • Real-world unit testing

Friday

  • Back in Portland with a Venti cappucino and a stack of defects to fix!

Hope to see you there!

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList
Categories:  
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed

DataView for objects: Implementation Part V

Sunday, 5 November 2006 00:13 by jesse

 

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: IEnumerable, ICollection, 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 'properyName=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.

kick it on DotNetKicks.com

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList