More WPF Data Binding, Part 2

In Part 1 I covered the window settings and the main activity display. We’re now ready to look at the ActivityDataObject detail displays. Details are stored as an XML string in ActivityDataObject and consist of a Name and Description. A ComboBox lists all activities by name and determines which activities are displayed in the other controls. A TabControl shows 2 views of the details: a ListBox and a customized HeaderedItemsControl. There is also a pair of TextBoxes and a Button to add new details to the selected activity.

<StackPanel Grid.Column="1" Grid.Row="1" Margin="15,5,5,5">
    <TextBlock FontSize="12" Text="Activity Detail Settings" Foreground="Firebrick" HorizontalAlignment="Center" Margin="0,5,0,5"/>
    <ComboBox x:Name="detailItemList" ItemsSource="{Binding Activities}" DisplayMemberPath="Name" IsSynchronizedWithCurrentItem="True"/>
    <TabControl Height="170" Margin="0,5,0,0">
        <TabItem Header="ListBox">
            <ContentControl ContentTemplate="{StaticResource DetailContentTemplate}" Content="{Binding Activities}" />
        </TabItem>
        <TabItem Header="ItemsControl" DataContext="{Binding ElementName=detailItemList, Path=SelectedItem}">
            <TabItem.Resources>
                <local:DetailDisplaySelector x:Key="DetailDisplaySelector" DefaultTemplate="{StaticResource DetailBodyDefaultTemplate}" AlternateTemplate="{StaticResource DetailBodyAlternateTemplate}"/>
                <local:DetailsHeaderSelector x:Key="DetailsHeaderSelector" DefaultTemplate="{StaticResource DetailHeaderDefaultTemplate}" AlternateTemplate="{StaticResource DetailHeaderAlternateTemplate}"/>
            </TabItem.Resources>
            <HeaderedItemsControl DataContext="{Binding Path=Details, Converter={StaticResource XmlStringToXmlDocumentConverter}}"                              ItemsSource="{Binding Path=/}" Header="{Binding ElementName=detailItemList, Path=SelectedItem}"                              HeaderTemplateSelector="{StaticResource DetailsHeaderSelector}" ItemTemplateSelector="{StaticResource DetailDisplaySelector}"                              Style="{StaticResource HeaderedItemsControlStyle}"/>
        </TabItem>
    </TabControl>
    <DockPanel Margin="0,10,0,0">
        <Label Content="Name" DockPanel.Dock="Left"/>
        <TextBox x:Name="detailName" Text="name"/>
    </DockPanel>
    <DockPanel Margin="0,5,0,5">
        <Label Content="Description" DockPanel.Dock="Left"/>
        <TextBox x:Name="detailDescription" Text="description"/>
    </DockPanel>
    <Button Content="Add Detail" Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand.Command}"            local:CommandBinder.Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand}">
        <Button.CommandParameter>
            <MultiBinding Converter="{StaticResource ConcatenateStringsMultiConverter}" ConverterParameter=";">
                <Binding ElementName="detailName" Path="Text"/>
                <Binding ElementName="detailDescription" Path="Text"/>
            </MultiBinding>
        </Button.CommandParameter>
    </Button>
</StackPanel>

There are a lot of new concepts being used here. First we’ll look at the ListBox TabItem. Instead of using a ListBox directly here there’s a ContentControl using a ContentTemplate. By binding this to the same Activities collection used by the ComboBox we get just the single item that is selected by the ComboBox passed to the ContentTemplate. Here’s the template:

<DataTemplate x:Key="DetailContentTemplate">
    <StackPanel DataContext="{Binding Path=Details, Converter={StaticResource XmlStringToXmlDocumentConverter}}">
        <ListBox ItemsSource="{Binding XPath=DetailList/Detail}" ItemTemplate="{StaticResource XmlDetailTemplate}"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled" MaxHeight="135"/>
    </StackPanel>
</DataTemplate>

