[ Team LiB ] |
10.1 Data Sources and BindingsA data source is any object that provides information. The object could be of any type—it could be an ADO.NET DataSet, but it could also be a class that you have defined or a standard .NET array. All data sources provide one or more pieces of information. We can arrange for these pieces of information to be displayed by binding them to controls. Consider the simple class shown in C# in Example 10-1 and in VB in Example 10-2. It has just two properties, and as you can see, there is nothing unusual about the code. But despite its simplicity, it is able to act as a data source thanks to the very flexible nature of the Windows Forms binding architecture. Example 10-1. A simple class with two properties, written in C#public class MySource { public string Name { get { return nameVal; } set { nameVal = value; } } private string nameVal; public int Age { get { return ageVal; } set { ageVal = value; } } private int ageVal; } Example 10-2. A simple class with two properties, written in VBPublic Class MySource Private nameVal As String Private ageVal As Integer Public Property Name() As String Get Return nameVal End Get Set nameVal = Value End Set End Property Public Property Age() As Integer Get Return ageVal End Get Set ageVal = Value End Set End Property End Class Example 10-3 shows some C# code from a simple form containing two TextBox controls. (The standard InitializeComponent and Dispose methods have been omitted because they contain nothing unusual in this example.) Notice that the form also has a private field containing an instance of the MySource class we defined in Example 10-1. This object will act as a data source. The form's constructor binds the Text properties of the two TextBox controls to the Name and Age properties of the data source by adding appropriate entries to ControlBindingsCollection exposed by the control's DataBinding property. Example 10-3. Binding controls to a simple data sourcepublic class MyForm : System.Windows.Forms.Form { private System.Windows.Forms.TextBox txtName; private System.Windows.Forms.TextBox txtAge; private MySource myDataSource = new MySource(); public MyForm() { InitializeComponent(); myDataSource.Name = "Foo"; myDataSource.Age = 42; txtName.DataBindings.Add("Text", myDataSource, "Name"); txtAge.DataBindings.Add("Text", myDataSource, "Age"); } . . . } The ControlBindingsCollection class's Add method is overloaded. Here we are using the form that takes the name of the control property to be bound, the data source, and the name of the property on the data source. The Add method creates an instance of the Binding class (passing the three parameters directly in to its constructor) and places it in the collection. The other overload of the Add method takes a Binding object as a parameter. So the call to Add in Example 10-3 is equivalent to calling: txtAge.DataBindings.Add(new Binding(("Text", myDataSource, "Age")) The result of this is that whenever the user edits the text in one of the TextBox controls, the relevant property of the object will be updated to reflect the change. This connection between a control property and a data source property is called a binding. Any property of any control can be bound to any property of any data source, although the Text property is the most common binding target because it will display the relevant data on screen. But because any property is a valid target, including any properties you define for your own controls, a single control could have many bindings associated with it. (In theory, it could have one for each property, although for certain properties such as BorderStyle or TabIndex, binding would only make sense if you were using it to store form layout information in a database to support user-customization.) However, any single control property cannot be bound to more than one data source. Although a given control property can be bound to at most one data source property, the converse is not true: a single data source property can be bound to any number of control properties. For example, we could modify the example above to have two text boxes, each of which has its Text property bound to the Name property of the same MySource object. If the user edits one of these text boxes, the change will automatically be propagated to the other—Windows Forms tracks all the active data bindings for a given object, so it makes sure that when one control's property values change, any other control properties bound to the same source property are updated to reflect the change. This raises an interesting question: what if you want to change the data source's value yourself? If you write some code that modifies the value of a property of a data source, it is useful to be able to make sure that any bound control properties will reflect the update. If you are using the ADO.NET DataSet as your data source, it will automatically notify Windows Forms of these updates. But a simple class such as that in Examples Example 10-1 and Example 10-2 will not do this. It is possible to modify our class so that it raises special events known as property change notifications. This will ensure that whenever we change a property from our code, that change will be reflected in any bound controls. The data-binding architecture defines a standard idiom for doing this: whenever it binds to a property, it looks for an associated event whose name will be the property's name with Changed appended. So if binding to the Name property, it will look for a NameChanged event. If such an event exists, it will register an event handler, and every time the event is raised, it will refresh any controls that are bound to this property. Examples Example 10-4 and Example 10-5 show an appropriately modified version of the MySource class from Examples Example 10-1 and Example 10-2. Example 10-4. A data source with property change notifications in C#public class MySource { public event EventHandler NameChanged; protected virtual void OnNameChanged() { if (NameChanged != null) NameChanged(this, EventArgs.Empty); } public string Name { get { return nameVal; } set { if (nameVal != value) { nameVal = value; OnNameChanged(); } } } private string nameVal; public event EventHandler AgeChanged; protected virtual void OnAgeChanged() { if (AgeChanged != null) AgeChanged(this, EventArgs.Empty); } public int Age { get { return ageVal; } set { if (ageVal != value) { ageVal = value; OnAgeChanged(); } } } private int ageVal; } Example 10-5. A data source with property change notifications in VBImports System Public Class MySource Private ageVal As Integer Private nameVal As String Public Event NameChanged(sender As Object, e As EventArgs) Public Event AgeChanged(sender As Object, e As EventArgs) Public Property Name() As String Get Return nameVal End Get Set If nameVal <> Value Then nameVal = Value OnNameChanged() End If End Set End Property Public Property Age() As Integer Get Return ageVal End Get Set If ageVal <> Value Then ageVal = Value OnAgeChanged() End If End Set End Property Protected Sub OnNameChanged() RaiseEvent NameChanged(Me, EventArgs.Empty) End Sub Protected Sub OnAgeChanged() RaiseEvent AgeChanged(Me, EventArgs.Empty) End Sub End Class Property change notification events always use the standard EventHandler delegate type; that is, the event handler takes two parameters. The first is an Object instance representing the event's sender. The second is an EventArgs instance containing information about the event. Here we have also used the standard event handling idiom, where for each public event there is an associated protected method that raises the event. So the NameChanged event is always raised by calling the OnNameChanged method and AgeChanged is raised by calling OnAgeChanged. These methods are not mandatory—the data binding architecture doesn't care how we raise the events—but this style, which we also use for events raised by controls, provides derived classes with a hook into the event handling. With the code for raising property change notifications in place, our data source in Example 10-4 will always be displayed correctly, even when it is modified directly by code such as that in Example 10-6. Example 10-6. Modifying a data sourceprivate void GetOlder() { myDataSource.Age += 1; } But what if we are unable to change the data source's implementation? We might want to bind to a class that we did not write, and that does not provide property change notifications. One of the aims of the data-binding architecture is to be able to use any object as a data source, so it is possible to force the display to be updated even if the object in question does not raise property change notifications. To achieve this, a little more work is required, but first we must understand how Windows Forms tracks bound data sources. 10.1.1 Binding ManagersFor every distinct data source in use on a form, Windows Forms creates a binding manager.[1] This is an object that acts as a kind of clearing house for all changes to the data source. It knows about all the data bindings for the source, which is what enables changes made by one control to be propagated to any other controls bound to the same source. If we want updates to be pushed out this way when the data source does not provide property change notifications, we too must use the binding manager.
It is easy to get hold of the binding manager for a particular data source. The Form class has a property called BindingContext, which is a collection containing all the binding managers for the whole form. (So far we have only got one data source, but it is possible to have several distinct data sources on a single form, each of which would have its own binding manager.) It is an indexed collection, so to acquire the binding manager for the source, we just pass in the source itself as an index, as shown in Example 10-7. Example 10-7. Acquiring a binding manager// C# BindingManagerBase bindMgr = BindingContext[myDataSource]; ' VB Dim bindMgr As BindingManagerBase = BindingContext(myDataSource) BindingManagerBase is the base class from which all binding managers derive. The exact type of manager returned will depend on the nature of the data being bound to. (In this case, it will be a PropertyManager, which is the manager for binding directly to properties of a single object.) Having got a reference to the binding manager for our data source, we need to make it refresh all the control properties that are bound to it. Unfortunately, there is no method designed to do this, but there is one that has this side effect: CancelCurrentEdit. As with Example 10-6, the method in Example 10-8 modifies a property on the data source. Example 10-8. Using CancelCurrentEdit to make property changes visibleprivate void GetOlder() { myDataSource.Age += 1; BindingContext[myDataSource].CancelCurrentEdit(); } Because Example 10-8 assumes that the data source does not provide property change notifications, it explicitly forces that change to be propagated to all bound controls. The use of CancelCurrentEdit might be regarded as a hack—this method is intended for when the user has started to modify some data in a text field, but then has a change of heart. Calling CancelCurrentEdit pushes the value stored in the source out to any bound control properties, overwriting any edits that the user might have made. We are not quite using this method as intended—we have modified the data source directly and want that change to be pushed out. But it has the effect that we require. If at all possible you should use data sources that provide change notifications. This will mean that you don't have to remember to force an update every time you change a property. 10.1.2 List SourcesSo far we have just used a single object as a data source. However, many data sources return lists of information, database tables being a particularly important example. The data-binding architecture therefore has support for binding to lists of data as well as to individual items. As with single data sources, Windows Forms is extremely flexible about what kinds of list-like sources it will bind to—it can bind to lists of data contained in any object that implements the IList interface. Because all arrays implement this interface, we can bind to an array of the simple class defined earlier, as shown in the C# code in Example 10-9 and in the VB code in Example 10-10. Example 10-9. Binding to an array of simple objects using C#public class MyForm : System.Windows.Forms.Form { private System.Windows.Forms.TextBox txtName; private System.Windows.Forms.TextBox txtAge; private MySource[] myDataListSource; public MyForm() { InitializeComponent(); myDataListSource = new MySource[10]; for (int i = 0; i < myDataListSource.Length; ++i) { myDataListSource[i] = new MySource(); myDataListSource[i].Name = "Foo" + i; myDataListSource[i].Age = 20 + i; } txtName.DataBindings.Add("Text", myDataListSource, "Name"); txtAge.DataBindings.Add("Text", myDataListSource, "Age"); } . . . } Example 10-10. Binding to an array of simple objects using VBPublic Class MyForm : Inherits Form Private WithEvents txtName As TextBox Private WithEvents txtAge As TextBox Dim myDataListSource(10) As MySource Public Sub New() MyBase.New() InitializeComponent() ReDim myDataListSource(10) Dim i As Integer For i = 0 to myDataListSource.Length - 1 myDataListSource(i) = new MySource() myDataListSource(i).Name = "Foo" + CStr(i) myDataListSource(i).Age = 20 + i Next txtName.DataBindings.Add("Text", myDataListSource, "Name") txtAge.DataBindings.Add("Text", myDataListSource, "Age") End Sub . . . End Class The code in Examples Example 10-9 and Example 10-10 is almost identical to that in Example 10-3, the only difference being that we have replaced a single MySource object with an array of MySource objects. As it stands, it is not very interesting, because it will only allow the first object in the array to be edited. However, the binding manager for our data source provides us with ways of iterating through the list. We might expose this by adding Previous and Next buttons to the form. Examples Example 10-11 and Example 10-12 show Click event handlers for Previous and Next buttons, which allow the user to move back and forth through the list. They work by acquiring the binding manager for the data source and adjusting its Position property. The Position property determines which list item's properties are currently displayed in the bound controls. (The binding manager ignores this property when binding to a single object as in Example 10-3.) Example 10-11. Scrolling through items in a bound list using C#private void btnPrevious_Click(object sender, EventArgs e) { BindingManagerBase bm = BindingContext[myDataListSource]; if (bm.Position == 0) return; bm.Position -= 1; } private void btnNext_Click(object sender, EventArgs e) { BindingManagerBase bm = BindingContext[myDataListSource]; if (bm.Position == bm.Count - 1) return; bm.Position += 1; } Example 10-12. Scrolling through items in a bound list using VBPublic Sub btnPrevious_Click(sender As Object, e As EventArgs) _ Handles btnPrevious.Click Dim bm As BindingManagerBase = BindingContext(myDataListSource) If bm.Position = 0 Then Return bm.Position -= 1 End Sub Public Sub btnNext_Click(sender As Object, e As EventArgs) _ Handles btnNext.Click Dim bm As BindingManagerBase = BindingContext(myDataListSource) If bm.Position = bm.Count - 1 Then Return bm.Position += 1 End Sub Because the Position is a property of the binding manager, and each data source has exactly one binding manager, all controls bound to a particular data source will reflect the property values for the same list entry at any given time. So adjusting the position on our example will cause the two bound TextBox controls to be updated. If you examine the binding manager returned for lists of data, you will see that it is of type CurrencyManager. This is because it is responsible for keeping track of the current position. (It doesn't have anything to do with money.) Remember that in Example 10-6, we wrote a method that modified one of the data source's properties. This example now needs to be modified to work in the context of a list. It must determine which particular item in the list it should modify. This is easy to do because the BindingManagerBase class provides a Current property that returns the current item from the list. We can also use the Position property as an index into the array. We will use the latter here because otherwise we would need to cast the object returned by Current back to MySource. As with a single data source, if a list entry's class provides property change notification events, the modification that Example 10-13 makes to the Age property will automatically be propagated to any bound controls. But if you are working with a data source that does not supply such events, once again you will need to provide manual notification of the updates. Although the CancelCurrentEdit technique shown in Example 10-8 will work, a slightly more elegant solution is available when binding to a list; it is shown in Example 10-14. Example 10-13. Modifying the current item in a listprivate void GetOlder() { int pos = BindingContext[myDataListSource].Position; MySource src = myDataListSource[pos]; src.Age += 1; } Example 10-14. Modifying a list item that does not provide property change notificationsprivate void GetOlder() { CurrencyManager cm = (CurrencyManager) BindingContext[myDataListSource]; MySource src = myDataListSource[cm.Position]; src.Age += 1; cm.Refresh(); } In Example 10-14, we are exploiting the fact that the binding manager will always be an instance of the CurrencyManager class when dealing with a list-like source. And unlike PropertyManager, CurrencyManager provides a Refresh method to push modifications out to any bound controls. The Refresh method can also be useful even if your data source class provides property change notifications. If you want to add or remove items from the list, the fact that the objects in the list raise notifications when individual properties are changed will not be sufficient to notify the binding architecture that the list now has new entries. This is particularly important if you are using controls that can display information from all the entries in the list at once and not just the current one. (We will see how to do this shortly.) Certain data sources (notably the DataSet) can raise automatic notifications when the contents of the list change as well as when individual entries' values change, but simple arrays will not do this, nor will the ArrayList class. So when adding, removing, or replacing objects from such data sources, you should always call the Refresh method to inform the CurrencyManager that the list's contents have changed. It is not clear why the Refresh method is defined by CurrencyManager instead of its base class, BindingManagerBase—it would be useful in all binding scenarios, not just list-based binding. (This is precisely the method we wanted earlier when we had to resort to the less satisfactory CancelCurrentEdit.) |
[ Team LiB ] |