Fun with WPF Data Binding

I’ve been exploring WPF data binding features and ended up with a simple example that illustrates many concepts that I haven’t really seen all combined together in other examples. Features used in this app include Data Templates, MultiBinding and IMultiConverters, sharing DataContext, implicit binding to a DataSource’s CurrentItem, Control Templates, XPath and RelativeSource binding inside a DataTemplate, and DependencyProperty hijacking (used just as a shortcut to save some coding, not really to demonstrate it).

The application allows you to pick one of five animals from a dropdown and displays the name and a picture of the animal in a button. There is also a checkbox which switches the image from a photo of the selected animal to a drawing. The images are located at a url under 2 folders: animals and sketches. Each folder has 5 identically named images. The displayed image comes from a url created based on the dropdown selection, checkbox state, and a base url hardcoded in the app XAML.

runningBindingApp.png

The list of animals comes from an XML file (ImageData.xml) which simply lists the animal name and the image file name. The file is in the project at the root directory and compiled as a Resource so it can be accessed with the pack://application syntax in XAML.

    <?xml version="1.0" encoding="utf-8" ?>      <Animals>        <Animal>          <Name>Eagle</Name>          <Image>eagle.jpg</Image>        </Animal>        <Animal>          <Name>Giraffe</Name>          <Image>giraffe.jpg</Image>        </Animal>        <Animal>          <Name>Penguin</Name>          <Image>penguin.jpg</Image>        </Animal>        <Animal>          <Name>Tortoise</Name>          <Image>tortoise.jpg</Image>        </Animal>        <Animal>          <Name>Whale</Name>          <Image>whale.jpg</Image>        </Animal>      </Animals>

To get the MultiBinding to work correctly there needs to be a Value Converter that combines the binding inputs to produce a single output. An IMultiValueConverter looks like the standard IValueConverter but instead of an object as the Convert input, it takes an object array (ConvertBack also returns an object array). Here the converter takes an image name and folder name as values, with a base url as the parameter. Since BitmapImage URIs can’t be assigned in XAML (I don’t know the full explanation or if it’s just a bug, but they can’t) we’re going to just return a BitmapImage pointing to our constructed Uri rather than returning the Uri itself. Here’s the complete code for our converter:

    using System;      using System.Windows.Data;      using System.Windows.Media.Imaging;      namespace BindingApp      {          public class SubFolderImageConverter : IMultiValueConverter          {              #region IMultiValueConverter Members              public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)              {                  string urlBase = parameter.ToString();                  string imageName = values[0].ToString();                  string folderName = values[1].ToString();                  Uri uri = new Uri(urlBase + folderName + "/" + imageName);                  BitmapImage source = new BitmapImage(uri);                  return source;              }              public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)              {                  throw new System.NotImplementedException();              }              #endregion          }      }

Now that the supporting files are ready, the rest is going to be all XAML. First we need to set up the converter and data source as Resources:

        <local:SubFolderImageConverter x:Key="imagePathConverter"/>          <XmlDataProvider x:Key="AnimalData" Source="pack://application:,,,/ImageData.xml" XPath="/Animals/Animal"/>

There’s plenty of good info out there on the pack syntax if you want a full explanation of the three commas. Note that I set the initial XPath so anything using this data source will start out at the animal level and the DataSource CurrentItem property will be pointing at an Animal element.

Next we need some DataTemplates. These will control the rendering of a single item from our DataSource into displayable content. When attached to a ContentControl like a Button the DataTemplate defines the look of the Content, but leaves the rest of the control template alone. We’re going to need two templates, one for our dropdown that just shows the animal name as text, and a second more complicated one for our Button that will display the image.

        <DataTemplate x:Key="DataNameTemplate">            <StackPanel>              <TextBlock Text="{Binding Mode=OneWay, XPath=Name}"/>            </StackPanel>          </DataTemplate>          <DataTemplate x:Key="DataImageTemplate">            <StackPanel>              <TextBlock TextAlignment="Center" Text="{Binding Mode=OneWay, XPath=Name}"/>              <Image Stretch="Uniform" Height="80">                <Image.Source>                  <MultiBinding Converter="{StaticResource imagePathConverter}"                                 ConverterParameter="http://blogs.interknowlogy.com/downloads/johnbowen/images/">                    <Binding Mode="OneWay" XPath="Image" />                    <Binding Mode="OneWay" Path="Tag"                             RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ContentControl}}" />                  </MultiBinding>                </Image.Source>              </Image>            </StackPanel>          </DataTemplate>

The DataImageTemplate is where some of the really interesting stuff is happening. The Image Source is going to be set using the Converter that was set up earlier. Since the Binding being used is a MultiBinding we need to use the exploded XAML syntax with nested Binding elements instead of the normal {Binding} syntax like used in the TextBlock. One of the Converter input Bindings being used is an XPath pointing to the Image element from the DataSource. The second Binding is to the Tag DependencyProperty of the the Button the DataTemplate will be bound to. The Tag will be used to hold the name of the folder to load images from. To make it more flexible a RelativeSource binding is used to find the nearest ContentControl ancestor.

