FlexibleBehavior class: More on reuse and Behaviors
A recent post explained how encapsulating functionality (e.g. Animations, etc) into a Behavior promoted reuse in that once this functionality is encapsulated into a Behavior, it can then easily be applied to multiple Controls in various projects by a designer using Blend (remember “write once run anywhere”!)
Thinking deeper I thought: “That’s not really as reusable as it could be. The triggering event is essentially hardcoded”, let me explain:
The typical workflow may go something like this:
- the Designer gives the Developer specs for a needed Behavior: “I need to ‘dim’ a FrameworkElement via its Opacity value when the mouse enters my control and I want to control the duration of and amount its dimmed via Blend”
- the Developer codes up a Behavior packaged into a DLL that the designer then references in Blend. The coder exposes public Properties for the Duration and Opacity so the Designer can tweak those settings in the Blend Properties Panel. He makes the Behavior trigger on the MouseEnter Event by adding a listener in the OnAttached() method.
Great stuff! The much ballyhooed Holy Grail of the “Design/Developer Workflow” is in full flower. But the leaves of this flower may start to fall off if the Designer comes back to the Developer (a few times) and says: “You know, I think we should use the LostFocus Event instead of MouseEnter”. The developer needs to touch the code for the Behavior, recompile, redistribute the DLL and the Designer then references the newly updated DLL. Can you see the problem? The Event is hardcoded in the Behavior. Wouldn’t it be great if the Designer could specify the Event in the Properties Panel like the other properties of the Behavior?
If what you are doing in the handler for whatever Event triggers the Behavior does not rely on the specific Event Type passed to it (like MouseEventArgs), and you can live with the basic Event class, then you can use a generic handler for any of the 15 odd Events exposed by the FrameworkElement class. You can then add a public property to the Behavior that allows the Designer to specify which event to use as the triggering event right in Blend and logic (say a switch) to figure out what Event to listen for in the Behavior by examining the value the designer put into the EventName field in Blend. The result is that the Event is no longer hardcoded and the Designer controls which event to use via Blend. He can apply this Behavior to any project for any FrameworkElement and use any of the available events.
General functionality (like tweaking the Opacity) may be applied to many different types of FrameworkElements on many different types of events. I created a “FlexibleBehavior” class that inherits from Behavior as follows:
using System; using System.ComponentModel; using System.Windows; using System.Windows.Interactivity; namespace FlexibleBehavior { public class FlexibleBehavior : Behavior { private string triggeringeventname = "MouseEnter"; public string TriggeringEventName { get { return triggeringeventname; } set { triggeringeventname = value; } } protected override void OnAttached() { base.OnAttached(); switch (TriggeringEventName) { case "BindingValidationError": AssociatedObject.BindingValidationError += AssociatedObject_GenericEventHandler; break; case "GotFocus": AssociatedObject.GotFocus += AssociatedObject_GenericEventHandler; break; case "KeyDown": AssociatedObject.KeyDown += AssociatedObject_GenericEventHandler; break; case "KeyUp": AssociatedObject.KeyUp += AssociatedObject_GenericEventHandler; break; case "LayoutUpdated": AssociatedObject.LayoutUpdated += AssociatedObject_GenericEventHandler; break; case "Loaded": AssociatedObject.Loaded += AssociatedObject_GenericEventHandler; break; case "LostFocus": AssociatedObject.LostFocus += AssociatedObject_GenericEventHandler; break; case "LostMouseCapture": AssociatedObject.LostMouseCapture += AssociatedObject_GenericEventHandler; break; case "MouseEnter": AssociatedObject.MouseEnter += AssociatedObject_GenericEventHandler; break; case "MouseLeave": AssociatedObject.MouseLeave += AssociatedObject_GenericEventHandler; break; case "MouseLeftButtonDown": AssociatedObject.MouseLeftButtonDown += AssociatedObject_GenericEventHandler; break; case "MouseLeftButtonUp": AssociatedObject.MouseLeftButtonUp += AssociatedObject_GenericEventHandler; break; case "MouseMove": AssociatedObject.MouseMove += AssociatedObject_GenericEventHandler; break; case "MouseWheel": AssociatedObject.MouseWheel += AssociatedObject_GenericEventHandler; break; case "SizeChanged": AssociatedObject.SizeChanged += AssociatedObject_GenericEventHandler; break; default: break; } } protected override void OnDetaching() { base.OnDetaching(); switch (TriggeringEventName) { case "BindingValidationError": AssociatedObject.BindingValidationError -= AssociatedObject_GenericEventHandler; break; case "GotFocus": AssociatedObject.GotFocus -= AssociatedObject_GenericEventHandler; break; case "KeyDown": AssociatedObject.KeyDown -= AssociatedObject_GenericEventHandler; break; case "KeyUp": AssociatedObject.KeyUp -= AssociatedObject_GenericEventHandler; break; case "LayoutUpdated": AssociatedObject.LayoutUpdated -= AssociatedObject_GenericEventHandler; break; case "Loaded": AssociatedObject.Loaded -= AssociatedObject_GenericEventHandler; break; case "LostFocus": AssociatedObject.LostFocus -= AssociatedObject_GenericEventHandler; break; case "LostMouseCapture": AssociatedObject.LostMouseCapture -= AssociatedObject_GenericEventHandler; break; case "MouseEnter": AssociatedObject.MouseEnter -= AssociatedObject_GenericEventHandler; break; case "MouseLeave": AssociatedObject.MouseLeave -= AssociatedObject_GenericEventHandler; break; case "MouseLeftButtonDown": AssociatedObject.MouseLeftButtonDown -= AssociatedObject_GenericEventHandler; break; case "MouseLeftButtonUp": AssociatedObject.MouseLeftButtonUp -= AssociatedObject_GenericEventHandler; break; case "MouseMove": AssociatedObject.MouseMove -= AssociatedObject_GenericEventHandler; break; case "MouseWheel": AssociatedObject.MouseWheel -= AssociatedObject_GenericEventHandler; break; case "SizeChanged": AssociatedObject.SizeChanged -= AssociatedObject_GenericEventHandler; break; default: break; } } protected virtual void AssociatedObject_GenericEventHandler(object sender, EventArgs e) { } } } |
With that DLL referenced in a new Silverlight Class Library project, I created the DimmerBehavior class that extends FlexibleBehavior and added my Behavior specific functionality (mess with the Opacity via a Storyboard) and did not specify any specific event:
using System; using System.ComponentModel; using System.Windows; using System.Windows.Media.Animation; namespace DimmerBehavior { [Description("Animates the Opacity of a FrameworkElement")] public class DimmerBehavior : FlexibleBehavior.FlexibleBehavior { private Storyboard storyboard; private double opacityTo = 0.2; private TimeSpan dimmer_duration = TimeSpan.FromSeconds(.2); // public Property needed for the Blend Properties Panel [Description("The Event of FrameworkElement object that triggers this Behavior")] public string Eventname { get { return base.TriggeringEventName; } set { base.TriggeringEventName = value; } } [Description("Value of the AssociatedObject's Opacity property to animate to")] public double OpacityTo { get { return opacityTo; } set { opacityTo = value; } } [Description("Duration of the animation")] public TimeSpan Dimmer_duration { get { return dimmer_duration; } set { dimmer_duration = value; } } protected override void OnAttached() { base.OnAttached(); } protected override void OnDetaching() { base.OnDetaching(); } protected override void AssociatedObject_GenericEventHandler(object sender, EventArgs e) { base.AssociatedObject_GenericEventHandler(sender, e); var doubleanimation = new DoubleAnimation(); doubleanimation.To = opacityTo; doubleanimation.Duration = dimmer_duration; storyboard = new Storyboard(); storyboard.Duration = dimmer_duration; storyboard.Children.Add(doubleanimation); Storyboard.SetTarget(doubleanimation, AssociatedObject); Storyboard.SetTargetProperty(doubleanimation, new PropertyPath(FrameworkElement.OpacityProperty)); storyboard.Begin(); } } } |
Creating a Silverlight Application in Blend and dragging the DimmerBehavior from the Assets Panel onto a FramerworkElement on the artboard you’ll see where the Designer can specify the name of the Event in the Properties panel (more on that below):

And here’s a tester Application with the MouseLeave event dialed in via Blend. Run it and see it dim when the mouse leaves the rectangle:
Your browser does not support iframes.
If you later want to use a different event, just change it in Blend – no need to wake up the developer! The code for all of this is available here.
There’s one glaring improvement that can be made here and that is to put Blend’s Design-time extensibility into play so that the Designer can not hurt himself by misspelling (or not knowing which events are available) the Event in the EventName field in the Blend Properties Panel by using a ComboBox that lists all these events for easy selection as well as precluding anything hand-typed.
But that subject (Blend design time extensibility) is a vast and somewhat confusing one as well as being somewhat of a moving target with Silverlight and WPF and Visual Studio 2008 and 2010 (with a designer!) and a new version of Blend to deal with. Its a fascinating subject and required knowledge for Custom Control authors but its not explored here. If you are interested see these links:
- Justin Angel rolled up his sleeves and wrote the first definitive guide
- Karl Shifflett has started a blog series recently. Here’s Part 1
- Karl Shifflett’s Part 2
- Ning Zhang also has a helpful post
Cheers!
[...] FlexibleBehavior class: More on reuse and Behaviors (Bob Bartholomay) [...]
[...] FlexibleBehavior class: More on reuse and Behaviors [...]
One other way to do this is to split the Behavior into a Trigger and a TriggerAction (both base classes along with Behavior). Blend comes with a EventTrigger out of the box including designer support for selecting the event. The TriggerAction would then be your Animation.
I find splitting them like this allows a lot more composiblity by the designer. The actual Behavior base class should only really be used when the Trigger and Action are intertwined.
Social comments and analytics for this post…
This post was mentioned on Twitter by SilverlightNews: FlexibleBehavior class: More on reuse and Behaviors – http://snurl.com/u7gzy...