9. Implementing Custom Data-Bound Business Objects and Collections

Throughout most of the samples in this book I have used relational data sources, specifically typed data sets, as the data source to which you connect binding sources and controls from the form’s perspective. There are several reasons for this. One is that the predominate way of storing application data is in relational databases, and thus bringing the data into your application as a relational data collection provides a low impedance mismatch between the data that your application operates on and the data that your application persists and loads. Another is that the use of relational data in the presentation tier is still the most common way of presenting collections of data and letting users interact with that data. And the final reason is that in the past, it has typically been easier to work with relational data in data-binding scenarios than it was to work with custom business objects, partly because of the tools and the effort required to properly define business objects and collections of those objects that are suitable for data binding.

The problem with using relational data in the presentation layer is that it makes it more likely that your application will be tightly coupled from the data tier all the way into the presentation layer. When you are tightly coupled in this way, small changes in the data schema at the database level require changes all the way through your data access, business, and presentation layers. You are forced to go find all the places in your application that touched a particular part of your database so that you can update the data access that is working against that data. This doesn’t have to be the case, because you can decouple your business layer through a combination of stored procedures and defining separate data set types in the business layer from those defined in the data access layer. But sometimes defining custom business objects makes more sense than to decouple your business layer and give you more explicit control of everything that is going on in the objects that you use for data presentation and manipulation.

Luckily in .NET 2.0, in both the Windows Forms and ASP.NET portions of the Framework, it is far easier than in previous versions of the Framework to work with custom object collections and still get a rich data-binding experience, both at design time and runtime. It still requires more work than using data sets, but the tooling and the classes in the .NET Framework have significantly reduced the amount of work involved.

Chapter 5 showed how you can use the Data Sources window to add an object data source, which allows you to bind to collections of objects and follow the same process of connecting and configuring that data source to your bound controls. This chapter focuses on what it takes from the perspective of the data objects and collections to be suitable for use in data binding, and how to implement custom objects and collections to maximize their capabilities in a data-binding scenario.

Defining and Working with Data-Bound Business Objects

A lot of people object to the term business object (pun intended). What is a business object? If your application is a game, are your objects really “business” objects? Probably not. But for the purposes of this book, it helps to have a common term to use to refer to “those objects that are part of your application that are neither presentation nor data access; that usually have some logic, rules, validation, or workflow embedded; may contain data themselves; and live somewhere between what the user sees on the screen and the code that executes queries against your data store.” So I’ll use the term business object, but you can expand it to this more detailed definition every time mentally if you prefer.

Business objects come in many forms. Some may be pure data containers, others may just contain logic—code that manipulates data that comes from somewhere else—and some may be a mix of the two. The bottom line for the purposes of presenting data in Windows applications is that some business objects can contain data, and you may want to use that object directly to present that data. If that’s the case, you would really like to use them in the same way that you use a data set—point the data source to a collection of these objects or a single instance, and have the control do the work from there.

Achieving this data binding with business objects in .NET 2.0 is quite easy. As discussed in previous chapters, Windows Forms data binding is all based on a set of interfaces that define different capabilities of objects and collections within the context of accessing and navigating through data. As long as the objects that you are using for data binding support the appropriate interfaces, it will be transparent to data-bound controls that they are working with custom objects instead of the intrinsic .NET Framework relational data objects.

Just to make things more concrete, let me define a simple business object to use for some of the examples in this chapter. I will stick to a common construct and draw the parallels to the relational data used throughout the rest of the book. Specifically, let’s say you want to work with Customer objects. These objects will define the data associated with a customer and may include some behavior, or logic, that operates on that data.

The following is a simple representative definition of a Customer object:

class Customer
{
    private string m_CustomerID;
    private string m_CustomerName;
    private string m_PhoneNumber;
    private string m_ContactName;

    public string CustomerID
    {
        get { return m_CustomerID; }
        set { m_CustomerID = value; }
    } 
    public string CustomerName
    {
       get { return m_CompanyName; }
       set { m_CompanyName = value; }
    }
    // more properties...
}

As defined so far, the Customer class is a simple strongly typed container for some data values associated with a single business entity. Each of those data values is a primitive in .NET; in fact, they are all the easiest type of primitive to use for data binding—strings. If this were all there was to most business objects, our lives would be easy. But the reality is that when you transition to managing everything as business objects, there is usually a whole complex hierarchy of objects—and relationships between those objects—that are part of the object model and that you have to account for and manage when it comes to using those objects in a data-binding scenario.

For the purposes of demonstration, this next example expands the Customer definition to include a couple of other related objects.

class Order
{
    private int m_OrderID;
    private Customer m_Customer;
    private DateTime m_OrderDate;
    private Employee m_SalesPerson;

    public int OrderID
    {
        get { return m_OrderID; }
        set { m_OrderID = value; }
    }

    public Customer Customer
    {
        get { return m_Customer; }
        set { m_Customer = value; }
    }

    public DateTime OrderDate
    {
        get { return m_OrderDate; }
        set { m_OrderDate = value; }
    }

    public Employee SalesPerson
    {
        get { return m_SalesPerson; }
        set { m_SalesPerson = value; }
    }
}
class Employee
{
    private int m_EmployeeID;
    private string m_Name;

    public int EmployeeID
    {
        get { return m_EmployeeID; }
        set { m_EmployeeID = value; }
    }

    public string Name
    {
        get { return m_Name; }
        set { m_Name = value; }
    }
}

This code models the way that a relational store manages object relationships: the child objects hold references to the parent objects through properties, specifically, the Customer and SalesPerson properties on the Order class. However, depending on the way you plan to use the objects, it is usually much more natural in an object-oriented system to allow easy navigation from the parent object down to the child objects through a collection property. The following code adds the modification to the Customer class to allow this.

class Customer
{
   // other members
   private BindingList<Order> m_Orders;

   public BindingList<Order> Orders
   {
       get { return m_Orders; }
       set { m_Orders = value; }
   }
   // other properties...
}

The Orders property on the Customer class uses the member m_Orders (which is a BindingList<Order>, a generic strongly typed collection of Order references) to allow direct access to all the related orders from a customer object. This may or may not make sense based on the way your objects are going to be used. If you are going to use Customer objects in a number of contexts, and in some of those contexts orders aren’t relevant or wouldn’t be in scope, then having Orders as a property on your Customer class could be considered to be polluting your Customer class definition with details that don’t always belong there. However, if there are a number of use cases in your application where you need to iterate through the orders associated with a customer, or you need to perform data binding against them for presentation purposes, then this is probably exactly what you want.

So when it comes to business object definitions and data binding, the main thing that you are concerned with is the properties that they expose that represent the state or data contained in that entity. Those properties may represent single valued objects, such as numbers, strings, or dates, or they may represent references to other entities, single or multi-valued.

There could also be any number of methods and events exposed on the object related to the logic that is embedded that operates on the object state. Those methods aren’t usually directly involved in the data binding of that object to the UI, but there are several ways that they may be tied in. Properties exposed on a class can invoke any amount of functionality they need to in their get and set blocks—either other methods in the class, or even other methods on other objects that are held through member variables on the class. For example, you might call methods from the set blocks of your properties that check the values being set against business or validation rules before letting the property value change. Additionally, the other methods on the class can in general be invoked from anywhere at any time, and those methods can potentially alter the state of the object that is being used for data binding.

In a Windows Forms application, you usually want any changes in state in a data-bound object to be immediately updated and accessible in the application. So with data-bound business objects, you have to account for the fact that the contained data in the object could change at any time through mechanisms other than the data-bound UI, and you have to decide whether those changes should be immediately reflected in the bound controls. Once again, the ways these considerations manifest themselves is through the interfaces that the individual classes implement, as well as those that their containing collections implement. These will be discussed through the remaining sections in this chapter.

Defining and Working with Data-Bound Business Object Collections

When dealing with data, objects rarely exist as single instances of an object type in a system. Usually there are collections of objects of a particular type, and you often want to present those collections of objects together through a tabular interface or one where you can navigate through the objects to view them through the same interface and interact with them. To do that, you need some way to keep them all together and make them easily accessible within the same context. The way you do that is through an object type that contains a collection of other objects.

A good background in data structures is essential to becoming a good object-oriented systems programmer. If you aren’t comfortable with the differences and core concepts of lists, sets, stacks, queues, and hash tables, I suggest picking up a good data structures book. As you have seen from the other chapters, the main collection data structure you will deal with in data binding is the list. Some of the other collection types have other semantics, such as the key-value pairs of a hash table, or the order that things go onto and come off of stacks and queues. Ultimately they are all just sets of data objects in memory that have some ordering to them and that can be accessed in a variety of ways. Most of the other collection types have ways to access their contents (such as the Keys and Values properties on a Hashtable) as collections as well.

The DataSet type is itself just a fancy collection class. It is a class that contains a collection of data tables (of type DataTable). The data tables contain collections of rows (of type DataRow) and columns (of type DataColumn). The equivalent to a business object with a data set is an instance of a row, and that row could be untyped (a DataRow instance) or typed, such as a CustomersRow in a typed data set containing customer data. So from that perspective, a typed data set row is really just an autogenerated business object that contains no logic associated with the contained data. However, because typed data rows and tables are defined as partial classes in the autogenerated code in .NET 2.0, you could even add business logic to them as well.

