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 a WPF application for keeping track of people's nicknames. Something like Figure 1-15 would do the trick.
In Figure 1-15,
we've got two TextBox
controls, one
for the name and one for the nickname. We've also got the actual
nickname entries in a ListBox
in the
middle and a Button
to add new
entries. We could easily build the core data of such an application with
a class, as shown in Example 1-25.
Example 1-25. A custom type with data binding support
public class Nickname : INotifyPropertyChanged {
// INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
void Notify(string propName) {
if( PropertyChanged != null ) {
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
string name;
public string Name {
get { return name; }
set {
name = value;
Notify("Name"); // notify consumers
}
}
string nick;
public string Nick {
get { return nick; }
set {
nick = value;
Notify("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 so that
appropriate events are fired when Nickname
objects are added or removed (Example 1-26).
Example 1-26. 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-27.
Example 1-27. 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 that the window's class constructor adds 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.
In WPF, data binding is about
keeping object properties and collections of objects synchronized with
one or more controls' views of the data. The goal of data binding is to
save you the time required to write the code to update the controls when
the data in the objects changes, and 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 example nickname data and the nickname data itself both notify consumers when there are changes, we can hook up controls using WPF data binding, as shown in Example 1-28.
Example 1-28. An example data binding usage
<!-- Window1.xaml --> <Window ...> <DockPanel x:Name="dockPanel"> <TextBlock DockPanel.Dock="Top"> <TextBlock VerticalAlignment="Center">Name: </TextBlock> <TextBoxText="{Binding Path=Name}"
/> <TextBlock VerticalAlignment="Center">Nick: </TextBlock> <TextBoxText="{Binding Path=Nick}"
/> </TextBlock> <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-15 using a dock panel
to arrange things top to bottom and a text block to contain 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 (back in Example 1-27). 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
its 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.
Before we take a look at the results of our data binding, let's
take a moment to discuss XAML markup
extensions, which is what you're using when you set an
attribute to something inside of curly braces (e.g., Text="{Binding Path=Name}"
). Markup
extensions add special processing to XAML attribute values. For
example, this:
<TextBox Text="{Binding Path=Name}" />
is just a shortcut for this (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, read Appendix A.
With the data binding markup syntax explained, let's turn back to our example data binding application, which so far doesn't look quite like what we had in mind, as seen in Figure 1-16.
It's clear that the data is making its way into the application,
because 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, it's
calling the ToString
method of each
object, which results in only the name of the type. To show the data,
we need to compose a data template, like the one in Example 1-29.
Example 1-29. Using a data template
<ListBox ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock> <TextBlockText="{Binding Path=Name}"
/>: <TextBlockText="{Binding Path=Nick}"
/> </TextBlock> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
A data template is a set of
elements that should be inserted somewhere. In our case, we are
specifying a data template to be inserted for each listbox item by
setting the ItemTemplate
property.
In Example 1-29, 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 back in Figure 1-15.
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, including binding to XML and relational data, master-detail binding, and hierarchical binding, which you'll see in Chapter 6 and Chapter 7.
Get Programming WPF, 2nd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.