7. Understanding Data-Binding Interfaces

To really understand data binding and how to implement either custom data sources or custom data-bound controls, you first have to understand the interface-based contracts that drive data binding. This chapter describes each of the key interfaces that are involved in data binding from the perspective of the contract that the interface represents. It discusses the purpose of the interface, when it is used, and what the interface defines. This chapter is less hands-on than most of the other chapters, and is meant to familiarize you with the interfaces and provide a reference for using and implementing them. Probably the best strategy for reading this chapter is to first skim through each section, reading the portion that highlights the purpose and use of each interface. Then if you want to know more about that interface, read the following details on the members and implementation of that interface. A certain amount of coupling in the definitions of the interfaces is unavoidable, so in some cases I have to mention an interface’s relationship to some other interface that hasn’t been described yet. When that is the case, feel free to jump ahead to read the summary for that other interface to get a sense of it, then return to the one that you are focusing on.

The interfaces involved in data binding are designed to set up a two-way contract between data-bound controls and collections of data objects that are used by those controls. Those collections may be custom business object collections containing business objects, or they may be one of a number of .NET Framework collection and object types, such as a DataView collection containing DataRowView objects. By using these interfaces to define the communications between bound controls and data objects, data-bound controls don’t need to know the specific types of the objects they bind to, and the data objects don’t need to know anything about the specific capabilities of the controls that will bind to them. Once you understand these interfaces and what they are designed to support, the “magic” of data binding should be no more than smoke and mirrors to you—or rather just interfaces at work that aren’t magic at all, but very powerful nonetheless.

Although this stuff may seem like raw plumbing to you, there are several good reasons for reading and understanding the information in this chapter. The first is that to really understand what is going on when data binding occurs and to be able to troubleshoot problems, you need to understand what’s happening behind the scenes. Data binding is driven by the interfaces covered in this chapter, and the .NET Framework controls and components work against objects that implement those interfaces. The second reason is that if you plan to use custom object collections for data binding, you have to implement some of these interfaces to make your collection bindable and your objects editable through bound controls. Finally, if you are going to implement custom controls that support data binding or just code against the collection in the logic of your application, you will be a consumer of these interfaces to implement your data-binding code and logic code.

What Does Data Binding Have to Do with Interfaces?

When you deal with data, you typically work with collections of data items, where each of those items is an object that contains the data values or properties that represent the discrete pieces of data that you are binding to. When dealing with relational data—the collection of data in a table—the data items are rows within the table, and the properties are the columns within the rows. You usually focus at the table level, and think of the rows and columns as the details of the tabular data. In reality, any time you data bind to a DataTable, you are actually data binding to the table’s default view. So the DataView class is really the key class to focus on for data binding when working with data sets in .NET, and its data items are instances of DataRowView.

When dealing with custom business objects, the focus is more at the data item level, where each data item is an instance of a business object. These objects may be contained within some sort of collection object that acts as a container or parent object for the data items in the same way that a DataView is a container for DataRowView objects. These collection objects let you maintain a grouping of object instances, so you can hold on to a reference to the collection itself and use that reference to get to the individual objects when you need them.

Because everything is an object and the term is so overloaded, it can get confusing when talking about data or business objects. As a result, I will often use the term data item instead of object to describe the individual objects in a collection. A data item could be a custom business object instance, an instance of a .NET Framework type that contains some data of interest, such as a FileInfo object, or a relational data object, such as a DataRowView instance that belongs to a DataView.

The .NET Framework has a number of built-in collection types, and you can implement your own collection types if you need your collection to support features that the available collection types don’t support. Before .NET 2.0, it was fairly common to need to create custom collections if you wanted to have type-safe collections of data or business objects. With the introduction of generics in .NET 2.0, the need to implement custom collection classes should be fairly rare. The List<T>, Dictionary<T>, Queue<T>, Stack<T>, LinkedList<T>, and SortedDictionary<T> classes should address most situations where you want to implement a custom strongly typed object collection. Additionally, there is the BindingList<T> class for data binding in Windows Forms that implements most of the interfaces you will need. BindingList<T> is covered in detail in Chapter 9, but you have already seen it in action in many of the samples in earlier chapters.

Given all of that, there are potentially infinite numbers of collection and object types to which you might want to data bind in your applications. So how can you possibly cover all the possibilities? You do it by defining a common contract that you expect all types that want to play nicely together in data binding to support. The best way to specify a contract for code that is decoupled from the implementation of that contract is through an interface.

Interface definitions have to be considered from two perspectives: from that of the implementer and of the consumer.

•    The implementer is the class that provides an implementation of the members defined on the interface.

•    The consumer is the code that obtains an interface reference to an object and invokes the functionality represented by that interface through the reference.

This chapter describes each of the interfaces involved in data binding and the contract that it represents, followed by the details of that interface’s members. There are a number of examples of consuming the interface to demonstrate the concepts, but the full details for implementing or consuming the interfaces will be demonstrated and discussed in Chapters 8 and 9. Table 7.1 lists the interfaces discussed in this chapter and the kind of object responsible for implementing the interface. The consumers of all of these interfaces are either data-bound controls or client code that is programmatically interacting with the data collections.

TABLE 7.1: Data Binding Interfaces

Image

One of the most important interfaces for data binding is the IList interface. This interface derives from two base interfaces, IEnumerable and ICollection, so these are discussed first, as well as their cousin generic interfaces and what they do for you and when you need to worry about them. Then some additional collection-oriented interfaces that further enhance data binding capabilities are covered, including the IListSource, ITypedList, IBindingList, IBindingListView, ICancelAddNew, and IRaiseItemChangedEvents interfaces. Next are the four interfaces that individual data objects can implement to support editing and design-time features through bound controls: IEditableObject, INotifyPropertyChanged, IDataErrorInfo, ICustomTypeDescriptor. The last interfaces covered control oriented interfaces, the ISupportInitialize, ISupportInitializeNotification, and ICurrencyManagerProvider interfaces.

The IEnumerable and IEnumerator Interfaces: Supporting Iteration Through Collections

Implement IEnumerable to allow consumers of your collection to iterate through all of the objects in the collection in various ways. When you implement IEnumerable, you also have to implement IEnumerator on at least one class and return an instance of that class from the IEnumerable.GetEnumerator method. The implementation of IEnumerator provides the methods and properties that let you iterate over the collection. You can provide multiple implementations of IEnumerator to allow different kinds of iteration over the collection.

The need to iterate through collections of objects goes well beyond data binding. In past languages and technologies, the way you provided support for iteration and the way you actually performed iterations was not at all consistent. The architects of the .NET Framework addressed this by specifying a pattern and implementation for iteration that all collections in .NET are expected to support, regardless of the implementation language. Additionally, most .NET languages have added direct support for iteration based on this pattern, so you rarely have to deal directly with the IEnumerable and IEnumerator interfaces, even though they are what are driving the iteration behind the scenes.

This pattern is based on two interfaces: IEnumerable and IEnumerator. A collection type should implement the IEnumerable interface, which indicates that it supports iteration through its contained objects.

The IEnumerable interface contains only one method that the type needs to implement: GetEnumerator. This method is expected to return an object that implements the IEnumerator interface.

The IEnumerator interface has three members (described in Table 7.2) and works like a logical cursor in the collection of data. It starts out initialized to a position just before the first item in the collection. You start using it by calling the MoveNext method on the enumerator, which positions the cursor on the first item if there is one and returns true. If the collection is empty, the first call to MoveNext returns false. Subsequent calls to MoveNext move the cursor to the next logical item in the collection until there are no more items. MoveNext continues to return true as long as the cursor is positioned on an item in the collection at the completion of the call. When there are no more items, MoveNext returns false. This pattern lets you put the call to MoveNext in a while loop to set up the iteration in a nice compact way.

TABLE 7.2: IEnumerator Members

Image

The order that the logical cursor moves is up to the implementer of the IEnumerator interface and doesn’t have to be tied to the physical order of the items in the collection. For example, if you were implementing an enumerator for a sortable collection, you would want to modify the order that the cursor moved through the collection based on the sort order. However, if you plan to support sorting, you should look into implementing the IBindingList interface as well.

You access the items that are being iterated over through the Current property. The Current property on the IEnumerator interface returns an Object reference that should point to the current item at the logical cursor’s position in the collection. The IEnumerator interface also includes a Reset method, which returns the cursor to its initial position, allowing you to iterate over the same collection again using the same enumerator.

The following code snippet shows a typical loop using the IEnumerable and IEnumerator interfaces to iterate over a collection.

List<int> myvals = new List<int>();
myvals.Add(42);
myvals.Add(38);
myvals.Add(13);

IEnumerable enumerable = myvals;
IEnumerator enumerator = enumerable.GetEnumerator();
while (enumerator.MoveNext())
{
   int val = (int)enumerator.Current;
}
enumerator.Reset();

