Wednesday, 23 December 2009

Specifying Resource Keys using Data Binding in WPF

Imagine you’re wanting to show a list of things in an ItemsControl, with each item having a different image. Using WPF’s implicit Data Templating support, and giving each item Type its own Data Template is one way of implementing this: but if there are many items, and the image is the only thing that’s different in each case, and the Data Template is of any complexity, your code will soon start to suffer DRY rot.

You could just pinch your nose and put the image in your ViewModel so that it can be databound in the normal way. Of course, images should really live in a ResourceDictionary: but how can you pick a resource out of a ResourceDictionary using data binding? Let me show you.

The example: AutoTherapist

Here’s what I want to build:ResourceBindingSampleImage

I’ve got a very simple ViewModel with a property exposing a list of the commands that sit behind the buttons in my Window:

public class WindowViewModel
{
    public IList<ICommand> Commands
    {
        get
        {
            return new ICommand[] { new AngryCommand(), new HappyCommand(), new CoolCommand() };
        }
    }
}

And here’s the relevant part of the View:

<ItemsControl Grid.Row="1" ItemsSource="{Binding Commands}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Button Command="{Binding}" Padding="2" Margin="2" Width="100" Height="100">
          <StackPanel>
            <Image HorizontalAlignment="Center"
                   Width="60"
                   app:ResourceKeyBindings.SourceResourceKeyBinding="{Binding Converter={StaticResource ResourceKeyConverter}, ConverterParameter=Image.{0}}"/>
            <TextBlock Text="{Binding Name}" HorizontalAlignment="Center" FontWeight="Bold" Margin="0,2,0,0"/>
          </StackPanel>
        </Button>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal"/>
      </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

The ItemsControl is bound to the Commands property on my ViewModel. Each command is rendered with the same DataTemplate. The magic happens in line 8 where I’m using the attached property ResourceKeyBindings.SourceResourceKeyBinding. This property allows me to data-bind the key of the resource I want to use for the Image.Source property. I’ll show you how that works in a minute, but first: where are the resource keys coming from?

You’ll notice that, since there’s no Path specified in the Binding, we’re binding directly to the data object - in this case, one of the commands. Then we’re using a converter to turn that object into the appropriate key. Here’s the code for the converter:

class TypeToResourceKeyConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var formatString = parameter as string;
        var type = value.GetType();
        var typeName = type.Name;

        var result = string.Format(formatString, typeName);

        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

What this is doing is getting the name of the Type of the data object and pushing that through the format string given as the parameter to the converter. Given the way the converter is set up in our case, this will produce resource keys like “Image.AngryCommand”, “Image.HappyCommand”, etc.

So now all we need to make the AutoTherapist work is to define those resources. Here’s App.xaml:

<Application x:Class="ResourceKeyBindingSample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
      <BitmapImage x:Key="Image.AngryCommand" UriSource="Angry.png"/>
      <BitmapImage x:Key="Image.CoolCommand" UriSource="Cool.png"/>
      <BitmapImage x:Key="Image.HappyCommand" UriSource="Happy.png"/>
    </Application.Resources>
</Application>

(The icons are from VistaIcons.com, by the way).

The implementation

So what does that attached property look like? It’s actually rather simple:

public static class ResourceKeyBindings
{
    public static DependencyProperty SourceResourceKeyBindingProperty = ResourceKeyBindingPropertyFactory.CreateResourceKeyBindingProperty(
        Image.SourceProperty,
        typeof(ResourceKeyBindings));

    public static void SetSourceResourceKeyBinding(DependencyObject dp, object resourceKey)
    {
        dp.SetValue(SourceResourceKeyBindingProperty, resourceKey);
    }

    public static object GetSourceResourceKeyBinding(DependencyObject dp)
    {
        return dp.GetValue(SourceResourceKeyBindingProperty);
    }
}

As you can see, I’ve factored out the magic into ResourceKeyBindingPropertyFactory. This makes it easy to create equivalent properties for any other target property (in the download, for example, I’ve made a StyleResourceKeyBinding property for binding to FrameworkElement.Style). ResourceKeyBindingPropertyFactory looks like this:

public static class ResourceKeyBindingPropertyFactory
{
     public static DependencyProperty CreateResourceKeyBindingProperty(DependencyProperty boundProperty, Type ownerClass)
     {
         var property = DependencyProperty.RegisterAttached(
             boundProperty.Name + "ResourceKeyBinding",
             typeof(object),
             ownerClass,
             new PropertyMetadata(null, (dp, e) =>
             {
                 var element = dp as FrameworkElement;
                 if (element == null)
                 {
                     return;
                 }

                 element.SetResourceReference(boundProperty, e.NewValue);
             }));

         return property;
     }
}

All we do here is register an attached property and set it up with a PropertyChanged handler: the handler simply takes the new value of the property – which in our case will be the resource key – and passes it to SetResourceReference along with the target property. SetResourceReference is the programmatic equivalent of using DynamicResource in XAML – it looks up the appropriate resource (from the current element’s ResourceDictionary or one of its ancestors’) and assigns it to the given property.

So there you have it: data binding for Resource Keys. Full source code for this sample is available from the MSDN Code Gallery.

Not for Silverlight

Unfortunately I don’t think it is possible to port this to Silverlight without a lot of work because Silverlight has no support for Dynamic Resources. From a cursory look, I don’t think there is even runtime support built in for finding a Resource in the Resource Dictionary chain up the ancestor tree of an element as there is in WPF: it looks like the Silverlight parser is responsible for doing this when required by StaticResource references. I would be delighted if someone could show me otherwise

0 comments:

Post a Comment