electric beach

Christian Schormann

Friday, April 3, 2009

Blend 3 Behaviors: A Sample Action

Right before the weekend, another post on Behaviors…

In my last post, I discussed a few of the building blocks of the Behaviors mechanism in Blend 3: Triggers, Actions and Behaviors. I also talked a little bit about the Source for Triggers and the Target for actions. Let’s jump right into a sample.

Here is the code for an action that does, well, nothing:

   1: using System;

   2: using System.Collections.Generic;

   3: using System.Text;

   4: using System.Windows;

   5: using System.Windows.Controls;

   6:  

   7: using Microsoft.Expression.Interactivity;

   8:  

   9: namespace SampleAction

  10: {

  11:     public class SimpleSampleAction: TargetedTriggerAction<FrameworkElement>

  12:     {

  13:         protected override void Invoke(object parameter)

  14:         {

  15:  

  16:         }

  17:     }

  18: }

Some of the interesting parts:

Line 7:

Your project needs to reference Microsoft.Expression.Interactivity.dll, which you can find in your Blend installation directory, in a folder Libraries. There are two sub-folders there, with versions of this library for WPF and Silverlight. You need to have a using statement for this namespace in your action class.

Line 11:

Your new action needs to inherit from one of the TriggerAction classes, which, not surprisingly, define actions that are fired by a trigger. This example uses a TargetedTriggerAction, which is an action that can be set up to target a different object than the object it is attached to. I’ll explain that in a moment. This action base class is also generic. Without going into too much details for now, the generic parameter allows you to limit to which types of objects an Action can be attached.

Line 13:

This is where the Action happens. All that your Action needs to do is to override the Invoke method with whatever activity you want your Action to perform. This sample of course very elegantly does nothing.

Creating a Project

To try out this sample, open Blend 3, and create a new WPF or Silverlight application project. I am using WPF, just to prove that Visual State Manager, which we will use in this sample, actually works in WPF. If you want to follow along with that, you must have the WPF Toolkit installed. If you want to use Silverlight instead, there might be a few minor code changes required.

Once you have created the project, add a reference to Microsoft.Expression.Interactivity.dll (in your Blend install directory, Libraries\WPF) and to the WPFToolkit.dll (by default in C:\Program Files\WPF Toolkit).

Finally, add a new item, using the new Class project item template:

Item Dialog

Call the class SimpleSampleAction.cs and click OK.

This gives you an immediate opportunity to give the new code editor in Blend a spin. Just paste the code from the sample above into the editor:

CodeEditor

Before we try this out, we should maybe make our sample action do something, otherwise this is not going to be an interesting experience. Change the Invoke() method as follows:

   1: protected override void Invoke(object parameter)

   2: {

   3:     if (Target == null)

   4:         return;

   5:     

   6:     MessageBox.Show("Hello World!");

   7: }

We just added Line 6, where we call up a message box saying “Hello!”

Now, go to the main window and draw a rectangle and make it red. While you are at it, also make a button.

WindowRectButton

Before you can apply the simple behavior, build your project by pressing Ctrl-Shift-B, or by selecting Project>Build Project.

Then, open the asset tool and switch to the Behaviors category:

AssetTool

Drag the simple sample action onto the red rectangle.

Repeat, but this time drag to the button.

Dragging an Action onto an object attaches the Action to this object. At this point, it is probably time to look what behaviors do in the object tree and in the XAML code. Here’s the object tree:

ObjectTree

As you can see, Actions appear as children to the objects they are attached to.

And here’s the XAML:

   1: <Rectangle Fill="#FFE53434" Stroke="#FF000000" HorizontalAlignment="Left" Margin="40,32,0,0" VerticalAlignment="Top" Width="168" Height="56">

   2:     <i:Interaction.Triggers>

   3:         <i:EventTrigger EventName="Loaded">

   4:             <local:SimpleSampleAction/>

   5:         </i:EventTrigger>

   6:     </i:Interaction.Triggers>

   7: </Rectangle>

   8: <Button HorizontalAlignment="Left" Margin="40,128,0,0" VerticalAlignment="Top" Width="168" Height="56" Content="Button">

   9:     <i:Interaction.Triggers>

  10:         <i:EventTrigger EventName="Loaded">

  11:             <local:SimpleSampleAction/>

  12:         </i:EventTrigger>

  13:     </i:Interaction.Triggers>

  14: </Button>