The List<T> generic collection implements the IEnumerable interface (and its IEnumerable<T> generic counterpart), so the code obtains an IEnumerable interface reference to the collection through an implicit cast. It then calls GetEnumerator on that interface reference, which returns an interface reference of type IEnumerator. Once you have an enumerator, you set up a while loop on its MoveNext method. The first call to MoveNext positions the cursor on the first item and returns true if anything is in the collection and enters the loop block. If the collection is empty, MoveNext returns false and never enters the loop. Each time through the loop, the cursor’s position is advanced when MoveNext is called until the last item is reached. Then the next time the while statement call to MoveNext returns false and exits the loop. Figure 7.1 depicts what is going on in this process.

FIGURE 7.1: Moving Through a Collection with IEnumerator

Moving Through a Collection with IEnumerator

Because the IEnumerable and IEnumerator interfaces are a fundamental pattern in .NET, support for them is baked in at the language level. Both C# and VB.NET support a foreach construct that lets you conveniently iterate through a collection like this:

List<int> myvals = new List<int>();
myvals.Add(42);
myvals.Add(38);
myvals.Add(13);
foreach (int val in myvals)
{
   Console.WriteLine(val.ToString());
}

Under the covers, the foreach operator uses the Current property and MoveNext method defined by the IEnumerator interface on the object returned from GetEnumerator to iterate through the collection, as described earlier. The generated Intermediate Language (IL) code doesn’t actually use the IEnumerator interface reference to access the current item; it just calls the Current property on the object itself. As a result, if the type implementing the IEnumerator interface contains a Current property that returns a specific type instance (such as Int32 or Double), then the foreach loop can avoid boxing and unboxing those values as it iterates through the collection. See the sidebar “The Problem with Type Unsafe Enumeration” for details on why foreach is implemented this way.

When implementing collections in .NET 2.0 that will contain specific types, you should also implement the generic versions of these interfaces, IEnumerable<T> and IEnumerator<T>. Generic interface types are used in .NET 2.0 to provide type safety for the contained types and to avoid performance penalties that could result for collections that contained value types when using the IEnumerable and IEnumerator interfaces. The untyped interfaces still have a place; they are used by the data-bound controls in .NET because they don’t want to make assumptions about the specific types contained in collections that you will use in data binding, which would limit the scenarios that data-bound controls could support.

The IEnumerator<T> interface doesn’t include a Reset method in order to simplify the implementation of enumerator objects. For simple collections where you use an explicit index into an array of objects, resetting the enumerator is a simple matter. But for more intricate scenarios, implementing a Reset method can get more complicated than it is worth. If you need to iterate over a collection multiple times, you can simply obtain multiple enumerators by calling GetEnumerator more than once. IEnumerator<T> is also different from IEnumerator in that it derives from IDisposable because of this assumption that enumerators are designed to be used once and then disposed of. If your enumerator maintains any internal state to manage the current context of the logical cursor, the Dispose method of the IDisposable interface is where you should clean them up. You should also implement a finalizer to perform that same clean up if your collection’s client fails to call Dispose.

You might be wondering why two separate interfaces are needed. Why not just define one interface that the collection itself implements that directly supports iterating through the collection? The answer is that you might want to support more than one form of iteration on a collection, so separating the iteration logic into a separate object lets you plug in additional forms of iteration if you need to. You can then expose those additional iteration approaches through properties on the collection class that return an IEnumerator interface reference to an object that iterates through your collection in a different way. This pattern also allows you to provide multiple enumerators to the same collection, so your code can maintain more than one current position in the collection. If your code will iterate over the collection using multiple threads, then you’ll want to look into implementing the ICollection interface to provide thread-safe access to the collection.

Typically, the object that implements the iteration methods of IEnumerator are created as a nested class within the collection class, but you are free to implement things however you want. It should be fairly rare that you would need to do a raw implementation of IEnumerable and IEnumerator, because the built-in collection classes in .NET already do that for you.

IEnumerable and its related IEnumerator interface are really all that is needed to support a read-only presentation of bound data. If you can provide a collection reference to a control, it can iterate over the items in the collection and use specific type information—helper classes like the PropertyDescriptor class—or reflection to access the properties on the data items and display them. This is how the BindingSource component lets you bind to anything that implements IEnumerable.

The ICollection Interface: Controlling Access to a Collection

You can use the ICollection interface to find out how many items exist within a collection, copy the items from a collection into an array, discover whether the collection provides synchronization for multithreaded scenarios, or access the synchronization object for the collection to implement your own synchronization locking.

The next collection-oriented interface up the inheritance hierarchy from IEnumerable is the ICollection interface. ICollection is derived from IEnumerable, so to implement ICollection, you have to implement IEnumerable as well. This interface adds a few members that define some additional attributes about a collection to help control access to the collection items, as described in Table 7.3 and the following paragraphs.

TABLE 7.3: Icollection Interface Members

Image

Probably the most important and frequently used member that is part of the ICollection interface is the Count property. If you have a collection that only implements IEnumerable, the only way to figure out how many items are in the collection is to obtain an enumerator for it and iterate through all of the elements, counting how many there are in your own loop. That kind of code gets very tedious and is expensive, so collections that implement ICollection let users of the collection obtain the number of items in the collection directly from the Count property, which puts the burden of determining that value on the collection implementer—where it belongs. Usually when you implement ICollection, you will maintain an internal count of the items as they are added and removed, so that the implementation of Count will be fast and easy.

The CopyTo method lets the consumer of a collection obtain a copy of all the items in the collection in the form of a typed array so that the consumer can operate on those items without worrying about affecting other consumers of the collection. CopyTo takes two arguments: a reference to an array and an index that indicates which item in the collection the copying should start with. The array that is passed in by the caller has to be of an appropriate type to contain the collection’s items and needs to be of sufficient size for all of the items in the collection, starting with the provided index, to be copied into the array. Of course, you need to take into account the performance impact of creating a copy of every item in the collection before you use this method. If it is acceptable to directly manipulate the items in the collection, don’t bother copying them to a separate array first.

If a collection supports being accessed by multiple threads at a time, it needs to be synchronized. This means that it has to implement a locking mechanism to ensure that race conditions cannot arise, such as one thread trying to access an item in the collection that might have been removed by another thread just moments before. The details involved in doing this are beyond the scope of this chapter, but there are a number of other books that provide information on that topic. (I would suggest Programming .NET Components, second edition, by Juval Löwy as the definitive source for multithreaded programming and synchronization in .NET.)

The IsSynchronized property indicates whether the collection supports synchronous access from multiple threads. If this property returns true, then the collection is expected to ensure that it is safe for multiple threads to add, remove, and access items in the collection concurrently. If it returns false, then it’s up to the user of the collection to ensure that only one thread at a time accesses the collection. The latter is the more common case, because adding locking mechanisms to a collection to support concurrent access can have a significant performance penalty for everyone who uses the collection. It is usually better to make the few who need concurrent access pay for it in the form of writing more code than to make the many who consume it in a single-threaded way suffer in performance.

The SyncRoot property provides an object reference that consumers of the collection can use to implement their own locking to support concurrent access. The collection is expected to return an object reference that can be used with a Monitor or lock statement, and if the consumer locks on the returned object, then the lock should prevent any other thread from accessing the collection’s members while the lock is in place. Usually the collection just returns a reference to itself if it is a reference type, and locking on the object itself blocks access to any of the members of the class from another thread until the lock is released. However, in certain situations you might use a contained object for locking purposes instead of the collection object itself. The bottom line to consumers is that they should be able to take the object reference returned from SyncRoot, lock on it, and know that other threads won’t be able to access, add, or delete items in the collection while the lock is in place.

ICollection<T> is a generic version of the same interface. You should also implement this interface if you are creating a strongly typed collection that exposes its count and supports copying and multithreaded access scenarios. The factoring of the ICollection<T> and IList<T> interfaces are a little different from their untyped cousins. Several of the members of the IList interface are implemented on ICollection<T> in the generic interface inheritance hierarchy, including Add, Remove, Contains, Clear, and IsReadOnly (these are discussed in the next section). These all really would make sense to be down at the collection level, since they aren’t specific to the ordered, indexed collection notion that IList represents. But Microsoft cannot refactor the ICollection and IList interfaces at this point, since they have been around since .NET 1.0 and would break immense amounts of legacy code.

The IList Interface: Enabling Data Binding

The IList interface lets you treat a collection as an ordered, indexed set of data items. The IList interface is one of the most important interfaces in data binding, because complex data-bound controls can only be bound to collections that implement IList . Using an IList interface reference to a collection, you can manage the collection’s data by adding, removing, inserting, and accessing items.

The IList interface is a key interface that enables runtime data binding in most Windows controls. IList derives from ICollection, so you need to also implement ICollection and IEnumerable to support IList. IList defines a set of members that further defines a collection as an ordered, indexed collection of data that can be accessed and modified in a random order. The good news is that you should never have to implement the IList interface yourself in a .NET 2.0 application, because the List<T> generic type provides a full implementation for any type that you want to contain in a collection. However, you many need to consume the interface to use a collection within your application code or in a custom data-bound control. The members of the IList interface are described in Tables 7.4 and 7.5 and in the following paragraphs.