For other types of business objects, though, you will need to come up with an appropriate collection class to contain those objects. You could use some of the .NET Framework collection classes that have been available since .NET 1.0. Better yet, you can use the generic collection classes that are being introduced in .NET 2.0. Finally, in some specialized situations, you might need to implement your own custom collection classes. When you work with the Framework collection classes, you don’t have to do much work at all, because in general they already implement most of the interfaces that you need for your collections to work nicely in a data-binding situation. But if you need to step into the deep end and implement completely custom collections, you will have a fair amount of work to do to implement the appropriate interfaces, depending on how rich the data-binding scenarios are that you want your class to support.

.NET Framework Generic Collection Classes

In .NET 1.1, you implemented custom collection classes using the CollectionBase class and its underlying ArrayList. This had some problems, because everything was stored and accessed as a raw Object reference under the covers, which incurs performance problems due to boxing and unboxing of value types and casting of any type. One of the most important new features at a Framework level in .NET 2.0 is the introduction of generics. You have already seen the use of generics in a number of samples in this book, and a full discussion of generics is beyond the scope of this book. However, the use of generic collection classes is core to building data-bound Windows Forms applications with custom business objects, because generic collections solve all of the problems discussed earlier in this chapter. (For a good understanding of generics and how they fit into other Framework programming capabilities, check out Programming .NET Components, second edition, by Juval Löwy.)

When you use generic collection classes, you effectively declare a new strongly typed collection class just by declaring an instance of the generic class with a parameterized type. For example, consider the following line of code:

List<Customer> m_Customers = new List<Customer>();

This line of code declares a member variable of type List<Customer> and creates an instance of that type. The generic type parameter is Customer, specifying that a new generic List type that contains Customer objects should be created. Generics are similar to C++ templates in syntax and concept, but the implementation is very different and more efficient. This simple declaration is equivalent in function, but far more efficient in storage and execution, to declaring a whole class such as that shown in Listing 9.1. It provides you with a type-safe collection class for containing any kind of object, just by declaring a new instance of the generic List<T> type with an appropriate type parameter (T) specifying the type of objects the collection is intended to hold. In fact, the List<T> class is more powerful than the ArrayList class in other ways. It includes advanced searching, sorting, and read-only options that you typically had to manually implement prior to .NET 2.0.

The List<T> type implements IList, ICollection, and IEnumerable for untyped data-binding support, and it implements generic versions of these interfaces as well (IList<T>, ICollection<T>, and IEnumerable<T>), giving you strongly typed access to the collection contents through interface-based programming. This lets you write more strongly typed and loosely coupled code to manipulate the contents of the collections programmatically through interface references, rather than programming directly against the specific collection type.

Because of the IList implementation on the List<T> type, you can easily data bind a collection of objects of any type by creating an instance of List<T> using the custom object type as the type parameter to the List<T> instance, as was shown earlier for the Customer type. Therefore, you can use an instance of a List<T> as the data source for binding to controls on a form, and you can also keep track of collections of child objects through a List<T> member on an object that contains references to the child objects.

Other generic collection classes have been added through the System.Collections.Generic namespace, including Dictionary<T>, Queue<T>, Stack<T>, and SortedDictionary<T>. An additional generic collection class that is extremely useful in data-binding scenarios is the BindingList<T> type, discussed later in this chapter. The BindingList<T> type should actually be your first choice for collections of objects that you intend to use for data binding.

The CustomBusinessObjects Example

Let’s look at a simple example. Say you have an application that needs to track customers and their related orders. You want to have a form that has two grids: one that shows a list of customers, and another that shows the list of orders associated with the selected customer in the first grid. This is a standard master-details kind of data-binding scenario, but now you want to support it with custom business objects instead of data sets. You first need to define the custom business object types that will contain the application’s data, as shown in Listing 9.1.

LISTING 9.1: Customer and Order Classes


public class Customer
{
   private int m_CustomerId;
   private string m_Name;
   private List<Order> m_Orders = new List<Order>();

   public int CustomerId
   {
      get { return m_CustomerId; }
      set { m_CustomerId = value; }
   }
   public string CustomerName
   {
      get { return m_Name; }
      set { m_Name = value; }

   }

   public List<Order> Orders
   {
      get { return m_Orders; }
   }
}

public class Order
{
   private int m_OrderId;
   private string m_ProductName;
   private Customer m_Customer;
   private DateTime m_OrderDate;

   public Customer Customer
   {
      get { return m_Customer; }
      set { m_Customer = value; }
   }

   public int OrderId
   {
      get { return m_OrderId; }
      set { m_OrderId = value; }
   }
   public string ProductName
   {
      get { return m_ProductName; }
      set { m_ProductName = value; }
   }

   public DateTime OrderDate
   {
      get { return m_OrderDate; }
      set { m_OrderDate = value; }
   }
}


In this case, Customer objects contain a CustomerName and a CustomerId property, as well as an Orders property that contains the collection of orders associated with the customer. Note that the collection property Orders on the Customer class is a read-only property. Typically, if your class encapsulates a collection, you will want to maintain the lifetime of that collection internally. By exposing the collection through a List<Order> reference in this case, users of the Customer class can easily access the Orders collection, add Order object references to it, and basically use any part of the public API of the List<T> class. However, because it is a read-only property on the Customers class, they cannot replace that collection with a new one (which might result in the unintentional loss of order information) or set the collection reference to null (which might violate certain assumptions the Customer class code might make about the underlying m_Orders variable always containing a live instance of a collection).

Order objects consist of an OrderId, an OrderDate, and a ProductName. They also contain a reference back to the Customer object to which they belong. Because you are dealing with object references, rather than copies of the data that they contain, there is nothing stopping you from maintaining references in both directions—from the parent to the children, and from the child to the parent as this example is doing. This tight coupling between the Customer and Order objects is often something you want to avoid, but it may make sense if you frequently need to navigate from the parent down to the children and from the children back to their parent. This is similar to the two-way navigation enabled by DataRelations in related tables of a data set.

To support master-details types of data binding, the collection of children has to show up on the parent objects, as is the case with this Customer object. The link back from the Order to the Customer is just done for demonstration purposes in this case, but it does have some effect on the data-binding process, as you will see shortly.

Binding the Customers and Orders Objects to Form Controls

To demonstrate the use of these object types with data-bound controls, I put together the form shown in Figure 9.1. I used the designer to add the controls and components, but the rest is wired up in code—there is no magic being done by the designer-generated code other than declaring the members for the controls and components you see in the designer in Figure 9.1.

Figure 9.1: CustomBusinessObjectClient Sample Application

CustomBusinessObjectClient Sample Application

The two grids, m_CustomersGrid and m_OrdersGrid, top and bottom repectively, are intended to show a list of customers in the top grid with the orders for the selected customer shown in the bottom grid. Two binding sources, m_CustomersBindingSource and m_OrdersBindingSource, will be used to hook up their respective grids.

The m_CustomersBindingSource will be bound directly to a collection of Customer objects, and more specifically, starting with a List<Customer> collection. The m_OrdersBindingSource will be bound to the m_CustomersBindingSource as its DataSource property, with DataMember set to Orders—the Orders property on the Customer object, which is itself a List<Order> and therefore implements the IList interface, making it a candidate for data binding to a table.

To hook all this up and bind to some sample data, the form code looks like Listing 9.2.

Listing 9.2: CustomBusinessObjectsClient Form Class


partial class Form1 : Form
{
   public Form1()
   {
      InitializeComponent();
      m_CustomersGrid.AutoGenerateColumns = true;
      m_OrdersGrid.AutoGenerateColumns = true;

      // Set up grid data binding
      m_CustomersGrid.DataSource = m_CustomersBindingSource;
      m_OrdersGrid.DataSource = m_OrdersBindingSource;
   }

   private void OnGetList(object sender, EventArgs e)
   {
      m_CustomersBindingSource.DataSource =
         TestDataGenerator.GetTestCustomers();
      // Set up expected relations between connectors
      m_OrdersBindingSource.DataSource = m_CustomersBindingSource;
      m_OrdersBindingSource.DataMember = "Orders";
   }
}


The constructor starts by setting each grid’s DataSource to its respective binding source and then setting the AutoGenerateColumns to true. The rest of the data-binding initialization has to be deferred until the actual data is populated, because you cannot set up master-details type relations or refer to data members within a data source until that data member has been populated.

The OnGetList method is called based on a button-click from the form, and goes out to a test class within the project containing the Customer and Order object definitions shown earlier. It sets the data source of the m_CustomersBindingSource to the list of customers that is returned, then sets the child m_OrdersBindingSource data source and data member to set up the master-details data binding. This results in the data-binding graph shown in Figure 9.2.

Figure 9.2: CustomBusinessObjectsClient Data-Binding Relationships

CustomBusinessObjectsClient Data-Binding Relationships

In Figure 9.2, each grid is bound to its own binding source with its DataSource property pointing to that component. The customers binding source is the one that is actually bound to the data source, by setting its DataSource property to the List<Customer> returned from the GetTestCustomers method. Its DataMember is left set to null. The orders binding source has its DataSource set to the customers’ binding source and its DataMember set to Orders. This sets up the master-details binding so that it will only show the related collection of orders for whichever Customer object is the current object in the collection maintained by its parent binding source, as described in Chapters 3 and 4.

Generating Some Test Data to Bind Against

The GetTestCustomers static method on the TestDataGenerator static class that provides the runtime sample data is shown in Listing 9.3.

