Wednesday, December 1, 2010

WPF ItemContainerStyleSelector unlimited: going data-bound

Use case
You have a View Model which contains a collection of items. In View, items are presented in an ItemsControl, say ListBox, and containers for each item should have different styles. Styles are selected depending on some property of view model items, e.g. Type property. An example of such situation is when each Type has its own combination of colors.
At this point, the task can be easily solved using a StyleSelector which encapsulates the selection logic. It's rather simple, so I won't discuss it.
To make task really interesting, let's require the selection logic to track changes in data items like Bindings do, i.e. if the value of the Type property changes, Style should be changed in the moment.


Designing solution
There's actually no problem in setting up data-bound Style for a control that is created in XAML:
<UserControl>
  <UserControl.Resources>
    <StyleSelectingConverter x:Key="mapper"/>
  </UserControl.Resources>
  <TextBox Style="{Binding Type, Converter={StaticResource mapper}}"/>
</UserControl>
But in case of ItemsControl's containers, we cannot set their properties directly - containers are generated dynamically.
We also can't use ItemContainerStyle or implicit style with relevant TargetType, because WPF doesn't allow a Style change the value of the Style property of the object that Style is applied to: neither by setting a Binding, nor by a Trigger.
ItemContainerStyleSelector won't fit because its output is applied to the container once and doesn't track the data item's property changes (although it respects virtualization). We also can't set Binding from the StyleSelector.SelectStyle method, because it will be reset when the ItemsControl applies selector output to the container.

Thus, we have to write some class that will track item container generation and set bindings on Style property once a container is created by the generator. Let me introduce, the

DynamicStyleInjector
First of all, we need some way to assign the binding to be injected into containers:
 public class DynamicStyle
 {

  #region ItemContainerStyleBinding attached dependency property

  public static BindingExpressionBase GetItemContainerStyleBinding(ItemsControl obj)
  {
   return (BindingExpressionBase)obj.GetValue(ItemContainerStyleBindingProperty);
  }

  public static void SetItemContainerStyleBinding(ItemsControl obj,
    BindingExpressionBase value)
  {
   obj.SetValue(ItemContainerStyleBindingProperty, value);
  }

  // Using a DependencyProperty as the backing store for ItemContainerStyleBinding.  This enables animation, styling, binding, etc...
  public static readonly DependencyProperty ItemContainerStyleBindingProperty =
    DependencyProperty.RegisterAttached("ItemContainerStyleBinding",
      typeof(BindingExpressionBase), typeof(DynamicStyle),
      new UIPropertyMetadata(null, OnItemContainerStyleBindingChanged));
...
With this attached property we can apply data-bound style to containers in any ItemsControl. The property has BindingExpressionBase type in order to allow us pass bindings created in XAML. It's because the XAML parser automatically calls ProvideValue method on any markup extension, which in case of BindingBase returns a BindingExpressionBase instance, and then passes the result to the DependencyObject.SetValue method. The latest will apply binding to the target property unleast the type of the property is BindingExpressionBase - in that case it will simply store the expression as property value.

If the value changes to non-null, a DynamicStyleInjector object is created and stored in a private attached property:
  static void OnItemContainerStyleBindingChanged(ItemsControl control,
      BindingExpressionBase newBindingExpressionBase,
      BindingExpressionBase oldBindingExpressionBase)
  {
   Debug.Assert(control != null, "control != null");

   if (oldBindingExpressionBase != null)
   {
    var injector = GetInjector(control);
    Debug.Assert(injector != null, "injector != null");

    injector.Detach();
    SetInjector(control, null);
   }

   if (newBindingExpressionBase != null)
   {
    SetInjector(control, new DynamicStyleInjector(control));
   }
  }
We need this property to be able to dispose off an active injector if the binding changes (oh, I know that having this binding change on-the-fly would be a really mad scenario, but it's a good habbit to make a feature able to switch off and besides it's not difficult).

Once created, DynamicStyleInjector gets the current value of the ItemContainerStyleBinding attached property and assigns this binding to all existing containers in the control:
  void AttachBinding(BindingExpressionBase bindingExpression)
  {
   Debug.Assert(binding != null, "binding != null");

   //filter out only those containers that do not yet have correct value (optimizing performance) 
   foreach (var container in this.GetAllExistingContainers()
      .Where(x => bindingExpression != BindingOperations.GetBindingExpressionBase(x, FrameworkElement.StyleProperty)))
   {
    container.SetBinding(FrameworkElement.StyleProperty,
        bindingExpression.ParentBindingBase);
   }
  }

  private IEnumerable<FrameworkElement> GetAllExistingContainers()
  {
   var observedControl = this._ObservedControl;
   Debug.Assert(observedControl != null, "observedControl != null");
   var generator = observedControl.ItemContainerGenerator;
   Debug.Assert(generator != null, "generator != null");

   return (from object item in observedControl.Items
           let container = generator.ContainerFromItem(item)
           where container != null
           select container).OfType<FrameworkElement>().ToArray();
  }
We have to refer to the ParentBindingBase property of the bindingExpression in the AttachBinding method, because a BindingExpressionBase can only be set to one object, and if we use BindingBase and SetBinding, a new BindingExpressionBase is created for each target, so everything works fine.

Finally, the injector attaches to the StatusChanged event of the observed control's ItemContainerGenerator and in the event handler updates Style property binding depending on the active ItemContainerStyleBinding property value:
  void generator_StatusChanged(object sender, EventArgs e)
  {
   var observedControl = this._ObservedControl;
   Debug.Assert(observedControl != null, "observedControl != null");
   var generator = observedControl.ItemContainerGenerator;
   Debug.Assert(generator != null, "generator != null");
   Debug.Assert(sender == generator, "sender == generator");

   switch (generator.Status)
   {
    //this status means that several items were likely to be generated
    case GeneratorStatus.ContainersGenerated:
     var bindingExpression = DynamicStyle.GetItemContainerStyleBinding(observedControl);
     if (bindingExpression != null) this.AttachBinding(bindingExpression);
     else this.DetachBinding();
     break;
    //still need to wait before generation process completes
    case GeneratorStatus.GeneratingContainers:
    case GeneratorStatus.NotStarted:
    case GeneratorStatus.Error:
     break;
    default:
     Debug.Assert(false, String.Format("Unexpected GeneratorStatus: {0}", generator.Status));
     break;
   }
  }
With this handler, the injector applies binding to dynamically generated containers (in fact, it won't work without this, because there are usually no existing containers on the ItemsControl when the ItemContainerStyle property value is set during XAML processing).

Usage
Now we can set any binding (simple or multi-binding) to an ItemsControl's containers' Style property like this:
<ListBox ItemsSource="{Binding Items}"
   src:DynamicStyle.ItemContainerStyleBinding="{Binding Key, Converter={StaticResource mapper}}"
   DisplayMemberPath="Title" SelectionMode="Extended">
</ListBox>
The technique allows us dynamically swap containers' styles as data item's source property ('Key') changes, even if the change happens after the ListBox has been shown. Is also supports dynamic modifications of source items collection, as well as UI virtualization.

Full Sample
Complete source code of described classes, together with a sample WPF application can be found at http://www.filefactory.com/file/b481cd4/n/DynamicItemsControlItemStyle.zip

A real world task that requires the described technique is described in another post.

No comments:

Post a Comment