TABLE 7.4: IList Interface Properties

Image

TABLE 7.5: IList Interface Methods

Image

The IList interface adds the methods and properties to a collection that make it behave like a full-fledged random access, editable, and modifiable collection of data. As discussed in an earlier chapter, Windows Forms data binding is two way—many data-bound controls not only support presenting data for viewing, but also let the user edit the data within the control, resulting in updates to the underlying data source. You can access any item in the collection through the bound control at any time because of the stateful model of Windows Forms and controls, so random access to the collection’s contents becomes important.

If you only need to support one-way data binding (forward-only, read-only iteration through the data items), IEnumerable would be enough to get to all the data items, and late bound discovery could be used to display the data. As mentioned before, the BindingSource component is able to iterate over collections that only implement IEnumerable to put those objects into its own internally maintained list of items, at which point you can data bind in Windows Forms to the BindingSource component because it implements the IList interface.

The Add, Insert, Remove, RemoveAt, and Clear methods make it easy to manage the items in the collection as a variable-length ordered list (this should be fairly intuitive from the descriptions in Table 7.4). The Contains and IndexOf methods, in conjunction with the indexer, let you have random access to the collection’s contents to either read data out for presentation or to modify the contents of a given position in the list.

The IsFixedSize and IsReadOnly properties are intended to give you an indication of whether you can modify the contents of the collection, and if so, to what degree. Because the other members of IList are specifically designed to support modifying the list, it is rare that you will run into a collection that is never modifiable, and usually the default is that they are modifiable. However, you might find collections that support switching the collection into a read-only mode or a frozen-size mode. Those properties let you check for the presence of that mode before trying to modify the contents of the collection, which should result in an exception if the collection is marked as read-only or fixed-size.

Working with a collection that implements IList from code is straightforward. You can declare an instance of the collection, add items to it with the Add method, locate items with IndexOf, determine how many are in the collection through the implemented Count member of the inherited ICollection interface, iterate over all of the collection’s items and index into the collection with the IList indexer, or access the items through the inherited support from the IEnumerable interface, all in a very few lines of code:

List<int> myCollection = new List<int>();
myCollection.Add(42);
myCollection.Add(38);
Debug.Assert(myCollection.IndexOf(42) == 0);
for (int i = 0; i < myCollection.Count; i++)
{
   myCollection[i] = 99;
}
foreach (int val in myCollection)
{
   Console.WriteLine("Value = {0}", val);
}

This code uses an instance of the List<T> generic type, which implements IList and all of its base interfaces for you for any type parameter that you specify. As mentioned earlier in the book, when you declare a variable of a generic type with a type parameter, such as the declaration of myCollection in this code with int as the type parameter, you are really creating a new type that is a strongly typed list of integers, as well as declaring an instance of that type, all in one line of code.

As long as a collection supports the IList interface, you will be able to bind it to any of the .NET Framework Windows Forms controls that support data binding, using either simple binding (Binding objects) or complex data binding (DataSource and DataMember properties). If the control supports modifying the collection (such as the DataGridView), you can add items to the collection, remove them, or edit the values of an item exposed through properties on the contained objects. Internally, the data-binding code of a control iterates over the collection’s contents and indexes into it using the IList methods and properties. Once the control has obtained access to an item in the collection, it can use the PropertyDescriptor class to dynamically discover information about the properties of the items in the collection and can use those to populate whatever properties or display elements of the control are bound to the data. See the later section on property descriptors for more information and the samples in Chapter 8 for implementing custom data-bound controls.

One limitation of the IList interface with respect to data binding is that although it is sufficient to allow the data to be displayed and support updates, additions, and deletions from the bound control, it doesn’t support all scenarios for the collection’s modifiable data. Specifically, if the data collection is changed programmatically by code outside of the bound control, the control won’t know of those changes, and the data it displays will be out of sync with the actual data contained by the collection (see the examples in Chapter 9 for more information on this). To remedy this shortcoming, you have to implement IBindingList in collections that you want to use for data binding (described later in this chapter). You will also want to implement the INotifyPropertyChanged interface on the data item’s object type.

Like its parent interfaces, IList also has a generic cousin interface, IList<T>, that you should implement if your collection will be a strongly typed collection of objects. IList<T> only defines the Insert, IndexOf, RemoveAt, and indexer members, because the other IList members are inherited from ICollection<T>. The List<T> generic collection type implements these generic interface versions for you as well.

The IListSource Interface: Exposing Collections of Collections

The IListSource interface allows a type to indicate whether it contains a collection of collections and to expose a default collection for use in data binding. Data-binding code can use the IListSource interface implementation on an object to obtain a list to bind against when the data source itself is not a list, or to obtain a list of collections that the object contains.

Frequently you will need to work with collections of data that are related to one another. The most common case of this is working with data sets in .NET. A data set can be thought of as a collection of collections because it can contain multiple data tables. Each DataTable is a collection of data items, which are the rows of data (of type DataRow). You might also implement your own custom container type that contains other collections, such as a list of lists. Or perhaps your container type only contains a single list, but you want to let consumers use your object as their data source and obtain the list from your object for the purposes of data binding.

For example, suppose you had a business class called InventoryManager that lets you manage your inventory items and gain access to them for various use cases. You might want to allow applications to data bind to your inventory manager and have the manager control what gets exposed for data binding based on some other criteria that the class exposes:

public class InventoryManager : IListSource
{
  public string InventoryFilter { get { ... } set { ... } }

  public IList GetList()
  {
     // Lazy load inventory items based on current filter criteria
  }

  public bool ContainsListCollection { return false; }
}

You might also have a container with multiple collections, such as an inventory manager class that contains lists of in-stock inventory items, on-order items, and suppliers. In that case you might want to provide an easy way for consumers to get a list of the lists that your object contains.

The IListSource interface supports these scenarios through its two members (described in Table 7.6). It lets you return a default collection (specifically, an object that implements IList) from a call to GetList. That list could either be a simple list of objects for data binding or a list of lists contained by the object. If you return true from the ContainsListCollection property, then it is assumed that the list you return from GetList is itself a list of lists.

TABLE 7.6: IListSource Interface Members

Image

Property Descriptors: Allowing Dynamic Data Item Information Discovery

An important class for allowing data-binding code to work against any collection and data item type is the PropertyDescriptor class. This class is defined in the System.ComponentModel namespace and provides dynamic discovery of type information about the individual properties of an object. You can obtain a property descriptor in a number of ways. As discussed in the following sections, some of the data-binding interfaces have methods or properties that will return property descriptors. You can also obtain them for an object any time you have a reference. The TypeDescriptor class, also in the System.ComponentModel namespace, lets you request the property descriptors for an object’s properties with the GetProperties method. In fact, the TypeDescriptor class has a bunch of other methods that let you dynamically discover almost anything there is to know about an object, including its methods, properties, events, attributes, and so on.

As their name implies, property descriptors are used to describe information about an object’s property. The PropertyDescriptor class derives from MemberDescriptor, which provides many of the properties that you can use to discover information about the described property on the object. You can find out the name of the property, its type, what its type converter is, and what attributes have been applied to its declaration in the type that it belongs to.

For the most part, when it comes to data binding, you will mainly be interested in four properties exposed on the PropertyDescriptor class: Name, PropertyType, ComponentType, and Converter. Each of these is a read-only property.

•    The Name property tells you the name of the property as it was declared in the data object type definition. You can use this for display purposes, such as setting the column headers in a grid, or you can use its reflection to get or set the value of the property.

•    The PropertyType gives you the type of the property itself.

•    The ComponentType gives you the type of the component (or data item type) on which the property is defined.

•    The Converter property gives you back a reference to a TypeConverter object for the property type, if there is one. As discussed in Chapter 4, the Binding class uses type converters to perform automatic formatting of bound-data values to corresponding types. Other complex data-bound controls, such as the DataGridView, can use type converters to do the same thing internally when rendering the values of data members within a data source.

Additionally, you can use the PropertyDescriptor to access the value of the property for instances of your data objects. The GetValue method takes an object reference to the data item for which you want to obtain the value of the property described by the property descriptor. It returns an object reference that contains the value of that property on the data object’s instance that you pass in. See Listing 7.1 later in this chapter for an example of using GetValue to obtain the values of a data item’s properties. The SetValue method works in a similar fashion, allowing you to set the value of an object’s specific property without having any compile-time type information about that object.