LISTING 9.3: GetTestCustomers Data Generation Method


public static List<Customer> GetTestCustomers()
{
   List<Customer> results = new List<Customer>();
   Customer c = new Customer();
   c.CustomerId = 1;
   c.CustomerName = "Barney's Biscuits";
   results.Add(c);

   Order o = new Order();
   o.Customer = c;
   o.OrderDate = new DateTime(2004, 12, 22);
   o.OrderId = 1;
   o.ProductName = "12 Pack Lambchops";
   c.Orders.Add(o);

   o = new Order();
   o.OrderId = 2;

   o.Customer = c;
   o.OrderDate = new DateTime(2004, 7, 6);
   o.ProductName = "2000 yards cellophane";
   c.Orders.Add(o);

   c = new Customer();
   c.CustomerName = "Fred's Fritters";
   c.CustomerId = 2;
   results.Add(c);

   o = new Order();
   o.Customer = c;
   o.OrderDate = new DateTime(2004, 9, 8);
   o.OrderId = 3;
   o.ProductName = "Deep fryer fat";
   c.Orders.Add(o);
   return results;
}


The GetTestCustomers method is a simple test data generation method, which creates a List<Customer> collection and then populates it with two Customer objects, each of which has some associated Order objects. Notice that the way client code uses the Orders collection on the Customer class is to just call methods on the List<T> class, such as Add, by referencing the Orders property itself and calling the methods on it. Also notice that the code sets the back reference from the Order object to its parent Customer object simply by setting the Customer property on the Order object to the instance of the Customer that is being created. Once the list has been constructed, the list is returned to the calling form and can be used for data binding.

If you code all this up, or download the CustomBusinessObjects sample from the book’s download site, and run it, you should see the data populating both grids. Whatever the current customer is in the top grid should drive which orders are shown in the bottom grid, and the text boxes at the bottom should continue to show the current customer’s properties, as shown in Figure 9.3.

Figure 9.3: CustomBusinessObjectsClient in Action

CustomBusinessObjectsClient in Action

Setting the Textual Data-Binding Behavior of Custom Objects

One thing that you will notice right off the bat is that if a single-valued property on a class is a reference type, such as the Customer property in the Order class, it will show up in the grid by default since this example autogenerates columns. Unfortunately, the values that show up there are not particularly meaningful, because what happens is that the grid calls the ToString method on those objects to get a string value for presentation in the grid cell as part of the formatting process. The same thing would happen if you bound that property for display in a ComboBox, ListBox, or TextBox control. The default ToString method inherited from the System.Object base class returns the fully qualified type name as a string, which probably isn’t what you want to display.

You could handle this by not autogenerating columns and skipping the display of the Customer property in the grid. But depending on the situation, you may want the Customer property to be able to be used for data-binding scenarios, in which case you would want it to display something meaningful when it is used for binding to a textual control. The way to do this is to simply override the ToString method in your custom business object. In this example, this means adding a ToString override in the Customer class, which you can implement by simply returning the customer name:

public override string ToString()
{
   return m_Name;
}

Now when you run the sample, the customer name for the parent customer will show up in the Customer column of the orders grid, instead of just the fully qualified type name repeating in every cell of that column.

Supporting Transacted Object Editing with IEditableObject

If you ran the CustomBusinessObjects sample from the last section, and edited a property on either a Customer or Order object in the grid, then tabbed into the next field, the edit to that property is immediately written to the underlying bound object property. If there are any other controls that are data bound to the same object and displaying it, you should see their display immediately update to the new value as well. This is because the change was made through a data-bound control, which notifies the currency manager controlling the data binding of the change, and that change will then be rippled to any other bound controls that are in communication with the same currency manager.

This behavior may or may not be what you want. Sometimes when you are editing an object, you want changes to that object to be treated in a transacted way, with all changes to properties of a single object being done all together or not at all. Say you have a custom object with three properties bound to controls on a form. You edit properties one and two of the object, and then something goes wrong while editing property three. Do you want the changes to properties one and two to remain in effect, or do you want them to revert to what they were before the editing operation on that object was initiated? Or perhaps you have interdependencies between properties, where the first property can only be between 1 and 10 if it is Tuesday, but otherwise must be between 10 and 20. If the second property tells you what day it is, you won’t know whether you have a valid combination of row values until they have all been edited. In a transacted world, the changes to those properties could be rolled back to their previous values automatically if something went wrong or if you decided you wanted to abort the object editing process.

The DataRowView class has built-in support for this kind of thing, as does the DataGridView. If you have a data set bound to a grid and start editing the fields in a row and then press the Esc key, the changes you made to that row will be rolled back, changing the values back to their original values before the editing of that row commenced. If you shift the focus to a new row with the arrow keys, tab off the last field in a row, press the Enter key, or click on a different control or row in the form, the changes are accepted (committed), and you will no longer have the opportunity to revert them to their previous values.

Likewise, if your grid is set up to let users add new rows, and they commence entering values for a new row, those entered values have to go somewhere, and the grid is only displaying rows that are actually part of the bound table. So once you start editing fields within the empty row at the bottom of a grid that is marked with an asterisk in the row header cell, a new DataRow is actually added to the table and you are editing the fields in that row as you edit the row in the grid. But if you press the Esc key during the editing of the new row, the grid is able to remove the row from the table and pretend that it never existed. This is another form of transacted object editing, where a new object isn’t considered to be fully part of the collection until the initial editing of that newly created object is complete. However, for data-binding display purposes, you usually need that object to physically be added to the collection before the editing is complete, so that the entered field or property values have an object in memory on which they can be set and which can be displayed by the normal data-binding mechanisms.

To support this kind of transacted editing of your objects in a form, you need to implement the IEditableObject interface on the custom object definition. This interface includes three methods that you need to implement on your class: BeginEdit, EndEdit, and CancelEdit. As you might expect, from a transaction perspective, these operations logically correspond to beginning a transaction, committing the transaction, and rolling back the transaction.

•    BeginEdit should be called by a data-bound control when an editing operation is commenced against a single instance of an object.

•    EndEdit should be called when the editing process is complete.

•    If CancelEdit is called anytime between BeginEdit and EndEdit, any changes made since BeginEdit was called should be rolled back.

•    CancelEdit shouldn’t be called after EndEdit unless BeginEdit is called again to initiate a new transaction.

What you typically need to do inside the implementation of the interface methods is to cache changes to the object using some temporary variables to keep track of the property’s original values before the editing operation commenced from the BeginEdit method. Then if editing is completed normally with a call to EndEdit, you can discard the cached original values and treat the current values of the properties as permanent. If CancelEdit is called, you should use the cached original values to reset the values of the object properties to their original values.

To do this on your simple Customer object type requires the following additions to the class:

public class Customer : IEditableObject
{
   private int m_OldCustomerId;
   private string m_OldName;
   private bool m_Editing;
   // other fields, properties, and methods ...

   public void BeginEdit()
   {
      if (!m_Editing)
      {
         m_OldCustomerId = m_CustomerId;
         m_OldName = m_Name;
      }
      m_Editing = true;
   }

   public void CancelEdit()
   {   

      if (m_Editing)
      {

         CustomerId = m_OldCustomerId;
         CustomerName = m_OldName;
      }
      m_Editing = false;
   }

   public void EndEdit()
   {
      m_Editing = false;
   }
}

First, you add the IEditableObject interface to your class definition. You then need somewhere to stuff the cached original values of properties when an edit operation is in progress. To keep the sample simple, this just caches the values of the object’s name and ID properties in additional private fields on the class named m_OldCustomerId and m_OldName. For a more complex object with many fields, having two of every field would get cluttered, so you could also simplify this process by creating a new instance of your custom object type, populating its fields with the original values, and holding a private reference to that object as the original values to revert to if CancelEdit is called. Note that this sets the current values through the properties, rather than directly on the member variables. The reason for this will become apparent in the next section.

As you can see from the code, the BeginEdit method stores the current values of the fields in the cached fields and sets a flag to indicate that you are in edit mode. This is necessary because depending on how data binding is hooked up, BeginEdit will likely be called multiple times by the form, and you want to store the old values just when the edit operation really begins, not repeatedly as the rest of the editing process gets going.

EndEdit effectively accepts the current values by setting the editing flag back to false. CancelEdit rolls back the changes by setting the current property values back to the original value using the cached values.

By implementing IEditableObject like this, your custom business objects will now behave like a data set when being edited in a grid like the DataGridView. Specifically, if several properties of a given row have been edited, and the user presses the Esc key, the changes will be rolled back and the original values will be reset on the object.

This simple implementation doesn’t account for the fact that conceptually the Orders collection could also be changed as part of the logical editing of a Customer object. You would want to be able to roll back those changes as well since they are child objects, and conceptually they are “part of” a Customer object. However, since the IEditableObject interface is primarily used in a data-binding context, you would have to switch the focus to the Orders grid to modify the Orders collection associated with the selected Customer, and that change of control focus is going to call EndEdit on the Customer object anyway. There may be more advanced scenarios where you might want to use the IEditableObject implementation to support fully transacted editing of an object, whether through a bound control or through programmatic edits, but that isn’t really what it was designed for.

Supporting Object Edit Notifications with Property Change Events

