4.2. Data Binding
Our manual code to keep the UI and the data synchronized has the
effect of implicitly binding together two sets of properties, one from the
Person object and one from the controls showing the Person object.
Data binding is the act of explicitly binding properties from one
object to another, keeping them synchronized and converting types as
appropriate, as shown in Figure 4-7.
4.2.1. Bindings
Instead of setting the Text property of the TextBox
objects manually in code and then keeping them up to date, data binding allows
us to set the Text property using an instance of a Binding object,
as in Example 4-8.
Example 4-8. Binding a dependency property to a CLR
property
<TextBox ...>
<TextBox.Text>
<Binding Path="Age" />
</TextBox.Text>
</TextBox>
In Example 4-8,
we've used the property-element syntax introduced in
Chapter 1 to create an instance of the Binding class,
initialize its Path property to the string "Age" and set the Binding
object as the value of the TextBox object's Text property.
Using the binding markup extension (also introduced in
Chapter 1), we can shorten Example
4-8 to Example 4-9.
Example 4-9. The shortcut binding syntax
<TextBox TextContent="{Binding Path=Age}" />
As an even shorter cut, you can drop the Path designation
altogether, and the Binding will still know what you mean, as in
Example 4-10.
Example 4-10. The shortest cut binding syntax
<TextBox TextContent="{Binding Age}" />
I prefer to be more explicit, so I won't use the syntax in
Example 4-10, but I won't judge if you like it.
The Binding class has all kinds of interesting
facilities for managing the binding between properties, but the one that we're
most interested in is the Path property. For most cases, you can think
of the Path as the name of the property on the object serving as the
data source. So, the binding statement in
Example 4-10 is creating a binding between the Text property
of the TextBox and the Name property of some object to be
named later, as shown in Figure 4-8.
In this binding, the TextBox control is the
binding target , as it acts as a
consumer of changes to the binding source
, which is the object that provides the data. The binding target can be a WPF
element, but you're only allowed to bind to the element's dependency properties
(described in Chapter 9).
On the other hand, you can bind to any public CLR property on
the binding source object; the binding source is not named in this example
specifically so that we can have some freedom as to where it comes from at
runtime and so that it's easier to bind multiple controls to the same object
(like our name and age text-box controls bound to the same Person object).
Commonly, the binding source data comes from a data context
.
4.2.2. Implicit Data Source
A data context is a place for
bindings to look for the data source if they don't have any other special
instructions (which we'll discuss later). In WPF, every FrameworkElement
and every FrameworkContentElement has a DataContext property.
The DataContext property is of type object so you can plug anything
you like into ite.g., string, Person, List<Person>,
etc. When looking for an object to use as the binding source, the binding
object traverses up the tree from where it's defined, looking for a non-null DataContext
property.
This traversal is handy because it means that any two controls
with a common logical parent can bind to the same data source. For example,
both of our text box controls are children of the grid, and they each search
for a data context, as shown in Figure
4-9.
The steps work like this:
-
The binding looks for a non-null DataContext on the
TextBox itself
-
The binding looks for a non-null DataContext on the
Grid
-
The binding looks for a non-null DataContext on the
Window
Providing a non-null DataContext for both of the text
box controls is a matter of setting the shared Person object as the
value of the grid's DataContext property in the Window1 constructor,
as in Example 4-11.
Example 4-11. Editor code simplified with data binding
// Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace PersonBinding {
public partial class Window1 : Window {
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Let the grid know its data context
grid.DataContext = person;
this.birthdayButton.Click += birthdayButton_Click;
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
// Data binding keeps person and the text boxes synchronized
++person.Age;
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
}
So, while the functionality of our app is the same as shown in
Figure 4-9, the data-synchronization code has been reduced to a binding
object for each property in the XAML where data is to be shown, and a data
context for the bindings to find the data. There is no need for the UI
initialization code or the event handlers that copy and convert the data
(notice the lack of ellipses in Example
4-11).
To be clear, the use of the INotifyPropertyChanged
implementation is not an accident. This is the
interface that WPF's data-binding engine uses to keep the UI synchronized when
an object's properties change. Without it, a UI change can still propagate to
the object, but the binding engine will have no way of knowing when to update
the UI.
 |
It's not quite true that the binding engine will
have no way of knowing when a change happens
on an object that does not implement the INotifyPropertyChanged interface.
One way it can know is if the object implements the PropertyNameChanged
events as prescribed in .NET 1.x data bindinge.g. SizeChanged, TextChanged,
etc.with which WPF maintains backward compatibility. Another way is a manual
call to the UpdateTarget method on the BindingExpression object
associated with the Binding in question.
However, it's safe to say that implementing INotifyPropertyChanged
is the recommended way to enable property change notifications in WPF data
binding.
|
|
4.2.3. Declarative Data
While our application is attempting to simulate a more
complicated application that, perhaps, loads its "person data" from some
persisted form and saves it between application sessions, it's not hard to
imagine cases where some data is known at compile time. Maybe it's sample data
(like our Tom) or well-known data that doesn't change between sessions, such as
application settings defaults or error messages. Lots of applications have
string resources that are kept separate from the workings of the UI, but are
still bundled with the application. Keeping the data bundled with the app makes
it easier to maintain and localize, while keeping it out of the UI logic itself
reduces coupling between the data and the UI. In our sample thus far, we've
been keeping this well-known data in the code, but XAML is a better choice,
both because of the ease of maintaining data in XAML and XAML's support for
localization (as described in
Chapter 6).
As discussed in
Chapter 1, XAML is a language for describing object graphs, so
practically any type with a default constructor can be initialized in XAML.
Luckily, as you'll recall from
Example 4-2, our Person class has a default constructor, so we
can create an instance of it in our application's XAML, as shown in
Example 4-12.
Example 4-12. Creating an instance of a custom type in
XAML
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window ... xmlns:local="local">
<Window.Resources>
<local:Person x:Key="Tom" Name="Tom" Age="9" />
</Window.Resources>
<Grid>...</Grid>
</Window
Here we've created a little "data island" inside the window's
resources element, bringing the Person type in using the XAML mapping
syntax described in Chapter
1.
With a named Person in our XAML code, we can
declaratively set the grid's DataContext, instead of setting it in the
code-behind file programmatically, as in
Example 4-13.
Example 4-13. Binding to an object declared in XAML
<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window ... xmlns:local="local">
<Window.Resources>
<local:Person x:Key="Tom" Name="Tom" Age="9" />
</Window.Resources>
<Grid DataContext="{StaticResource Tom}">
...
<TextBlock ...>Name:</TextBlock>
<TextBox ... Text="{Binding Path=Name}" />
<TextBlock ...>Age:</TextBlock>
<TextBox ... Text="{Binding Path=Age}" />
<Button ... x:Name="birthdayButton">Birthday</Button>
</Grid>
</Window>
Now that's we've moved the creation of the Person object
to the XAML, we have to update our Birthday button click handler from
using a member variable to using the data defined in the resource, as in
Example 4-14.
Example 4-14. Binding to an object declared in XAML
public partial class Window1 : Window {
...
void birthdayButton_Click(object sender, RoutedEventArgs e) {
Person person = (Person)this.FindResource("Tom"));
++person.Age;
MessageBox.Show(...);
}
}
In Example 4-14,
we're using the FindResource method (introduced in
Chapter 1 and further detailed in
Chapter 6) to pull the Person object from the main window's
resources. With this minor change, the result is brought again into parity with
Figure 4-6. The only thing that's different is that you don't have to
touch the code-behind file to maintain or localize the data known at compile
time. (Chapter 6 discusses
the localization of XAML resources.)
4.2.4. Explicit Data Source
Once you've got yourself a named source of data, you can be
explicit in the XAML
about the source in the binding object instead of relying on implicitly binding
to a non-null DataContext property somewhere in the tree. Being
explicit is useful if you've got more than one source of datae.g., two Person
objects. Setting the source explicitly is accomplished with the Source
property in the binding, as in Example
4-15.
Example 4-15. Data binding using the Source property
<!-- Window1.xaml -->
<Window ...>
<Window.Resources>
<local:Person x:Key="Tom" ... />
<local:Person x:Key="John" ... />
</Window.Resources>
<Grid>
...
<TextBox x:Name="tomTextBox"
Text="
{Binding
Path=Name,
Source={StaticResource Tom}}" />
<TextBox x:Name="johnTextBox"
Text="
{Binding
Path=Name,
Source={StaticResource John}}" />
...
</Grid>
</Window>
In Example 4-15,
we're binding two text boxes to two different person objects, using the Source
property of the Binding object to bind each person explicitly.
 |
