Mixing Style Setters and Bindings in WinRT

To say the truth, this caught me by surprise. When I stumbled upon the fact that in WinRT you cannot just assign a Binding to a Property Setter. Surprisingly enough, this is not supported.

So no <Setter Property=»Blah» Value=»{Binding Path=Bleh}» />

But some smart dudes have coined a method to make it work. At least, the did in Silverlight 4 (the version 5 already supports Bindings in those Setters). Later, the trick came to WinRT… they say WinRT was forked from Silverlight 4. Now a lot of things make sense :S

What’s the trick? Using Attached Properties.

It uses a helper class to wire up everything. It may seem ugly at first sight, but while WinRT is so restrictive (and much more for a WPF developer like me), it’s the only way I can think of that is more of less XAML oriented.

This is the helper class (WinRT, of course):

// Copyright (C) Microsoft Corporation. All Rights Reserved.
// This code released under the terms of the Microsoft Public License
// (Ms-PL, http://opensource.org/licenses/ms-pl.html).

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Markup;

namespace Delay
{
    /// <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>
    [ContentProperty(Name = "Values")]
    public class SetterValueBindingHelper
    {
        /// <summary>
        /// Gets or sets an 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>
        /// Gets or sets a property name for the normal/attached
        /// DependencyProperty on which to set the Binding.
        /// </summary>
        public string Property { get; set; }

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

        /// <summary>
        /// Gets a Collection of SetterValueBindingHelper instances to apply
        /// to the target element.
        /// </summary>
        /// <remarks>
        /// Used when multiple Bindings need to be applied to the same element.
        /// </remarks>
        public Collection<SetterValueBindingHelper> Values
        {
            get
            {
                // Defer creating collection until needed
                if (null == _values)
                {
                    _values = new Collection<SetterValueBindingHelper>();
                }
                return _values;
            }
        }

        /// <summary>
        /// Backing store for the Values property.
        /// </summary>
        private Collection<SetterValueBindingHelper> _values;

        /// <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)
            {
                // Item value present
                if ((null == item.Values) || (0 == item.Values.Count))
                {
                    // No children; apply the relevant binding
                    ApplyBinding(element, item);
                }
                else
                {
                    // Apply the bindings of each child
                    foreach (var child in item.Values)
                    {
                        if ((null != item.Property) || (null != item.Binding))
                        {
                            throw new ArgumentException(
                                "A SetterValueBindingHelper with Values may not have its Property or Binding set.");
                        }
                        if (0 != child.Values.Count)
                        {
                            throw new ArgumentException(
                                "Values of a SetterValueBindingHelper may not have Values themselves.");
                        }
                        ApplyBinding(element, child);
                    }
                }
            }
        }

        /// <summary>
        /// Applies the Binding represented by the SetterValueBindingHelper.
        /// </summary>
        /// <param name="element">Element to apply the Binding to.</param>
        /// <param name="item">SetterValueBindingHelper representing the Binding.</param>
        private static void ApplyBinding(FrameworkElement element, SetterValueBindingHelper item)
        {
            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
            TypeInfo type = null;
            if (null == item.Type)
            {
                // No type specified; setting for the specified element
                type = element.GetType().GetTypeInfo();
            }
            else
            {
                // Try to get the type from the type system
                type = System.Type.GetType(item.Type).GetTypeInfo();
                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.DefinedTypes
                            .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 allProperties = type.GetAllProperties();
            var field = allProperties.FirstOrDefault(info => info.Name.Equals(item.Property + "Property"));

            if (null != field)
            {
                property = field.GetValue(null) as DependencyProperty;
            }
            if (null == property)
            {
                // Unable to find the requsted 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>
        /// Gets a sequence 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).GetTypeInfo().Assembly;

#if SILVERLIGHT && !WINDOWS_PHONE
                // 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);
                    }
                }
#endif
            }
        }

    }

    public static class ReflectionExtensions
    {
        public static IEnumerable<PropertyInfo> GetAllProperties(this TypeInfo type)
        {
            var list = type.DeclaredProperties.ToList();

            var subtype = type.BaseType;
            if (subtype != null)
                list.AddRange(subtype.GetTypeInfo().GetAllProperties());

            return list.ToArray();
        }
    }
}

And the other important thing is to know how to use it inside XAML:

 <Button
            Grid.Column="1"
            Grid.ColumnSpan="2"
            DataContext="Coco">
            <Button.Style>
                <Style TargetType="Button">
                    <!-- Equivalent WPF syntax:
                    <Setter Property="Content" Value="{Binding}"/> -->
                    <Setter Property="delay:SetterValueBindingHelper.PropertyBinding">
                        <Setter.Value>
                            <delay:SetterValueBindingHelper
                                Property="Content"
                                Binding="{Binding}"/>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Button.Style>
        </Button>

Another example from my Project VisualDesigner:

   <designSurface:DesignSurface Background="PowderBlue"
                                     ItemTemplateSelector="{StaticResource TypedTemplateSelector}"
                                     ItemsSource="{Binding  Items}" Grid.Row="1">
            <!--<designSurface:DesignSurface.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </designSurface:DesignSurface.ItemsPanel>-->
            <designSurface:DesignSurface.ItemContainerStyle>
                <Style TargetType="winRt:CanvasItemControl">
                    <Setter Property="delay:SetterValueBindingHelper.PropertyBinding">
                        <Setter.Value>
                            <delay:SetterValueBindingHelper>
                                <delay:SetterValueBindingHelper
                                    Type="Canvas"
                                    Property="Left"
                                    Binding="{Binding Left, Mode=TwoWay}" />
                                <delay:SetterValueBindingHelper
                                    Type="Canvas"
                                    Property="Top"
                                    Binding="{Binding Top, Mode=TwoWay}" />
                                <delay:SetterValueBindingHelper
                                    Property="Width"
                                    Binding="{Binding Width, Mode=TwoWay}" />
                                <delay:SetterValueBindingHelper
                                    Property="Height"
                                    Binding="{Binding Height, Mode=TwoWay}" />
                            </delay:SetterValueBindingHelper>
                        </Setter.Value>
                    </Setter>
                </Style>
            </designSurface:DesignSurface.ItemContainerStyle>
        </designSurface:DesignSurface>

Finally, I would like to give thanks to Mark Smith (@marksm in Twitter), for pointing me out some interesting posts to the solution. He has supported me from the beginning. These were the links that he gave me 🙂

Thanks to them, too!

Un pensamiento en “Mixing Style Setters and Bindings in WinRT

  1. Thank you, this greatly helped me.
    However, I had to modify one line:
    type = System.Type.GetType(item.Type).GetTypeInfo();
    to
    type = System.Type.GetType(item.Type)?.GetTypeInfo();

    When specifying «Canvas» as the «Type», it couldn’t find it in the first assembly. This caused «GetType» to return a null, and GetTypeInfo() a NullRefrenceException. Solved using the Null-Conditional Operator.

    Solved this StackOverflow with it: http://stackoverflow.com/questions/33290099/catastrophic-failure-in-xaml-binding/35828207#35828207

Deja un comentario