Another problem people frequently encounter when first trying to use collections of objects as their data source is detecting changes in the bound data objects if those changes aren’t made directly through the bound control getting or setting properties on the object. When you change an object property by editing it through a bound control, the control will change the values through the property descriptors for the object. Containers for those objects, such as the binding source, can hook up a callback to be notified when the property values are changed through the property descriptor (as discussed in Chapter 7 and demonstrated later in this chapter). When this happens, other controls that are bound directly or indirectly to the same source can be updated by the container raising ListChanged events on the IBindingList interface implementation.

However, if an object property’s value changes behind the scenes due to other means, the values presented in a bound control won’t automatically be updated. For example, you could have a background thread that is continuously updating the objects based on some activity that it is monitoring, or you could have another control in the form, such as a button, invoke some code that modifies an object in memory directly.

For example, if you add a button to your form in the CustomBusinessObjectsClient example, and call the following code from that button’s Click event handler, the first object in the customers list collection would have its CustomerName property changed to IDesign. However, the customers grid would not immediately update with the changed value. You would have to perform some interaction with the grid or form to make it refresh its data binding to see the changed customer value.

private void button1_Click(object sender, EventArgs e)
{
   List<Customer> custs = m_CustomersBindingSource.DataSource
      as List<Customer>;
   custs[0].CustomerName = "IDesign";
}

One way to address this problem is to implement property change events on the Customer object (or any object that you plan to support data binding against). Property change events are events that you define on an object that notify anyone who is interested any time a particular property on the object changes. For the data-binding mechanisms in Windows Forms, the expected naming convention for these events is to publish a <propertyname>Changed event for each property, where <propertyname> is replaced with the name of the property in question.

For your Customer object and your limited support for changes against the CustomerName and CustomerId properties, this means defining two events on the class: CustomerNameChanged and CustomerIdChanged. These events need to be of type EventHandler and should be fired whenever the corresponding properties change. This can easily be done from the set blocks of the properties themselves, as shown in the following modified code for the Customer class:

public class Customer : IEditableObject
{
   // other fields...
   public event EventHandler CustomerIdChanged;
   public event EventHandler CustomerNameChanged;

   public int CustomerId

   {
      get { return m_CustomerId; }
      set
      {
          m_CustomerId = value;
          if (CustomerIdChanged != null)
             CustomerIdChanged(this, EventArgs.Empty);
      }
   }

   public string CustomerName
   {
      get { return m_Name; }
      set
      {
         m_Name = value;
         if (CustomerNameChanged != null)
            CustomerNameChanged(this, EventArgs.Empty);
      }
    }

    // other methods and properties...
}

By making this simple change to the class, any bound controls will be automatically notified whenever an object’s properties change, whether it is due to editing those objects in a data-bound control or due to some other code execution that sets the properties in memory. Basically, the data-binding mechanisms detect the presence of these events and set up subscriptions to them so that the controls can refresh their values when the underlying data source’s values change. Note that if you write your own data-bound controls from scratch as discussed in the last chapter, you will have to take responsibility for performing these subscriptions yourself.

So with this code in place, if you press the button added earlier that directly modified properties on objects in the collection of customers, the grid would immediately reflect the change. You will also see that if you edit a cell in the customers grid, then press Esc to abort the change, the Customer field in the orders grid will also update based on that rollback, because the control will call CancelEdit. If you hadn’t done the rollback by setting the properties in the implementation of CancelEdit as mentioned earlier, then the property change events wouldn’t fire, and any other controls bound to the same objects wouldn’t be immediately updated. You could alternatively have fired the appropriate property change events from CancelEdit instead, but that would result in more code duplication than just setting the properties in CancelEdit, which call whatever related behavior is needed through the property set block.

Supporting Object Edit Notifications with INotifyPropertyChanged

Adding a separate event for each property on an object can significantly clutter up your object. In .NET 1.1 and 1.0, this was the only way that you could ensure that bound controls would be updated when the underlying object properties changed. However, a more elegant approach has been introduced in .NET 2.0 with the INotifyPropertyChanged interface. This interface has a single member, an event named PropertyChanged. If you implement this interface on your custom business objects and then fire the event whenever any property changes, the net effect will be the same as implementing a separate event for each property. The BindingSource and Binding classes will look to see whether an object type implements this interface when the data binding is set up through one of these object types, and if so, they will update any bound controls if a property changes. Additionally, if the object is contained in a BindingList<T> collection, the collection will raise a ListChanged event if any of its contained objects raises a PropertyChanged event.

Implementing this interface on your Customer object from the earlier examples changes the class code as follows:

public class Customer : IEditableObject, INotifyPropertyChange
{
   public event PropertyChangedEventHandler PropertyChanged;

   public int CustomerId
   {

      get { return m_CustomerId; }
      set
      {
         m_CustomerId = value;
         FirePropertyChangedNotification("CustomerId");
      }
   }

   public string CustomerName
   {

      get { return m_Name; }
      set
      {
         m_Name = value;
         FirePropertyChangedNotification("CustomerName");
      }
   }

   private void FirePropertyChangedNotification(string propName)
   {
      if (PropertyChanged != null)
         PropertyChanged(this,
            new PropertyChangedEventArgs(propName));
   }
   // other members...
}

With this code in place, each time someone programmatically sets a property on the custom object, the PropertyChanged event will be fired. Containers can subscribe to the PropertyChanged event on those objects so that they will be notified and can refresh the display or bubble up ListChanged events when the bound object’s properties change.

Using BindingList<T> to Create Rich Object Collections

So far we have been using the List<T> class for many of the custom business object collections in data-binding scenarios. And for many scenarios, creating a customer collection type using the List<T> generic type may be all you need. However, when you are doing data binding in Windows Forms, the BindingList<T> class is more powerful than the List<T> type, and should be the one you favor in general.

One of the biggest differences is that the BindingList<T> class takes care of raising ListChanged events any time objects from a collection are programmatically added or removed. If you make similar changes to a bound List<T> collection, bound controls will not update automatically, because they have no way of knowing that the contents of the list changed. So if you added the following code to a button Click event handler on the CustomBusinessObjects form, the customers grid wouldn’t update to show the new item in the list unless you interact with the form in a way that causes it to refresh the data binding, such as using the arrow keys to move down rows within the grid.

private void AddCustomer(object sender, EventArgs e)
{

   IList custs = m_CustomersBindingSource.DataSource as IList;
   Customer c = new Customer();
   c.CustomerName = "Brian";
   c.CustomerId = 99;
   custs.Add(c);
}

The BindingList<T> generic class is not only capable of fixing this problem, but also gives you more control over the data-bound collection and provides a way to support sorting and searching. For whatever type you supply as a type parameter, the BindingList<T> generic class provides a generic implementation of the IBindingList interface, which enables sorting, searching, and change notifications.

By simply declaring a type based on BindingList<T> instead of List<T>, you get automatic support for ListChanged events being fired by the collection whenever an item is added, removed, or replaced within the collection. For example, if you add the following method to the TestDataGenerator class to return the test data as a BindingList<Customer> instead of a List<Customer>, and modify the data-binding code to call this method instead of GetTestCustomers, then when you programmatically add an item to the collection as shown in the AddCustomers method, the grid will immediately update to show the new item in the collection.

public static BindingList<Customer> GetCustomersBindingList()
{
   List<Customer> coll = GetTestCustomers();
   BindingList<Customer> custs = new BindingList<Customer>(coll);
   return custs;
}

In addition to firing events for additions, removals, or replacements of items in the collection, the BindingList<T> class also supports transacted additions to the collection through the ICancelAddNew interface implementation. This interface defines two methods, EndNew and CancelNew. When AddNew is called on the IBindingList interface, a new item is created in the collection. If CancelNew is called, the item is removed from the collection. Once EndNew is called, the new item is considered a completed addition to the collection. It can always be removed later, but this lets a new object be created and incrementally initialized. However, if any part of the initialization process fails, this lets you rollback the addition to the collection. This is conceptually very similar to the IEditableObject interface implementation that was discussed in an earlier section, but it solves an implicit coupling that occurs with objects that implement IEditableObject and their container collections, as was described in Chapter 7.

The BindingList<T> class implementations for sorting and searching aren’t implemented, and the SupportsSearching and SupportsSorting properties return false. If you call the ApplySort or Find methods on an instance of a BindingList<T> type, they will throw exceptions of type NotSupportedException, because there isn’t a general-purpose way for the class to figure out what kind of algorithm it should use for sorting and searching properties of arbitrary types.

If you want to support searching and sorting, you will need to implement a custom type that derives from BindingList<T> with the appropriate type parameter. The base class will still do most of the work for you for containing the objects and supporting data binding; you just need to override a couple of methods to provide the actual sorting and searching algorithms. By implementing your own custom type, you can also take over the construction process when objects are dynamically added to the collection through a bound control, such as when a user starts typing into the row at the bottom of a DataGridView control that lets them add items to the collection, as you will see later in this chapter.

Creating a Custom Collection Type Based on BindingList<T>

Say you want to create a strongly typed collection of Customer objects that support searching and sorting, and that you want to take over the construction process of instances of Customers that get added to the collection through data-binding mechanisms. The first step in this process is to declare a class derived from BindingList<T> with the appropriate type parameter:

public class CustomerCollection : BindingList<Customer>
{
}

Taking Over the Construction Process

