John Bowen's Blog

Blog.WriteLine(new Random().NextThoughts(".NET"));

My Links

Blog Stats

News

Archives

Post Categories

Image Galleries

Dev Tools

MSBuild

Team Foundation Server

WPF

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>
            <!--<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"/>
        <Setter TargetName="startingRadio" Property="IsEnabled" Value="false"/>
        <Setter TargetName="onRadio" Property="IsEnabled" Value="false"/>
        <Setter TargetName="stoppingRadio" Property="IsEnabled" Value="false"/>
        <Setter TargetName="offRadio" Property="IsChecked"
                Value="{Binding Path=State, Mode=OneWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Off}"/>
        <Setter TargetName="readyRadio" Property="IsChecked"
                Value="{Binding Path=State, Mode=OneWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Ready}"/>
        <Setter TargetName="startingRadio" Property="IsChecked"
                Value="{Binding Path=State, Mode=OneWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Starting}"/>
        <Setter TargetName="onRadio" Property="IsChecked"
                Value="{Binding Path=State, Mode=OneWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=On}"/>
        <Setter TargetName="stoppingRadio" Property="IsChecked"
                Value="{Binding Path=State, Mode=OneWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Stopping}"/>
      </DataTrigger>
    </DataTemplate.Triggers>
  </DataTemplate>

In this template there is a set of RadioButtons all bound to an Enum State property through a converter. The RadioButtons are shown or hidden with an Expander that also displays the selected State value in its header. This app also provides a global State setting that overrides the value on all data items. Since this value exists in the top level ViewModel rather than the data item itself, a RelativeSource FindAncestor Binding is used to get a value from the parent Window's DataContext to check for the override. Notice that even though I'm calling a DataContext (type Object) I can still use normal . syntax to access a WindowViewModel Property. When the override is active the RadioButtons are disabled but still get updated with the data item's State value that is now being set globally. I won't go over the Enum converter here but take a look at the code for this one for a good example of many of the capabilities of IValueConverter. You may notice that instead of providing a GroupName for the RadioButtons I used a Binding here too. This is because as an ItemTemplate, these controls will be generated many times and the GroupName property will end up being the same for every item instance using this data template. By binding to the unique Id each item will have a unique GroupName and won't interfere with the selections of other items' RadioButtons.

The other two templates provide different looks but don't do too much out of the ordinary so I'll let you peruse those on your own. The next post will cover the detail display and global data settings in the right side and bottom sections of the window.

posted on Thursday, June 21, 2007 11:37 PM