Property descriptors also support providing change notifications for changes to property values, if those changes are made through the SetValue method exposed by the property descriptor. When data-bound controls let the user edit a bound data item through the control, they do so with the SetValue method on that item’s property descriptors without being coupled to the particular object type. If some other piece of code needs to be notified when property values are changed in this way, you can use the AddValueChanged method to provide a callback object and method (through a delegate). This will be called whenever that property’s value is changed by the property descriptor’s SetValue method. The BindingSource component uses this capability to be notified any time a property changes on one of the data items in the collection it contains. It then raises the ListChanged event on its IBindingList implementation, enabling it to keep multiple controls that are bound to the same data source synchronized when the values are edited through those controls.

The ITypedList Interface: Exposing Data-Binding Properties

The ITypedList interface lets you expose or access type information about the properties that the items in a collection define. It allows consuming code to say to a collection, “Tell me all about the data items that you contain.” This information is used extensively by the Visual Studio designer to let you declaratively configure bound controls at design time, such as configuring columns on a DataGridView control based on the typed properties of the items in the bound collection. It can also be used at runtime to modify behavior based on the dynamically discovered types of the properties of a collection’s data items.

Types that implement the ITypedList interface can support design-time or runtime discovery of type information about the contained data items. Both the DataView class and the BindingSource class implement ITypedList, and the information exposed through their implementations let you dynamically discover type information about the contained data items. Because every DataTable exposes a default DataView, this makes the type information about the data in a DataTable available through ITypedList. The members of the ITypedList interface are shown in Table 7.7.

TABLE 7.7: ITypedList Interface Members

Image

The GetItemProperties method may look a little like a circular definition—you pass in an array of property descriptors to get back a collection of property descriptors. It is easiest to understand how this works if you understand the DataView implementation of this interface.

If you obtain an ITypedList interface reference to a DataView and then call GetItemProperties on that reference with a null parameter, you get back a collection of PropertyDescriptors that describe each field in the data table to which the data view refers. See Listing 7.1 later in this chapter for an example of using GetItemProperties to obtain property descriptors for the columns in a table.

If you call GetItemProperties with a null parameter, you get back the property descriptors for the list’s data items. What is the parameter good for, then? Well, that depends on what collection is implementing it, but in the case of a DataView, you can use it to obtain property descriptors for related tables. If the table that the DataView wraps has a related table (defined through a DataRelation), there will be a property descriptor in the returned collection for the view that represents the relation that exposes child rows in some other table. If you pass that property descriptor to the view’s GetItemProperties method, you will then get back the property descriptors for the related view (table).

The GetListName method just returns the name of the list that will be returned by the GetItemProperties method. The DataView behavior for this is that if you pass in a complete array of descriptors that matches the collection that is returned from GetItemProperties, you get back the table’s name. If you have a complex container of collections, like a DataSet, you can return the names of other collections in the container when their property descriptors are passed in. This method isn’t really used anywhere except in the old DataGrid control for displaying the names of child lists when you navigate through a hierarchical collection of collections.

The following is a simple implementation of ITypedList on a BindingList that sorts the property descriptors.

public class SortedTypesBindingList<T> : BindingList<T>, ITypedList
{
   public PropertyDescriptorCollection
      GetItemProperties(PropertyDescriptor[] listAccessors)
   {
      PropertyDescriptorCollection pdc = null;

      if (null == listAccessors) // Told to get all properties
      {
         // Get browsable properties only
         pdc = TypeDescriptor.GetProperties(typeof(T),
            new Attribute[] { new BrowsableAttribute(true) });
         // Sort the properties alphabetically
         pdc = pdc.Sort();
      }
      else // Expect only one argument representing a child collection
      {
            // Return child list shape
            pdc = ListBindingHelper.GetListItemProperties(
               listAccessors[0].PropertyType);
      }

      return pdc;
   }

   public string GetListName(PropertyDescriptor[] listAccessors)
   {
      // Not really used anywhere other than in DataTable and DataGrid
      return typeof(T).Name;
   }
}

The IBindingList Interface: Providing Rich Binding Support

The IBindingList interface is the most important data-binding interface for supporting full-featured data binding. It defines capabilities for controlling changes to the list, sorting and searching the list, and providing change notifications when the contents of the list change. You can easily create a partial implementation of IBindingList (minus sorting and searching) using the BindingList<T> generic collection class in .NET 2.0.

As mentioned earlier, implementing the IList interface is the minimum bar to pass to support Windows Forms control data binding. However, just implementing that interface isn’t sufficient to support the kind of rich data binding that developers are accustomed to when working with a collection like a DataView. The IBindingList is the most important interface for supporting data binding with modifications to the data collection. This can be done either through the bound control itself or through programmatic modifications to the collection behind the scenes. IBindingList also lets the collection specify whether it supports sorting or searching, and provides the methods that will drive those processes. IBindingList derives from IList, so you will need to implement all of the members of IList, ICollection, and IEnumerable as well when you implement IBindingList. The best way to do this is to use or derive from the BindingList<T> generic class. Chapter 9 develops a full sample showing how to do this, as well as adding implementations for IBinding ListView, ITypedList, and IRaiseItemChangedEvents. For more information on consuming the IBindingList interface and what functionality each of its members provide, read on.

Getting to Know the IBindingList Members

The properties of the IBindingList interface are read-only and are described in Table 7.8. The methods of IBindingList are described in Table 7.9, and the one event in Table 7.10.

TABLE 7.8: IBindingList Interface Properties

Image

TABLE 7.9: IBindingList Interface Methods

Image

TABLE 7.10: IBindingList Interface Event

Image

Raw changes to the collection, in the form of adding items directly, removing items, or accessing individual items, are all done through the methods and properties of the IList inherited interface, as described in the section on IList. The change functionality of the IBindingList interface lets you control whether changes can be made to the list, as well as the construction of new items in the list.

The AddNew method provides a convenient way for a data-bound control to add a new item to the collection that can then be edited directly through the control, without the control needing to know anything about the item type at compile time. This method returns an object reference to the created item that can then be used with late-bound methods or reflection to discover that item’s properties for presentation.

The AllowEdit, AllowNew, and AllowRemove properties let the collection tell a bound control what kinds of modifications it supports, so a bound control can render itself differently to match the capabilities of the underlying collection. For example, it wouldn’t make sense to have a button that adds a new item to a collection in a bound control if the collection that is currently bound is read-only. These properties are read-only, so it is up to the collection itself to decide whether it supports those things. Although not part of the IBindingList interface, the collection could expose other methods that allow the consuming code to switch the collection in and out of read-only or fixed-size modes; this way, the values that the collection returns from these properties could change over time. These properties are primarily designed with the expectation that these aspects of a collection won’t change: either the collection supports changes or it does not. Other controls can be used at a presentation level to decide whether you are going to let users make modifications at any given time.

Notifying Consumers of Changes to the Collection

If the collection supports changes, it should also support firing a ListChanged event when the collection changes. To indicate that, it should return true from the SupportsChangeNotification property. The collection itself should be able to raise ListChanged events when items are added or removed from the collection. Ideally, it will also be able to provide ListChanged notifications when existing items in the collection change because their properties have changed. However, the collection’s ability to do this will be dictated by how the properties are changed and what support the contained object types provide.

As mentioned earlier, if changes are made through a property descriptor’s SetValue method, a container can call the AddValueChanged method on the property descriptor and provide a callback delegate so that the container will be notified when the property changes. It can then raise the ListChanged event in response to notification from the property descriptor that the property changed. This is exactly what an implementation of the IRaiseItemChangedEvents interface, discussed in a later section, is expected to do. However, if the property is changed directly through its property setter through a reference to the object, there is no way for the collection to know about the change unless the object itself notifies the collection. The support for that comes from the INotifyPropertyChanged interface.

Another form of change that a collection can support is a dynamically changing schema, where new properties are added to the collection items at runtime or design time. The ListChanged event also supports notification of this type of change through its event arguments.

The ListChanged event is of type ListChangedEventHandler, which carries an event argument of type ListChangedEventArgs along with it. This event argument’s properties, listed in Table 7.11, give you more information on the changes to the list.

TABLE 7.11: ListChangedEventArgs Properties

Image

Image

TABLE 7.12: ListChangedType Enumeration Values

Image

Exercising IBindingList Change Notifications

The following simple console application shows the results of ListChanged events being raised from a data view as the underlying data collection is modified in several ways.

class Program
{
   static void Main(string[ ] args)
   {
      // Get some data to work with
      NorthwindDataSet nwData = new NorthwindDataSet();
      CustomersTableAdapter adapter = new CustomersTableAdapter();
      adapter.Fill(nwData.Customers);

      // Get an IBindingList interface reference
      IBindingList list = nwData.Customers.DefaultView;
      // Subscribe to change events
      list.ListChanged += new ListChangedEventHandler(OnListChanged);

      // Delete a row
      list.RemoveAt(1);

      // Add a column
      nwData.Customers.Columns.Add("New Column", typeof(string));

      // Change an item in the collection
      nwData.Customers[0].CompanyName = "IDesign";
   }