For a next step, let’s take over the process of adding new Customer objects to the collection when called by the data-binding mechanism. When you add a new row to a DataGridView, or call AddNew on a BindingSource, the AddNew method gets called on the IBindingList implementation of your collection class. The default implementation of AddNew on the BindingList<T> base class creates a new instance of your object type using its default constructor, places that into the collection, and passes a reference to that new object back to the caller.

You can take over the construction process and do whatever custom initialization you want. You might need to do this if the objects that will go in the collection don’t have a default constructor. For example, say that when each new Customer object is created, you want to initialize its CustomerId to a value that is one greater than the largest CustomerId currently in the collection (similar to an autoincrementing feature for an integer data set field). Also, you want to set the CustomerName to the prompt “<Enter Customer Name>”. To do this, you need to override the base class AddNewCore method.

The BindingList<T> class takes the approach of making the IBindingList methods and properties nonvirtual, and its implementation of the members call virtual members that you can override in your derived class. These virtual members have a naming convention of the name of the corresponding member on the base class, with Core appended, such as AddNewCore, ApplySortCore, and IsSortedCore.

The following is the AddNewCore implementation for the CustomersCollection class:

public class CustomerCollection : BindingList<Customer>
{
   public CustomerCollection() : base() {}

   public CustomerCollection(List<Customer> custs) : base(custs) {}

   protected override object AddNewCore()
   {
      Customer c = new Customer();
      c.CustomerId = FindMaxId() + 1;
      c.CustomerName = "<Enter Customer Name>";
      this.Add(c);
      return c;
   }

   private int FindMaxId()
   {
      int maxId = -1;
      foreach (Customer c in this)
      {
         if (c.CustomerId > maxId)
            maxId = c.CustomerId;
      }
      return maxId;
   }
}

This code declares a number of things:

•    It declares default and parameterized constructors that delegate to the base class constructors. This is done so that you can initialize an instance of the CustomerCollection class using a preexisting List<Customer>, modeling what the base class is capable of.

•    It defines the AddNewCore method, which performs the following:

–    It constructs an instance of a Customer object.

–    It calls a helper method to determine what the maximum CustomerId value is by iterating through the collection.

–    It uses the value returned from the helper method to set the new Customer object’s CustomerId to one greater than that maximum value.

–    It sets the CustomerName text to the prompt “<Enter Customer Name>”.

–    It adds the customer object to the collection.

–    It returns the new Customer object as an object reference, which will be handed back to the caller of the AddNew method on the base class.

•    It defines the FindMaxID helper method called by AddNewCore.

Getting Some Test Data to Work With

If you now add a method to the TestDataGenerator class to return a CustomerCollection:

public static CustomerCollection GetCustomerCollection()
{
   List<Customer> coll = GetTestCustomers();
   CustomerCollection custs = new CustomerCollection(coll);
   return custs;
}

you can call this method in the CustomBusinessObjectsClient form when the button is clicked to get the data, and bind to the returned CustomerCollection instead of directly to a List<T> or BindingList<T> as was done before. If you do so, as soon as you click in the new row of the Customers grid, you will see that the code has done its work, as shown in Figure 9.4.

Figure 9.4: AddNew Functionality in Play

AddNew Functionality in Play

A nice little side effect of this functionality is that the DataGridView is smart enough to look at the values of the fields in the new row, and if they haven’t changed since it was added, it will remove the row if you change the focus from it if no editing has been done. So even though you set the CustomerName to “<Enter Customer Name>” and calculated a new CustomerId, if users click off the row without having edited either of those fields, the grid will remove it. This is probably exactly what you want to happen if users haven’t filled in the prompted field, instead of having it become a valid new row. However, if this isn’t the behavior you want, you could handle events on the grid and call EndNew on the collection to get it to accept those values as valid.

Adding Search Functionality to the Collection

The next functionality we will add is also fairly simple—the ability to search the collection to find a particular Customer object. There are, of course, a lot of different algorithms that you could use for performing a search, and there are also a lot of different approaches that you could use for declaring a match. The Find method of the IBindingList interface is designed to let you search the collection for an object and specify which property on those objects to inspect. The method should return the object’s index in the collection when it finds one whose property value matches that of the key value passed into the Find method.

To provide search capability, you need to override both the SupportsSearchingCore property and the FindCore method. Instead of continuing with a specific collection class for customers, however, you want to provide a generic implementation for sorting and searching that can be reused for any kind of custom data object. To do this, you can define a new generic class called BindingListView<T> that derives from BindingList<T>. You will implement the sorting and searching functionality on this, and you will implement the IBindingListView interface on this later in this chapter. You can then derive the CustomersCollection class from BindingListView<T>, specifying Customer as the type T as was done earlier with the BindingList<Customer> base class. You can then start taking advantage of the added functionality in a generic way. To start building up that class, use this initial definition to support searching the collection:

public class BindingListView<T> : BindingList<T>{
   
   public BindingListView() : base() { }

   public BindingListView(List<T> list) : base(list) { }

   protected override bool SupportsSearchingCore
   {
      get { return true; }

   }

   protected override int FindCore(PropertyDescriptor property,
      object key)
   {
      for (int i = 0; i < Count; i++)
      {
         T item = this[i];
         if (property.GetValue(item).Equals(key))
         {
            return i;
         }
      }
      return -1; // Not found
   }
}

The BindingListView<T> class so far does the following:

1.   Defines the same two constructors as for the CustomerCollection earlier, a default constructor and a parameterized constructor that lets you initialize the list with an instance of List<T>.

2.   Overrides the SupportsSearchingCore method and returns true, indicating that you are adding that capability to the collection class.

3.   Overrides the FindCore method, which iterates over the collection, using the GetValue method of the PropertyDescriptor class to extract the value of the current object’s specified property, and compares it to the key value. If they match, the FindCore method returns the index of that object in the collection. If no match is found, it returns –1, a standard convention in .NET.

The CustomBusinessObjects download sample shows an alternative implementation for the FindCore method that uses the FindIndex method of the List<T> class. That approach uses generics and anonymous delegates, and it leaves the iteration up to the List<T> class implementation. Both approaches will work fine, though the simple iteration shown in this code is easier to understand. The List<T> implementation includes support for defining indexes, which would be more efficient for searching large collections.

Adding Sorting Capabilities to the Collection

Implementing sorting is a little more involved. First off, there are a number of base class methods and properties that you must override. The code and algorithm used for sorting is necessarily a little more complex as well. There are four properties you need to override: SupportsSortingCore, IsSortedCore, SortDirectionCore, and SortPropertyCore. There are two methods you also need to override: ApplySortCore and RemoveSortCore.

If you just needed to support sorting of the collection, things wouldn’t be too complicated. It is the ability to remove the sort that complicates things. When you sort the collection, you need to make it so that any code that uses the collection from that point forward sees the collection in its new order. That means that you have to actually change the contents of the collection to be in a different order, so that when any code iterates over the collection using its base IEnumerator interface implementation, it sees the sorted order. Because you have to support removing the sort, you also need a way to get the contents of the collection back into their original order.

The simplest way to do this is to create a copy of the collection in its unsorted state before you apply the sort, and then you can restore that original collection when the sort is removed. However, this too introduces another level of complexity—what happens if a new item is added or an item is removed while the collection is being sorted? That item will be added to the sorted collection, but unless you intercept every modification to the collection, the new item or the removed item won’t be reflected in the copy of the original collection that you are going to restore when you remove the sort.

To intercept the additions and removals, you need to override some more methods from the BindingList<T> base class, such as InsertItem, RemoveItem, and ClearItems, and make sure that the same changes get made to the unsorted collection as well as the primary collection. You also have to worry about transacted additions to the collection, so you will need overrides of EndNew and CancelNew, as well as transacted removals from the unsorted collection. As you can see, things can get complicated pretty quickly.

To demonstrate some basic sorting capability, let’s not support the addition or removal of items in the collection when it is sorted. You still have to make a copy of the existing collection before applying the sort and revert to that collection when the sort is removed, but you don’t have to worry about items being added or removed and maintaining the parallel collection while sorted.

The List<T> class has sorting capability built in, as mentioned before, and you can use that to do the actual sorting for you. It does the sort based on a generic implementation of the IComparer<T> interface that you provide to the method. To implement this interface, you create a class with a method named Compare that compares to objects of type T and returns an integer that indicates whether the first object is equal to, greater than, or less than the second object. What criteria you use to decide which value to return is up to you and your implementation of the interface.

The implementation in Listing 9.4 was inspired by the “Wonders of Windows Forms” column by Michael Weinhardt in MSDN Online, which itself was inspired by an earlier article by Rocky Lhotka. I chose a different implementation strategy and added capability for multiproperty sorts, as discussed later in the implementation of the IBindingListView interface, but some of the basic details of the following implementation were based on Michael’s Custom Data Binding series from the winter of 2004.

The SortComparer class shown in Listing 9.4 provides the basic comparison implementation to be used with the List<T>.Sort method.

LISTING 9.4: SortComparer Simple Implementation


