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 Binding
s 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:
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 DependencyProperty
s, 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".
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.)