The blog of dlaa.me

One more platform difference more-or-less tamed [SetterValueBindingHelper makes Silverlight Setters better!]

Earlier this week I wrote about the "app building" exercise my team conducted and posted my sample application, a simple organizational hierarchy viewer using many of the controls in the Silverlight Toolkit. One of the surprises I had during the process of building this application was that Silverlight (version 2 as well as the Beta for version 3) doesn't support the scenario of providing a Binding in the Value of a Setter. I bumped into this when I was trying to follow one of the "best practices" for TreeView manipulation - but I soon realized the problem has much broader reach.

First, a bit about why this is interesting at all. :) Because of the way TreeView works on WPF and Silverlight, it turns out that the most elegant way of manipulating the nodes (for example, to expand all the nodes in a tree) is to do so by manipulating your own classes to which the TreeViewItem's IsExpanded property is bound. Josh Smith does a great job explaining why in this article, so I won't spend more time on that here. However, as Bea Stollnitz explains near the bottom of this post (and as I mention above), the XAML-based Setter/Value/Binding approach doesn't work on Silverlight.

In her post, Beatriz outlines a very reasonable workaround for this problem which is to subclass TreeView and TreeViewItem in order to override GetContainerForItemOverride and hook up the desired Bindings there. However, there are two drawbacks with this approach that I hoped to be able to improve upon: 1. It's limited to ItemsControl subclasses (because other controls don't have GetContainerForItemOverride) and 2. it moves design concerns into code (where designers can't see or change them).

As part of my app building work, I came up with a one-off way of avoiding the ItemsControl coupling, but it wasn't broadly useful in its original form. Fortunately, in the process of extracting that code out in generalizing it for this post, I realized how I could avoid the second drawback as well and accomplish the goal without needing any code at all - it's all XAML, all the time! [Yes, designers, I [heart] you. :) ]

The trick is to use a Setter to set a special attached DependencyProperty with a Value that specifies a special object which identifies the DependencyProperty and Binding to set. It's that easy! Well, okay, I have to do a bit of work behind the scenes to make this all hang together, but it does work - and what's more it even works for attached properties!

 

Here's an example of SetterValueBindingHelper in action:

SetterValueBindingHelperDemo sample

First, the relevant XAML for the TreeViewItem:

<Style TargetType="controls:TreeViewItem">
    <!-- WPF syntax:
    <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/> -->
    <Setter Property="local:SetterValueBindingHelper.PropertyBinding">
        <Setter.Value>
            <local:SetterValueBindingHelper
                Property="IsExpanded"
                Binding="{Binding IsExpanded}"/>
        </Setter.Value>
    </Setter>
</Style>

Yes, things end up being a little bit more verbose than they are on WPF, but if you squint hard enough the syntax is quite similar. Even better, it's something that someone who hasn't read this post should be able to figure out on their own fairly easily.

Here's the XAML for the top Button:

<Style TargetType="Button">
    <!-- WPF syntax:
    <Setter Property="Content" Value="{Binding}"/> -->
    <Setter Property="local:SetterValueBindingHelper.PropertyBinding">
        <Setter.Value>
            <local:SetterValueBindingHelper
                Property="Content"
                Binding="{Binding}"/>
        </Setter.Value>
    </Setter>
</Style>

There's really nothing new here, but I did want to show off that SetterValueBindingHelper works for non-ItemsControls as well.

Finally, here's the XAML for the right Button where two properties are being set by SetterValueBindingHelper:

<Style TargetType="Button">
    <!-- WPF syntax:
    <Setter Property="Grid.Column" Value="{Binding}"/>
    <Setter Property="Grid.Row" Value="{Binding}"/> -->
    <Setter Property="local:SetterValueBindingHelper.PropertyBinding">
        <Setter.Value>
            <local:SetterValueBindingHelper
                Type="System.Windows.Controls.Grid, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e"
                Property="Column"
                Binding="{Binding}"/>
        </Setter.Value>
    </Setter>
    <Setter Property="local:SetterValueBindingHelper.PropertyBinding">
        <Setter.Value>
            <local:SetterValueBindingHelper
                Type="Grid"
                Property="Row"
                Binding="{Binding}"/>
        </Setter.Value>
    </Setter>
</Style>

As you can see, SetterValueBindingHelper also supports setting attached DependencyPropertys, so if you find yourself in a situation where you need to style such a creature, SetterValueBindingHelper's got your back. It's also worth pointing out that the Setter for "Column" is using the official assembly-qualified naming for the Type parameter of the SetterValueBindingHelper object. This form is completely unambiguous - and it's also big, ugly, and pretty much impossible to type from memory... :) So I added code to SetterValueBindingHelper that allows users to also specify the full name of a type (ex: System.Windows.Controls.Grid) or just its short name (ex: Grid, used by the Setter for "Row"). I expect pretty much everyone will use the short name - but sleep soundly knowing you can fall back on the other forms "just in case".

 

[Click here to download the complete source code for SetterValueBindingHelper and the sample application.]

 

Lastly, here's the code for SetterValueBindingHelper in case that's all you care about:

