More WPF Data Binding, Part 1

In a previous post I demonstrated some of the data binding features available in WPF. There are a lot more ways to use data binding so here is another sample app that gets even deeper into it. The code can be downloaded here. Some of the features used in this app include:

  • ItemTemplateSelectors
  • HeaderedItemsControl with custom template and selectors for HeaderTemplateSelector and ItemTemplateSelector
  • View and ViewModel elements of the M-V-VM pattern (WPF oriented variation on MVC/MVP)
  • Selected collection item synchronization
  • CollectionView filtering
  • Nesting DataContexts
  • Binding to data objects and object collections
  • Binding to XML properties on data objects
  • Commands as data object properties
  • MultiBindings
  • Triggers
  • DataTriggers
  • Binding multiple controls to the same data
  • Merging a ResourceDictionary
  • Different Binding and RelativeSource Modes
  • Binding RadioButtons to Enum values
  • RadioButton grouping in ItemTemplates
  • Different types of custom IValueConverters
  • Templating a Window

Let’s start at the top of the main XAML file. In the root Window element there are a few properties to set up transparency for the window (see this post for more) and an xmlns:local declaration to reference to the local assembly. The first element under the Window is the usual Window.Resources ResourceDictionary.

<Window x:Class="DataBinder.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataBinder"
        Title="DataBinder" Height="420" Width="500"
        ResizeMode="NoResize" WindowStyle="None"
        AllowsTransparency="True" Background="Transparent"      >
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="ConverterDictionary.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary> ...
    </Window.Resources> ...
</Window>

The first object in Resources is a MergedDictionary from a separate file. The other file contains declarations for IValueConverters. I’ve started doing this regularly to get around a Visual Studio issue where XAML IntelliSense breaks below custom xmlns references. This technique really becomes useful when you have multiple files sharing things like Styles or converters. Next in the Resources is a long list of DataTemplates and Styles with a few Brushes thrown in. We’ll skip over these for now and cover some of them as they’re used. Following Resources are two more Window property elements:

<Window.DataContext>
    <local:WindowViewModel />
</Window.DataContext>
<Window.Template>
    <ControlTemplate TargetType="{x:Type Window}">
        <Border x:Name="mainBorder" Background="#D0FFFFFF" CornerRadius="5" BorderBrush="{StaticResource WindowFrameBrush}" BorderThickness="2,0,2,2">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="30" />
                    <RowDefinition/>
                    <RowDefinition Height="20" />
                </Grid.RowDefinitions>
                <Border Background="{StaticResource WindowFrameBrush}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                        CornerRadius="5,5,0,0" Margin="-1,0,-1,0" MouseLeftButtonDown="DragWindow">
                    <Grid>
                        <TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Title}"
                                   Foreground="White" FontWeight="Bold" VerticalAlignment="Center" Margin="10,2,10,2"/>
                        <Button Content="X" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5" FontSize="7"
                                Width="15" Height="15" Padding="0" Command="ApplicationCommands.Close"/>
                    </Grid>
                </Border>
                <ContentPresenter Grid.Row="1"/>
                <ToggleButton x:Name="transToggle" Grid.Row="2" Content="Transparent Window OFF" FontSize="9" FontFamily="Arial"/>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger SourceName="transToggle" Property="IsChecked" Value="true">
                <Setter TargetName="mainBorder" Property="Background" Value="White"/>
                <Setter TargetName="transToggle" Property="Content" Value="Transparent Window ON"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Window.Template>

Here we’re setting the DataContext to a new class that will be handling all of the bindable data and logic. This is the ViewModel part of the Model-View-ViewModel pattern (see here and here for more on this design pattern). Using this separate file keeps code out of the Window1 code-behind and allows for much better separation of the design and logic. Declaring the DataContext here also allows this file to be used in Blend including setting bindings to properties on the ViewModel class. The next element is a template for the Window that sets up a custom frame and adds a button to toggle transparency on or off. The reason the template is declared here rather than in a Style Resource as would usually be done is that the title bar Border uses a MouseLeftButtonDown event to support dragging and events can’t be wired up inside a ResourceDictionary. Notice the ContentPresenter in the middle of the Grid. Even though there are no explicit bindings this element will automatically receive whatever is assigned to the Window’s Content property, which is the next part of the XAML file. The Content is a Grid that provides the main layout for the Window. The Grid contains 4 Child elements, the first of which is an ItemsControl inside a Border and ScrollViewer:

<ScrollViewer Margin="5" Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
    <Border BorderBrush="LightBlue" BorderThickness="1">
        <ItemsControl x:Name="itemList" ItemsSource="{Binding Activities}" >
            <ItemsControl.ItemTemplateSelector>&lt!--
                <local:DisplayLevelSelector /> -->
                <local:BlendFriendlyDisplayLevelSelector Template1="{StaticResource Activity1DataTemplate}" Template2="{StaticResource Activity2DataTemplate}"
                                                         Template3="{StaticResource Activity3DataTemplate}" Template4="{StaticResource Activity4DataTemplate}" />
            </ItemsControl.ItemTemplateSelector>
        </ItemsControl>
    </Border>
</ScrollViewer>