Next we’re going to make a Style to create a new template for the Button. The template is going to change the Button’s look, assign the DataTemplate to the Content and set up a Trigger to switch the Tag between the two available folders.

        <Style x:Key="dataButton" TargetType="{x:Type Button}" >            <Setter Property="Background" Value="White" />            <Setter Property="ContentTemplate" Value="{StaticResource DataImageTemplate}" />            <Setter Property="Tag" Value="animals"/>            <Setter Property="Template">              <Setter.Value>                <ControlTemplate TargetType="{x:Type Button}">                  <Border BorderThickness="2" BorderBrush="SlateBlue" Padding="5,5,5,5" CornerRadius="5"                           Background="{TemplateBinding Background}">                    <ContentPresenter />                  </Border>                  <ControlTemplate.Triggers>                    <Trigger Property="Selector.IsSelected" Value="True">                      <Setter Property="Tag" Value="sketches" />                    </Trigger>                  </ControlTemplate.Triggers>                </ControlTemplate>              </Setter.Value>            </Setter>          </Style>

Notice here that the Trigger is using the Selector.IsSelected attached property even though there are no Selectors related to the Button. This is property hijacking and is generally bad practice. I use it here for brevity, but in a real application if you need an extra property use subclassing or some other means to add a new property that can be used for its designed purpose. Also note that only 1 Trigger exists to set the Tag and there is no explicit means of resetting it. This is because the default value is automatically re-applied when the IsSelected=True Trigger is not active.

Last but not least we have the controls.

    <StackPanel DataContext="{StaticResource AnimalData}" Margin="20,20,20,20" >          <ComboBox x:Name="comboBox" Width="150" Height="25" IsSynchronizedWithCurrentItem="True"                    ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataNameTemplate}" />          <CheckBox x:Name="sketchSelector" Margin="10,10,10,10" Content="Show as drawing" Width="150" Height="25" />          <Button x:Name="detailButton" Content="{Binding}" Width="200" Height="120" Style="{StaticResource dataButton}"                   Selector.IsSelected="{Binding ElementName=sketchSelector, Path=IsChecked}" />      </StackPanel>

The parent StackPanel of the controls includes a DataContext pointing to the AnimalData Resource so the {Binding} statements used for Content and ItemsSource will both automatically point there. The Selector.IsSelected hijacked property to be passed into the Button template’s Trigger is bound to the CheckBox checked state.

Bringing it all together, here’s the complete XAML for the application:

    <Window          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"          x:Class="BindingApp.Window1"          Title="BindingApp" Height="300" Width="300"          xmlns:local="clr-namespace:BindingApp"          >        <Window.Resources>          <local:SubFolderImageConverter x:Key="imagePathConverter" />          <XmlDataProvider x:Key="AnimalData" Source="pack://application:,,,/ImageData.xml" XPath="/Animals/Animal"/>          <DataTemplate x:Key="DataNameTemplate">            <StackPanel>              <TextBlock Text="{Binding Mode=OneWay, XPath=Name}"/>            </StackPanel>          </DataTemplate>          <DataTemplate x:Key="DataImageTemplate">            <StackPanel>              <TextBlock TextAlignment="Center" Text="{Binding Mode=OneWay, XPath=Name}"/>              <Image Stretch="Uniform" Height="80">                <Image.Source>                  <MultiBinding Converter="{StaticResource imagePathConverter}"                                 ConverterParameter="http://blogs.interknowlogy.com/downloads/johnbowen/images/">                    <Binding Mode="OneWay" XPath="Image" />                    <Binding Mode="OneWay" Path="Tag"                             RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ContentControl}}" />                  </MultiBinding>                </Image.Source>              </Image>            </StackPanel>          </DataTemplate>          <Style x:Key="dataButton" TargetType="{x:Type Button}" >            <Setter Property="Background" Value="White" />            <Setter Property="ContentTemplate" Value="{StaticResource DataImageTemplate}" />            <Setter Property="Tag" Value="animals"/>            <Setter Property="Template">              <Setter.Value>                <ControlTemplate TargetType="{x:Type Button}">                  <Border BorderThickness="2" BorderBrush="SlateBlue" Padding="5,5,5,5" CornerRadius="5"                           Background="{TemplateBinding Background}">                    <ContentPresenter />                  </Border>                  <ControlTemplate.Triggers>                    <Trigger Property="Selector.IsSelected" Value="True">                      <Setter Property="Tag" Value="sketches" />                    </Trigger>                  </ControlTemplate.Triggers>                </ControlTemplate>              </Setter.Value>            </Setter>          </Style>        </Window.Resources>        <StackPanel DataContext="{StaticResource AnimalData}" Margin="20,20,20,20" >          <ComboBox x:Name="comboBox" Width="150" Height="25" IsSynchronizedWithCurrentItem="True"                    ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataNameTemplate}" />          <CheckBox x:Name="sketchSelector" Margin="10,10,10,10" Content="Show as drawing" Width="150" Height="25" />          <Button x:Name="detailButton" Content="{Binding}" Width="200" Height="120" Style="{StaticResource dataButton}"                   Selector.IsSelected="{Binding ElementName=sketchSelector, Path=IsChecked}" />        </StackPanel>      </Window>

One thought on “Fun with WPF Data Binding

  1. Spot on with this write-up, I honestly feel this site needs a great deal more attention.

    I’ll probably be back again to see more, thanks for the advice!

Leave a Reply

Your email address will not be published. Required fields are marked *