Thursday, December 2, 2010

WPF: Color your items with data-binding

I've recently run into a tricky task while implementing a Windows Eplorer-like browser feature in the project I work at. I have a collection of data items (in my View Model) that have a 'Type' property of enumeration type, and I want to display them in a ListBox. The tough requirement is that items should be colored depending on their Type property value, including custom Foreground and Background of selected items (for various combinations of IsSelected, IsSelectionActive and IsEnabled properties).


Requirements to solution
  1. Coloring has to be data-bound and work like a binding or a data trigger.
  2. Leave the default ListBoxItem template as is, namely
    <Style targettype="{x:Type ListBoxItem}" x:key="ListBoxItemStyle1">
     <Setter Property="Background" Value="Transparent"/>
     <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
     <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
     <Setter Property="Padding" Value="2,0,0,0"/>
     <Setter Property="Template">
      <Setter.Value>
       <Controltemplate TargetType="{x:Type ListBoxItem}">
        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
         <Contentpresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
        </Border>
        <ControlTemplate.Triggers>
         <Trigger Property="IsSelected" Value="true">
          <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
          <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
         </Trigger>
         <Multitrigger>
          <MultiTrigger.Conditions>
           <Condition Property="IsSelected" Value="true"/>
           <Condition Property="Selector.IsSelectionActive" Value="false"/>
          </MultiTrigger.Conditions>
          <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
          <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
         </MultiTrigger>
         <Trigger Property="IsEnabled" Value="false">
          <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
         </Trigger>
        </ControlTemplate.Triggers>
       </ControlTemplate>
      </Setter.Value>
     </Setter> 
    </Style>
    
    This would also keep UI-related triggers and ViewModel-related triggers separated from each other.
  3. Keep brush combinations grouped by the same Type value.
  4. Take some of the brushes from the application's ResourceDictionary.
Possible solution
The best solution is to override SystemColor.* brushes and swap their combinations according to the type of the item. In simple cases this could be done by adding brushes to some control's resources like this:
<UserControl.Resources>
  <SolidColorBrush Color="Red"
      x:Key="{x:Static SystemColors.HighlightTextBrushKey}"/>
</UserControl.Resources>
But in my case, things are much more complicated:
  1. I need to put several styles to some resource dictionary, each having several SystemColor brushes overrides.
  2. I have to use StaticResourceExtensions to create aliases for the brushes defined in my application's resources.
  3. In order for DynamicResourceExtension from ListBoxItem's default template to work fine, brushes must be added to the resources of the ListBoxItems themselves or their styles/templates. (Since their parent is the ListBox)
  4. Style selection logic has to be situated on the ListBoxItems.
  5. The only built-in way to inject something into ItemsControl's containers in XAML is to use ItemContainerStyle, ItemContainerStyleSelector properties or implicit styling.
So the brushes should be organized like this:
<UserControl.Resources>
  <Style x:Key="style1">
    <Style.Resources>
      <StaticResourceExtension ResourceKey="Type1HighlightTextBrush"
          x:Key="{x:Static SystemColors.HighlightTextBrushKey}"/>
      <StaticResourceExtension ResourceKey="Type1HighlightBrush"
          x:Key="{x:Static SystemColors.HighlightBrushKey}"/>
    </Style.Resources>
  </Style>

  <Style x:Key="style2">
    <Style.Resources>
      <StaticResourceExtension  ResourceKey="Type2HighlightTextBrush"
          x:Key="{x:Static SystemColors.HighlightTextBrushKey}"/>
      <StaticResourceExtension ResourceKey="Type2HighlightBrush"
          x:Key="{x:Static SystemColors.HighlightBrushKey}"/>
    </Style.Resources>
  </Style>
...
</UserControl.Resources>
And we need some way to inject style selection logic (binding with converter or triggers) into the ListBoxItems.

Here come the troubles
Unfortunately, WPF has several disappointing restrictions that make the task very difficult:
  1. Since I want to swap styles of containers, I cannot place selection logic into ItemContainerStyle or implicit ListBoxItem style - WPF doesn't allow an object's style to change that object's Style property value at the same time.
  2. ItemContainerStyleSelector would work fine even with UI virtualization, but it will fail to update if the type of a data item changes after the container for the item has been generated and applied.
  3. ItemContainerStyleSelector cannot set bindings on Style property - they would be removed by the ItemsControl when it would apply the style returned by the selector.
  4. WPF doesn't support StaticResourceExtensions in a Style's Resources. I suppose, this is a XAML parser's bug, since it doesn't return any reasonable error message, but it unconditionally fails with strange exceptions in run-time if there is a StaticResourceExtension in a Style.Resources tag.
