Summary: Rocky Lhotka shows you how to write code that you can add to your business and collection classes to better support the features of Windows Forms data binding. (19 printed pages)
Download the vbobjectbinding.exe sample file.
Microsoft worked hard to make data binding useful when creating both Windows Forms and Web Forms interfaces. For the first time since data binding was introduced to Microsoft Visual Basic® years ago, it is truly practical in a wide range of application scenarios.
One key advancement is that data binding supports not only the DataSet, but also objects, structures and collections of objects, or structures. The basic ability to bind an object or collection to a control on a Windows Form or Web Form requires no extra work on our part. It is an automatic feature of data binding.
Web Forms data binding is read-only. In other words, it pulls data from the data source (DataSet, object, or collection) and uses the data to populate controls that are then rendered to the client device. This is straightforward behavior and requires no extra work on our part as we create either objects or the UI.
Windows Forms data binding is read-write, and therefore more complex. In this case, data is pulled from the data source and displayed in our UI controls, but any changes to the values in the controls are automatically updated back into the data source as well. Much of this behavior is automatic and requires no extra work on our part, but there are a number of features available to us if we choose to write some extra code.
That is the focus of this article-code we can add to our business and collection classes to better support the features of Windows Forms data binding. These features include:
For simple objects, we can implement events to notify Windows Forms data binding when our property values have changed. By adding these events, we enable the UI to automatically refresh its display any time the data in our object is changed. We also need to understand how to notify the UI that a validation rule has been broken by newly entered data. Improper implementation of validation can make data binding behave in undesirable ways.
In addition, there are a number of optional features we can support in our collections. Collections are typically bound to list controls such as the DataGrid. By properly implementing strongly-typed collection objects, we enable the DataGrid to intelligently interact with our collection and its child objects. We can also implement the IBindingList interface so our collection can intelligently interact with the DataGrid in various ways.
Finally, there are some optional features we can support in objects that will be contained within a collection. We'll call these child objects. Child objects can implement the IEditableObject interface so the DataGrid can properly interact with the object during in-place editing. The child objects can also implement IDataErrorInfo so the DataGrid can mark the line as being in error if any validation rules are broken as the data of the object is changed.
In the end, by adding a bit of code to our classes and collections, we can take advantage of some very powerful features of Windows Forms data binding.
Simply binding properties of an object to properties of controls on a form is not complex. For instance, consider the following simple Order class:
Public Class Order Private mID As String = "" Private mCustomer As String = "" Public Property ID() As String Get Return mID End Get Set(ByVal Value As String) mID = Value End Set End Property Public Property Customer() As String Get Return mCustomer End Get Set(ByVal Value As String) mCustomer = Value End Set End Property End Class
The only special code here is in the declaration of the variables:
Private mID As String = ""
Private mCustomer As String = ""
Notice that the variables are initialized with empty values as they are declared. This is not typical code because Visual Basic .NET automatically initializes variables when they are declared.
The reason we explicitly initialize them like this is because if we don't, the data binding will fail. It turns out that the automatic initialization of the variables doesn't occur by the time data binding tries to interact with our object, causing a runtime exception to be thrown when data binding attempts to retrieve the value from the uninitialized variables.
However, explicit initialization of the variables occurs before data binding interacts with our object. This means that the variables are properly initialized by the time data binding retrieves their values so we avoid the runtime exception.
If we create a form similar to the one shown in Figure 1, we can simply bind the properties of the object to the controls as the form loads.
Figure 1. Simple Form layout for the Order object
The code to bind an Order object to the form would look like this:
Private mOrder As Order Private Sub OrderEntry_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load mOrder = New Order() txtID.DataBindings.Add("Text", mOrder, "ID") txtCustomer.DataBindings.Add("Text", mOrder, "Customer") End Sub
The secret lies in the fact that every Windows Forms control has a DataBindings collection. This collection contains a list of bindings between the properties of the control and the properties of our data source (or data sources). An interesting side effect of this scheme is that we can bind properties from a data source to several different control properties. Also, we can bind different control properties to properties from multiple data sources.
Just by using this simple data binding code we can create some pretty complex user interfaces. For example, in the sample code for this article you'll find that we bind the Enabled property of the Save button to an IsValid property on the business object. This way the button is only available to the user when the object is ready to be saved.
Remember that this data binding is bi-directional. Not only is the data from the object displayed on the form, but any changes the user makes to the data is automatically updated back into our object. This occurs when the user tabs off each field. For instance, if the user changes the value in the txtID control, the new data is updated into the object as the user tabs out of the control. The data is updated into our object through the property Set routine. This is nice because it means our existing Property code is automatically invoked; we don't need to do anything extra to support bi-directional data binding.
Now that we've seen how simple data binding an object and the controls is, let's discuss how we can enhance our object to support automatic notification of changed properties. The issue here is that if some other code in our application changes the data in an object, there is no way for the controls in the UI to know about that change. The result is that the UI and object get out of sync and the user won't see the correct data in their display.
What we need is a way for the object to notify the UI any time a property value changes. This is supported through events that we can optionally declare and raise from our object. When we bind a control to a property on our object, data binding automatically starts listening for a property changed event named propertyChanged, where property is the name of the property of the object .
For instance, our Order class defines an ID property. When the ID property is bound to a control, data binding starts listening for an IDChanged event. If this event is raised by our object, data binding automatically refreshes all controls bound to our object.
We can enhance our Order class by declaring these events:
Public Class OrderPublic Event IDChanged As EventHandler
Public Event CustomerChanged As EventHandler
Notice that the events are declared to be of type EventHandler. This is required for data binding to understand the event. If we don't declare the events this way, we'll get a runtime exception when data binding attempts to interact with our object.
The EventHandler is the standard event model used through Windows Forms. It defines the event with two parameters-sender (the object raising the event) and e (an EventArgs object).
With these events declared, we need to make sure to raise these events any time the corresponding property value changes. One obvious place to do this is in the Set routines. For instance, in the ID property we would do this:
Public Property ID() As String
Get
Return mID
End Get
Set(ByVal Value As String)
mID = Value
RaiseEvent IDChanged(Me, New EventArgs())
End Set
End Property
What can be trickier is to remember that any time we change mID
anywhere in our class, we also need to raise this event. Most classes include code that modifies internal variables in addition to property Set routines. We must raise the appropriate event any time a value is changed that will impact a property.
For a better example, let's assume that our Order object will have a collection of LineItem objects. We'll implement the collection a bit later, but right now let's look at the event and variable declarations of the basic LineItem class:
Public Class LineItem Public Event ProductChanged As EventHandler Public Event QuantityChanged As EventHandler Public Event PriceChanged As EventHandler Public Event AmountChanged As EventHandler Private mProduct As String Private mQuantity As Integer Private mPrice As Double
Notice that we have four events, one for each of our properties, but we only have three variables. The Amount property will be calculated by multiplying Quantity and Price:
Public ReadOnly Property Amount() As Double Get Return mQuantity * mPrice End Get End Property
It is a read-only property. However, it can change. In fact, it will change any time that the value of either Price or Quantity changes, and thus we can raise an event to indicate that it has changed. For instance, when the Price changes:
Public Property Price() As Double
Get
Return mPrice
End Get
Set(ByVal Value As Double)
mPrice = Value
RaiseEvent PriceChanged(Me, New EventArgs())
RaiseEvent AmountChanged(Me, New EventArgs())
End Set
End Property
Not only do we raise the PriceChanged event because the Price property changed, but we also raise the AmountChanged event because we've indirectly caused the Amount property to change as well. This illustrates how we must be vigilant in our coding to ensure that these events are raised when appropriate.
That said, it turns out that the AmountChanged event may not be strictly necessary. When we bind controls on a form to properties of an object, data binding listens for propertyChanged events for each property to which our controls are bound. If any one of them is raised, all the controls bound to that object are refreshed.
In other words, if our form has controls bound to both the Price and Amount properties, raising the PriceChanged event will cause data binding to refresh not only the control bound to the Price property, but also the one bound to the Amount property.
The downside to taking advantage of this fact is that the UI becomes tightly bound to the object implementation. If we later decide to only bind to Amount, our UI won't work correctly because no AmountChanged event will be raised. Because of this, it is best to declare and raise a propertyChanged event for each property on our object.
The rest of the code for the LineItem class is contained in the sample code download for this article.
As we've noted, our Order object contains a collection of LineItem objects. In our UI, we can bind a DataGrid to this collection, allowing the user to easily add, remove, or edit the LineItem objects. The resulting UI is shown in Figure 2.
Figure 2. Form with DataGrid bound to collection of objects
We can bind a DataGrid control to an array or collection with one line of code:
Dim arLineItems As New ArrayList()
dgLineItems.DataSource = arLineItems
While this works in that the data from the array is displayed in the grid, it doesn't provide all the features we'd expect from binding to a DataGrid control. This is because the basic array and collection classes don't provide enough information for the DataGrid to work the way it does when it is bound to a DataTable.
We can create our own strongly-typed collection object, which can include some extra code to better support the features of the DataGrid. In particular, we'll end up being able to support the following:
Let's tackle these issues one at a time. The first one, binding a DataGrid to an empty collection, is relatively easy to solve. The problem here is that the DataGrid needs to be able to figure out the columns that are available from the data source. If we bind the DataGrid to a simple array, ArrayList, or collection object, how does it figure this out?
The answer is that it looks at the first item in the collection and then uses reflection to get a list of the Public properties and fields of that item (be it a structure, object, or simple type such as Integer).
If the collection is empty, then this technique won't work and the DataGrid is unable to automatically generate a list of columns. The result is that the control is displayed to the user with no content whatsoever, as shown in Figure 3.
Figure 3. DataGrid bound to empty collection
To fix this, all we need to do is create a custom, strongly-typed collection that has a default, strongly-typed Item property. The DataGrid can use the type of the Item property to get a list of the specific Public properties and fields of that type. Because of this, it no longer needs to look at the first item in the collection to get the list of columns, so we can bind to an empty collection and have it work nicely.
Figure 4. DataGrid bound to empty, strongly-typed collection.
When you compare Figure 3 to Figure 4 it is obvious that the strongly-typed collection is preferable because the DataGrid displays the list of columns, even though no items are present in either case.
Implementing a strongly-typed collection is straightforward. The following steps outline the basic process:
For instance, in the code download you'll find a LineItems class that implements a strongly-typed collection of LineItem objects. The key to allowing the DataGrid to bind to an empty collection is the Item property:
Default Public ReadOnly Property Item(ByVal index As Integer) _ As LineItem Get Return CType(list(index), LineItem) End Get End Property
Note that the property is strongly-typed in that it returns objects of type LineItem. It is also important that it is marked as Default and is a Property, not a Function. Data binding requires this specific declaration in order to work.
In the class from the code download you'll also see that there are strongly-typed implementations of Add and Remove methods, which are part of any strongly-typed collection, but aren't required for data binding to work.
With very little extra coding, we've solved the first issue of allowing the DataGrid to bind to an empty collection. Let's move on and tackle the next couple issues in our list.
Data binding has a formal scheme by which a collection can indicate that the collection has been changed. This is done by having the collection implement the IBindingList interface, which includes a ListChanged event. Any time the collection changes (due to the addition, removal, or change of an item) it should raise this event to tell data binding that the underlying data has changed.
IBindingList supports more than just change notification. The following table lists the optional features we can support by implementing the interface:
Optional feature | Description |
---|---|
Change notification | Notifies the UI of any changes (additions, removals, or edits) to the collection |
Automatic addition of items | Allows the DataGrid to insert new items into the collection as the user moves to the end of the grid |
Automatic removal of items | Allows the DataGrid to remove items from the collection when the user presses the Delete key in the grid |
In-place editing of items | Allows the DataGrid to perform in-place editing of items in the collection (this also requires that the item implement IEditableObject, which we'll discuss later) |
Searching | Enables a Find method to search the collection for a specific item |
Sorting | Enables a Sort method, which sorts the collection on specific columns |
Note that it is up to us to implement each of these functions. The IBindingList interface merely defines the properties, methods, and events. We must write the actual code. Fortunately each of these features is optional. The IBindingList interface defines a set of Boolean properties we use to specify which features our particular collection supports:
We only return True for those features we choose to implement. In the code download, you'll find the LineItems collection that implements change notification, add, edit, and remove. It does not support searching or sorting, so those two properties return False.
Basic implementation of the interface requires that we use the Implements keyword:
Public Class LineItems
Inherits CollectionBase
Implements IBindingList
By adding this statement, we indicate that we're implementing the interface, so we must provide implementations for all the methods defined by the interface. This includes methods that we might not actually want to implement, such as SortDirection (since we're not implementing sorting). The thing is, we don't need to write any code inside the method; we just need to code the shell of the method:
Public ReadOnly Property SortDirection() _ As System.ComponentModel.ListSortDirection _ Implements System.ComponentModel.IBindingList.SortDirection Get End Get End Property
For the methods that we are implementing, we'll write real code.
For instance, we're supporting change notification, so we notify data binding anytime the collection is changed. This means that any time an item is added, removed, or changed, we need to write some code.
The first step is to indicate that we support change notification:
Public ReadOnly Property SupportsChangeNotification() _
As Boolean Implements _
System.ComponentModel.IBindingList.SupportsChangeNotification
Get
Return True
End Get
End Property
We also must declare the ListChanged event as defined by the interface:
Public Event ListChanged(ByVal sender As Object, _ ByVal e As System.ComponentModel.ListChangedEventArgs) _ Implements System.ComponentModel.IBindingList.ListChanged
Then all we need to do is raise this event any time the collection changes. This isn't as hard as it might sound because the CollectionBase class defines a set of methods we can override so we know exactly when the collection has been changed. The ones we need are:
We can simply raise the ListChanged event inside each of these methods. For instance, when an item is inserted, we raise the event:
Protected Overrides Sub OnInsertComplete(ByVal index As Integer, _ ByVal value As Object)RaiseEvent ListChanged( _
Me, New ListChangedEventArgs(ListChangedType.ItemAdded, index))
End Sub
Our collection object will now notify the UI of any changes, so any data bound controls (such as the DataGrid) can accurately reflect the contents of our collection at all times.
The IBindingList interface defines the AllowEdit and AllowRemove properties so we can control whether we want to allow the DataGrid to allow in-place editing and dynamic removal of items in the collection. There are no other IBindingList methods to support these features. They are simply toggles we can use to control what is allowed.
However, if we return True for AllowEdit, then our child objects themselves must support in-place editing by implementing the IEditableObject interface, which we'll discuss later in this article. No exceptions will be thrown if the child objects don't implement the interface, but to get proper behavior as expected by the user they must implement it.
We can also allow the DataGrid to dynamically add new items to the collection. This is a nice feature as it allows the user to simply navigate to the bottom of the grid and they automatically get a new row into which they can add data. This feature is dependant on our support for in-place editing. If we allow adding of items, we must also enable editing of items, so AllowEdit must return True, and our child objects must implement the IEditableObject interface.
IBindingList defines the AllowNew property, which must return True if we support this feature. It also defines an AddNew method that we must implement. This method must include code to create a new child object, add it to the collection, and return it as a result of the method.
For instance, we can create a new LineItem object as follows:
Public Function AddNew() As Object Implements _ System.ComponentModel.IBindingList.AddNewDim item As New LineItem()
list.Add(item)
Return item
End Function
The most common difficulty with this action is that we must be able to create child objects without any user input. The DataGrid itself requests the addition of the child object, so we must be able to programmatically create a new child object on request at any time as shown in the code.
Some object designs require that constructor data be provided as child objects are created, so they are preloaded with information. That type of model won't work in this case because there's no way to get child-specific information from the user before creating the child object.
Once we've implemented AllowNew and AddNew, the DataGrid will allow the user to navigate to the bottom of the grid, and new child objects will be automatically inserted into the collection (and thus the grid) for the user to edit.
As we've discussed, in-place editing requires not only the implementation of the IBindingList interface in our custom collection, but also the implementation of the IEditableObject interface in the child classes.
The IEditableObject interface appears deceptively simple. It merely defines three methods.
Method | Definition |
---|---|
BeginEdit | Called by data binding to indicate the start of an edit process, and that the object should take a snapshot of its current state. |
CancelEdit | Called by data binding to indicate that the edit process is over, and that the object should reset its state to original values. |
EndEdit | Called by data binding to indicate that the edit process is over, and that the object should keep any changed values. |
Implementing this interface requires that we have the ability to take a snapshot of the current state of our object. This typically means somehow making a copy of the values of all the instance variables in our object. There are a variety of ways to make this copy. Unfortunately, that discussion is outside the scope of this article. In this article, we'll simply copy the variable values to another set of variables:
mOldProduct = Product mOldPrice = Price mOldQuantity = Quantity
To restore the values, we simply copy the values the other direction.
It turns out that to properly implement IEditableObject we need to know whether this particular child object has been newly added to the collection. The reason is that we need to remove a new child object from the collection if the user presses the Esc key to cancel the edit. On the other hand, if the user is editing a preexisting child object and they press Esc, we don't want to remove the object from the collection. We just want to restore its state to the previous values.
To solve this issue, we need a variable to track whether the object is new or not. We'll declare a variable named mIsNew and we'll set the value to True when it is first declared:
Private mIsNew As Boolean = True
It will get set to False after the first edit process is complete. This is when CancelEdit or EndEdit are called.
What makes this tricky is that BeginEdit can (and will) be called numerous times during the edit process. The on-line help for the .NET Framework SDK makes it clear that we should only honor the first call, and ignore all others. To do this, we'll declare and use a Boolean variable, mEditing, as follows:
Public Sub BeginEdit() Implements _ System.ComponentModel.IEditableObject.BeginEditIf Not mEditing Then
mEditing = True
mOldProduct = Product mOldPrice = Price mOldQuantity = QuantityEnd If
End Sub
This variable will be set to False in both the CancelEdit and EndEdit methods, so any future edit process will work properly.
The EndEdit method is the easiest to implement because this method is called when the edit process is over and we want to keep any changes to the data. All we need to do is set mEditing to False to indicate that the edit process is over:
Public Sub EndEdit() Implements _
System.ComponentModel.IEditableObject.EndEdit
mEditing = False
mIsNew = False
End Sub
Note that we also set mIsNew to False in this method. At this point we know that the user has accepted the child object, so it is no longer new (at least from the perspective of data binding and the DataGrid).
The CancelEdit method is perhaps the most complex of the three. It not only needs to restore the values of the object to the values we stored when BeginEdit was called, but we also need to see if the child object is new. If the child object is new, we need to ensure that it is removed from the collection.
To tell the collection object that we want to be removed, we'll declare an event:
Friend Event RemoveMe(ByVal LineItem As LineItem)
The event takes a parameter of type LineItem, so we can pass a reference to the child object itself. This way we can make sure that the collection code will know which item to remove.
The CancelEdit code then looks like this:
Public Sub CancelEdit() Implements _ System.ComponentModel.IEditableObject.CancelEdit mEditing = False Product = mOldProduct Price = mOldPrice Quantity = mOldQuantity If mIsNew Then mIsNew = False RaiseEvent RemoveMe(Me) End If End Sub
First, we indicate that the edit process is complete by setting mEditing to False. Next, we restore the state of the object to the values we stored in BeginEdit. Each property value is restored to the previous state. Finally, we check the mIsNew variable to see if this was a newly added child object. If so, we raise the RemoveMe event to tell the collection that it should be removed.
Since our child objects may now raise events to the collection, we need to enhance the code in the custom collection class to handle this event. The first step is to add a method to the LineItems collection class to handle the event. This code will be run when the event is raised:
Private Sub RemoveChild(ByVal Child As LineItem) list.Remove(Child) End Sub
All it does is remove the specified child object from the collection. Notice that there's no Handles clause on this method. How does it get the event? The answer lies in the AddHandler function. This function allows us to dynamically connect an event to an event handler at runtime. It is particularly useful when creating collection classes, as it allows us to hook child object events as each child object is added to the collection.
In the collection class, we already have an OnInsertComplete method where we raise the ListChanged event. We can add the AddHandler call to this method, so it is sure to be invoked for any newly added child object:
Protected Overrides Sub OnInsertComplete(ByVal index As Integer, _
ByVal value As Object)
AddHandler CType(value, LineItem).RemoveMe, AddressOf RemoveChild
RaiseEvent ListChanged( _
Me, New ListChangedEventArgs(ListChangedType.ItemAdded, index))
End Sub
Now, when a new child object is added to the collection, we establish a link between the RemoveMe event of that object and our RemoveChild event handler. When the user presses Esc on a newly added line item, that child object will automatically be removed from the collection through this mechanism.
There's one last optional feature we can add to our child class-the ability to tell the DataGrid when the line item is valid or invalid. This is done by implementing the IDataErrorInfo interface in our child LineItem class.
The IDataErrorInfo interface defines two methods.
Method | Description |
---|---|
Error | Returns text describing what is wrong with this object (an empty string indicates no problem). |
Item | Returns text describing what is wrong with a specific property or field on the object (an empty string indicates no problem). |
The key method is the Error method. The Item method allows us to provide more detailed information if we choose, but the DataGrid keys off the text returned by Error to determine if the line item is valid or not. In the code download, you'll see that I only implemented code in the Error method:
Private ReadOnly Property [Error]() As String Implements _ System.ComponentModel.IDataErrorInfo.Error Get If Len(Product) = 0 Then Return "Product name required" If Quantity <= 0 Then Return "Quantity must be greater than zero" Return "" End Get End Property
This method checks a couple business rules for the line item. We're saying that a line item must have a product name and a quantity that is greater than zero. If either of these conditions is not met, we return text that indicates the nature of the problem. On the other hand, if the object is valid, we return an empty string.
The result is that the DataGrid graphically indicates which line items are valid, as shown in Figure 5 below.
Figure 5. DataGrid indicating the validity of line items
Notice that the Error property is marked as Private. If we make it Public, then the error text will be displayed as a column in the DataGrid so the user can clearly see what is wrong with the line item.
Data binding has finally come of age. The implementation of data binding in both Web Forms and Windows Forms is practical and useful in many cases. One of the biggest benefits is that we can now data bind to objects and collections, not just to the DataSet and related ADO.NET objects.
As we've seen in this article, with a relatively small amount of extra code in our business and collection classes, we can allow Windows Forms data binding to interact with our objects in very rich and powerful ways. No longer are we stuck using pure data technologies in RAD development. Now we can be object-oriented and RAD at the same time!
本文地址:http://com.8s8s.com/it/it45572.htm