   static void OnListChanged(object sender, ListChangedEventArgs e)
   {
      Console.WriteLine("ListChangedType Value: {0}",
         e.ListChangedType);
      Console.WriteLine("NewIndex value: {0}",e.NewIndex);
      Console.WriteLine("OldIndex value: {0}",e.OldIndex);
      if (e.PropertyDescriptor != null)
      {
         Console.WriteLine("PropertyDescriptor Name: {0}",
            e.PropertyDescriptor.Name);
         Console.WriteLine("PropertyDescriptor Type: {0}",
            e.PropertyDescriptor.PropertyType);
      }
      Console.WriteLine();
   }
}

In this code, a typed data set is created and filled with customer data. An IBindingList interface reference to the default view of the Customers table is then obtained and used to subscribe to the ListChanged event. The collection is then modified in three ways:

•   A row (a data item) is deleted from the table (the collection).

•   A new column (property) is added to the table.

•   The value of one of the columns (properties) of a row is modified.

Running this program results in the following output:

ListChangedType Value: ItemDeleted
NewIndex value: 1
OldIndex value: -1

ListChangedType Value: PropertyDescriptorAdded
NewIndex value: 0
OldIndex value: 0
PropertyDescriptor Name: New Column
PropertyDescriptor Type: System.String

ListChangedType Value: ItemChanged
NewIndex value: 0
OldIndex value: 0
PropertyDescriptor Name: CompanyName
PropertyDescriptor Type: System.String

From this you can see that deleting items gives the deleted item’s index in the NewIndex property of the event arguments; the property descriptor is returned for a new column, describing that column; and when an item in the collection changes, its index is returned, along with a property descriptor for the property that changed.

Supporting Sorting with IBindingList

The next category of functionality specified by the IBindingList members is sorting. The SupportsSorting property lets the collection specify whether it even supports sorting. If it doesn’t, then a bound control shouldn’t even expose sorting controls to the user. If the collection does support sorting, then the ApplySort and RemoveSort methods let a control invoke or remove the sorting functionality provided by the collection. The ApplySort method takes two parameters: the PropertyDescriptor identifies the property on which you want to sort, and the ListSortDirection enumeration, whose value can be either Ascending or Descending. The sorting support defined by the IBindingList interface only supports sorting on a single property at a time. For multi-property sorts, you need to implement the IBindingListView interface, as described in the next section.

Listing 7.1 shows an example of using the IBindingList interface sorting properties and methods to sort a collection in various ways.

LISTING 7.1: Sorting a List Through IBindingList


class Program
{
   static void Main(string[] args)
   {
      // Get some data to work with
      object dataCollection = GetData();
      IBindingList list = dataCollection as IBindingList;
      ITypedList typedList = dataCollection as ITypedList;
      // Dump the raw data view
      DumpList(list, "Raw Data");
      // Check to see if the list supports sorting
      if (list.SupportsSorting)
      {
         // Get property descriptors for table
         PropertyDescriptorCollection props =
            typedList.GetItemProperties(null);
         // Apply Sort on column 1
         list.ApplySort(props[0], ListSortDirection.Ascending);
         DumpList(list, "Sorted Key1 Ascending");
         // Apply Sort on column 2
         list.ApplySort(props[1], ListSortDirection.Descending);
         DumpList(list, "Sorted Key2 Descending");
         // Remove Sort
         list.RemoveSort();
         DumpList(list, "Unsorted");
      }


   }

   private static object GetData()
   {
      // Create a data set with some sortable sample data
      DataSet data = new DataSet();
      // Add a table

      DataTable table = data.Tables.Add();
      // Add two columns, Key1 and Key2
      table.TableName = "TestTable";
      table.Columns.Add("Key1", typeof(string));
      table.Columns.Add("Key2", typeof(int));
      // Add some data rows
      table.Rows.Add(".NET", 2005); // Row 1
      table.Rows.Add("ZZZZ", 9999); // Row 2
      table.Rows.Add("Rocks",2); // Row 3
      return data.Tables[0].DefaultView;
  }

  private static void DumpList(IBindingList list, string prompt)
  {
     Console.WriteLine(prompt);
     // Loop through each data item without knowing its type
     // Use a type descriptor to obtain the property descriptors
     PropertyDescriptorCollection props =
        TypeDescriptor.GetProperties(list[0]);
     foreach (object dataobject in list)
     {
        StringBuilder builder = new StringBuilder();
        // Loop through the properties, outputting name
        // and value for this data object
        foreach (PropertyDescriptor prop in props)
        {
           builder.Append(prop.Name);
           builder.Append(" = ");
           builder.Append(prop.GetValue(dataobject).ToString());
           builder.Append("; ");
        }
        // Write it out to screen
        Console.WriteLine(builder.ToString());
      }
      Console.WriteLine();
   }
}


The code in Listing 7.1 first constructs a simple data set on the fly to work against and then returns the table’s default view as the data collection. The DataView class implements both IBindingList and ITypedList, which lets us dynamically determine the data collection’s behavior and content. Obviously, in production code you should check the cast’s results to see if it succeeded and do something appropriate if it doesn’t succeed. After obtaining the interface references to the data collection, the code first calls the DumpList helper method to output the list’s contents before any sorting has been applied.

The DumpList method uses the TypeDescriptor class (described earlier in this chapter) to obtain the collection of property descriptors for one of the data items in the collection. Usually the lists you deal with will be homogeneous collections of objects; otherwise, you aren’t likely to be able to data bind with them in the first place. As a result, you only need to obtain the property descriptors once using one of the objects in the collection. Using those property descriptors, the consuming code can output the name and value of each data object as it iterates over the list with a foreach loop (which is enabled by the base interface IEnumerable). Note that this method has no specific type information about the list or its data items, other than that the collection is represented by an interface reference of type IBindingList.

After the raw list is dumped, the code uses the IBindingList reference to see if the collection supports sorting. If so, the sample enters the block of code that applies sorts. The code first uses the ITypedList interface reference to get the property descriptors for the data view’s columns through the GetItemProperties method. It then uses the property descriptors for the first and second columns to apply an ascending and descending sort on each one, respectively. After applying each sort, it calls DumpList again to show that the list’s iteration order has in fact changed to reflect the sort. Finally, it calls RemoveSort to show that the list order is restored to its original order.

If you run this sample, you will see the following results of sorting in the output:

Raw Data
Key1 = .NET; Key2 = 2005;
Key1 = ZZZZ; Key2 = 9999;
Key1 = Rocks; Key2 = 2;

Sorted Key1 Ascending
Key1 = .NET; Key2 = 2005;
Key1 = Rocks; Key2 = 2;
Key1 = ZZZZ; Key2 = 9999;

Sorted Key2 Descending
Key1 = ZZZZ; Key2 = 9999;
Key1 = .NET; Key2 = 2005;
Key1 = Rocks; Key2 = 2;

Unsorted
Key1 = .NET; Key2 = 2005;
Key1 = ZZZZ; Key2 = 9999;
Key1 = Rocks; Key2 = 2;

This example demonstrates that sorting should modify the order that the list returns items when it is iterated over. If you are creating your own object collection type, how you apply sorting is up to you, but it is usually nontrivial to support sorting effectively. The RemoveSort method removes just the current sort. You can check whether a collection is sorted using the IsSorted property. You can also obtain the sort direction and property with the SortDirection and SortProperty properties, respectively, on the binding list.

There are a couple of important things to note from this example. The first is that this is a good example of the power of the data-binding interfaces. Once the data collection is constructed by the helper method, the main part of the code has no specific type information about the collection or the data items it is working on. Yet the sample code is able to iterate over the data, output the data, and modify the sorting of the data all based on the various data-binding interfaces. This is what you want, because in data-binding situations you are going to be given an object reference for a collection, and you have to take it from there without knowing or assuming any specific type information other than the data-binding interfaces. The way you will typically do that is to try to cast to the various data-binding interface types, and if the cast succeeds, then you can count on using the behavior defined on that interface. If the cast fails, then you will have do whatever is appropriate for your control—either downgrade the functionality of the control or throw an exception if the minimum required interface support isn’t met.

Supporting Searching with IBindingList

The final category of functionality described by the IBindingList interface is searching. To search effectively, you usually need to apply some sort of indexing over the collection. The SupportsSearching property indicates whether a collection even supports searching. If it returns true, then you should be able to safely call the Find method with a property descriptor for the property you want to match against and an object reference that contains the value to match with.

If you are implementing a collection that supports searching and you will have large sets of data, you might want to also support indexing of the data for more efficient searches. If the collection supports indexing, you can call AddIndex to direct the collection to establish an index on a particular property, and you can call RemoveIndex to remove one that has been previously added. For example, you could add the following code to the end of the Main method in Listing 7.1 to perform a search for a particular item, using indexing to speed the search (although in this simple case, the cost of establishing the index would probably greatly outweigh the performance benefit for the search over three items):