The Solution
(partial)
To solve the task, I created several simple classes:
  1. DictionaryValueConverter - an implementation of IValueConverter that uses an IDictionary to map raw binding values to something, namely styles in my case. Raw values play Key roles. In essence, the class is as simple as the following code:
    public class DictionaryValueConverter : IValueConverter
    {
      public IDictionary Dictionary { get; set; }
    
      public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
      {
        return this.Dictionary[value];
      }
    }
    
    although the final implementation involves some additional details, such as exception handling, etc.
    This converter allows me create a resource implementing IDictionary, which stores all required styles keyed by corresponding data item Type property value, and then write a simple binding from data item to container Style which uses the converter. In other words, I can declaratively map data item types to item container styles, which is rather elegant.
  2. DynamicStyleInjector - a class that tracks ItemsControl item containers generation and sets bindings to their Style property on the fly. This class solves style selection issue (troubles 1-3). In fact, this is the cornerstone of the solution, because it allows me set binding to ListBox's item containers' Style property.
I haven't solved the StaticResourceExtension and Style integration issue yet, so for now I have to create copies of brushes. Perhaps, the issue will be solved in a couple of weeks.

For now, the solution is set up in the following way:
<Window.Resources>
  <Style x:Key="defaultStyle">
   <Setter Property="TextElement.Foreground" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
   <Setter Property="Border.Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
  </Style>
  
  <!--Define all possible styles, each style's key is the type of an item-->
  <Collections:Hashtable x:Key="styleMap">
   <Style BasedOn="{StaticResource defaultStyle}" x:Key="a">
    <Style.Resources>
     <ResourceDictionary>
      <SolidColorBrush Color="Brown" x:Key="{x:Static SystemColors.WindowTextBrushKey}"/>
      <SolidColorBrush Color="Red" x:Key="{x:Static SystemColors.ControlTextBrushKey}"/>
      <SolidColorBrush Color="Green" x:Key="{x:Static SystemColors.ControlBrushKey}"/>
      <SolidColorBrush Color="Blue" x:Key="{x:Static SystemColors.HighlightTextBrushKey}"/>
      <SolidColorBrush Color="Yellow" x:Key="{x:Static SystemColors.HighlightBrushKey}"/>
     </ResourceDictionary>
    </Style.Resources>
   </Style>
   <Style BasedOn="{StaticResource defaultStyle}" x:Key="b">
    <Style.Resources>
     <SolidColorBrush Color="Green" x:Key="{x:Static SystemColors.ControlTextBrushKey}"/>
     <SolidColorBrush Color="Brown" x:Key="{x:Static SystemColors.ControlBrushKey}"/>
     <SolidColorBrush Color="Lime" x:Key="{x:Static SystemColors.HighlightTextBrushKey}"/>
     <SolidColorBrush Color="Maroon" x:Key="{x:Static SystemColors.HighlightBrushKey}"/>
    </Style.Resources>
   </Style>
   <Style BasedOn="{StaticResource defaultStyle}" x:Key="c">
    <Style.Resources>
     <SolidColorBrush Color="Blue" x:Key="{x:Static SystemColors.ControlTextBrushKey}"/>
     <SolidColorBrush Color="Red" x:Key="{x:Static SystemColors.ControlBrushKey}"/>
     <SolidColorBrush Color="Orange" x:Key="{x:Static SystemColors.HighlightTextBrushKey}"/>
     <SolidColorBrush Color="Black" x:Key="{x:Static SystemColors.HighlightBrushKey}"/>
    </Style.Resources>
   </Style>
   <Style BasedOn="{StaticResource defaultStyle}" x:Key="d">
    <Style.Resources>
     <SolidColorBrush Color="Yellow" x:Key="{x:Static SystemColors.ControlTextBrushKey}"/>
     <SolidColorBrush Color="Green" x:Key="{x:Static SystemColors.ControlBrushKey}"/>
     <SolidColorBrush Color="Blue" x:Key="{x:Static SystemColors.HighlightTextBrushKey}"/>
     <SolidColorBrush Color="Yellow" x:Key="{x:Static SystemColors.HighlightBrushKey}"/>
    </Style.Resources>
   </Style>
  </Collections:Hashtable>
  
  <src:DictionaryValueConverter x:Key="mapper" Dictionary="{StaticResource styleMap}"/>
 </Window.Resources>
 
 <DockPanel>
    <!--Items are taken from DataContext's property Items.
      Each of the items has Key property which denotes the item's type.
      ListBox is capable of tracking items addition/removal,
      and each item updates its style dynamically when Key property value changes.-->
  <ListBox ItemsSource="{Binding Items}"
       src:DynamicStyle.ItemContainerStyleBinding="{Binding Key, Converter={StaticResource mapper}}"
       DisplayMemberPath="Title" SelectionMode="Extended">
  </ListBox>
  
  
 </DockPanel>
Sources of the sample demonstrating all this put together can be found here: http://www.filefactory.com/file/b481cd4/n/DynamicItemsControlItemStyle.zip

About my original task and real life applications
I must note, that my original task, described in the intro section, did not require such complex solution. In fact, a StyleSelector that uses an IDictionary to choose styles would have been absolutely anough, because my view model items had constant Type property value (requirement #1 didn't exist for me).

But I can give a sample task that would fit to all the requirements. Imagine that you have to display a list of, say, stock tickers, styled depending on their price or daily growth/fall. You want it to be real-time, because it is a part of a stock trading software. In this case using the given two classes would allow you create a beautiful loosely-coupled solution.

1 comment:

  1. Link to source code is not working anymore. Could you please provide a new link?

    ReplyDelete