What you see in Line 2 and Line 9 might surprise you: In the XAML, you can see that Blend has inserted a Trigger collection (Interaction.Triggers) with a single EventTrigger (Line 3 / Line 10). That EventTrigger contains the sample action you dragged out on the rectangle and button.

And this is indeed how it works: Actions are never directly attached to objects. Instead, the object has a list of triggers, and every trigger in the list has an attached action. The good news is that you can largely ignore this fact.

In a later post, we will have a look at how this works for actual Behaviors.

As you can see from the XAML Blend has generated, the SimpleSampleAction by default gets fired by an Event trigger, and the event that Blend set up by default is “Loaded”. Or in other words, if we run the app, as soon as the rectangle and button are loaded, we should see the message boxes, one for the rectangle and one for the button.

Let’s try that. Press F5 to run.

And indeed, this is exactly what happens. This is good because it was what we expected, but not so good because it is rather useless in this case. So how can we change the Trigger for an Action?

Luckily, there is nothing you need to do when you write an Action: Blend automatically provides UI to edit the Trigger for any Action. However, if you know that your Action works best with a particular default Trigger, you can add an attribute to the code of your Action that tells Blend which Trigger to insert by default. If you do not add such code, Blend will default to EventTrigger set for a Loaded event. We will look at the configuration for this in a future post.

For now, let’s look at editing the Trigger. To do this, click on one of the actions in the Object Tree, and then look at the Property Inspector:

PropertyInspector

In this screenshot, all areas are expanded. Our SimpleSampleAction shows two sections: One for setting the Trigger, and one for setting properties of your Action itself.

Our sample Action does not have any properties in the code we have written, but it inherits two properties by default:

  • A switch to enable or disable the Action
  • A Target property that determines which object is going to be targeted by this action. Our sample currently does not use the Target property (it just calls up a MessageBox) but if it did, here you have the UI to tell your Action where to target.

Every Action also inherits the Trigger UI by default. This allows you to pick a Trigger type (this is where you would pick custom triggers you have created yourself) and whatever properties the Trigger might offer. The current Trigger is an event trigger, and event triggers have one rather important property, which is the event to listen for. By default this is “Loaded”. Other Trigger types might have very different properties here.

Change the event to listen for to “MouseLeftButtonUp” for the Rectangle and to “Click” for the Button.

Now run your application again. Your message box will now fire when you click the button and the rectangle.

Before we continue, there is one more property for Triggers: The Source property for the trigger tells Blend where to listen for the trigger. It this property is not set, Blend listens for events from the object the Action is attached to.

Finally, A Real Sample

Ok, now that we spent all this effort to learn the basic, let’s do an Action that does something useful.

The Action I want to build is toggles between to visual states of some object on the screen. If you don’t know what Visual States are, please see this post for more information.

This Action will also give us a useful opportunity to try out the Target property. Let’s explain the Target quickly:

Imagine a push button that switches on a lamp. You want an action that switches the light on when the button is first pressed, and switches the light off when the button is pressed again.

Now, where do we attach the Action? We could attach it to the switch or to the light bulb.

If we attach it to the switch, the action needs to listen to the object it is attached to, but the object the Action actually changes when the switch is pressed is the light bulb. With Actions, we can accomplish this by making the light bulb the Target for the Action.

If we attach the Action to the switch, we don’t need to set the Target – remember that every Action by default operates on the object it is attached to. But in this case we need to listen for a Trigger from a different source. You can do that by setting the Source property of the Trigger to the switch.

ActionTargetTriggerSource

Blend supports both scenarios, because they are both equally useful. Note: If your Action is attached to a UserControl, the Source property does not work in current builds of Blend 3.

ToggleStateAction

ToggleStateAction is a little bigger than it strictly needed to be because I want to use this sample to illustrate a number of things:

  • How an Action can deal with being re-attached
  • One example of how an Action can store state so that it can be shared between multiple Actions. Also, the same technique allows Actions to coordinate and communicate.

But enough words, let’s look at code.

