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