class SortComparer<T> : IComparer<T>
 {
   private PropertyDescriptor m_PropDesc = null;
   private ListSortDirection m_Direction =
      ListSortDirection.Ascending;

   public SortComparer(PropertyDescriptor propDesc,
      ListSortDirection direction)
   {
      m_PropDesc = propDesc;
      m_Direction = direction;
   }

   int IComparer<T>.Compare(T x, T y)
   {    

      object xValue = m_PropDesc.GetValue(x);
      object yValue = m_PropDesc.GetValue(y);
      return CompareValues(xValue, yValue, m_Direction);
   }

   private int CompareValues(object xValue, object yValue,
      ListSortDirection direction)
   {

      int retValue = 0;
      if (xValue is IComparable) // Can ask the x value
      {
         retValue = ((IComparable)xValue).CompareTo(yValue);
      }
      else if (yValue is IComparable) //Can ask the y value
      {
         retValue = ((IComparable)yValue).CompareTo(xValue);
      }
      // not comparable, compare String representations
      else if (!xValue.Equals(yValue))
      {
         retValue = xValue.ToString().CompareTo(yValue.ToString());
      }
      if (direction == ListSortDirection.Ascending)
      {
         return retValue;
      }
      else
      {
         return retValue * -1;
      }
   }
}


The SortComparer class lets you construct an instance of the class and specify what property on the objects will be compared to determine equality, greater than, or less than values by passing in a PropertyDescriptor and a ListSortDirection enumeration value. When the List<T> class iterates over the list, applying its internal sort algorithm, it calls the Compare method, and passes in two objects that it is currently working with to set up the sort. The Compare implementation in Listing 9.4 tries a number of ways to compare the objects. It first extracts the specified property’s current value for each of the objects using the GetValue method on the property descriptor. It then sees if the object that those values represent implements the IComparer interface. This is a standard interface in the .NET Framework. It implements a CompareTo method that embeds the same logic that we are looking for—specifically, that if the object itself decides it is equal to the other object passed in, it will return 0; if it is greater, it will return 1; and if it is less than, it will return –1.

If the property values don’t implement IComparer, but the Equals method on the value says that it is equal to the other object, then Compare returns 0, indicating equality. Finally, if none of those tests succeed, the property values are converted to strings and their string representations are compared using the CompareTo method, which the String class implements. The final thing the method does is to look at the sort direction, and if it indicates that it should be a descending sort, it multiplies the returned value by –1 to reverse the greater than/less than meaning.

To perform the sort from your BindingListView<T> class, you need to provide the overrides of all the sort-related methods and properties from the base class. These are shown in Listing 9.5.

LISTING 9.5: Sorting Functionality on CustomerCollection Class


public class BindingListView<T> : BindingList<T>
 {
   private bool m_Sorted = false;
   private ListSortDirection m_SortDirection =
      ListSortDirection.Ascending;
   private PropertyDescriptor m_SortProperty = null;
   private List<T> m_OriginalCollection = new List<T>();

   // constructors, AddNew, etc... omitted

   protected override bool SupportsSearchingCore
   {
      get { return true; }
   }

   protected override bool SupportsSortingCore
   {
      get { return true; }
   }

   protected override bool IsSortedCore
   {
      get { return m_Sorted; }

   }

   protected override ListSortDirection SortDirectionCore
   {
      get { return m_SortDirection; }
   }

   protected override PropertyDescriptor SortPropertyCore
   {
      get { return m_SortProperty; }
   }

   protected override void ApplySortCore(PropertyDescriptor property,

      ListSortDirection direction)
   {
      m_SortDirection = direction;
      m_SortProperty = property;
      SortComparer<T> comparer = new
          SortComparer<T>(property,direction);
      ApplySortInternal(comparer);
  }

  private void ApplySortInternal(SortComparer<T> comparer)
  {
      // store the original order of the collection
      if (m_OriginalCollection.Count == 0)
      {
          m_OriginalCollection.AddRange(this);
      }

      List<T> listRef = this.Items as List<T>;
      if (listRef == null)
         return;

      // Let List<T> do the actual sorting based on your comparer
      listRef.Sort(comparer);
      m_Sorted = true;
      OnListChanged(new ListChangedEventArgs(
           ListChangedType.Reset, -1));
  }

  protected override void RemoveSortCore()
  {
     if (!m_Sorted)
        return;

     Clear();
     foreach (T item in m_OriginalCollection)
     {
        Add(item);

     }
        m_OriginalCollection.Clear();
        m_SortProperty = null;
        m_Sorted = false;
     }
}


The additions to the class include the following:

•    An override of IsSortedCore, which indicates whether the collection is currently sorted.

•    An override of SortDirectionCore, which provides the sort direction.

•    An override of the SortPropertyCore, which indicates which property the sort is based on.

•    An ApplySortCore method override, which sets the above properties when it is called by the base class in response to an IBindingList.ApplySort call. This method delegates to an ApplySortInternal helper method to do most of the work of the sorting.

•    A RemoveSortCore method override that resets the properties appropriately when it is called by the base class in response to an IBindingList.RemoveSort call.

•    A member variable of type List<T> is declared to hold the unsorted collection when a sort is applied.

The ApplySortCore method creates a SortComparer object for the specified sort property and direction, and passes that to the helper ApplySortInternal method. The ApplySortInternal method checks the unsorted collection to see if it has been populated, and if not, adds all the current values to it in the order that they exist in the collection at the time that ApplySort is called. The check for the list being populated is in case the list is sorted several times in succession. ApplySortInternal then casts the Items collection that your class inherited to a List<T> reference and calls the Sort method on it, passing in the SortComparer object. This causes the items in the collection maintained by the base class to be sorted according to the criteria provided to the SortComparer class and the comparison logic implemented in that class. Finally, the ApplySortInternal method sets the flag that indicates that the collection is sorted and fires an event through a call to the base class OnListChanged method indicating that the list has changed. The Reset type of change is most appropriate, since potentially every item in the list has been moved around.

The RemoveSortCore method clears the current collection again and refills it with the values that were stored in the original collection list. It then clears the original collection list and sets the member variables used by the algorithm appropriately to indicate that no sort is currently applied.

To make the list so that items cannot be changed when the collection is sorted, you need to add overridden implementations of the appropriate IBindingList methods:

bool IBindingList.AllowNew
{
   get
   {
      return CheckReadOnly();
   }
}

bool IBindingList.AllowRemove
{
   get
   {
      return CheckReadOnly();
   }
}

private bool CheckReadOnly()
 {
   if (m_Sorted || m_Filtered)
   {
      return false;
   }
   else
   {
      return true;
   }
}

This code conditionally returns a different value for AllowAdd and AllowRemove based on whether the collection is sorted or filtered (the filtering support is added later in this chapter).

With that code in place, you can now change your CustomersCollection derivation, as well as the Orders collection contained in the Customer class. Now both the CustomersCollection (bound to the first grid) and the child collection named Orders on each Customer object (bound to the second grid) will support sorting:

public class CustomerCollection : BindingListView<Customer>
 {
  // AddNew implementation shown earlier
 }

public class Customer : IEditableObject, INotifyPropertyChanged
{
     private BindingListView<Order> m_Orders =
        new BindingListView<Order>();
     // The rest of the Order class implementation
}

Managing Transacted Additions to a Collection

If you are supporting transacted editing of an object, you will usually also want to support transacted addition of the object type to whatever collection it belongs to. As mentioned before, if you are using the BindingList<T> class for your custom collections, you already get transacted additions for free through the combination of the AddNew method and the implementation of the ICancelAddNew interface methods EndNew and CancelNew. However, if the control that you are bound to doesn’t know to call those methods—and many controls may not, since this is a new interface in .NET 2.0—you may still be able to support transacted editing through the IEditableObject interface implementation.

To do this, you need to introduce some coupling between your custom collection class that contains a specific object type and the object type itself. The modification requires that each Customer object hold a reference to its containing collection, so it can remove itself when CancelEdit is called if it knows that EndEdit hasn’t been called yet on this instance of the object. You need two more member variables on the Customer class to support this: another Boolean flag to indicate when an object is “new,” meaning that it hasn’t had EndEdit called since it was created, and a reference to the parent collection instance. You then need to modify CancelEdit and EndEdit to use the flag and the reference to remove the object from the parent collection if CancelEdit is called before EndEdit on any new object. The following code shows the modifications needed to set up this relationship between the collection and the contained object.

public class Customer : IEditableObject, INotifyPropertyChanged
 {
   // other member variables...
   internal CustomerCollection m_ParentList;
   private bool m_NewObject = true;

   // other member properties and methods...

   public void CancelEdit()
    {
      if (m_Editing)
      {
         CustomerId = m_OldCustomerId;
         CustomerName = m_OldName;
      }
      if (m_NewObject && m_ParentList != null)
      {
        m_ParentList.Remove(this);
      }
      m_Editing = false;
}

public void EndEdit()
 {
     m_Editing = false;
     m_NewObject = false;
 }
}

The m_NewObject flag is set to true when an object is constructed. The only code that uses this is the IEditableObject methods CancelEdit and EndEdit. EndEdit sets the flag to false, meaning the object has been accepted by the bound control. The CancelEdit method has code added to check whether the current object is a new object. If the object is in a new state and has a valid reference to a parent collection, the object removes itself from the collection if CancelEdit is called.

The other modification that is required to complete this functionality is to add code to the AddNewCore method in the CustomerCollection class to set that parent reference in the object that it creates:

protected override object AddNewCore()
 {
   Customer c = new Customer();
   c.CustomerId = FindMaxId() + 1;
   c.CustomerName = "<Enter Customer Name>";
   c.m_ParentList = this;
   this.Add(c);
   return c;
 }

Now the Customer objects will remove themselves from the parent collection if added through a bound control, and that bound control calls CancelEdit before calling EndEdit. The code works fine even in combination with the ICancelAddNew interface implementation, which also ensures that objects are removed from the collection if the addition wasn’t accepted through a call to EndAdd.