Create another source class and call it ToggleStateAction. Here’s the code:

   1: using System;

   2: using System.Collections.Generic;

   3: using System.Linq;

   4: using System.Text;

   5: using System.Windows;

   6: using System.Windows.Controls;

   7: using Microsoft.Expression.Interactivity;

   8:  

   9: namespace SampleAction

  10: {

  11:     // Action to toggle the state of a UC between two defined state values

  12:     // Because we can't get the current state of a control, this behavior handles 

  13:     // the case where two or more toggle actions target the same element by storing 

  14:     // state per target element in a singleton.  

  15:     //

  16:     // Note: This action exposes a "Force First State" property that causes the 

  17:     // targeted control to get into the selected state as soon as the control is 

  18:     // instantiated. If multiple actions target the same control, it is enough if just one 

  19:     // has "Force First State" set to true to force the control to initialize to the first state.

  20:     public class ToggleStateAction: TargetedTriggerAction<FrameworkElement>

  21:     {

  22:         protected override void Invoke(object parameter)

  23:         {

  24:              Control c = Target as Control;

  25:             if (c == null)

  26:                 return;

  27:  

  28:             ToggleState state = ToggleGroupStateManager.Instance.GetState(c);

  29:  

  30:             switch (state)

  31:             {

  32:                 case ToggleState.None:

  33:                     if (FirstState == "" || FirstState == null)

  34:                         return;

  35:                     VisualStateManager.GoToState(c, FirstState, UseTransition);

  36:                     ToggleGroupStateManager.Instance.SetState(c, ToggleState.First);

  37:                     break;

  38:                 case ToggleState.First:

  39:                     if (FirstState == "" || FirstState == null)

  40:                         return;

  41:                     VisualStateManager.GoToState(c, SecondState, UseTransition);

  42:                     ToggleGroupStateManager.Instance.SetState(c, ToggleState.Second);

  43:                     break;

  44:                 case ToggleState.Second:

  45:                     if (FirstState == "" || FirstState == null)

  46:                         return;

  47:                     VisualStateManager.GoToState(c, FirstState, UseTransition);

  48:                     ToggleGroupStateManager.Instance.SetState(c, ToggleState.First);

  49:                     break;

  50:             }

  51:         }

  52:  

  53:         protected override void OnTargetChanged(FrameworkElement oldTarget, FrameworkElement newTarget)

  54:         {

  55:             Control c = Target as Control;

  56:             if (c != null && ForceFirstState == true && FirstState != null && FirstState != "")

  57:             {

  58:                 VisualStateManager.GoToState(c, FirstState, false);

  59:                 ToggleGroupStateManager.Instance.SetState(c, ToggleState.First);

  60:             }

  61:         }

  62:  

  63:         public bool ForceFirstState

  64:         {

  65:             get { return (bool)GetValue(ForceFirstStateProperty); }

  66:             set { SetValue(ForceFirstStateProperty, value); }

  67:         }

  68:  

  69:         // Using a DependencyProperty as the backing store for ForceFirstState.  This enables animation, styling, binding, etc...

  70:         public static readonly DependencyProperty ForceFirstStateProperty =

  71:             DependencyProperty.Register("ForceFirstState", typeof(bool), typeof(ToggleStateAction), new UIPropertyMetadata(true));

  72:  

  73:         public bool UseTransition

  74:         {

  75:             get { return (bool)GetValue(UseTransitionProperty); }

  76:             set { SetValue(UseTransitionProperty, value); }

  77:         }

  78:  

  79:         // Using a DependencyProperty as the backing store for UseTransition.  This enables animation, styling, binding, etc...

  80:         public static readonly DependencyProperty UseTransitionProperty =

  81:             DependencyProperty.Register("UseTransition", typeof(bool), typeof(ToggleStateAction), new UIPropertyMetadata(true));

  82:  

  83:         public string FirstState

  84:         {

  85:             get { return (string)GetValue(FirstStateProperty); }

  86:             set { SetValue(FirstStateProperty, value); }

  87:         }

  88:  

  89:         // Using a DependencyProperty as the backing store for FirstState.  This enables animation, styling, binding, etc...

  90:         public static readonly DependencyProperty FirstStateProperty =

  91:             DependencyProperty.Register("FirstState", typeof(string), typeof(ToggleStateAction), new UIPropertyMetadata(""));

  92:  

  93:         public string SecondState

  94:         {

  95:             get { return (string)GetValue(SecondStateProperty); }

  96:             set { SetValue(SecondStateProperty, value); }

  97:         }

  98:  

  99:         // Using a DependencyProperty as the backing store for SecondState.  This enables animation, styling, binding, etc...

 100:         public static readonly DependencyProperty SecondStateProperty =

 101:             DependencyProperty.Register("SecondState", typeof(string), typeof(ToggleStateAction), new UIPropertyMetadata(""));                                                                                                                                                                                                                                                                                                                                                                         

 102:     }

 103:  

 104:     enum ToggleState {

 105:         None,

 106:         First,

 107:         Second

 108:     }

 109:  

 110:     // The following class implements a singleton that is used 

 111:     // to store common toggle state for all toggle state actions

 112:     // targeting the same element.

 113:     internal class ToggleGroupStateManager

 114:     {

 115:         private static ToggleGroupStateManager instance; 

 116:  

 117:         protected ToggleGroupStateManager()

 118:         {

 119:             states = new Dictionary<FrameworkElement, ToggleState>();

 120:         }

 121:  

 122:         static public ToggleGroupStateManager Instance

 123:         {

 124:             get

 125:             {

 126:                 if (instance == null)

 127:                     instance = new ToggleGroupStateManager();

 128:                 return instance;

 129:             }

 130:         }

 131:  

 132:         private Dictionary<FrameworkElement, ToggleState> states;

 133:  

 134:         public void SetState(FrameworkElement element, ToggleState state)

 135:         {

 136:             if (!states.ContainsKey(element))

 137:                 states.Add(element, state);

 138:             else

 139:                 states[element] = state;

 140:         }

 141:  

 142:         public ToggleState GetState(FrameworkElement element)

 143:         {

 144:             if (!states.ContainsKey(element))

 145:             {

 146:                 states.Add(element, ToggleState.None);

 147:                 return ToggleState.None;

 148:             }

 149:             return states[element];

 150:         }

 151:         

 152:     }

 153: }