Here we see the first usage of a nested DataContext. To get binding access to the XML detail data a converter is used to create an XmlDocument from the Details string at the StackPanel level. The ListBox can then use XPath Binding to get to the individual details. Now each list data item consists of a single Detail element with Name and Description child elements available for XPath Binding.

The other TabItem uses another nested DataContext, this time bound directly to the ComboBox’s SelectedItem. This DataContext could also bind directly to Activities with the same result, thanks to automatic current item synchronization. A HeaderedItemsControl uses two TemplateSelectors which are declared in the TabItem’s Resources. These selectors choose from a Default or Alternate template which are passed in as properties. Here’s the code from the header selector:

    public class DetailsHeaderSelector : DataTemplateSelector
    {
        private DataTemplate _DefaultTemplate;

        public DataTemplate DefaultTemplate
        {
            get { return _DefaultTemplate; }
            set { _DefaultTemplate = value; }
        }

        private DataTemplate _alternateTemplate;

        public DataTemplate AlternateTemplate
        {
            get { return _alternateTemplate; }
            set { _alternateTemplate = value; }
        }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            ActivityDataObject dataItem = item as ActivityDataObject;
            if (dataItem == null) return null;
            if (dataItem.Name == dataItem.Name.ToLower()) return AlternateTemplate;
            return DefaultTemplate;
        }
    }

This method removes the usage of hardcoded template names and avoids exceptions that may be thrown by using FindResource. The HeaderedItemsControl also uses a further nested DataContext of its own that sets up the XmlDocument. The Header binds to the selected ActivityDataObject outside of the DataContext and the ItemsSource binds to the XmlDocument to access the list of details. Finally, a Style is applied to set up the template for the control. The default template for a HeaderedItemsControl doesn’t actually display the content assigned to the Header so this needs to be replaced here with a custom template. Here’s the template part of the Style:

<ControlTemplate TargetType="{x:Type HeaderedItemsControl}">
    <DockPanel>
        <ContentPresenter DockPanel.Dock="Top" Content="{TemplateBinding Header}" ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" ContentTemplate="{TemplateBinding HeaderTemplate}"/>
        <Border DockPanel.Dock="Bottom" SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
            <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            </ScrollViewer>
        </Border>
    </DockPanel>
</ControlTemplate>

A ContentPresenter is added to hold the Header content and an ItemsPresenter is used to implicitly bind to the ItemsSource. The items and header both change their appearance based on whether their data items have an all lowercase name. In flipping through the detail data you’ll notice the different text styles being applied by the selected templates. The last control in this section of the app is a button for adding details to the selected activity. Here’s its XAML again:

<Button Content="Add Detail" Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand.Command}"
        local:CommandBinder.Command="{Binding ElementName=detailItemList, Path=SelectedItem.AddDetailCommand}">
    <Button.CommandParameter>
        <MultiBinding Converter="{StaticResource ConcatenateStringsMultiConverter}" ConverterParameter=";">
            <Binding ElementName="detailName" Path="Text"/>
            <Binding ElementName="detailDescription" Path="Text"/>
        </MultiBinding>
    </Button.CommandParameter>
</Button>

Since all of our data and logic is being held in the ViewModel class rather that the code-behind we don’t want to use a Click event handler for the button (event handler methods can only exist in the code-behind of the XAML file using them, in this case Window1.xaml.cs). We also want to perform an action on a specific data item that is selected in another control. To accomplish this a Command is used to bind the Button’s click to an action in the ViewModel’s data. This is an important part of the separation in the M-V-VM pattern. The command technique used here is based on an example on Dan Crevier’s blog. Look at the CommandModel, CommandBinder, and ActivityDataObject classes to see how this works. The AddDetailCommand property exists on the ActivityDataObject itself and creates a new detail when its Command is executed. This way the WindowViewModel class doesn’t even need to keep track of the selected item outside of the ICollectionView itself. Although not the usual method of using commands this works well for maintaining the separation of the ViewModel and View. The MultiBinding here puts together the name and description fields to pass into the single CommandParameter property (there are many different ways to use this property since it is an Object).

The last two sections provide controls for setting the global State override and for filtering the displayed activities.

