The Expression SDK comes with a great feature called "Behaviors" which allows the designer to attach a unit of functionality (like drag-and-drop) to a visual element. Expression comes bundled with a set of general purpose behaviors, and developers can add more to give the designers functionality that is missing.
Behaviors are awesome, and it sure would be nice to be able to define a style that can be applied to any particular button -- for example, maybe your designer wants certain buttons to "pop" or "wiggle" when the mouse hovers over them.
The best way do that would be to define a style (perhaps in a shared resource dictionary) that looks something like this:
<Style TargetType="{x:Type Button}">
<Setter Property="[some property here]">
<Setter.Value>
<MyBehaviors:PopOnHoverBehavior />
</Setter.Value>
</Setter>
</Style>
The problem is that there isn't an attached property included in the Expression SDK to put in the "[some property here]" blank... But not to fear! It is easy to create an attached property to give us the ability to make this style work.
First, we'll define a new class to contain the new attached property:
#region using System.Windows;
using System.Windows.Interactivity;
#endregion
namespace LivingAgile.Common.WPF
{
public class StylizedBehaviors
{
}
}
Next we'll add the definition for the new attached property:
#region Fields (public)
public static readonly DependencyProperty BehaviorsProperty = DependencyProperty.RegisterAttached(
@"Behaviors",
typeof ( StylizedBehaviorCollection ),
typeof ( StylizedBehaviors ),
new FrameworkPropertyMetadata( null, OnPropertyChanged ) );
#endregion
#region Static Methods (public)
public static StylizedBehaviorCollection GetBehaviors ( DependencyObject uie )
{
return ( StylizedBehaviorCollection )uie.GetValue( BehaviorsProperty );
}
public static void SetBehaviors ( DependencyObject uie, StylizedBehaviorCollection value )
{
uie.SetValue( BehaviorsProperty, value );
}
#endregion
Then we will add the "OnPropertyChanged" method:
#region Static Methods (private)
private static void OnPropertyChanged ( DependencyObject dpo, DependencyPropertyChangedEventArgs e )
{
var uie = dpo as UIElement;
if ( uie == null )
{
return;
}
BehaviorCollection itemBehaviors = Interaction.GetBehaviors( uie );
var newBehaviors = e.NewValue as StylizedBehaviorCollection;
var oldBehaviors = e.OldValue as StylizedBehaviorCollection;
if ( newBehaviors == oldBehaviors )
{
return;
}
if ( oldBehaviors != null )
{
foreach ( var behavior in oldBehaviors )
{
int index = itemBehaviors.IndexOf( behavior );
if ( index >= 0 )
{
itemBehaviors.RemoveAt( index );
}
}
}
if ( newBehaviors != null )
{
foreach ( var behavior in newBehaviors )
{
int index = itemBehaviors.IndexOf( behavior );
if ( index < 0 )
{
itemBehaviors.Add( behavior );
}
}
}
}
#endregion
This method does the bulk of the work. When a new collection of behaviors is assigned to the attached property, this method will remove any old behaviors that are no longer required from the visual element (though it is doubtful whether this is useful), and it will add any new behaviors to the visual element.
The last thing we need is to define the StylizedBehaviorCollection class:
#region
using System.Windows;
using System.Windows.Interactivity;
#endregion
namespace LivingAgile.Common.WPF
{
public class StylizedBehaviorCollection : FreezableCollection<Behavior>
{
#region Methods (protected)
protected override Freezable CreateInstanceCore ( )
{
return new StylizedBehaviorCollection( );
}
#endregion
}
}
Now we are ready to go! The style from above can now be written as:
<Style TargetType="{x:Type Button}">
<Setter Property="WPF:StylizedBehaviors.Behaviors">
<Setter.Value>
<WPF:StylizedBehaviorCollection>
<MyBehaviors:PopOnHoverBehavior />
</WPF:StylizedBehaviorCollection>
</Setter.Value>
</Setter>
</Style>
For this example, assuming you use the same namespaces that I did, you would need to define the WPF namespace in your XAML file as:
xmlns:WPF="clr-namespace:LivingAgile.Common.WPF"
And that is all there is to it! Enjoy.
What about Blend's Attached Triggers?
The Expression Blend SDK also includes a concept of attached triggers, and you may want to extend this design to add another attached property for assigning these triggers from styles. I leave this as an exercise for the reader. The code above can be used as a template and it would be very easy to add this feature!
Why it works...
You might be thinking that this solution won't work if the same style is applied to several visual elements since an instance of a behavior can only be attached to one visual element at a time. The answer is that anything that derives from Freezable, and stores all of its state using dependency properties, can be cloned (i.e. deep copied).
When WPF applies a style to a visual element, it inspects the setter's values to see whether they are freezables and whether they are frozen. If they are frozen, the same instance will be shared across all visual elements that implement the style, which is fine because frozen means it cannot change or vary. This really only applies to things like visual brushes that don't have any internal state that will vary based on the inheritance context, etc.
When WPF sees that the setter's value derives from Freezable and isn't frozen, it clones the instance defined in the style for each visual element that implements the style and assigns the cloned values to the visual elements. That is why it is critical that the collection class derives from FreezableCollection<>. If the collection class was just an ObservableCollection<>, we would not get this cloning behavior and the solution would break.