Raising Item Changed Events

One other interface is implemented on the BindingList<T> class that you might want to override for advanced scenarios in your custom collection classes: the IRaiseItemChangedEvents interface. This interface has a single Boolean property, RaiseItemChangedEvents, defined on it. In the BindingList<T> class implementation, this property returns false, but you can override it in a derived class if desired.

If you return true to indicate that you do raise item changed events, it is expected that you will raise ListChanged events when the items in your collection change. If the items in your collection implement the INotifyPropertyChanged interface as described earlier, this will happen automatically. But there is a way that you can continue to support item changed events even if the objects in your collection don’t support INotifyPropertyChanged. However, you should be judicious about implementing the IRaiseItemChangedEvents interface: it can cause a significant performance hit for large collections, because you have to reflect on each object as it is added to your collection.

To implement the IRaiseItemChangedEvents interface, you need to provide the property descriptor a callback delegate for each property on the objects that are in your collection. Your callback will be invoked when the property value is set through the descriptor. Strangely enough, this isn’t exposed as an event that you can explicitly subscribe to; you have to call the AddValueChanged and RemoveValueChanged methods, passing in a delegate to the method to call back. Calling AddValueChanged makes the method that the delegate points to be called whenever the value of an object’s property is changed through its property descriptor’s SetValue method, which is how data-bound controls edit the data source. This won’t do anything for you if some code obtains a reference to the object and directly sets the property through its setter on the property definition. However, the callback will invoke the target method any time a property is changed through a data-bound control, because that is how data-bound controls set properties, since they don’t have compile-time information about the objects to which they are bound. The following code shows the additional methods added to the BindingListView<T> class to support item changed notifications.

 protected override void InsertItem(int index, T item)
 {
    foreach (PropertyDescriptor propDesc in
 TypeDescriptor.GetProperties(item))
   {
     if (propDesc.SupportsChangeEvents)
     {
         propDesc.AddValueChanged(item, OnItemChanged);
     }
 }
 base.InsertItem(index, item);
}

protected override void RemoveItem(int index)
{
   T item = Items[index];
   PropertyDescriptorCollection propDescs =
      TypeDescriptor.GetProperties(item)

foreach (PropertyDescriptor propDesc in propDescs)
 {
  if (propDesc.SupportsChangeEvents)
  {
     propDesc.RemoveValueChanged(item,OnItemChanged);
  }
 }
 base.RemoveItem(index);
}

void OnItemChanged(object sender, EventArgs args)
{
    int index = Items.IndexOf((T)sender);
    OnListChanged(new ListChangedEventArgs(
       ListChangedType.ItemChanged, index));
}

The BindingList<T> class already implements IRaiseItemChangedEvents, but returns false indicating no support. To indicate that you have added support, you need to reimplement the interface and return true from the RaisesItemChangedEvents property:

public class BindingListView<T> : BindingList<T>, IBindingListView,
   IRaiseItemChangedEvents
{
   // other members...
   bool IRaiseItemChangedEvents.RaisesItemChangedEvents
   {
     get { return true; }
   }
}

Adding IBindingListView Functionality

If you remember from Chapter 7, there is a level of data-binding functionality defined through the IBindingListView interface that you can support to make your collections even richer. The IBindingListView specifically adds the ability to perform sorting on more than one property at a time, and to filter the collection based on some filter expression to only show parts of the underlying collection at a time.

The IBindingListView interface defines four additional properties and two methods you will need to implement on your collection to fully support this interface.

•    The SupportsAdvancedSorting and SupportsFiltering Boolean properties indicate which of the two capabilities you support.

•    The SortDescriptions property returns a ListSortDescriptionCollection that contains whatever sort criteria is currently in effect. Each ListSortDescription object within that collection is just a pair associating a PropertyDescriptor and a ListSortDirection for each of the properties on which sorting is applied.

•    The Filter property supports getting and setting a string filter expression that you are using to tailor the collection contents that are presented to anyone using the collection.

•    The ApplySort method is similar to the one defined on the IBindingList interface, except that it takes a ListSortDescriptionCollection as a parameter instead of a single PropertyDescriptor and ListSortDirection. Each ListSortDescription in that collection contains a property descriptor and a sort direction, which enable you to sort on each criteria in turn.

•    The RemoveFilter method removes whatever filter is in effect and restores the collection to its full contents.

Although this sounds straightforward, implementing this interface is no trivial matter. Sorting on multiple properties actually is fairly easy to implement, but requires some extensions to the SortComparer<T> class and sorting logic presented earlier. Filtering can be done in a number of ways, but this further complicates matters if you want to allow additions and removals from the collection while it is in the filtered state (for the same reasons that those operations present difficulties when sorted, as discussed earlier). With a filtered list, your collection could also potentially be sorted, so you need to be able to get back to the original, possibly altered, list from sorted/unfiltered, filtered/unsorted, and filtered/sorted states.

To show a reasonable implementation that you can reuse if you can live with not adding and removing items from the list when it is sorted or filtered, I enhanced the BindingListView<T> class to provide an implementation of IBindingListView. The first step is to enhance the SortComparer<T> to support multiproperty comparisons. The full class listing is shown in Listing 9.6.

LISTING 9.6: SortComparer Class


class SortComparer<T> : IComparer<T>
{
   private ListSortDescriptionCollection m_SortCollection = null;
   private PropertyDescriptor m_PropDesc = null;
   private ListSortDirection m_Direction =
      ListSortDirection.Ascending;

   public SortComparer(PropertyDescriptor propDesc,
      ListSortDirection direction)
  {
      m_PropDesc = propDesc;
      m_Direction = direction;
  }

  public SortComparer(ListSortDescriptionCollection sortCollection)
  {
  m_SortCollection = sortCollection;
  }

  int IComparer<T>.Compare(T x, T y)
  {
      if (m_PropDesc != null) // Simple sort
      {
       object xValue = m_PropDesc.GetValue(x);
       object yValue = m_PropDesc.GetValue(y);
       return CompareValues(xValue, yValue, m_Direction);
      }
      else if (m_SortCollection != null &&
          m_SortCollection.Count > 0)
      {
          return RecursiveCompareInternal(x,y, 0);
      }
      else return 0;
}

private int CompareValues(object xValue, object yValue,
   ListSortDirection direction)

{

   int retValue = 0;
   if (xValue is IComparable) // Can ask the x value
   {
      retValue = ((IComparable)xValue).CompareTo(yValue);
   }
   else if (yValue is IComparable) //Can ask the y value
   {
      retValue = ((IComparable)yValue).CompareTo(xValue);
   }
   // not comparable, compare String representations
   else if (!xValue.Equals(yValue))
   {
     retValue = xValue.ToString().CompareTo(yValue.ToString());
   }
   if (direction == ListSortDirection.Ascending)
   {
      return retValue;
   }
   else
   {
      return retValue * -1;
   }
}

private int RecursiveCompareInternal(T x, T y, int index)
{
  if (index >= m_SortCollection.Count)
    return 0; // termination condition

  ListSortDescription listSortDesc = m_SortCollection[index];
  object xValue = listSortDesc.PropertyDescriptor.GetValue(x);
  object yValue = listSortDesc.PropertyDescriptor.GetValue(y);

  int retValue = CompareValues(xValue,
     yValue,listSortDesc.SortDirection);
  if (retValue == 0)
  {
     return RecursiveCompareInternal(x,y,++index);
  }
  else
  {
     return retValue;
  }
}
}


The additions to what was shown in Listing 9.4 are in bold.

•    A new parameterized constructor is added that lets you create a SortComparer<T> with a ListSortDescriptionCollection instead of a single property descriptor and sort direction.

•    Depending on which constructor was used, the Compare method calls either the CompareValues method directly as shown in Listing 9.4, or calls the new RecursiveCompareInternal method, which recursively compares the two values, starting with the first property specified in the ListSortDescriptionCollection.

•    If that property is equal between the two objects, then it proceeds to the next property, and then the next one, until all properties have been compared or a difference has been found.

With this in place, you can make the additions to the BindingListView<T> class to support multiproperty sorting. Filtering is completely separate, but it is also implemented in this version. For simplicity, the filtering code only supports filtering on a single property at a time, based on the string representation of that property’s value, and uses quotes to delimit the value that is being filtered. A filter expression that is supported would look something like:

IBindingListView listView = m_OrdersCollection as IBindingListView;
if (listView == null)
   return;

listView.Filter = "ProductName='Deep fryer fat'";

The full BindingListView<T> class is shown in Listing 9.7.

LISTING 9.7: BindingListView <T> Class