<Border Grid.Row="2" Margin="5">
    <StackPanel>
        <CheckBox x:Name="GlobalStatesCB" Content="Set all activity states" IsChecked="{Binding UseGlobalState}" />
        <StackPanel Orientation="Horizontal" IsEnabled="{Binding ElementName=GlobalStatesCB, Path=IsChecked}">
            <RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Off}" Content="Off" Margin="5"/>
            <RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Ready}" Content="Ready" Margin="5"/>
            <RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Starting}" Content="Starting" Margin="5"/>
            <RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=On}" Content="On" Margin="5"/>
            <RadioButton IsChecked="{Binding Path=GlobalState, Mode=TwoWay, Converter={StaticResource EnumMatchToBooleanConverter}, ConverterParameter=Stopping}" Content="Stopping" Margin="5"/>
        </StackPanel>
    </StackPanel>
</Border>
<Border Grid.Row="2" Grid.Column="1" Margin="5">
    <StackPanel>
        <TextBlock Text="Filter Out Categories"/>
        <StackPanel Orientation="Horizontal">
            <CheckBox IsChecked="{Binding Path=FilterCategory1, Mode=OneWayToSource}" Content="1" Margin="5"/>
            <CheckBox IsChecked="{Binding Path=FilterCategory2, Mode=OneWayToSource}" Content="2" Margin="5"/>
            <CheckBox IsChecked="{Binding Path=FilterCategory3, Mode=OneWayToSource}" Content="3" Margin="5"/>
            <CheckBox IsChecked="{Binding Path=FilterCategory4, Mode=OneWayToSource}" Content="4" Margin="5"/>
        </StackPanel>
    </StackPanel>
</Border>

The GlobalState RadioButtons use the same method shown in the Category2 template but bind to a different property. Changes to this property trigger code in the ViewModel that sets the State value for all activities. The FilterCategory CheckBoxes each bind to separate boolean properties, all of which are used in the Activities CollectionView Filter. Here’s the code for one of the properties along with the filtering method and the CollectionView initialization:

        public bool FilterCategory1
        {
            get { return _filterCategory1; }
            set
            {
                if (_filterCategory1 == value) return;
                _filterCategory1 = value;
                OnPropertyChanged("FilterCategory1");
                Activities.Refresh();
            }
        }

        private bool FilterMethod(object checkItem)
        {
            ActivityDataObject activity = checkItem as ActivityDataObject;
            if (activity == null) return false;
            if (activity.Category == ActivityCategory.Category1) return !FilterCategory1;
            if (activity.Category == ActivityCategory.Category2) return !FilterCategory2;
            if (activity.Category == ActivityCategory.Category3) return !FilterCategory3;
            if (activity.Category == ActivityCategory.Category4) return !FilterCategory4;
            return false;
        }

        public WindowViewModel()
        {
            _activities = new ObservableCollection<ActivityDataObject>(DataLoader.GetXmlData());
            _activitiesView = CollectionViewSource.GetDefaultView(_activities);
            _activitiesView.Filter = new Predicate<object>(FilterMethod);
            _useGlobalState = false;
            _globalState = ActivityState.Off;
        }

Each of the FilterCategory setters refreshes the Activities CollectionView to update the filtering. WindowViewModel also implements the INotifyPropertyChanged interface to send notices to WPF when bindings need to be updated. This is what the OnPropertyChanged call is for. Activities wraps _activitiesView which is actually an ICollectionView that is created by CollectionViewSource’s GetDefaultView static method.

Try out the app and try making some changes of your own. The activity data is loaded on startup from a TestData.xml file so you can easily try out different data settings. Window1.xaml can load in Blend RTM so you can try out all of the functionality Blend makes available for UI and data binding editing. When looking at the WindowViewModel code notice how there are no references to any part of the XAML UI. This type of class is easy to unit test and can be hooked up to completely different XAML or even a WinForms or web UI! Here again is the code.

One thought on “More WPF Data Binding, Part 2

Leave a Reply to Terrible Codeblocks Cancel reply

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