4.1. Without Data Binding
Consider a very simple application for editing a single person's
name and age, as shown in Figure 4-1.
Figure 4-1 can
be implemented with the simple XAML shown in
Example 4-1.
Example 4-1. A simple Person editor layout
<!-- Window1.xaml -->
<Window ...>
<Grid>
...
<TextBlock ...>Name:</TextBlock>
<TextBox x:Name="nameTextBox" ... />
<TextBlock ...>Age:</TextBlock>
<TextBox x:Name="ageTextBox" ... />
<Button x:Name="birthdayButton" ...>Birthday</Button>
</Grid>
</Window>
The data to be shown in our simple application can be
represented in a simple class, as shown in
Example 4-2.
Example 4-2. A simple Person class
public class Person {
string name;
public string Name {
get { return this.name; }
set { this.name = value; }
}
int age;
public int Age {
get { return this.age; }
set { this.age = value; }
}
public Person( ) {}
public Person(string name, int age) {
this.name = name;
this.age = age;
}
}
With this class, a naive implementation of the behavior of our
application could look like Example
4-3.
Example 4-3. Naive Person editor code
// Window1.xaml.cs
...
public class Person {...}
public partial class Window1 : Window {
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Fill initial person fields
this.nameTextBox.Text = person.Name;
this.ageTextBox.Text = person.Age.ToString( );
this.birthdayButton.Click += birthdayButton_Click;
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
++person.Age;
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
The code in Example 4-3
creates a Person object and initializes the text boxes with the Person
object properties. When the Birthday button is pressed, the Person
object's Age property is incremented, and the updated Person data
is shown in a message box, as shown in
Figure 4-2.
Our simple application implementation is, in fact, too simple.
The change in the Person Age property does show up in the message box,
but it does not show up in the main window. One way to keep the application's
UI up to date is to write the code so that whenever a Person object is
updated, it manually updates the UI at the same time, as shown in
Example 4-4.
Example 4-4. Manually updating the UI when a Person is
updated
void birthdayButton_Click(object sender, RoutedEventArgs e) {
++person.Age;
// Manually update the UI
this.ageTextBox.Text = person.Age.ToString( );
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
With a single line of code, we've "fixed" our application. This
is a seductive and popular road, but it does not scale as the application gets
more complicated and requires more of these "single" lines of code. To get
beyond the simplest of applications, we'll need a better way.
4.1.1. Object Changes
A more robust way for the UI to track object changes
is for the object to raise an event when a property changes. As of .NET 2.0,
the right way for an object to do this is with an implementation of the INotifyPropertyChanged
interface, as shown in Example 4-5.
Example 4-5. A class that supports property-change
notification
using System.ComponentModel; // INotifyPropertyChanged
...
public class Person : INotifyPropertyChanged {
// INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propName) {
if( this.PropertyChanged != null ) {
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
string name;
public string Name {
get { return this.name; }
set {
this.name = value;
OnPropertyChanged("Name");
}
}
int age;
public int Age {
get { return this.age; }
set {
this.age = value;
OnPropertyChanged("Age");
}
}
public Person( ) {}
public Person(string name, int age) {
this.name = name;
this.age = age;
}
}
In Example 4-5,
when either of the Person properties changes (as is caused by the
implementation of the Birthday button), a Person object
raises the PropertyChanged event. We can use this event to keep the UI
synchronized with the Person properties, as in
Example 4-6.
Example 4-6. Simple Person editor code
// Window1.xaml.cs
...
public class Person : INotifyPropertyChanged {...}
public partial class Window1 : Window {
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Fill initial person fields
this.nameTextBox.Text = person.Name;
this.ageTextBox.Text = person.Age.ToString( );
// Watch for changes in Tom's properties
person.PropertyChanged += person_PropertyChanged;
this.birthdayButton.Click += birthdayButton_Click;
}
void person_PropertyChanged(
object sender,
PropertyChangedEventArgs e) {
switch( e.PropertyName ) {
case "Name":
this.nameTextBox.Text = person.Name;
break;
case "Age":
this.ageTextBox.Text = person.Age.ToString( );
break;
}
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
++person.Age; // person_PropertyChanged will update ageTextBox
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
Example 4-6 shows
a single instance of the Person class that's created when the main
window first comes into existence, initializing the name and age text boxes
with the initial person values and then subscribing to the property-change
event to keep the text boxes up to date as the Person object changes.
With this code in place, the birthday-button click event handler doesn't have
to manually update the text boxes when it updates Tom's age; instead, updating
the Age property causes a cascade of events that keeps the age text box up to
date with the Person object's changes, as shown in
Figure 4-3.
The steps are as follows:
-
User clicks on button, which causes Click
event to be raised.
-
Click handler gets the age
(9) from the Person object.
-
Click handler sets the age
(10) on the Person object.
-
Person Age property setter
raises the PropertyChanged event.
-
PropertyChanged event is
routed to event handler in the UI code.
-
UI code updates the age TextBox
from "9" to "10".
-
Button click event handler displays
a message box showing the new age ("10").
By the time the message box is shown with Tom's new age, the age
text box on the form has already been updated, as shown in
Figure 4-4.
With the handling of the INotifyPropertyChanged event,
when the data in the object changes, the UI is updated to reflect that change.
However, that only solves half the problem; we still need to handle changes in
the UI and reflect them back to the object.
4.1.2. Control Changes
Without some way to track changes from the UI back into the
object, we could easily end up with a case where the user has made some change
(like changing the person's name), shows the object (as happens when pressing
the Birthday button) and expects the change to have been made, only to
be disappointed with Figure 4-5.
Notice in Figure 4-5
that the Name is "Thomsen Frederick" in the form, but "Tom" in the
message box, which shows that while one part of the UI has been updated, the
underlying object has not. To fix this problem, we have but to watch for the Text
property in our TextBox object to change, updating the Person
object as appropriate, as in Example
4-7.
Example 4-7. Tracking changes in the UI
public partial class Window1 : Window {
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Fill initial person fields
this.nameTextBox.Text = person.Name;
this.ageTextBox.Text = person.Age.ToString( );
// Watch for changes in Tom's properties
person.PropertyChanged += person_PropertyChanged;
// Watch for changes in the controls
this.nameTextBox.TextChanged += nameTextBox_TextChanged;
this.ageTextBox.TextChanged += ageTextBox_TextChanged;
this.birthdayButton.Click += birthdayButton_Click;
}
...
void nameTextBox_TextChanged(object sender, TextChangedEventArgs e) {
person.Name = nameTextBox.Text;
}
void ageTextBox_TextChanged(object sender, TextChangedEventArgs e) {
int age = 0;
if( int.TryParse(ageTextBox.Text, out age) ) {
person.Age = age;
}
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
++person.Age;
// nameTextBox_TextChanged and ageTextBox_TextChanged
// will make sure the Person object is up to date
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
Now, no matter where the data changes, both the Person object
and the UI showing the Person object are kept synchronized.
Figure 4-6 shows the name changes in the UI correctly propagating to
the Person object.
While we've gotten the functionality we wanted, we had to write
quite a bit of code to make it happen:
-
Window1 constructor code to set controls to initial
values
-
Window1 constructor code to hook up the PropertyChanged
event to track the Person object's property changes
-
PropertyChanged event handler to grab the updated data
from the Person object, converting data to strings as appropriate
-
Window1 constructor code to hook up the TextBox
object's TextChanged event to track the UI changes
-
TextChanged event handlers to push the updated TextBox
data into the Person object, converting the data as appropriate
This code allows us to write our birthday button event
handler safe in the knowledge that all changes are synchronized when we display
the message box. However, it's easy to imagine how this code could quickly get
out of hand as the number of object properties gets beyond two or as the number
of objects we're managing grows. Plus, this seems like such a common thing to
want to do that someone must have already provided a simpler way to do this.
And, in fact, they have; it's called data binding.
|