What does it do, and how does it work? First, a look at the class and the Invoke() method:

   1: public class ToggleStateAction: TargetedTriggerAction<FrameworkElement>

   2: {

   3:     protected override void Invoke(object parameter)

   4:     {

   5:         Control c = Target as Control;

   6:         if (c == null)

   7:             return;

   8:  

   9:          ...

  10:     }

  11:  

  12:     ...

  13: }

The class declaration is exactly the same as before – we allow this Action to be applied to any FrameworkElement. However, the purpose of this Action is to toggle between two visual states, and VisualStateManager, the framework class used to actually do the work, can only switch states on Control or one of its subclasses.

In line 5, we therefore ensure that we actually are targeting a Control. If the Action targets something different than a Control, we simply do nothing.

You may ask why we did not simply change the generic parameter of the class from FrameworkElement to Control? The reason is that the generic parameter only controls what objects the Action can be attached to. The Target however, is not necessarily the object the Action is attached to – if you have set the Target property for the Action, it might be any other object, potentially one of a completely different type. Therefore, the test.

In order to toggle between two states of the Target, we need to know in which state the Target is in. Unfortunately, it turns out that there is no method to tell us in which state a control currently is in. That leaves us with a number of options: One is to listen to state change events on the Target and follow state changes that way. Or in a real simple implementation, I could just force a state when the Action is first attached, and then store state internally.

When I first wrote this sample, I just stored the state internally using an enum value. That worked fine as long as just one switch toggled the light bulb. However, as soon as there were multiple switches, each using an instance of the Action, the Actions would store their state independently and run out of sync.

This little problem gave me the opportunity to show one possible technique how multiple Actions can communicate and store shared state.

Overall, this adds quite a bit of code that is unnecessary in the simplest case, but it is really simple code.

The way this works is this:

I created a backend object, in this case a singleton, that basically contains a dictionary to store state for a number of Control objects. The backend class looks like this:

   1: enum ToggleState {

   2:     None,

   3:     First,

   4:     Second

   5: }

   6:  

   7: // The following class implements a singleton that is used 

   8: // to store common toggle state for all toggle state actions

   9: // targeting the same element.

  10: internal class ToggleGroupStateManager

  11: {

  12:     private static ToggleGroupStateManager instance; 

  13:  

  14:     protected ToggleGroupStateManager()

  15:     {

  16:         states = new Dictionary<FrameworkElement, ToggleState>();

  17:     }

  18:  

  19:     static public ToggleGroupStateManager Instance

  20:     {

  21:         get

  22:         {

  23:             if (instance == null)

  24:                 instance = new ToggleGroupStateManager();

  25:             return instance;

  26:         }

  27:     }

  28:  

  29:     private Dictionary<FrameworkElement, ToggleState> states;

  30:  

  31:     public void SetState(FrameworkElement element, ToggleState state)

  32:     {

  33:         if (!states.ContainsKey(element))

  34:             states.Add(element, state);

  35:         else

  36:             states[element] = state;

  37:     }

  38:  

  39:     public ToggleState GetState(FrameworkElement element)

  40:     {

  41:         if (!states.ContainsKey(element))

  42:         {

  43:             states.Add(element, ToggleState.None);

  44:             return ToggleState.None;

  45:         }

  46:         return states[element];

  47:     }

  48:     

  49: }

  50: }

In Line 1, you see the enum used to define the possible states: None (Unknown), or the first or second state allowed by this Action.

Aside of that, there really is not much of interest in here: This is a simple singleton (a class that can only instantiated once, but the instance can be reached via a static Instance property), and that has methods to store state and read state.

The state is stored in a Dictionary (declared in line 29), with one state per targeted control. This way, multiple groups of ToggleStateActions can share state without stepping on each others toe’s.

The dictionary singleton is used in the rest of the sample to store and read state of the target.

Next, let’s look at the complete Invoke() method:

   1: protected override void Invoke(object parameter)

   2: {

   3:      Control c = Target as Control;

   4:     if (c == null)

   5:         return;

   6:  

   7:     ToggleState state = ToggleGroupStateManager.Instance.GetState(c);

   8:  

   9:     switch (state)

  10:     {

  11:         case ToggleState.None:

  12:             if (FirstState == "" || FirstState == null)

  13:                 return;

  14:             VisualStateManager.GoToState(c, FirstState, UseTransition);

  15:             ToggleGroupStateManager.Instance.SetState(c, ToggleState.First);

  16:             break;

  17:         case ToggleState.First:

  18:             if (FirstState == "" || FirstState == null)

  19:                 return;

  20:             VisualStateManager.GoToState(c, SecondState, UseTransition);

  21:             ToggleGroupStateManager.Instance.SetState(c, ToggleState.Second);

  22:             break;

  23:         case ToggleState.Second:

  24:             if (FirstState == "" || FirstState == null)

  25:                 return;

  26:             VisualStateManager.GoToState(c, FirstState, UseTransition);

  27:             ToggleGroupStateManager.Instance.SetState(c, ToggleState.First);

  28:             break;

  29:     }

  30: }

In line 7, we get the current state from the state manager singleton.

In the switch statement after line 9, we toggle to the right state we need to be in by calling VisualStateManager.GoToState() (line 14, 20 and 26), and then we store the new state back to the backend manager.

And that is almost all this Action does.

There’s just one more thing of interest:

   1: protected override void OnTargetChanged(FrameworkElement oldTarget, FrameworkElement newTarget)

   2: {

   3:     Control c = Target as Control;

   4:     if (c != null && ForceFirstState == true && FirstState != null && FirstState != "")

   5:     {

   6:         VisualStateManager.GoToState(c, FirstState, false);

   7:         ToggleGroupStateManager.Instance.SetState(c, ToggleState.First);

   8:     }

   9: }

This overridden method is called when the Target for the Action is changed for any reason. In this case, we make sure that an appropriate initial state is set, based on the settings of some properties on the Action class.

The rest of the code is just some DependencyProperty declarations for properties the Action exposes. These properties are:

  • FirstState and SecondState: These are the two states to toggle between.
  • ForceFirstState. When this property is true, the Action forces the first state the moment it is loaded.
  • UseTransition. This property determines if we switch between the first and second state immediately, or if we let VSM run a transition, provided one has been defined.

Trying the Sample

First, create a user control for a little light bulb, and give it two states, On and Off:

LampControl

Next build your project and add the lamp control to the scene:

Scene

Finally, wire up the ToggleStateBehavior to the rectangle and the button. These are the properties for the Button:

ToggleProps

As you can see, we have set up the Target to be the Lamp control. For the rectangle, the properties are very similar, except that the EventName property for the Trigger needs to be MouseLeftButtonUp.

Run the application. Both the button and the rectangle will trigger the state change in the light bulb. One press changes to on, the next press changes to off. You can mix and match the buttons as desired, the state is still preserved correctly.

Here is the sample project:

sampleaction.zip

posted by cs at 10:57  

3 Comments

  1. [...] beginning? Electric Beach has a great beginner series on Action, Triggers and [...]

    Pingback by Coffee’s World » Behaviors day — May 11, 2009 @ 07:20

  2. [...] electric beach ” Blend 3 Behaviors: A Sample Action [...]

    Pingback by Today is 100 inspirational links about Sketching, Art, Drawing, Painting. For Figure Artists. | Life Drawing and Figure Art — July 9, 2009 @ 13:17

  3. [...] Behaviours http://electricbeach.org/?p=171 [...]

    Pingback by Blend 3 and Sketchflow « C# Disciples — August 4, 2009 @ 16:17

RSS feed for comments on this post. TrackBack URI

Sorry, the comment form is closed at this time.

Powered by WordPress