Implicit Versus Explicit
Binding
In general, I find implicit binding to be most useful when I'm
sharing the same data between multiple controls, because all that's needed is a
bit of code to set the the DataContext property on a single parent
element. On the other hand, if I've got multiple data sources, I really like
using the Source property on my Binding objects to make it
clear where the data is coming from.
|
|
4.2.5. Value Conversion
So far, our sample application has shown the bound data as text
in text boxes. However, there's absolutely nothing stopping you from binding to
other properties of a controle.g., Foreground, FontWeight, Height,
etc. For example, we might decide that anyone over age 25 is cool, so should be
marked in the UI as red (or, they're in danger of dying soonerwhichever makes
you more likely to recommend this book to your friends...). As someone ages at
the press of the Birthday button, we want to keep the UI up to date,
which means we've got ourselves a perfect candidate for data binding. Imagine
the ability to do the following in Example
4-16.
Example 4-16. Binding to a non-Text property
<!-- Window1.xaml -->
<Window ...>
<Window.Resources>
<local:Person x:Key="Tom" ... />
</Window.Resources>
<Grid>
...
<TextBox
Text="{Binding Path=Age}"
Foreground="{Binding Path=Age, ...}"
...
/>
...
</Grid>
</Window>
In Example 4-16,
we've bound the age text box's Text property to the Person object's
Age property, as we've already seen, but we're also binding the Foreground
property of the text box to the exact same property on the Person object.
As Tom's age changes, we want to update the foreground color of the age text
box. However, since the Age is of type Int32 and Foreground
is of type Brush, there needs to be a mapping from Int32 to Brush
applied to the data binding from Age to Foreground. That's
the job of a value converter .
A value converter (or just
"converter" for short) is an implementation of the IValueConverter interface,
which contains two methods: Convert and ConvertBack. The Convert
method is called when converting from the source data to the target UI
datae.g., from Int32 to Brush. The ConvertBack method
is called to convert back from the UI data to the source data. In both cases,
the current value and the type wanted for the target data is passed to the
method.
To convert an Age Int32 into a Foreground Brush,
we can implement whatever mapping in the Convert function we feel
comfortable with, as in Example 4-17.
Example 4-17. A simple value converter
public class AgeToForegroundConverter : IValueConverter {
// Called when converting the Age to a Foreground brush
public object Convert(object value, Type targetType, ...) {
Debug.Assert(targetType == typeof(Brush));
// DANGER! After 25, it's all down hill...
int age = int.Parse(value.ToString( ));
return (age > 25 ? Brushes.Red : Brushes.Black);
}
// Called when converting a Foreground brush back to an Age
public object ConvertBack(object value, ...) {
// should never be called
throw new NotImplementedException( );
}
}
In Example 4-17,
we've implemented the Convert method to double-check that we're after
a Foreground brush and then we hand out the brush that's appropriate
for the age being displayed. Since we haven't provided any facility to change
the Foreground brush being used to display the age, there's no reason
to implement the ConvertBack method.
 |