if (list.SupportsSearching)
{
   // Get property descriptors for table
   PropertyDescriptorCollection props =
      typedList.GetItemProperties(null);
   list.AddIndex(props[0]);
   int index = list.Find(props[0], ".NET");
   list.RemoveIndex(props[0]);
   Debug.Assert(index == 0);
}

Implementing a collection supporting IBindingList from scratch is a ton of work, and most of it is stock code for all the straight collection-oriented things like adding items, clearing them, firing ListChanged events when the collection or items change, and so on. Before .NET 2.0, there weren’t a lot of options to avoid that work. However, the BindingList<T> class in .NET 2.0 makes this a lot easier, and it will be explored further in Chapter 9.

The IBindingListView Interface: Supporting Advanced Sorting and Filtering

The IBindingListView interface supplements the data-binding capabilities of the IBindingList interface by adding support for multi-property sorts and filtering of the list.

As described in the previous section, IBindingList gives you simple sorting and searching capability. However, sometimes you need additional functionality. You may want to be able to sort on multiple properties or columns in a data collection at the same time, or you may want to filter the items presented by the collection based on some criteria without having to search for matches one at a time. The IBindingListView interface is designed exactly to address these needs. IBindingListView derives from IBindingList, so all the stuff described for IBindingList and all of its base interfaces applies here as well. The properties and methods of IBindingListView are described in Tables 7.13 and 7.14.

TABLE 7.13: IBindingListView Interface Properties

Image

TABLE 7.14: IBindingListView Interface Methods

Image

To support advanced sorting, a collection must be able to set the order based on multiple properties on the data objects simultaneously. Before attempting to apply advanced sorting, the client code should check the SupportsAdvancedSorting property to see if it is true. If so, then the sort criteria are specified by constructing a collection of ListSortDescription objects and passing those to the ApplySort method on the IBindingListView interface. The ListSortDescription type is pretty straightforward: it is just a container for a pair containing a PropertyDescriptor property and a SortDirection property. Typically, the order of these items in the ListSortDescriptionCollection determines the order in which the sort criteria are applied. To remove an advanced sort that has been applied, just call the RemoveSort method that was inherited from the IBindingList base class. If you want to access the sort descriptions that are currently in use for the collection, you can check the IsSorted property from the base interface, and then use the SortDescriptions property to obtain the collection of ListSortDescriptions currently in play.

If a collection supports filtering, it should return true from the SupportsFiltering property. You can then set a filter string using the Filter property, which should immediately modify what data objects are exposed by the collection if you were to iterate over it. The format of the filter string is going to be an implementation detail that will be specified by each collection type. For example, the DataView class supports SQL-like filter strings that match the strings permissible on the DataColumn.Expression property. If you are implementing your own collection type, you have to decide on syntax for filter strings that makes sense for your scenario, and then document it well so your collection’s users know what filter strings they can provide.

If a filter has been applied to a collection, you can remove it with the RemoveFilter method. If you want to check what the current filter criteria is, just read the Filter property. This property should be null if there is no filter applied. In fact, this is an alternate way of removing a filter: just set the Filter property to null.

See Chapter 9 for a sample implementation of the IBindingListView interface on the BindingListView<T> generic type developed there.

The ICancelAddNew Interface: Supporting Transactional Inserts in a Collection

The ICancelAddNew interface lets you add and edit new items to a collection with the item in a transient state, which allows you to remove the new item before finalizing the addition if desired (if a problem occurs while initializing the object’s properties). This interface was added in .NET 2.0 to break an implicit coupling that occurs with the IEditableObject interface when adding new items to a collection. Prior to adding this interface, objects in the collection that implemented the IEditableObject interface had to notify their parent collection if editing was canceled on a newly added item so the collection could remove that item from the collection. With ICancelAddNew, the collection can take care of the removal, and the contained object no longer has to have a direct coupling to its containing collection for backing out a newly added item if initialization is canceled.

Another capability that can come in handy for a richer data-binding experience is to support transactional adding of items to the collection. To understand this, consider a data table bound to a grid. The grid presents a blank line at the bottom of the grid that lets you add new rows to the table. But what if there are constraints on the columns of that row or if there is validation that needs to occur based on the input to multiple fields before the item should be added to the data collection? How can you prevent inconsistent data from being added to the collection? After all, there needs to be an object instance somewhere to accept the data being input by the user as they tab from field to field. A new row in the data source is the most logical kind of object to create. But you don’t want to actually add the object until the addition is considered “complete”—whatever that means based on the collection and the data objects that go within it.

To support this scenario, the ICancelAddNew interface has been defined to allow a collection to decide whether to accept or reject a new item that has been added to the collection through this interface’s methods. If a collection supports transactional adding of items to the collection, it should implement the ICancelAddNew interface. Bound controls can then call EndNew(int index) to commit the transaction of adding a new item or CancelNew(int index) to roll back the addition. This lets the control call AddNew on the list, get a new item back, and start setting values on the new object. If the code calls CancelNew with the index of the item that was added, the new object can be discarded without actually adding it to the collection for good. If the code calls EndNew with the index or performs any other operation on the collection, the addition should be committed. The object itself never needs to know about its transient state with respect to membership in the collection; that is all handled by the collection itself.

This is a little different behavior than what you might expect. In the world of distributed and database transactions, you are expected to explicitly commit the transaction or it should roll back. In the case of ICancelAddNew, committing is the default behavior even if EndNew isn’t explicitly called based on the contract specified by the Framework. So inserting or removing other items, or setting the current item to another item in the collection, is considered to take the focus off the item being added and will commit the item to the collection.

Both the BindingList<T> class that will be discussed in detail in Chapter 9 and the BindingSource class implement this interface for the collections they manage. Because the BindingList<T> generic class cannot know what to do about the transactional semantics of a call to AddNew or CancelNew, you will need to derive a class from BindingList<T> and override the base class methods AddNewCore and CancelNewCore to provide the implementation that makes sense for your scenario.

The IRaiseItemChangedEvents Interface: Providing Item Modification Notifications on Collections

The IRaiseItemChangedEvents interface lets a collection indicate that it will monitor changes to the properties of the contained objects made through the SetValue method of their property descriptors. You would want to do this if you expected your collection type to contain objects that might not implement the INotifyPropertyChanged interface but wanted to provide some opportunity to notify bound controls that the underlying data item properties had changed. This interface doesn’t make any guarantees about notifying of changes if the properties change through any means other than the SetValue method of their property descriptors.

This interface is used as a signal to consuming code that your collection will raise ListChanged events when the property values contained in your objects change due to changes made through property descriptors—which is how all property updates are done through data-bound controls that support editing. This interface defines a single member, the RaiseItemChangedEvents Boolean property. If your collection returns true from this property, then consuming code can expect to be notified when some object in the collection has changed through a data-bound control.

The BindingSource component uses this internally to provide better currency of the presented data in bound controls, even if the underlying objects don’t support property change notifications themselves. To implement support for raising the ListChanged event in response to property value changes on the collection objects, you have to provide a callback delegate to the property descriptor for each property on each object in a collection, so that your collection will be notified when those properties change through the property descriptor. You do this through the AddValueChanged method on the PropertyDescriptor class. See the BindingListView<T> class implementation in Chapter 9 for an example and more discussion of this.

The IEditableObject Interface: Supporting Transactional Item Modifications

The IEditableObject interface lets you defer committing changes to object properties until the editing process is complete. Consuming code can then explicitly declare when an edit operation has commenced on an object, and then later choose to commit or reject the changes made to one or more properties on the object before those changes are made visible to other code that may be working with the object. Specifically, using this interface defers change notifications to bound controls while an object is being edited until the editing operation is completed through a call to EndEdit . You would typically want to implement this interface if your object has co-dependent properties, or if you need to validate multiple property values before declaring an edit to be a legal combination of values.

If you understood the discussion of the ICancelAddNew interface, then understanding IEditableObject should be easier. This interface lets you support the same kind of transactional semantics at the individual data item level for modifying an object in the collection. If you have ever edited a row in a data table that is bound to a grid as well as to other controls on a form, you have seen this interface in action. If you change a column value in the grid and then tab to another column, that changed value isn’t reflected in other bound controls on the form until the focus leaves the row that you are editing (typically by setting the current row to another row). If you press the Esc key before moving the focus off the row, the changes you made are rolled back and the old values are put back into the columns that you edited. If you switch the focus to another row, the changes to the row being edited are committed at that point, and the other bound controls on the form will then be updated to reflect the new values. This is because the edits to the column (property) values are all treated as part of a single editing transaction against the object, and the edits aren’t considered complete from the perspective of other controls until the editing operation is explicitly committed. This happens through a call to the object’s EndEdit method, either because the grid calls this method when the row is changed or because you explicitly call EndEdit to commit the changes.

The IEditableObject interface includes three methods: BeginEdit, EndEdit, and CancelEdit. None of these take any parameters, and they don’t return anything.

