Data Binding
Once we've got a set of controls and a way to lay them out, we
still need to fill them with data and keep that data in sync with wherever the
data actually lives. (Controls are a great way to show data but a poor place to
keep it.)
For example, imagine that we'd like to build an actual WPF
application for keeping track of people's nicknames. Something like
Figure 1-14 would do the trick.
In Figure 1-14,
we've got two TextBox controls, one for the name and one for the
nickname; the actual nickname entries in a ListBox in the middle; and
a Button to add new entries. The core data of such an application
could easily be built with a class, as shown in
Example 1-27.
Example 1-27. A custom type with data binding support
public class Nickname : INotifyPropertyChanged {
// INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propName) {
if( PropertyChanged != null ) {
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
string name;
public string Name {
get { return name; }
set {
name = value;
OnPropertyChanged("Name"); // notify consumers
}
}
private string nick;
public string Nick {
get { return nick; }
set {
nick = value;
OnPropertyChanged("Nick"); // notify consumers
}
}
public Nickname( ) : this("name", "nick") { }
public Nickname(string name, string nick) {
this.name = name;
this.nick = nick;
}
}
This class knows nothing about data binding, but it does have
two public properties that expose the data, and it implements the standard INotifyPropertyChanged
interface to let consumers of this data know when it has changed.
In the same way that we have a standard interface for notifying
consumers of objects when they change, we also have a standard way to notify
consumers of collections of changes called INotifyCollectionChanged.
WPF provides an implementation of this interface called ObservableCollection,
which we'll use to fire the appropriate event when Nickname objects
are added or removed, as in Example
1-28.
Example 1-28. A custom collection type with data
binding support
// Notify consumers
public class Nicknames : ObservableCollection<Nickname> { }
Around these classes, we could build nickname-management logic
that looks like Example 1-29.
Example 1-29. Making ready for data binding
// Window1.xaml.cs
...
namespace DataBindingDemo {
public class Nickname : INotifyPropertyChanged {...}
public class Nicknames : ObservableCollection<Nickname> { }
public partial class Window1 : Window {
Nicknames names;
public Window1( ) {
InitializeComponent( );
this.addButton.Click += addButton_Click;
// create a nickname collection
this.names = new Nicknames( );
// make data available for binding
dockPanel.DataContext = this.names;
}
void addButton_Click(object sender, RoutedEventArgs e) {
this.names.Add(new Nickname( ));
}
}
}
Notice the window's class constructor provides a click event
handler to add a new nickname and creates the initial collection of nicknames.
However, the most useful thing that the Window1 constructor does is
set its DataContext property so as to make the nickname data available
for data binding.
Data binding is about keeping
object properties and collections of objects synchronized with one or more
controls' view of the data. The goal of data
binding is to save you the pain and suffering associated with writing the code
to update the controls when the data in the objects change and with writing the
code to update the data when the user edits the data in the controls. The
synchronization of the data to the controls depends on the INotifyPropertyChanged
and INotifyCollectionChanged interfaces that we've been careful to use
in our data and data-collection implementations.
For example, because the collection of our sample nickname data
and the nickname data itself both notify consumers when there are changes, we
can hook up controls using WPF data binding, as in
Example 1-30.
Example 1-30. An example of data binding
<!-- Window1.xaml -->
<Window x:Class="DataBindingDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
Text="Nicknames">
<DockPanel x:Name="dockPanel">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center">Name: </TextBlock>
<TextBox Text="{Binding Path=Name}" />
<TextBlock VerticalAlignment="Center">Nick: </TextBlock>
<TextBox Text="{Binding Path=Nick}" />
</StackPanel>
<Button DockPanel.Dock="Bottom" x:Name="addButton">Add</Button>
<ListBox
ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True" />
</DockPanel>
</Window>
This XAML lays out the controls as shown in
Figure 1-14, using a dock panel to arrange things top-to-bottom and a
stack panel to arrange the editing controls. The secret sauce that takes
advantage of data binding is the {Binding} values in the control
attributes instead of hardcoded values. By setting the Text property
of the TextBox to {Binding Path=Name}, we're telling the TextBox
to use data binding to peek at the Name property out of the current Nickname
object. Further, if the data changes in the Name TextBox, the Path
is used to poke the new value back in.
The current Nickname object is determined by the ListBox
because of the IsSynchronizedWithCurrentItem property, which keeps the
TextBox controls showing the same Nickname object as the one
that's currently selected in the ListBox. The ListBox is
bound to its data by setting the ItemsSource attribute to {Binding}
without a Path statement. In the ListBox, we're not
interested in showing a single property on a single object, but rather all of
the objects at once.
But how do we know that both the ListBox and the TextBox
controls are sharing the same data? That's where setting the dock panel's DataContext
comes in. In the absence of other instructions, when a control's property is
set using data binding, it looks at its own DataContext property for
data. If it doesn't find any, it looks at its parent and then that parent's
parent, and so on, all the way up the tree. Because the ListBox and
the TextBox controls have a common parent that has a DataContext
property set (the DockPanel), all of the data-bound controls will
share the same data.
1.6.1. XAML Markup-Extension Syntax
Before we take a look at the results of our data binding, let's
take a moment to discuss the XAML markup-extension syntax,
which is what you're using when you set an attribute to something inside of
curly bracese.g., Text="{Binding Path=Name}". The markup-extension
syntax adds special processing to XAML attribute values. For example, the BindingExtension
class creates an instance of the Binding class, populating its
properties with the parsed string that comes afterward. Logically, the
following:
<TextBox Text="{Binding Path=Name}" />
turns into the following:
Binding binding = new Binding( );
binding.Path = "Name";
textbox1.Text =
binding.ProvideValue(textbox1, TextBox.TextProperty);
In fact, the binding-extension syntax is just a shortcut for the
following (which you'll recognize as the property-element syntax):
<TextBox.Text>
<Binding Path="Name" />
</TextBox.Text>
For a complete discussion of markup extensions, as well as the
rest of the XAML syntax, you'll want to read
Appendix A.
1.6.2. Data Templates
With the data-binding markup syntax explained, let's turn back
to our sample data-binding application, which so far doesn't look quite like
what we had in mind, as seen in Figure
1-15.
It's clear that the data is making its way into the application,
since the currently selected name and nickname are shown for editing. The
problem is that, unlike the TextBox controls which were each given a
specific field of the Nickname object to show, the ListBox is
expected to show the whole thing. Lacking special instructions, the ListBox
calling the ToString method of each object, which only results in the
name of the type. To show the data, we need to compose a data template, as
shown in Example 1-31.
Example 1-31. Using a data template
<ListBox
ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock TextContent="{Binding Path=Name}" />:
<TextBlock TextContent="{Binding Path=Nick}" />
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The ListBox control has an ItemTemplate property
that expects a data template: a template of
elements that should be inserted for each listbox item, instead of the results
of the call to ToString. In Example
1-31, we've composed a data template from a text block that flows
together two other text blocks, each bound to a property on a Nickname
object separated by a colon, as shown in
Figure 1-16.
At this point, we've got a completely data-bound application. As
data in the collection or the individual objects changes, the UI will be
updated and vice versa. However, there is a great deal more to say on this
topic, not least of which is pulling in XML as well as object data, which are
covered in Chapter 4.
|