I chose the name AgeToForegroundConverter because
I have specific semantics I'm building into my converter class that go above
simply converting an Int32 to a Brush. Even though this
converter could be plugged in anywhere that converted an Int32 to a Brush,
I might have very different requirements for a HeightToBackgroundConverter,
as just one example.
|
|
Once you've got a converter class, it's easy to create an
instance of one in the XAML, just like we've been doing with our Person
object, as shown in Example 4-18.
Example 4-18. Binding with a value converter
<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window ... xmlns:local="local">
<Window.Resources>
<local:Person x:Key="Tom" ... />
<local:AgeToForegroundConverter
x:Key="AgeToForegroundConverter" />
</Window.Resources>
<Grid DataContext="{StaticResource Tom}">
...
<TextBox
Text="{Binding Path=Age}"
Foreground="
{Binding
Path=Age,
Converter={StaticResource AgeToForegroundConverter}}"
... />
...
</Grid>
</Window>
In Example 4-18,
once we have a named converter object in our XAML, we establish it as the
converter between the Age property and the Foreground brush
by setting the Converter property of the binding object.
Figure 4-10 shows the result of our conversion.
In Figure 4-10,
notice that as Tom's age increases past the threshold, the converter switches
the foreground brush from black to red. This change happens immediately as the
data changes without any explicit code to force it, just as with any other kind
of data binding.
 |