There are a few unusual things to look at here. First is the ItemsSource binding. Since a DataContext has been set at the Window level all we need to do to bind to its properties are specify a property name, in this case Activities, which is an ICollectionView containing ActivityDataObjects. There is no template set for the items, but instead an ItemTemplateSelector is used to provide custom logic for choosing from multiple templates for each individual item. For demonstration purposes I created the selector that’s commented out, which uses a template selection method that breaks Blend’s renderer. Here’s the code for the unused selector:

    public class DisplayLevelSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            ActivityDataObject dataItem = item as ActivityDataObject;
            if (dataItem == null) return null;
            Window mainWindow = Application.Current.MainWindow;
            if (dataItem.Category == ActivityCategory.Category2)
                return mainWindow.FindResource("Activity2DataTemplate") as DataTemplate;
            else if (dataItem.Category == ActivityCategory.Category3)
                return mainWindow.FindResource("Activity3DataTemplate") as DataTemplate;
            else if (dataItem.Category == ActivityCategory.Category4)
                return mainWindow.FindResource("Activity4DataTemplate") as DataTemplate;
            else return mainWindow.FindResource("Activity1DataTemplate") as DataTemplate;
        }
    }

This selector uses the FindResource method to locate templates with specific names. Another set of selectors shown later use a different method to get templates. The BlendFriendly selector also uses this second method. The template used is decided by the Category value of the data item being bound. Here is the data template for Category1 from Resources:

<DataTemplate x:Key="Activity1DataTemplate">
    <Border Style="{StaticResource ItemBorder}" ToolTip="{Binding Description}">
        <StackPanel x:Name="textPanel" Orientation="Horizontal">
            <TextBlock Text="{Binding Name}" FontFamily="Impact" VerticalAlignment="Center" ToolTip="{Binding Description}" Margin="5"/>
            <TextBlock x:Name="stateText" Text="{Binding State}" VerticalAlignment="Center" Margin="5"/>
            <Border x:Name="lastDataBorder" VerticalAlignment="Center" Margin="5" BorderBrush="Lavender" BorderThickness="2" HorizontalAlignment="Left" Padding="2">
                <TextBlock x:Name="lastDataText" Text="{Binding RelativeSource={RelativeSource PreviousData}, Path=Name}" FontSize="9"/>
            </Border>
        </StackPanel>
    </Border>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding IsExpanded}" Value="true">
            <Setter TargetName="textPanel" Property="Orientation" Value="Vertical"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding State}" Value="Off">
            <Setter TargetName="stateText" Property="Foreground" Value="Red"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding State}" Value="On">
            <Setter TargetName="stateText" Property="Foreground" Value="Green"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding State}" Value="Ready">
            <Setter TargetName="stateText" Property="Foreground" Value="Yellow"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding State}" Value="Starting">
            <Setter TargetName="stateText" Property="Foreground" Value="LightGreen"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding State}" Value="Stopping">
            <Setter TargetName="stateText" Property="Foreground" Value="FireBrick"/>
        </DataTrigger>
        <Trigger SourceName="lastDataText" Property="Text" Value="">
            <Setter TargetName="lastDataBorder" Property="Visibility" Value="Hidden"/>
        </Trigger>
    </DataTemplate.Triggers>
</DataTemplate>

There are a few interesting things to look at here. The first thing you probably notice is the long list of DataTriggers. The first changes the main StackPanel’s orientation based on the IsExpanded data value. The next 5 set different text colors based on the State value. There’s also a normal Trigger to hide an empty text border. There are a few normal bindings to the data item and also a RelativeSource PreviousData binding to bind to a property on the previous data item in the source list. When you run the app with the provided test data, the first item is Category1 but has no previous data item, so just leaves a blank space. Here’s the Category2 DataTemplate:

<DataTemplate x:Key="Activity2DataTemplate">
    <Border Style="{StaticResource ItemBorder}">
        <Expander IsExpanded="{Binding IsExpanded}">
            <Expander.Header>
                <DockPanel Width="200" ToolTip="{Binding Description}" LastChildFill="False">
                    <TextBlock Text="{Binding Name}" DockPanel.Dock="Left"/>
                    <TextBlock Text="{Binding State}" DockPanel.Dock="Right"/>
                </DockPanel>
            </Expander.Header>
            <StackPanel>
                <RadioButton x:Name="offRadio" Content="Off" GroupName="{Binding Id}"
                             IsChecked="{Binding Path=State, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Off}" />
                <RadioButton x:Name="readyRadio" Content="Ready" GroupName="{Binding Id}"
                             IsChecked="{Binding Path=State, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Ready}"/>
                <RadioButton x:Name="startingRadio" Content="Starting" GroupName="{Binding Id}"
                             IsChecked="{Binding Path=State, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Starting}"/>
                <RadioButton x:Name="onRadio" Content="On" GroupName="{Binding Id}"
                             IsChecked="{Binding Path=State, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=On}"/>
                <RadioButton x:Name="stoppingRadio" Content="Stopping" GroupName="{Binding Id}"
                             IsChecked="{Binding Path=State, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Stopping}"/>
            </StackPanel>
        </Expander>
    </Border>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.UseGlobalState}" Value="true">
            <Setter TargetName="offRadio" Property="IsEnabled" Value="false"/>
            <Setter TargetName="readyRadio" Property="IsEnabled" Value="false"/>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

Leave a Reply

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