Introduction
In the first two articles in this series, we looked at the ADO.NET operations an object-oriented programmer would use to load data from a database to an object model, and the operations used to save the data back to the database. We also explored how to use the data binding capabilities of .NET 2.0 to bind an object model to UI controls in the presentation layer.
We will continue our discussion of both subjects in this, the third and last article in the series. First, we will see how to use ADO.NET to serialize an object model. It turns out that ADO.NET can be very helpful in situations where our model does not fit neatly into a single object graph. Once we have finished with serialization, we will look at how to bind our object model to user controls. It’s not difficult, but it’s not terribly well documented, either. With these two techniques in hand, an object-oriented programmer should be ready for most day-to-day challenges encountered in developing .NET applications.
The Demo Application
The demo application for this article illustrates these two techniques. It is based on the Authors/Books database from Part Two:
But this time, we aren’t going to load the object model from a database—we’re going to load it from an XML file. And instead of editing in the grid, as we did in Part Two, we provide a separate Edit Author Info panel to update author information.
Serializing and Deserializing Object Models
As you are probably aware, .NET classes can be serialized and deserialized by simply marking them as [Serializable]
. Serializable classes can be streamed to binary or XML files using the appropriate Formatter object (which we will call ‘direct serialization’). The technique is covered under Basic Serialization in the .NET Framework Developer's Guide.
The technique works well for simple object models that fit neatly into a single object graph. There are more advanced serialization techniques available for more complex object models, and even models with multiple object graphs can be handled with basic serialization by containing the various root objects within a master root object.
But as serialization users know all too well, serialization can be temperamental, and it isn’t always easy to get working correctly. ADO.NET provides the opportunity for a different strategy—transfer the object model data to an ADO.NET data set, then serialize the data set. This approach can be far easier than fiddling with direct serialization, and it can handle very complex object models without breaking a sweat.
An ADO.NET data set has a built in serializer. We don’t have to instantiate formatters or file streams—we can save a data set to an XML file with a simple command:
dataSet.WriteXml(filePath);
We can load an XML file into a data set just as easily:
DataSet dataSet = new DataSet();
dataSet.ReadXml(filePath);
What makes this approach particularly attractive is the fact that an ADO.NET data set can have as many tables as one needs to capture a complex object model. Each root class can have its own table, and each class contained within that root can have its own table, as well. Root class tables are related to the tables that correspond to their contained classes using DataRelation
objects, as we did in Part Two.
We will discuss serialization and deserialization more fully in our walkthrough of the demo application. For now, let’s turn our attention to the subject of data binding to user controls.
Data Binding to User Controls
When you look at the Presentation layer of the demo application, you will probably notice that the Edit Author Info group is contained in a separate user control. Wouldn’t it have been easier to dispense with the user control and place the group directly on the form?
In the demo app, it certainly would be easier, and you would probably never use a user control in an app as simple as this one. However, consider the following: I am currently working on an application that uses six different types of transactions. Each type of transaction derives from a TransactionBase
class, and each type has about a dozen properties that a user needs to populate and edit.
To make matters more challenging, the properties aren’t all simple strings or Booleans. There is a fair amount of presentation logic associated with each transaction type, and the logic is somewhat different for each type. It’s the sort of situation that cries out for a user control.
In the real-world application, my data entry form is laid out much the same as in the demo app. The grid is a detail view in a Customers-Transactions pair. It shows the transaction date and description; the edit form shows complete details for the transaction.
When the user selects an item in the grid, the app traps the BindingSource
's CurrentChanged
event, to determine the type of transaction selected. The app then ‘activates’ the user control for the selected type by making it visible and binding it to the BindingSource
control, using the control’s DataBindings
collection. The previous form is ‘deactivated’ by hiding it and clearing its bindings. The result is a very simple data entry form for a very complex object model.
The demo app is much simpler—it has only one user control, whose data bindings are set in the Designer, rather than code. However, the real-world app uses the same techniques as the demo app. The only difference is in the run-time activation and deactivation of multiple user controls.
Data binding to a user control: Data binding is simple when we can bind to .NET controls. As we saw in Part Two, we simply create and configure a BindingSource
component, and then set our control's DataSource
and DataMember
properties to the control. .NET takes care of the rest.
But things get more interesting when we data bind to user controls. We need to add some infrastructure to our user controls before they will support data binding. The first bit of infrastructure to add is a [Bindable]
attribute to each property, like this:
[Bindable(true)]
[Browsable(true)]
public string FirstName
{
get { return textBoxFirstName.Text; }
set { textBoxFirstName.Text = value; }
}
The [Bindable]
attribute, when combined with the [Browsable]
attribute, will enable the property to appear in the user control’s list of bindable properties:
The attribute enables design-time specification of data bindings, but without more, the bindings won’t work. To make them functional, we need a way to notify the BindingSource
component when a property changes.
A data binding listens for an event called PropertyNameChanged
, where PropertyName
is the name of the property to which the particular piece of data is bound (we’ll refer to the event as the PropertyChanged
event). For example, a data binding to a FirstName
property will be listed for an event named FirstNameChanged
. So, the second piece of infrastructure we have to add to the user control is a set of PropertyChanged
events:
#region Declarations
public event System.EventHandler FirstNameChanged;
public event System.EventHandler LastNameChanged;
public event System.EventHandler SocSecNoChanged;
#endregion
When a property changes, we need to fire the appropriate PropertyChanged
event. In the demo app, each of the user control properties is connected to a text box in the control, so we can fire the event whenever the text in one of these text boxes changes, like this:
private void textBoxFirstName_TextChanged(object sender, EventArgs e)
{
if (this.FirstNameChanged != null)
{
this.FirstNameChanged(this, new System.EventArgs());
}
}
So, with these two pieces of infrastructure in place, our object model can be bound to our user control.
Timing of the PropertyChanged event: By default, the firing of a PropertyChanged
event does not cause a data binding to read the changed property immediately. Instead, the data binding waits until a control’s Validated
event fires. In other words, no matter when the PropertyChanged
event fires, the changed value isn’t read by the data binding until the control loses focus.
This approach causes all kinds of problems. The most obvious occurs when the Validated
event doesn’t fire, for one reason or another. The user says “I typed in the value, but the program lost it.” That’s because the Validate
event didn’t fire.
I prefer to force the data binding to read the changed value as soon as the PropertyChanged
event fires. This behavior can be forced by setting each binding’s DataSourceUpdateMode
setting to OnPropertyChanged
:
void DataBindings_CollectionChanged(object sender,
CollectionChangeEventArgs e)
{
foreach (Binding binding in this.DataBindings)
{
binding.DataSourceUpdateMode =
DataSourceUpdateMode.OnPropertyChanged;
}
}
This approach avoids the “lost data” problem, and it creates a better experience for the end user. Notice in the demo app that information appears in the grid as each character is typed in the text boxes of the user control. This feature gives the user immediate confirmation that their entry is being recorded by the application.
Communicating Changes to the Object Model
In .NET 2.0, Microsoft introduced a new interface to handle communications from an object model to a data binding. The INotifyPropertyChanged
interface contains a single event, PropertyChanged
, which is used to notify a binding that an object model property has changed. Between the PropertyChanged
events in the user controls and the INotifyPropertyChanged.PropertyChanged
event in the object model, the developer can implement complete two way communication with a data binding.
Note that the demo app doesn’t use the INotifyPropertyChanged
interface. Instead, it uses the BindingSource
component to add and delete items from the Authors list. Is that a good idea? A purist will say it isn’t, because I am mixing business logic in my Presentation layer. Instead, I should create Command objects for each operation, which invoke Add and Delete operations in the AuthorList
object.
That point may be valid. On the other side, I would argue that the BindingSource
component is invoking those same operations in the AuthorList
object. (Actually, it invokes AddNew()
and Remove()
operations in the BindingList<T>
base class for the collection.) So, the only difference is whether I use a Command object or a BindingSource
component to get to my business layer.
Nonetheless, if you are a strict constructionist on such matters, you may prefer to take the Command object route to the object model. In that case, your collections and their members should implement the INotifyPropertyChanged
interface.
Demo Application Walkthrough
And that brings us back to the demo application. It is organized in much the same manner as the demo app in the Part Two of this series. The major differences between the two are:
- The application in this article reads from and writes to an XML file, rather than a SQL Server database.
- The main window presents a summary-detail view of the
AuthorList
collection, rather than a master-detail view of authors and books.
The application deliberately simplifies things to the point of over-simplification. For example, the edit panel shows the same information as the grid. In a real-life application, the edit panel would show far more detail than the grid.
The User Interface: The UI consists of a DataGridView
control for the grid, a UserControl
for the edit panel, a BindingSource
component, and four buttons. The code is straightforward and should require no further explanation. See Part Two of this series for a discussion of the Command objects instantiated in the UI.
The Main form has four buttons; Open, Save, Add, and Delete. The Open button invokes a standard File Open dialog to open the XML data file. The file is located in the top level of the solution folder (the same folder as the .sln file). The Save button saves the file to the same location from which it was opened—no Save As functionality is provided.
The Add and Delete buttons invoke add and delete operations on the bindingSourceAuthorList
component to add new authors to the list or to delete authors.
Architecture: The architecture of the application is basically the same as the demo app in Part Two of this series. The application is organized according to the Model-View-Controller pattern, and Command objects are used to encapsulate the functionality of the Controller. The use of an XML file, rather than a database, simplifies the control aspects of the application, resulting in fewer Command objects than in Part Two.
Data Access: The DAO object in this application differs from that in Part Two, in that we don’t need a lower-level DataProvider
component. Instead, the DAO class contains internal methods to load and save an XML file, and a private method to initialize the data set used in these operations.
The DAO loads both the Authors and Books lists into the object model, even though the Presentation layer only uses the AuthorList
collection. It does so in order to provide complete code for loading and saving a containment hierarchy using XML file storage. The DAO code is commented and should require no further explanation.
The Object Model: The object model is virtually unchanged from Part Two. The only changes made involves removing the methods that linked business objects to their corresponding DAO objects. These methods were replaced by two methods in the AuthorList
object to load and save the object graph.
The UserControl: UserControlEditAuthor
contains three text properties that are directly bound to text boxes contained in the control. In a real-world application, these properties would probably not be directly bound in this manner. Instead, the control would contain methods to validate and perform UI processing of user input. The user control’s properties would be the return values of these methods.
For example, a user control might contain a property representing a business rule, and three radio buttons representing options for that rule. When the user selects a radio button, user control code would set the property to the appropriate option. Thus, the contained controls would be connected to the user control property only indirectly, through code.
UserControlEditAuthor
subscribes to the CollectionChanged
event of its DataBindings
collection. The handler for this event, DataBindings_CollectionChanged()
, resets the DataSourceUpdateMode
of all bindings in the collection to OnPropertyChanged
. As we discussed above, this mode provides immediate update of the object model and any other bound controls (in this case, the grid) as text is typed into one of the user control’s text boxes.
As we discussed above, note the PropertyChanged
event declarations at the top of the user control’s class. These events take a System.EventHandler
delegate with no arguments. Note also that each property is marked with [Bindable(true)]
and [Browsable(true)]
attributes. These attributes enable design-time data binding, as used in the demo app.
Conclusion
Well, hopefully, that should do it. As I mentioned at the outset of this series, I began it as a learning exercise, and continued it as I encountered issues during development of real-world applications. So where are we now?
At this point, you should be able to move data from a database or an XML file to and from an object model. You should be able to work with that model using MVC architecture, and you should be able to use .NET 2.0 data binding to move information between the object model and the UI of your application. Moreover, you should be able to create UIs that range from simple master-detail patterns to very complex summary-detail patterns involving multiple, alternating data-entry forms.
For the life of me, I can’t think of anything else that needs to be covered. Of course, that’s what I said after Part Two. If you read Part One, you may remember I said that the point of ADO.NET for object programmers is to be able to spend more time with one’s spouse and children. I began this series just after Christmas—as I finish it, it’s nearly Valentine’s Day. If I stop now and rush out to the mall, I should have just enough time to buy my wife a Valentine’s Day present and card. See you later!