Chapter 3

Data Binding in WPF

IN THIS CHAPTER

Bullet Understanding dependency properties

Bullet Working with binding modes

Bullet Defining an example binding object

Bullet 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.

Remember You don't have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the CSAIO4D2EBK05CH03 folder of the downloadable source. See the Introduction for details on how to find these source files.

Getting to Know Dependency Properties

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:

  • A default value: These are predefined on the property.
  • A calculated expression (similar to CSS, Cascading Style Sheets, expressions in the web world): This can be a data binding expression or a reference to resources defined in the application.
  • Data binding: This actually is built upon the preceding bullet using binding expressions on the binding source object.
  • Property value inheritance: Not to be confused with object inheritance, property value inheritance allows values set on parent properties to be propagated down to its children. For instance, if you set 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.
  • Styling: Each style typically contains setters to set one or more property values.

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.

Exploring the Binding Modes

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:

  • The 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.
  • The 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.
  • The 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.
  • The 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.

Investigating the Binding Object

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.

Defining a binding with XAML

Just follow these steps to create a binding with XAML (also found in the BindingSample1 example in the downloadable source):

  1. Create a new WPF App (.NET Framework) project and name it BindingSample1.
  2. Define a simple 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() { }
    }
    }

  3. In 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.

  4. Open the 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).

  5. 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.

    Technicalstuff 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.

  6. 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.)

Snapshot of data binding to properties of a DataContext.

FIGURE 3-1: Data binding to properties of a DataContext.

Defining a binding with C#

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.

Technicalstuff BindingOperations is a helper class provided to you by WPF. It has static methods that give you the power to add and clear data binding definitions on application elements.

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.

Remember Dependency properties are typically defined with the suffix Property, but you see them this way only when navigating MSDN documentation and accessing them through code. In XAML, you specify dependency property attributes by dropping the Property suffix from the name.

Editing, Validating, Converting, and Visualizing Your Data

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:

  1. Create a new WPF Application project and name it BindingSample2.
  2. 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.

  3. Highlight the Car.cs file located in the BindingSample1 folder and click Add.

    Visual Studio adds the Car class file to your project.

  4. Double-click Car.cs in Solution Explorer.

    You see the file open in the editor.

  5. 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.

  6. Define two buttons, one that shows a message box containing the current value of the 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>

  7. In the code-behind file, 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";
    }

  8. In the constructor for MainWindow(), add a call to the GenerateData() method immediately following the InitializeComponents() call.
  9. 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.)

    Snapshot of editing data using a TwoWay binding mode.

    FIGURE 3-2: Editing data using a TwoWay binding mode.

  10. 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.

  11. 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.

    Technicalstuff 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.

    Technicalstuff 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.

  12. Stop the application, open your Car class, and add the using System.ComponentModel; statement to allow access to INotifyPropertyChanged.
  13. 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.

  14. 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.

  15. To implement this in the 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));
    }
    }

    Technicalstuff Checking to see whether PropertyChanged is not null essentially means you're checking to see whether anyone is listening (subscribed) to the PropertyChanged event.

  16. Modify the 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");
    }
    }
    }

  17. 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.)

Snapshot of two-way data binding with INotifyPropertyChanged.

FIGURE 3-3: TwoWay data binding with INotifyPropertyChanged.

Validating data

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:

  1. Open the 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();

  2. Edit the 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.

  3. To define and enforce these rules, edit the 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;

  4. To enable the text being entered into the TextBoxes to be validated against the constraints defined on the underlying property, edit the binding expressions in each 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>

    Technicalstuff 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.

  5. 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.

  6. Add the following XAML directly below the 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>

  7. Add a 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.)

Snapshot of displaying error messages using Styles.

FIGURE 3-4: Displaying error messages using Styles.

Converting your data

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.

Remember A user control is a collection of reusable XAML. It can be made up of any number of elements and is implemented with the same rules as when you implement a normal Window (for instance, you can have only one root element). You can also define properties (including dependency properties!) on user controls. Follow these steps to create your sample:

  1. Create a new WPF application named BindingSample3.
  2. Add a new User Control (WPF) to the project by choosing Project ⇒  Add User Control and name it GreenCheck.xaml.
  3. Replace the 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.

    Tip 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/.

  4. Add another User Control (WPF) to the project; name it RedX.xaml.
  5. Replace the 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>

  6. Add a new class called BooleanToIconConverter.cs.
  7. Add the following using directive to your class:

    using System.Windows.Data;

  8. Have the BooleanToIconConverter implement the IValueConverter interface.

    The code should now contain the Convert() and ConvertBack() methods.

  9. Modify the 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();
    }
    }

  10. Add a new class called ServerStatus.cs.
  11. Add three properties: the Server name, a Boolean indicator if the server is up, and a number of currently connected users, as shown here:

    public class ServerStatus
    {
    public string ServerName { get; set; }
    public bool IsServerUp { get; set; }
    public int NumberOfConnectedUsers { get; set; }

    public ServerStatus() { }
    }

  12. In 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.

  13. Add a call to GenerateData() immediately after the call to the InitializeComponent() method in the Window constructor).
  14. 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.

  15. Add the following 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>

  16. Add the following code to the 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 Window resources, you initialize an instance of your BooleanToIconConverter, which is available to you through the local namespace.

<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}"/>

Technicalstuff WPF provides a number of controls called ItemsControls that allow you to bind collections of objects to them. Examples of ItemsControls are ListBox and ComboBox (among others). Collections are bound to an ItemsControl through the ItemsSource attribute. A data template can also be applied to each object being bound through the ItemsControl ItemTemplate attribute.

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.)

Snapshot of rendering a collection of data using a value converter and data templates.

FIGURE 3-5: Rendering a collection of data using a value converter and data templates.

Finding Out More about WPF Data Binding

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:

  • Using DataTemplateSelector, which is a base class that allows you to render a data template based on some logical condition.
  • Using data templates as a means to provide data adding/editing capabilities to the user.
  • Switching a data template at runtime at the preference of the user. This allows users to switch a data template at will. For instance, in a 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.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset