A Look at Some Advanced Behaviors Features

Jeff Kelly is back with Part III of his behaviors coverage. This time, he focuses on the more advanced uses of Behaviors - Kirupa

In previous posts, I’ve touched on the basic pieces of the Behaviors API and some basic usages of them. In this post, I want to wrap up the discussion of the basic Behaviors API with some discussion of some of the more advanced features exposed by the Behaviors API.

Advanced Behaviors
A large amount of interactive scenarios can be described in the language of triggers and actions; that is, when X happens, do Y. However, say I have two actions, A and B, both of which want to read and store information in a hidden (i.e. not exposed to the designer) but shared Foo object. If I implement A and B as actions, each action needs to know how to access the Foo object. There are a number of different ways to approach this, each with various merits and drawbacks. Rather than explore these in depth here, I want to point out a way to approach this problem using Behaviors.

The complexity in this scenario comes from multiple independent actions sharing a dependency on the Foo object. Consider that rather than having multiple Actions that share and operate on the same data objects, you can instead encapsulate the shared data objects into a Behavior and expose the functionality that would have been provided via Actions as commands on the Behavior. The benefits are two-fold: cohesion is greatly improved, as the data and the commands that act upon that data are now encapsulated in a single class, and second, the interface exposed to the designer in Blend 3 is much more explicit (no more wondering which Actions in a large class library are meant to be used together). Rather than relying on class-naming conventions or first-hand implementation knowledge to convey the relationships between several Actions that all act upon the same data object, the user is presented a single Behavior with each possible operation enumerated and customizable by the designer.

image

This is all tied into the Behavior system with the appropriately named InvokeCommandAction. By placing these actions directly on the behavior that exposes the ICommands, we get a really nice benefit: we can use any Trigger built against the Behavior Trigger API to invoke both Actions AND ICommands exposed on Behaviors! In addition to the assorted tooling work in Blend 3 to make invoking these ICommands from XAML easy for a designer, we’ve also added the ActionCommand support class for developers to make creating Behaviors exposing ICommands a breeze. A bare-bones implementation of ICommand that takes a function with either zero or one parameter, ActionCommand saves you the trouble of writing your own ICommand implementation each time you want to write a Command-able Behavior:

 public class MyCommandBehavior : Behavior<DependencyObject>{   public MyCommandBehavior()  {       this.MyCommand = new ActionCommand(this.MyFunction);    }   public ICommand MyCommand   {       get;        private set;    }       private void MyFunction()   {       // do work...   }}

 Figure 2: Example Behavior exposing an ICommand

 <Button x:Name="BehaviorHostButton" Content="Behavior Host>   <i:Interaction.Behaviors>      <local:MyCommandBehavior>         <i:Interaction.Triggers>            <i:EventTrigger SourceName=" BehaviorHostButton" EventName="Click">               <i:InvokeCommandAction CommandName="MyCommand"/>            </i:EventTrigger>         </i:Interaction.Triggers>      </local:MyCommandBehavior>   </i:Interaction.Behaviors></Button>

Figure 3: Example invocation of an ICommand on a Behavior using InvokeCommandAction

There is one major caveat to consider with this approach. Because InvokeCommandAction is not a TargetedTriggerAction, you cannot set TargetName to target an arbitrary Behavior; the InvokeCommandAction must be hosted on the Behavior it is invoking commands on. This means that the Trigger that fires the InvokeCommandAction must either be retargeted (by setting SourceName), or must be source agnostic. Also note that like any string-based reference system, the CommandName parameter of InvokeActionCommand is susceptible to falling victim to changing property names or refactorings down the road. Special care must be taken to avoid breaking these implicit links if these names are still in flux.

DefaultTriggerAttribute

You may find yourself writing Actions and Behaviors that have a fairly common use case, and you may want to improve the default design-time experience around that use case. By default when you create an Action, Blend 3 will trigger it with an MouseLeftButtonDown  EventTrigger where we can, or a Loaded EventTrigger if MouseLeftButtonDown isn’t available. However, you may want a custom Trigger instead of an EventTrigger, or maybe just a different default event in an EventTrigger. In order to facilitate this, we’ve added the DefaultTriggerAttribute in the System.Windows.Interactivity namespace:

 public DefaultTriggerAttribute(Type targetType, Type triggerType,                                                 params object[] parameters)

By setting this attribute on an Action class definition or an ICommand property definition exposed on a Behavior, you can specify one or more Triggers to instantiate automatically when that Action or Behavior is created by a user in Blend 3. The first parameter is the type for which to create this Trigger, the second is the type of Trigger to create, and the final is a list of constructor arguments to pass to the Trigger when it is created. If more than one attribute is defined, the most derived targetType that is applicable when the user created the Action/Behavior will be used.

For example, in the following definition:

 [DefaultTrigger(typeof(UIElement), typeof(EventTrigger), "MouseLeftButtonDown")] [DefaultTrigger(typeof(ButtonBase), typeof(EventTrigger), "Click")] public class MyAction : TriggerAction<UIElement> {     … } 

If MyAction is dragged onto a Button (or another ButtonBase derived class), the Action will be created under a Click EventTrigger. However, if MyAction is dragged onto any other UIElement, a MouseLeftButtonDown trigger will be created for it instead.

TypeConstraintAttribute

If you’ve read my previous point, you know that each of the core Behaviors classes constrain their AssociatedObject type via a generic argument in the class inheritance definition. However, there is a somewhat subtle complication here in the case of TargetedTriggerAction<T> and EventTriggerBase<T>. In both cases, these classes extend the more basic TriggerAction<T> and TriggerBase<T> and provide a “retargeting” mechanism, allowing Behavior authors to easily target and act upon elements other than the AssociatedObject.

For TargetedTriggerAction<T> and EventTriggerBase<T>, the generic parameter T does not constraint the type of their AssociatedObject as it does for TriggerAction<T>and TriggerBase<T>. Instead, it constrains the type of the retargeted element (Target/Source properties, respectively).  Because retargeted Behavior classes are usually acting solely on their target, rather than their AssociatedObject, this is usually sufficient. But what if it isn’t?

Cue the TypeConstraintAttribute. You can apply this attribute to any TargetedTriggerAction<T> or EventTriggerBase<T> in order to constrain its AssociatedObject to the specified type. Blend 3 will enforce it when you are creating an instance of the Behavior type, and the run-time will throw if you manage to violate it anyway (via by-hand XAML manipulation or code behind).

Conclusion

This post covered quite a bit of ground, but there are a few more topics that we need to go over before calling it a day. Stay tuned for a future post on some of our behaviors-related extensibility features.

Thanks,

Jeff