This interface is useful when an object is going to be edited by a data-bound control or other client code; that code can then check to see if the object implements this interface. If so, it can call BeginEdit before starting to modify the object, and either call CancelEdit to roll back the changes or EndEdit to commit them. The object that implements this interface is expected to maintain a temporary copy of the property values being edited so that it can roll back to the original version if CancelEdit is called. Chapter 9 shows an example of implementing this interface.

The INotifyPropertyChanged Interface: Publishing Item Change Notifications

The INotifyPropertyChanged interface lets an object notify its container any time a property on the object changes. This allows the containing collection to raise a ListChanged event when an item in the collection has one or more of its property values changed. The BindingList<T> class uses this interface to bubble up ListChanged events when contained objects are edited either through programmatic code or data-bound control. This results in consistent synchronization of object values that are bound to multiple controls.

The IBindingList interface defines a ListChanged event that is designed to notify a collection’s client when anything about the list has changed. One kind of change it is designed to support is modifications to the data items that are contained in the collection. However, there needs to be a way for the list itself to be notified when an item within the collection changes. A control can index into a collection and obtain a direct reference to an object. The control can use the object in a variety of ways and maintain the reference to it for a long period of time. Other controls also bound to that object will want to know when the object has changed so they can refresh the way they are rendered or react to the change. The INotifyPropertyChanged interface provides a contract for objects to notify their containers that they have changed, so the container can bubble that information up to any bound controls.

It is a very simple interface. It defines a single event member, PropertyChanged, of type PropertyChangedEventHandler. The event signature includes the usual object as the first parameter for the sender and a PropertyChangedEventArgs second parameter. The PropertyChangedEventArgs is itself simple: it tells the name of the property that was changed. Once you have been notified that a particular property has changed, your consuming code can refresh whatever dependencies you have on that property. The main consumer of this interface is the BindingList<T> class, and it uses this to raise ListChanged events to any bound controls, or to the BindingSource component when properties are modified on the collection’s items. You will see this interface in action in the samples in Chapter 9 as well.

The IDataErrorInfo Interface: Providing Error Information

The IDataErrorInfo interface lets an object store and expose error information that can be used by bound controls to notify the user when errors were encountered while using the object. It exposes a top-level error message for the object as a whole, as well as an indexer that can expose per-property error messages. Controls such as the DataGridView can use this information to expose in-place error indications and messages to the user.

When a control is bound to a collection of data items, and the data items can be modified either by the control itself or through other code in the application, things can go wrong. Someone could try to stuff a value of an inappropriate type into a business object’s loosely typed property. Someone could pass in a value that is outside of an allowable validation range. An error could occur when the value contained in the object is used to try to persist the data to a database. In any of these situations, the bound control might want to know that an error occurred and may be designed to present some information to the user about the error.

A good example of this is a DataGridView control that is data bound to a DataView. If an error occurs in any of the columns of a row in the underlying data table, the DataRow class can store the error information in its Errors collection. When this occurs, it stores not only what the error was, but specifically what column within the row was affected by the error. When an error occurs in a data row, it is reflected in the grid with an error icon next to the offending cell. When a user’s mouse cursor hovers over an error icon, the error message for the problem that occurred displays in that column in that row.

This all happens when a DataRowView (the row objects within a DataView) implements the IDataErrorInfo interface, and the DataGridView is coded to look for that interface on the data items in any collection it is bound to. If the grid sees that the data items it is presenting in the grid’s rows implement this interface, the grid will use the interface’s properties shown in Table 7.15 to determine if any of the columns have errors that should be presented or whether the object itself has a general error to be displayed.

TABLE 7.15: IDataErrorInfo Properties

Image

By implementing this interface on your custom data objects, and storing error information at the object or property level that you expose through these interface properties, you enable data-bound controls to provide a richer error-handling experience to the user. Likewise, if you implement a custom data-bound control, you can check the object your control is bound to for an implementation of this interface, and then use the information you obtain and present it to the user in some form that makes sense for your control.

The ICustomTypeDescriptor Interface: Exposing Custom Type Information

The ICustomTypeDescriptor interface lets an object provide custom type information for the information it wants to expose publicly, so that consuming code can ask the object to describe itself, rather than using raw reflection against the type definition. If you don’t implement this interface, the TypeDescriptor class can describe the public properties that are defined on your object using reflection for data binding and other purposes. But if you implement this interface, you can take control of providing the PropertyDescriptors to the TypeDescriptor class yourself. By doing this, you can expose things for data-binding purposes that may not even be declared as public properties on your class, and you can hide properties that you don’t want exposed to code that doesn’t have explicit type information about your object. The DataView does this to expose child row collections in some other table that are defined through a DataRelation as child collection property on the DataView.

The ICustomTypeDescriptor interface isn’t one that you should normally have to implement. But if you need to take explicit control over which properties are exposed through the TypeDescriptor class when it reflects on your object type, then implementing this interface gives you that control. When the TypeDescriptor class goes to obtain the properties implemented on an object, it first checks to see if that object type implements the ICustomTypeDescriptor interface. If so, it will ask the object to provide its own PropertyDescriptors through this interface’s GetProperties method.

In fact, the ICustomTypeDescriptor interface goes well beyond just allowing you to describe your properties. When you implement this interface, you have to provide implementations for all of the methods shown in Table 7.16, most of which won’t be directly used in data-binding scenarios.

TABLE 7.16: ICustomTypeDescriptor Interface Methods

Image

Image

The ISupportInitialize Interface: Supporting Designer Initialization

The ISupportInitialize interface lets controls defer acting on values set on interdependent properties until the container tells the control that all values have been set. This avoids having a control try to take actions based on a property’s setting if those actions might fail if another property needs to be set first, and those properties can be set in any order. The Windows Forms designer uses this interface, so the code it generates to set properties doesn’t need any insight into the correct order to set interdependent properties.

Sometimes components have interdependent properties that all need to be logically set at the same time for things to work correctly. But because only one line of code can execute at a time, supporting this notion of having multiple properties set simultaneously becomes a problem. For example, if you specify a DataMember property for a BindingSource component or a DataGridView control, that property provides information about what part of the object that you set as the DataSource property should be used for data binding. Any change to the DataMember property necessitates refreshing the data bindings. However, the DataMember property doesn’t understand this unless the DataSource has been set first. You can’t be sure that they will be set in the right order, with DataSource first and then DataMember second. Also, what if you want to take a component or control that was already bound to some other data source and change it to a new data source? You might change the DataSource property first, or you might change the DataMember property first. When the designer writes code for you based on interactions in the designer such as selections in Smart Tags or setting properties in the Properties window, there is no way to be sure what order the code will be written to initialize those properties. So there needs to be a way to signal a control or component to tell it that you will be entering a period of initialization, and then notify it again when you are done with that period of initialization. If you can do that, then the component can defer enforcing any interdependencies or using the values of any of the properties until you signal it that initialization is complete. This is precisely what the ISupportInitialize interface is designed for.

ISupportInitialize defines two methods: BeginInit and EndInit. Neither takes any parameters or returns anything; they are just signal methods to the implementing class of when initialization is starting and when it is complete from some consuming code’s perspective. The Visual Studio designer is aware of this interface and looks for it on any component or control that you drag and drop onto a designer surface. If it sees that something you added through the designer implements this interface, the designer adds calls to BeginInit and EndInit that bracket the setting of any properties for that component in the designer-generated code. Doing so ensures that the order that properties are set by the designer is not important, just that it properly signals when it is starting to set properties, and when it is done setting them. The following code shows a trimmed down version of the InitializeComponent method from the designer code file for a form in a Windows Forms application.

private void InitializeComponent()
{
  // Code to create component instances omitted...

  // BeginInit calls
  ((System.ComponentModel.ISupportInitialize)
      (this.bindingSource1)).BeginInit();
  ((System.ComponentModel.ISupportInitialize)
      (this.northwindDataSet1)).BeginInit();

  // Property initialization—order not important
  this.bindingSource1.DataMember = "Customers";
  this.bindingSource1.DataSource = this.northwindDataSet1;
  this.northwindDataSet1.DataSetName = "NorthwindDataSet";

  // EndInit calls
  ((System.ComponentModel.ISupportInitialize)
      (this.bindingSource1)).EndInit();
  ((System.ComponentModel.ISupportInitialize)
      (this.northwindDataSet1)).EndInit();
}

Notice that it calls BeginInit on each component at the beginning of the initialization phase (after casting the component variable to the ISupportInitialize reference type), then sets properties, and then calls EndInit. Doing this allows the control or component to internally defer acting on the properties that are being set until EndInit is called, which avoids the challenges of setting interdependent properties in the correct order.

Listing 7.2 shows a simple implementation of ISupportInitialize on a class that contains a string collection. For demonstration purposes, the class is designed to support initialization by caching any values that are set on the StringCollection property until initialization is complete. To support this, the class does a number of things.

•    It implements the ISupportInitialize interface and its methods BeginInit and EndInit.