public class BindingListView<T> : BindingList<T>, IBindingListView,
   IRaiseItemChangedEvents
{
   private bool m_Sorted = false;
   private bool m_Filtered = false;
   private string m_FilterString = null;
   private ListSortDirection m_SortDirection =

      ListSortDirection.Ascending;
   private PropertyDescriptor m_SortProperty = null;
   private ListSortDescriptionCollection m_SortDescriptions =
    new ListSortDescriptionCollection();
   private List<T> m_OriginalCollection = new List<T>();

public BindingListView() : base()
{
}

public BindingListView(List<T> list) : base(list)
{
}

protected override bool SupportsSearchingCore
{
   get { return true; }
}

protected override int FindCore(PropertyDescriptor property,
object key)
{
  // Simple iteration:
 for (int i = 0; i < Count; i++)
 {
   T item = this[i];
   if (property.GetValue(item).Equals(key))
   {
      return i;
   }
}
return -1; // Not found

// Alternative search implementation
// using List.FindIndex:
//Predicate<T> pred = delegate(T item)
//{
//     if (property.GetValue(item).Equals(key))
//         return true;
//      else
//          return false;
//};
//List<T> list = Items as List<T>;
//if (list == null)
//          return -1;
//return list.FindIndex(pred);
}

protected override bool SupportsSortingCore
 {

  get { return true; }
}
protected override bool IsSortedCore
{
  get { return m_Sorted; }
}

protected override ListSortDirection SortDirectionCore
{
  get { return m_SortDirection; }
}

protected override PropertyDescriptor SortPropertyCore
{
  get { return m_SortProperty; }
}

protected override void ApplySortCore(PropertyDescriptor property,
  ListSortDirection direction)
{
  m_SortDirection = direction;
  m_SortProperty = property;
  SortComparer<T> comparer =
     new SortComparer<T>(property,direction);
  ApplySortInternal(comparer);
}

private void ApplySortInternal(SortComparer<T> comparer)
{
  if (m_OriginalCollection.Count == 0)
  {
      m_OriginalCollection.AddRange(this);
  }
  List<T> listRef = this.Items as List<T>;
  if (listRef == null)
     return;
  listRef.Sort(comparer);
  m_Sorted = true;
  OnListChanged(new ListChangedEventArgs(
    ListChangedType.Reset, -1));
}

protected override void RemoveSortCore()
{
  if (!m_Sorted)
     return;

  Clear();
  foreach (T item in m_OriginalCollection)
  {
     Add(item);
  }
  m_OriginalCollection.Clear();
  m_SortProperty = null;
  m_SortDescriptions = null;
  m_Sorted = false;
}

void IBindingListView.ApplySort(ListSortDescriptionCollection sorts)
{
 m_SortProperty = null;
 m_SortDescriptions = sorts;
 SortComparer<T> comparer = new SortComparer<T>(sorts);
 ApplySortInternal(comparer);
}

string IBindingListView.Filter
{
 get
 {
   return m_FilterString;
 }
set
 {
  m_FilterString = value;
  m_Filtered = true;
  UpdateFilter();
 }
}

void IBindingListView.RemoveFilter()
{
   if (!m_Filtered)
      return;
   m_FilterString = null;
   m_Filtered = false;
   m_Sorted = false;
   m_SortDescriptions = null;
   m_SortProperty = null;
   Clear();
   foreach (T item in m_OriginalCollection)
   {
     Add(item);
   }
   m_OriginalCollection.Clear();
}

ListSortDescriptionCollection IBindingListView.SortDescriptions
{
 get
 {
   return m_SortDescriptions;
 }
}

bool IBindingListView.SupportsAdvancedSorting
{
 get
 {
   return true;
 }
}

bool IBindingListView.SupportsFiltering
{
 get
 {
   return true;
 }
}

protected virtual void UpdateFilter()
{
  int equalsPos = m_FilterString.IndexOf('='),
  // Get property name
  string propName = m_FilterString.Substring(0,equalsPos).Trim();
  // Get filter criteria
  string criteria = m_FilterString.Substring(equalsPos+1,
     m_FilterString.Length - equalsPos - 1).Trim();
  // Strip leading and trailing quotes
  criteria = criteria.Substring(1, criteria.Length - 2);
  // Get a property descriptor for the filter property
  PropertyDescriptor propDesc =
     TypeDescriptor.GetProperties(typeof(T))[propName];
  if (m_OriginalCollection.Count == 0)
  {
     m_OriginalCollection.AddRange(this);
  }
  List<T> currentCollection = new List<T>(this);
  Clear();
  foreach (T item in currentCollection)
  {
    object value = propDesc.GetValue(item);
    if (value.ToString() == criteria)
    {
       Add(item);

      }
   }
}

bool IBindingList.AllowNew
{
   get
   {
      return CheckReadOnly();
   }
}

bool IBindingList.AllowRemove
{
  get
  {
     return CheckReadOnly();
  }
}

private bool CheckReadOnly()
{
    if (m_Sorted || m_Filtered)
    {
       return false;
    }
    else
    {
      return true;
    }
}

protected override void InsertItem(int index, T item)
{
   foreach (PropertyDescriptor propDesc in
      TypeDescriptor.GetProperties(item))
{
      if (propDesc.SupportsChangeEvents)
      {
         propDesc.AddValueChanged(item, OnItemChanged);
      }
}
base.InsertItem(index, item);
}

protected override void RemoveItem(int index)
{
   T item = Items[index];
   PropertyDescriptorCollection propDescs =
      TypeDescriptor.GetProperties(item)
foreach (PropertyDescriptor propDesc in propDescs)

{
   if (propDesc.SupportsChangeEvents)
   {
      propDesc.RemoveValueChanged(item,OnItemChanged);
    }
 }
 base.RemoveItem(index);
}

void OnItemChanged(object sender, EventArgs args)
{
      int index = Items.IndexOf((T)sender);
      OnListChanged(new ListChangedEventArgs(
        ListChangedType.ItemChanged, index));
}

bool IRaiseItemChangedEvents.RaisesItemChangedEvents
{
   get { return true; }
 }
}


Binding to Business Objects Through the Data Sources Window

Now that we have stepped through how to declare business objects and collections that support the full range of functionality for data binding, let’s look again at the easiest way to use them in data-binding scenarios using the Data Sources window. The CustomBusinessObjects sample actually declared the Customer, Order, CustomerCollection, and TestDataGenerator classes in a separate class library assembly from the Windows application project that was being used to test them. To show how easy it is to use these types with data binding, let’s add a new form, called CustomersForm, to the Windows application project CustomBusinessObjectsClient.

1.   Set a reference to the CustomBusinessObjects class library in that project.

2.   Bring up the Data Sources window, which is initially blank.

3.   Click on the Add New Data Source link, which displays the Data Source Configuration wizard.

4.   Select Object as the Data source type in the first step.

5.   On the next page, Select the Object you wish to bind to,”navigate to the CustomerCollection type, as shown in Figure 9.5.

Figure 9.5: Selecting the Object Type for a Data Source

Selecting the Object Type for a Data Source

6.   Click Finish.

A data source for the CustomerCollection with the CustomerId, CustomerName, and Orders properties will display, as shown in Figure 9.6.

Figure 9.6: Data Sources Window with Custom Object Collection

Data Sources Window with Custom Object Collection

7.   Drag the CustomerCollection onto the CustomerForm design surface from the Data Sources window, and a DataGridView, BindingSource, and BindingNavigator are added and wired up so that the grid is ready to present customer collections.

8.   Add enough code to get an instance of the CustomersCollection (through the TestDataGenerator class), and set the DataSource of the BindingSource to that live instance of a CustomersCollection:

private void OnFormLoad(object sender, EventArgs e)
{
  m_CustomerCollectionBindingSource.DataSource =
     TestDataGenerator.GetCustomerCollection();
}

With these few steps, you can see that it is just as easy to set up data binding to custom objects and collections using the designer as it is to set up data binding for a data set. However, as you saw in the rest of this chapter, there is certainly a lot more work involved in defining the custom object and collection types themselves so that they will work properly in a data-binding scenario.

Where Are We?

In this chapter, you stepped through the concepts behind defining and using custom business objects for data binding in Windows Forms 2.0. It all comes down to supporting the various data-binding interfaces at the object and collection level. You learned how to define objects that support transacted editing and property change notifications, and about the various kinds of collections you can define and use in data-binding scenarios. The bottom line to take away is that you should favor using the BindingList<T> generic type, or a derived class from it, to define collections of business objects that you expect to use for data binding. You may also want to provide implementations for sorting, searching, and filtering depending on your needs through your derived collection class implementation.

In case it isn’t clear by now, creating collections and objects that support the full spectrum of data binding is a lot of work. I am a big proponent of using typed data sets as containers for business entity data, rather than defining custom objects when they will be used in data-binding scenarios. One of the main reasons why is because the data set and its contained objects already do all of this for you in a well-tested and proven way. Specifically, the DataView and DataRowView classes provide full implementations of all of the interfaces covered, and the classes will work fine for just about any kind of data that you need to put into them. If you are bringing data into your presentation layer for the purposes of data binding, your first instinct should be to use the power of the data set to contain that data, rather than needing to go to a lot of work to properly implement the objects and collections just to make them suitable for data binding in the interest of object-oriented purity.

Some key takeaways from this chapter are:

•    The DataSet class and its contained objects already do all of this for you, so use typed data sets whenever you can to save having to try and recreate the rich data containment functionality that is already written for you.

•    Favor using BindingList<T> for a strongly typed collection class that supports rich data binding.

•    Inherit from BindingList<T> to add support for sorting, searching, and filtering.

•    Add implementations of INotifyPropertyChanged to support ListChanged notifications from a BindingList<T> collection when property values change.

The next chapter finishes the coverage of the concepts behind building data-bound Windows Forms applications with a discussion of validation and error handling—two things that are key to building rich and robust data applications that do what the user expects them to do. It discusses both the built-in mechanisms for validation and error handling in the Windows Forms controls, and how to supplement what is provided to save repetition in writing your validation and error-handling code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.171.52