Update (2010-10-31): At the suggestion of a reader, I've removed the implementation of SetterValueBindingHelper from this post because a newer version of the code (that works well on Silverlight 4, too) can be found in this more recent post. (FYI: The download link is the same for both posts and therefore always up-to-date.)

/// <summary>
/// Class that implements a workaround for a Silverlight XAML parser
/// limitation that prevents the following syntax from working:
///    &lt;Setter Property="IsSelected" Value="{Binding IsSelected}"/&gt;
/// </summary>
public class SetterValueBindingHelper
{
    /// <summary>
    /// Optional type parameter used to specify the type of an attached
    /// DependencyProperty as an assembly-qualified name, full name, or
    /// short name.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods",
        Justification = "Unambiguous in XAML.")]
    public string Type { get; set; }

    /// <summary>
    /// Property name for the normal/attached DependencyProperty on which
    /// to set the Binding.
    /// </summary>
    public string Property { get; set; }

    /// <summary>
    /// Binding to set on the specified property.
    /// </summary>
    public Binding Binding { get; set; }

    /// <summary>
    /// Gets the value of the PropertyBinding attached DependencyProperty.
    /// </summary>
    /// <param name="element">Element for which to get the property.</param>
    /// <returns>Value of PropertyBinding attached DependencyProperty.</returns>
    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
        Justification = "SetBinding is only available on FrameworkElement.")]
    public static SetterValueBindingHelper GetPropertyBinding(FrameworkElement element)
    {
        if (null == element)
        {
            throw new ArgumentNullException("element");
        }
        return (SetterValueBindingHelper)element.GetValue(PropertyBindingProperty);
    }

    /// <summary>
    /// Sets the value of the PropertyBinding attached DependencyProperty.
    /// </summary>
    /// <param name="element">Element on which to set the property.</param>
    /// <param name="value">Value forPropertyBinding attached DependencyProperty.</param>
    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
        Justification = "SetBinding is only available on FrameworkElement.")]
    public static void SetPropertyBinding(FrameworkElement element, SetterValueBindingHelper value)
    {
        if (null == element)
        {
            throw new ArgumentNullException("element");
        }
        element.SetValue(PropertyBindingProperty, value);
    }

    /// <summary>
    /// PropertyBinding attached DependencyProperty.
    /// </summary>
    public static readonly DependencyProperty PropertyBindingProperty =
        DependencyProperty.RegisterAttached(
            "PropertyBinding",
            typeof(SetterValueBindingHelper),
            typeof(SetterValueBindingHelper),
            new PropertyMetadata(null, OnPropertyBindingPropertyChanged));

    /// <summary>
    /// Change handler for the PropertyBinding attached DependencyProperty.
    /// </summary>
    /// <param name="d">Object on which the property was changed.</param>
    /// <param name="e">Property change arguments.</param>
    private static void OnPropertyBindingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // Get/validate parameters
        var element = (FrameworkElement)d;
        var item = (SetterValueBindingHelper)(e.NewValue);
        if ((null == item.Property) || (null == item.Binding))
        {
            throw new ArgumentException(
                "SetterValueBindingHelper's Property and Binding must both be set to non-null values.");
        }

        // Get the type on which to set the Binding
        Type type = null;
        if (null == item.Type)
        {
            // No type specified; setting for the specified element
            type = element.GetType();
        }
        else
        {
            // Try to get the type from the type system
            type = System.Type.GetType(item.Type);
            if (null == type)
            {
                // Search for the type in the list of assemblies
                foreach (var assembly in AssembliesToSearch)
                {
                    // Match on short or full name
                    type = assembly.GetTypes()
                        .Where(t => (t.FullName == item.Type) || (t.Name == item.Type))
                        .FirstOrDefault();
                    if (null != type)
                    {
                        // Found; done searching
                        break;
                    }
                }
                if (null == type)
                {
                    // Unable to find the requested type anywhere
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                        "Unable to access type \"{0}\". Try using an assembly qualified type name.",
                        item.Type));
                }
            }
        }

        // Get the DependencyProperty for which to set the Binding
        DependencyProperty property = null;
        var field = type.GetField(item.Property + "Property",
            BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static);
        if (null != field)
        {
            property = field.GetValue(null) as DependencyProperty;
        }
        if (null == property)
        {
            // Unable to find the requested property
            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                "Unable to access DependencyProperty \"{0}\" on type \"{1}\".",
                item.Property, type.Name));
        }

        // Set the specified Binding on the specified property
        element.SetBinding(property, item.Binding);
    }

    /// <summary>
    /// Returns a stream of assemblies to search for the provided type name.
    /// </summary>
    private static IEnumerable<Assembly> AssembliesToSearch
    {
        get
        {
            // Start with the System.Windows assembly (home of all core controls)
            yield return typeof(Control).Assembly;

            // Fall back by trying each of the assemblies in the Deployment's Parts list
            foreach (var part in Deployment.Current.Parts)
            {
                var streamResourceInfo = Application.GetResourceStream(
                    new Uri(part.Source, UriKind.Relative));
                using (var stream = streamResourceInfo.Stream)
                {
                    yield return part.Load(stream);
                }
            }
        }
    }
}