•    Member variables are defined to hold the primary string collection that the class encapsulates, as well as a flag to indicate when the class is being initialized, and another string collection to hold onto a temporary copy of a value that is being set for the string collection during initializing.

•    The StringCollection property sets block checks to see if the class instance is being initialized through the flag, and if so, places any values set for that property into a temporary copy. If the class isn’t initializing, then it just writes the value into the primary string collection member variable.

•    The implementation of BeginInit sets the flag to indicate to the rest of the class that it is in initialization mode.

•    The EndInit method copies the reference to the last value set for StringCollection from the temporary variable into the primary string collection variable and resets the flag.

You will see a real-world implementation of ISupportInitialize in Chapter 8 for a custom data-bound control.

LISTING 7.2: ISupportInitialize Implementation


public class SomeContainerClass : ISupportInitialize
{
  private List<string> m_Data = null;
  private bool m_Initializing = false;
  private List<string> m_TempData = null;

  public List<string> StringCollection
  {
    get
    {
      return m_Data;
    }
    set
    {
      if (m_Initializing)
         m_TempData = value;
      else
         m_Data = value;
    }
  }

  void ISupportInitialize.BeginInit()
  {
    m_Initializing = true;
  }

  void ISupportInitialize.EndInit()
  {
   m_Data = m_TempData;
   m_Initializing = false;
  }
}


The ISupportInitializeNotification Interface: Supporting Interdependent Component Initialization

The ISupportInitializeNotification interface lets interdependent child objects be notified when other objects have completed initialization. This allows an object that depends on another object’s state to wait until the other object has completed its own initialization before the dependent object tries to complete its own initialization.

The ISupportInitialize interface just discussed helps you work with components with interdependent properties on a single component. But what if you have multiple components that are interdependent in terms of the order that those components are initialized? For example, when you set up data binding, you often bind a control to a BindingSource, and then bind the BindingSource to a data set. The properties being set during initialization on the BindingSource will probably reference a table in the data set. But the table in the data set may be getting created as part of the initialization steps for the data set. So if EndInit is called on the binding source before EndInit is called on the data set, the data set won’t have completed the initialization that makes that table available to the binding source. Therefore, when the binding source tries to start iterating over the data in the table, users will get an error message because the referenced table isn’t there.

To make this more concrete, let’s use the SomeContainerClass from Listing 7.2 as a data source for a binding source. The following code shows a Load event handler for a form that uses the initialization methods of the components, but calls EndInit in the wrong order, with unexpected results.

private void OnFormLoad(object sender, EventArgs e)
{
   SomeContainerClass dataContainer = new SomeContainerClass();
   ISupportInitialize bindSourceInit = m_BindingSource;
   ISupportInitialize dataInit = dataContainer;

   bindSourceInit.BeginInit();
   dataInit.BeginInit();

   dataContainer.StringCollection = new List<string>();
   m_BindingSource.DataSource = dataContainer;
   m_BindingSource.DataMember = "StringCollection";

   // Binds against the null default value for the collection,
   // not the collection just set above
   bindSourceInit.EndInit();
   // Now the new string collection is set on the container class,
   // but binding is already complete, so unexpected results
   dataInit.EndInit();
}

The problem with this code is that because EndInit is called on the binding source before the data object, the binding will be done against incomplete initialization of the data object.

What’s needed here is a way for interdependent objects like this to ensure that they get initialized in the correct order. The ISupportInitializeNotification interface is new in .NET 2.0, and it’s designed to address these kinds of initialization order dependencies.

Specifically, the ISupportInitializeNotification interface allows one component (call it ComponentA) to ask another component (ComponentB) to notify it when ComponentB has completed its initialization. This allows ComponentA to wait until ComponentB is done with its initialization before ComponentA completes its own initialization.

ISupportInitializeNotification defines one property and one event. The IsInitialized property returns a Boolean value that indicates whether the component that implements the interface has completed initialization. The Initialized event, of type EventHandler, is fired by the implementing component when initialization completes. So if a component might depend on another component’s initialization, the first component can check that other component for this interface’s implementation. If it finds that interface, it can see if the component is already initialized, and if not, can subscribe to the Initialized event to be notified when that occurs. The code in Listing 7.3 shows an example where ISupportInitializeNotification is used in the EndInit method for the BindingSource component.

LISTING 7.3: Use of ISupportInitializeNotification in BindingSource


void ISupportInitialize.EndInit()
{
   // See if data source implements ISupportInitializeNotification
   ISupportInitializeNotification notification1 =
      this.DataSource as ISupportInitializeNotification;

   // If so, and not initialized
   if ((notification1 != null) && !notification1.IsInitialized)
   {
      // Subscribe to notification event
      notification1.Initialized += new
         EventHandler(this.DataSource_Initialized);
   }

   else
   {
      EndInitCore(); // Complete initialization
   }
}

// End initialization event handler
private void DataSource_Initialized(object sender, EventArgs e)
{
   ISupportInitializeNotification notification1 =
       this.DataSource as ISupportInitializeNotification;
   if (notification1 != null)
   {

      // Unsubscribe from the event—one time process
      notification1.Initialized -=
         new EventHandler(this.DataSource_Initialized);
   }
   // Complete initialization now
   this.EndInitCore();
}


The comments I added in Listing 7.3 describe what is going on. When EndInit is called on the BindingSource component, it checks the object that was set as its DataSource to see if it implements the ISupportInitializeNotification interface (through an attempted cast with the as operator). If so, it checks the object through the IsInitialized property to see if it has already completed initialization. If not, it then subscribes to the object’s Initialized event and does no further work in EndInit. If the data source object has already completed initialization or doesn’t implement the interface, then the method completes the initialization process by calling the EndInitCore method, which is where the real work of completing initialization is done. If the object did support the interface and indicated that it wasn’t complete with its own initialization, then BindingSource waits until the object fires the Initialized event to complete its own initialization through EndInitCore. In addition, the event handler for Initialized unsubscribes from that event, since it shouldn’t fire more than once in a given initialization scenario.

So if you implement a class that can be used as a data source, and that class requires an ISupportInitialize implementation to control interdependencies among properties, then you should also implement ISupportInitializeNotification, return false from the IsInitialized property while you are in the initialization process (signaled by a call to your BeginInit method), and fire the Initialized event to any subscribers when you are done with initialization (signaled by a call to your EndInit method).

The ICurrencyManagerProvider Interface: Exposing a Data Container’s CurrencyManager

The ICurrencyManagerProvider interface lets a container indicate whether it provides its own currency manager for any contained data collections. This interface is implemented by the BindingSource component, and you shouldn’t have to implement it yourself. However, you may occasionally need to use this interface to access the CurrencyManager for a container from a custom bound control.

The ICurrencyManagerProvider interface has two members: a method named GetRelatedCurrencyManager and a property named CurrencyManager. The CurrencyManager property is the main thing you’ll use to get a reference to the underlying currency manager to subscribe to change notifications on that currency manager. (An example using this is shown in Chapter 8 on the FilteredGrid control.) The GetRelatedCurrencyManager method lets you specify a data member parameter, which lets you get a child collection’s currency manager in a master-details data-binding scenario.

Where Are We?

This chapter has covered all the major data-binding interfaces that you might need to either implement or consume. I presented them from the perspective of describing the contract that the interface represents, followed by the details represented by the interface’s members. I used a few examples to describe and demonstrate the more complex concepts, and also described some things in terms of the implementations and use of the interfaces as they exist in the .NET Framework controls and collections that you should already be getting familiar with, such as the DataView and DataGridView. There are a few more interfaces at work behind the scenes, such as the IBindableComponent interface implemented on the base Control class of Windows forms. But because you really shouldn’t have to mess with these interfaces directly, I didn’t bother going into any detail on them.

Some key takeaways from this chapter are:

•    IList is the minimum interface implementation that lets you bind a collection directly to a control.

•    The BindingSource component can iterate through a collection that only implements IEnumerable and add the items to its own internal list collection, so IEnumerable is sufficient to support data binding through a BindingSource.

•    IBindingList is the minimum support you should strive for on a data-bound collection, because it allows synchronization between bound controls when items are added or removed from the collection.

•    INotifyPropertyChanged is important for rich data binding on custom objects—it lets the UI stays up to date when the objects are modified through programmatic code.

•    PropertyDescriptor objects are the key to runtime getting and setting of property values on a data object, regardless of whether that object is a row in a data table or a custom business object in a custom collection.

To really gain a full appreciation of these interfaces and how to use them, you have to assume one of two perspectives—that of the interface’s consumer or that of the interface’s implementer. The consumer could be any form of client code that interacts with the collection, but will often be data-bound controls. The implementer will be a collection, a data object, or a control.

The next two chapters further explore these different perspectives. Chapter 8 shows how to implement custom data-bound controls and uses some of the interfaces defined in this chapter to consume the data to which they are bound. Chapter 9 shows how to implement custom collections and objects that support rich data binding, and how to implement all of the collection- and object-level interfaces described in this chapter.

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

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