Chapter 3
IN THIS CHAPTER
Understanding dependency properties
Working with binding modes
Defining an example binding object
Making sense out of data
Data binding allows data from your application objects (the binding source) to be displayed in your user-interface elements (the binding target). What this means is that you can bind the Text
property of a TextBox
(for example) to the Name
property of an instance of your Car
class. Depending on the binding mode used when setting up the relationship, changes in the Text
property value of the TextBox
can automatically update the underlying Name
property of your Car
object (and vice versa) without requiring any additional code.
It's no mystery these days that most applications deal with data. As a WPF developer, you have full creative reign on how data is presented and how information entered by your user can be validated and used to update your underlying objects. One of WPF's strengths is its rich data binding support. This chapter walks you through the details.
Data binding happens when you set up a relationship between a binding source property and binding target property. The binding target object must be a DependencyObject
, and the target property must be a DependencyProperty
.
Understanding dependency properties is crucial to obtaining a firm grasp on WPF technology. Dependency properties are found in objects that inherit from DependencyObject
. At its root, a dependency property extends the functionality of a regular property that already exists on a CLR object by adding a set of services that is also known as the WPF Property System. (Together, DependencyObject
and DependencyProperty
make up this property system.) Dependency properties can have their values determined by multiple input sources, meaning that their values can be obtained through a Style
or a data binding expression. Dependency properties act like regular properties, but they allow you to set values based on the following:
FontSize
values on the Window
element (the root element), child elements such as TextBlock
and Label
automatically inherit those font property values. You can see another example of this by reviewing the concept of attached properties introduced in Chapter 1 of this minibook.The WPF property system also provides built-in property value change notification and property value validation functionality, which is reviewed in the section “Editing, Validating, Converting, and Visualizing Your Data,” later in this chapter.
At the end of the day, dependency properties give the developer the capability to set property values directly in XAML as well as in code. The advantage to this is that you can keep your code clean and leave initializing object property values to XAML.
You have full control over how the binding relationship you create behaves. Multiple types of binding modes are available to you in WPF. These include the following:
OneTime
binding mode is used when you want the source property to only initially set the target property value. Subsequent changes to the source property are not reflected in the target property. Similarly, changes to the target property are not reflected in the source property.OneWay
binding mode is typically used for read-only properties. In this binding mode, data from the source property sets the initial value of the target property. Subsequent changes to the source property will automatically update the binding target property value. Conversely, any subsequent changes made to the target property value are not reflected in the source property.OneWayToSource
binding mode is essentially the opposite of the OneWay
binding mode. In this binding mode, data from the source property initializes the target property value. Subsequent changes to the source property value will not update the target property. However, updates to the target property value will automatically update the source property value.TwoWay
binding mode merges the functionality of the OneWay
and OneWayToSource
binding modes. In this binding mode, the source property value initializes the target property value. Subsequent changes to the source property value update the target property value. Similarly, updates to the target property value will update the source property value.Bindings can be defined using code or XAML. Here you begin with the XAML version. To see how to bind data to your UI elements, you first define a test set of data to work with.
Just follow these steps to create a binding with XAML (also found in the BindingSample1
example in the downloadable source):
BindingSample1
.Car
class by adding a new class template file to your solution named Car.cs
and code it as follows (note that you don't need any using
directives in this case):
namespace BindingSample1
{
public class Car
{
private string _make;
public string Make
{
get { return _make; }
set { _make = value; }
}
private string _model;
public string Model
{
get { return _model; }
set { _model = value; }
}
public Car() { }
}
}
MainWindow.xaml
, replace the grid with one that defines a double column and single row grid; then add a label in each grid cell, like this:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Label x:Name="lblCarMake" Grid.Row="0" Grid.Column="0"
Content="{Binding Path=Make, Mode=OneTime}"/>
<Label x:Name="lblCarModel" Grid.Row="0" Grid.Column="1"
Content="{Binding Path=Model, Mode=OneTime}"/>
</Grid>
Look at the Content
dependency property value. The information contained within the curly braces defines the binding for the content to be displayed in the labels. The next section describes what this Binding
expression means, but first you need some data to bind to.
MainWindow.xaml.cs
code-behind file and create a method called GenerateData()
in the MainWindow
class that instantiates a Car
object and assigns it to the DataContext
of the window, like this:
private void GenerateData()
{
Car car1 = new Car() { Make = "Athlon", Model = "XYZ" };
this.DataContext = car1;
}
DataContext
defines the root object relative to which all child elements obtain their values (as long as the DataContext
value on the child elements isn't directly set via XAML or code — this property is an example of property value inheritance; its value is obtained from its parent element unless otherwise specified).
Call the GenerateData()
method in the MainWindow()
constructor method (public MainWindow()
), immediately following InitializeComponents()
call.
Now, looking back to the XAML file (MainWindow.xaml
), the first label lblCarMake
will bind to the DataContext
's Make
property. The value is retrieved from the property specified in the binding’s Path
component. Similarly, the second label, lblCarModel
, will bind to the DataContext
's Model
property as specified in the binding expression’s Path
property. Each of these bindings is using a OneTime
mode, which means that the label content will be bound only once, regardless of the underlying object property being bound to changes.
The Path
component of the XAML Binding expression simply tells the XAML processor to take its value from a specific property of its DataContext
. The Path
value can also express properties that are nested, such as in the case of nested complex objects. In these cases, you use dot notation to reach the desired property, such as Property.SomeObject.SomeOtherProperty.
Run the application.
You can see that the labels now display the Make
and Model
of the Car
object that was assigned to the DataContext
of the window. (See Figure 3-1.)
You can also use C# to define bindings. The DataBinding1CSharp
example shows how to perform this task. To demonstrate C# data binding, remove the Content
attribute entirely from both labels in the XAML file. The label markup should now resemble the following:
<Label x:Name="lblCarMake" Grid.Row="0" Grid.Column="0"/>
<Label x:Name="lblCarModel" Grid.Row="0" Grid.Column="1"/>
Modify the GenerateData()
method in MainWindow.xaml.cs
to implement the Binding
definitions in code. To do this, you must instantiate Binding
objects directly. The constructor of the Binding
object takes in the string Path
value. Use the BindingOperations
class to apply the Binding
to the Content
dependency property of your labels.
The following code shows you how to define the Binding
objects and assign the binding to the Content
of the labels:
private void GenerateData()
{
Car car1 = new Car() { Make = "Athlon", Model = "XYZ" };
Binding makeBinding = new Binding("Make");
makeBinding.Mode = BindingMode.OneTime;
BindingOperations.SetBinding(lblCarMake,
Label.ContentProperty, makeBinding);
Binding modelBinding = new Binding("Model");
modelBinding.Mode = BindingMode.OneTime;
BindingOperations.SetBinding(lblCarModel,
Label.ContentProperty, modelBinding);
this.DataContext = car1;
}
Note that this change also requires the use of the following using
directives:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
Run the application and observe that it runs the same way as when the bindings were defined using XAML.
In the preceding section, you got a taste of binding syntax and saw data appear on the screen. This section builds on this knowledge and shows you the SimpleBinding2 example that includes updating data from user-interface elements, as well as updating the user interface with changes happening to objects behind the scenes. To create an environment in which updates can occur, follow these steps:
BindingSample2
.Right-click the project entry in Solution Explorer and choose Add ⇒ Existing Item from the context menu.
You see the Add Existing Item dialog box.
Highlight the Car.cs
file located in the BindingSample1
folder and click Add.
Visual Studio adds the Car
class file to your project.
Double-click Car.cs
in Solution Explorer.
You see the file open in the editor.
Change the BindingSample1
namespace to BindingSample2
.
The Car
class is now ready for use in this example.
In this example, you display the make and model of a Car
object (the DataContext
) in TextBox
controls. This control enables you to edit the values of the Car
properties. You will also use a TwoWay
data binding mode so that changes made from the user interface will be reflected in the underlying Car
object, and any changes made to the Car
object from code-behind will be reflected in the user interface.
DataContext
and another that forces changes to the DataContext
through code-behind using the following code:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0"
Grid.Column="0">
<Label Content="Make"/>
<TextBox x:Name="lblCarMake" VerticalAlignment="Top"
Text="{Binding Path=Make, Mode=TwoWay}"
Width="200" Height="25"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="0"
Grid.Column="1" >
<Label Content="Model"/>
<TextBox x:Name="lblCarModel" VerticalAlignment="Top"
Text="{Binding Path=Model, Mode=TwoWay}"
Width="200" Height="25"/>
</StackPanel>
<Button x:Name="btnShowDataContextValue"
Click="btnShowDataContextValue_Click"
Content="Show Current Data Context Value"
Grid.Row="1" Grid.Column="0"/>
<Button x:Name="btnChangeDataContextValue"
Click="btnChangeDataContextValue_Click"
Content="Change Data Context Value with Code-Behind"
Grid.Row="1" Grid.Column="1"/>
</Grid>
MainWindow.xaml.cs
, add the following methods:
private void GenerateData()
{
Car car1 = new Car() { Make = "Athlon", Model = "XYZ" };
this.DataContext = car1;
}
private void btnShowDataContextValue_Click(object sender,
RoutedEventArgs e)
{
Car dc = this.DataContext as Car;
MessageBox.Show("Car Make: " + dc.Make + "
Car Model: "
+ dc.Model);
}
private void btnChangeDataContextValue_Click(object sender,
RoutedEventArgs e)
{
Car dc = this.DataContext as Car;
dc.Make = "Changed Make";
dc.Model = "Changed Model";
}
MainWindow()
, add a call to the GenerateData()
method immediately following the InitializeComponents()
call.Run this application.
You see that the values from the DataContext
display properly in the TextBox
controls. Feel free to change the values in the TextBox
controls. For instance, change the Make
value to Athlon X
, and the model to ABC
. When you finish with your edits, click the Show Current Data Context Value button. The changes you made to the values in the TextBox
are now reflected in the underlying DataContext
object. (See Figure 3-2.)
Click OK to get rid of the message box.
If you look in the Click
event handler of the Change Data Context Value With Code-Behind button (btnChangeDataContextValue_Click()
), you will note that the DataContext
Car
object properties will be changed to Changed Make
and Changed Model
, respectively.
Click the Change Data Context Value With Code-Behind button.
Hmmm. Nothing is happening. What's up with that? If you click the Show Current Data Context Value button, you see that the properties have in fact been changed. Because you're using a TwoWay binding, your settings should automatically update your UI, right? Wrong! This is where another feature of WPF, the concept of INotifyPropertyChanged
, comes into play.
INotifyPropertyChanged
is a simple interface that allows your objects to raise an event that notifies its subscribers (namely your application) that a property value on the object has changed. Client applications subscribe to these events and update the user interface with the new values only when changes occur.
A similar interface exists for collections as well — the INotifyCollectionChanged
interface. WPF also provides a generic class called ObservableCollection<T>
that already implements INotifyCollectionChanged
for you. When creating an ObservableCollection
or your own collection that implements INotifyCollectionChanged
, you need to ensure that the objects that will be contained within the collection also implement INotifyPropertyChanged
interface.
The INotifyPropertyChanged
interface contains a single event that must be implemented. This event is called PropertyChanged
, and its parameters are the object that owns the property that has changed (the sender), and the string name of the property that has changed.
Car
class, and add the using System.ComponentModel;
statement to allow access to INotifyPropertyChanged
.Type : INotifyPropertyChanged after internal class Car
to add the required interface to your class.
Note that INotifyPropertyChanged
has a red underline beneath it. You see this underline because your class doesn't implement the required members. This event happens every time you add a new interface, so knowing the fastest way to handle it is a good idea.
Right-click INotifyPropertyChanged
, choose Quick Actions and Refactorings from the context menu, and click Implement Interface.
Visual Studio adds the following code for you:
public event PropertyChangedEventHandler PropertyChanged;
For the application to be notified of the changes that occur in Car
objects, the PropertyChanged
event must be fired each time a property value has changed.
Car
class, create a helper method called NotifyPropertyChanged
that takes in a string property name and fires the PropertyChanged
event for the object instance and the name of the property that has changed, like this:
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
this.PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
Checking to see whether PropertyChanged
is not null
essentially means you're checking to see whether anyone is listening (subscribed) to the PropertyChanged
event.
Set
methods in each of the public properties on the Car
object to call the NotifyPropertyChanged
helper method each time the property value has changed; edit the public properties, like this:
private string _make;
public string Make
{
get { return _make; }
set
{
if (_make != value)
{
_make = value;
NotifyPropertyChanged("Make");
}
}
}
private string _model;
public string Model
{
get { return _model; }
set
{
if (_model != value)
{
_model = value;
NotifyPropertyChanged("Model");
}
}
}
Run the application again.
Now when you click the Change Data Context Value with Code-Behind button, the changed values get reflected automatically in the TextBox
elements. This is due to the combination of the TwoWay
binding mode as well as the implementation of INotifyPropertyChanged
. (See Figure 3-3.)
It's good practice to validate any input provided to you from the user. People aren't perfect, and some people can be downright malicious. WPF provides a built-in framework for data validation and error notification. It’s available to you through the implementation of the IDataErrorInfo
interface on your classes shown in the BindingSample2Validate
example. You can add validation to the Car
class you already created in BindingSample2
from the preceding section. Just follow these steps to add validation to your Car
class:
Car.cs
file and edit the class to also implement the IDataErrorInfo
interface, like this:
public class Car : INotifyPropertyChanged, IDataErrorInfo
Implementing this interface adds the following methods to the Car
class:
public string Error => throw new NotImplementedException();
public string this[string columnName] =>
throw new NotImplementedException();
Get
method of the Error
property to return null by modifying the code to look like this:
public string Error => null;
Now it's time to add some validation rules to the properties of the Car
object. The Car Make
and Model
properties should enforce the rule that they must always be at least three characters in length. The public string this[string columnName]
method is used by the DataBinding
engine to validate the properties of the object as they are changed, based on the name of the property (which is what they mean by columnName
in the method signature). This method returns any error messages related to the property being edited.
public string this[string columnName]
method, like this:
public string this[string columnName]
{
get
{
string retvalue = null;
if (columnName == "Make")
{
if (String.IsNullOrEmpty(this._make)
|| this._make.Length < 3)
{
retvalue = "Car Make must be at least 3 " +
"characters in length";
}
}
if (columnName == "Model")
{
if (String.IsNullOrEmpty(this._model)
|| this._model.Length < 3)
{
retvalue = "Car Model must be at least 3 " +
"characters in length";
}
}
return retvalue;
}
}
In MainWindow.xaml
, the Make
and Model
properties are bound to TextBox
controls in the user interface. Note that you must also add another using directive to the class:
using System;
TextBox
, as shown in bold:
<StackPanel Orientation="Horizontal" Grid.Row="0"
Grid.Column="0">
<Label Content="Make"/>
<TextBox x:Name="txtCarMake" VerticalAlignment="Top"
Text="{Binding Path=Make, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True}"
Width="200" Height="25"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="0"
Grid.Column="1" >
<Label Content="Model"/>
<TextBox x:Name="txtCarModel" VerticalAlignment="Top"
Text="{Binding Path=Model, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True}"
Width="200" Height="25"/>
</StackPanel>
UpdateSourceTrigger
identifies when the validation calls take place. In this example, validations occur as the text is changing, and UpdateSourceTrigger
is fired when the underlying object property fires the PropertyChanged
event.
ValidatesOnDataErrors
is what enables the IDataErrorInfo
validation method to be called on the property.
ValidatesOnExceptions
will invalidate the TextBox
if the underlying data source throws an exception, such as when, for instance, you have an integer property and the user enters a string. WPF automatically throws the exception that the input string was not in the correct format.
Run the Sample and remove all text from the Make
and Model TextBox
controls.
You see that the TextBox
controls are now rendered in red; as you enter text into the TextBox
, as soon as you reach three characters, the red stroke disappears.
The red stroke is sufficient to indicate that an error has occurred, but it's of little use to the users because they’re not informed of the details of the error. A simple way to display the error is to add a tooltip on the TextBox
. Do this by adding a Style
resource to your window that defines a style that will trigger the tooltip when the data is in an invalid state.
Window
tag at the top of MainWindow.xaml
, like this to provide an error tooltip:
<Window.Resources>
<Style x:Key="errorAwareTextBox" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
Style
attribute to your TextBox
, like this:
Style="{StaticResource ResourceKey=errorAwareTextBox}"
Now when you run the application and remove the text from the TextBox
controls, the TextBox
displays a tooltip with the actual error message when you hover the mouse over the element. (See Figure 3-4.)
WPF provides you with the capability to create an intuitive user interface. Sometimes this means allowing users to enter data in different formats that make sense to them, giving you the responsibility of translating the user's data entry into a format allowable by your data source. The same is true vice versa; you want to translate data from your data source into a more intuitive form for the user. A popular use-case for this type of conversion is the string representation of a date value, or if you want to display a red or green circle instead of the values True or False. WPF makes converting data easy by providing a simple interface called IValueConverter
to implement. This interface contains two methods:
Convert
: This method obtains values from the data source and molds them to the form to be displayed to the user onscreen.ConvertBack
: This method does the opposite — it takes the value from the user interface and molds it into a form that the data source expects.Be sure to note that with these methods, you're not held to the same data type as the value being bound. For instance, your data source property being bound can be a Date data type, and the Convert
method can still return a string value to the user interface.
To demonstrate this feature, create a new WPF application project called BindingSample3
. This project is a dashboard application that can show the status of servers on the network. In this project, you implement two user controls, RedX
and GreenCheck
. You also create a value converter named BooleanToIconConverter
that converts a Boolean
False
value to display the RedX
control and converts a True
value to display the GreenCheck
control. These values indicate whether the server is available.
BindingSample3
.GreenCheck.xaml
.Grid
found in GreenCheck.xaml
with this XAML:
<Canvas x:Name="CheckCanvas" Width="50.4845" Height="49.6377"
Canvas.Left="0" Canvas.Top="0">
<Path x:Name="CheckPath" Width="43.4167" Height="45.6667"
Canvas.Left="0" Canvas.Top="1.3113e-006"
Stretch="Fill" Fill="#FF006432"
Data="F1 M 19.0833,45.6667L 43.4167,2.16667L 38,
1.3113e-006L 19.0833,42.5833L 2.41667,25.3333L 0,
27.9167L 17.4167,44.25"/>
</Canvas>
You're not expected to come up with things like the CheckPath
off the top of your head. (The <Path>
is what describes how the check mark is drawn.) Various designer tools help you to draw items graphically and export your final graphics in a XAML format. Expression Design was the tool used to create the user controls in this example.
Unfortunately, Microsoft decided not to maintain Expression Design, so now you have to get it from a third party such as http://expressiondesign4.com/
or https://github.com/leeenglestone/ExpressionDesign4
. If you don't like Expression Design, you can find a list of alternative tools at https://alternativeto.net/software/microsoft-expression-design/
.
RedX.xaml
.Grid
in the RedX.xaml
file with this XAML:
<Canvas Width="44.625" Height="45.9394">
<Path x:Name="Line1Path" Width="44.625" Height="44.375"
Canvas.Left="0" Canvas.Top="0" Stretch="Fill"
Fill="#FFDE0909"
Data="F1 M 0,3.5L 3.5,0L 44.625,41L 42.125,44.375"/>
<Path x:Name="Line2Path" Width="43.5772" Height="45.3813"
Canvas.Left="0.201177" Canvas.Top="0.55809"
Stretch="Fill"
Fill="#FFDE0909" Data="F1 M 3.7719,45.9394L 0.201177,
42.5115L 40.353,0.55809L 43.7784,2.98867"/>
</Canvas>
BooleanToIconConverter.cs
.using System.Windows.Data;
Have the BooleanToIconConverter
implement the IValueConverter
interface.
The code should now contain the Convert()
and ConvertBack()
methods.
Convert()
method so that it looks like this:
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null)
{
return value;
}
if ((bool)value == true)
{
return new GreenCheck();
}
else
{
return new RedX();
}
}
ServerStatus.cs
.public class ServerStatus
{
public string ServerName { get; set; }
public bool IsServerUp { get; set; }
public int NumberOfConnectedUsers { get; set; }
public ServerStatus() { }
}
MainWindow.xaml.cs
, create the GenerateData()
method shown here:
private void GenerateData()
{
ServerStatus ss = new ServerStatus()
{
ServerName = "HeadquartersApplicationServer1",
NumberOfConnectedUsers = 983,
IsServerUp = true
};
ServerStatus ss2 = new ServerStatus()
{
ServerName = "HeadquartersFileServer1",
NumberOfConnectedUsers = 0,
IsServerUp = false
};
ServerStatus ss3 = new ServerStatus()
{
ServerName = "HeadquartersWebServer1",
NumberOfConnectedUsers = 0,
IsServerUp = false
};
ServerStatus ss4 = new ServerStatus()
{
ServerName = "HQDomainControllerServer1",
NumberOfConnectedUsers = 10235,
IsServerUp = true
};
List<ServerStatus> serverList = new List<ServerStatus>();
serverList.Add(ss);
serverList.Add(ss2);
serverList.Add(ss3);
serverList.Add(ss4);
this.DataContext = serverList;
}
This code initializes a list of a few ServerStatus
objects and makes that list the DataContext
of the Window
.
GenerateData()
immediately after the call to the InitializeComponent()
method in the Window
constructor).Save and build your application.
You do this step so that the user control classes that you've defined are available to your XAML files.
Window.Resources
to MainWindow.xaml
before the Grid
:
<Window.Resources>
<local:BooleanToIconConverter
x:Key="BooleanToIconConverter"/>
<DataTemplate x:Key="ServerTemplate">
<Border BorderBrush="Blue" Margin="3" Padding="3"
BorderThickness="2" CornerRadius="5"
Background="Beige">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding
Path=IsServerUp,
Converter={StaticResource
BooleanToIconConverter}}"/>
<StackPanel Orientation="Vertical"
VerticalAlignment="Center">
<TextBlock FontSize="25"
Foreground="Goldenrod"
Text="{Binding Path=ServerName}"/>
<TextBlock FontSize="18"
Foreground="BlueViolet"
Text="{Binding
Path=NumberOfConnectedUsers}"/>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
Grid
in MainWindow.xaml
:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<ListBox x:Name="lstServers" Width="490" Height="350"
ItemsSource="{Binding}" Grid.Row="0" Grid.Column="0"
ItemTemplate="{StaticResource
ResourceKey=ServerTemplate}"/>
</Grid>
The first thing to note in MainWindow.xaml
is that the namespace for the local assembly (BindingSample3
) was added to the Window
(identified by the namespace definition in the Window
tag with the prefix local
). This enables you to instantiate classes that are defined in the current assembly in XAML, such as BooleanToIconConverter
.
<Window x:Class="BindingSample3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BindingSample3"
mc:Ignorable="d"
Title="MainWindow" Height="400" Width="500">
In the
resources, you initialize an instance of your Window
, which is available to you through the local namespace. BooleanToIconConverter
<local:BooleanToIconConverter x:Key="BooleanToIconConverter"/>
The next Window
resource that is defined is a data template. This data template provides a way to look at the data associated with a server's current status. The data template is defined as follows:
<DataTemplate x:Key="ServerTemplate">
<Border BorderBrush="Blue" Margin="3" Padding="3"
BorderThickness="2" CornerRadius="5"
Background="Beige">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding
Path=IsServerUp,
Converter={StaticResource
BooleanToIconConverter}}"/>
<StackPanel Orientation="Vertical"
VerticalAlignment="Center">
<TextBlock FontSize="25"
Foreground="Goldenrod"
Text="{Binding Path=ServerName}"/>
<TextBlock FontSize="18"
Foreground="BlueViolet"
Text="{Binding
Path=NumberOfConnectedUsers}"/>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
Chapter 1 of this minibook states that one of the main reasons to adopt WPF as a user-interface technology is its data visualization flexibility. Data templates enable you to represent data contained in an object by using any number of XAML elements. The world is your oyster, and you can get as creative as you want to relay application information to your user in the most usable, intuitive fashion by using data templates.
Analyze the ServerTemplate
data template. This data template represents the display of an instance of a ServerStatus
object. Look at the Label
element in the data template.
The Content
property of the label is bound to the Boolean IsServerUp
property of the ServerStatus
object. You'll also notice that there is another component to the binding expression, called Converter
. This is where the Boolean value (IsServerUp
) gets passed into the BooleanToIconConverter
and is rendered as the RedX
or the GreenCheck
user control, depending on its value.
The rest of the data template simply outputs the server name of the ServerStatus
object in yellow and the number of connected users in blue-violet.
Within the Grid
on the window, a ListBox
control is defined that displays a list of servers on the network. Look at the definition of the ListBox
:
<ListBox x:Name="lstServers" Width="490" Height="350"
ItemsSource="{Binding}" Grid.Row="0" Grid.Column="0"
ItemTemplate="{StaticResource
ResourceKey=ServerTemplate}"/>
Through Property Value inheritance, the ItemsSource
of the ListBox
is defaulted to the DataContext
of Window
. The empty {Binding}
element simply states that it will use the current binding of its parent, which uses recursion up the element tree until it reaches a place where a binding is set. Remember that in the GenerateData()
method, you're setting the DataContext
binding to the list of servers to the Window
itself, so the ListBox
will inherit that list as its ItemSource
.
The data template you defined in resources to describe a ServerStatus
object renders each object being bound. You see this through the ItemTemplate
attribute that uses the StaticResource
to point to the ServerTemplate
defined in resources. Now when you run the application, you see the ServerStatus data presented in a visually pleasing way! (See Figure 3-5.)
This chapter is not meant to be inclusive of all functionality possible through WPF's amazing data binding support. Other aspects of WPF data templates worth looking into include these concepts:
DataTemplateSelector
, which is a base class that allows you to render a data template based on some logical condition.ListBox
, you may display only summary information; however, you can provide a button in your data template that will enable users to switch between the summary template and a more detailed template on demand.