You are browsing the archive for Action.

Hidden Object: Episode 14 – Shake it up

November 25, 2009 in Hidden Object Game, Silverlight

So far in the game, we have particles when an item is clicked, a hint to show the location of an unfound item, but what should we do if the player goes berserk and wildly clicks all over the place in hopes of finding a difficult-to-find item? In this episode of Creating a Hidden Object Game is Silverlight 3 we will add an earthquake effect if the player clicks too many times in a 5 second interval.

Let’s first start with the shake effect.  The magnifierCanvas Canvas contains the background image and all Paths for each clickable item. We will create a new storyboard called ShakeStoryboard that will animate the Left property of the Canvas:

 

Each of the 4 key frames set the Left property to a different value as shown in the storyboard XAML:

<Storyboard x:Name="ShakeStoryboard" RepeatBehavior="5x" AutoReverse="False" SpeedRatio="5">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
        Storyboard.TargetName="magnifierCanvas"
        Storyboard.TargetProperty="(Canvas.Left)">
            <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
            <EasingDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-10"/>
            <EasingDoubleKeyFrame KeyTime="00:00:00.4000000" Value="10"/>
            <EasingDoubleKeyFrame KeyTime="00:00:00.6000000" Value="0"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

 

The Storyboard has the RepeatBehavior property set to repeat 5 times and the SpeedRatio property set to speed up the animation.

If the player clicks on the background image 10 times within a 5 second interval, then the ShakeStoryboard will play. To accomplish this we need three SetGlobalCounterAction instances and one ControlStoryboardAction added to the UserControl:

The first SetGlobalCounterAction sets the values for the TooManyClicks counter when the UserControl loads:

 

The ControlStoryboardAction plays the ShakeStoryboard when the GlobalCounterMaxReachedTrigger is fired for the TooManyClicks key which we set previously to 10.

When the counter reaches 10 we need to set it back to zero which is what the second SetGlobalCounterAction does:

The final SetGlobalCounterAction uses the TimerTrigger to reset the counter every 5 seconds:

 

The only thing left to do is add the IncrementGlobalCounterAction to the background image to increment the counter by 1:

 

So with 5 Action instances, 3 Trigger types, 1 Storyboard, and 0 lines of code we were able to quickly add this feature to the game.

Zip Source Code

silverlight Demo

Hidden Object: Episode 13 – Give me a Hint

November 17, 2009 in Hidden Object Game, Silverlight

This is episode 13 of Creating a Hidden Object Game is Silverlight 3. In this episode, we will add a hint feature to the game to help the players when they can’t find an item. This will require various animations and a custom behavior.

The hint feature can be segmented into three parts:

  • Recharging hint button
  • Hint overlay image with animation
  • HintBehavior to randomly position the hint overlay image

 

Hint Button

Hint Button

To make the hint button, we will use an image of a laptop, a TextBlock (hintTextBlock), and a ProgressBar (progressBar) wrapped in a Canvas (hintCanvas):

The idea is that the TextBlock will contain the text “HINT” and act as a button to trigger the hint feature. When the TextBlock is clicked, the TextBlock is hidden and the ProgressBar shown. This is accomplished by adding a HintStates group to the main UserControl:

When the HintState is active, the TextBlock is shown and its IsHitTestVisible property is set to true so that it can be clicked. When the RechargeState is active, the TextBlock’s Opacity property is set to 0 and its IsHitTestVisible property is set to false so that it can’t be clicked.

To set the RechargeState, we add a GoToStateAction to the TextBlock:

A storyboard is added to change the value of the ProgressBar to indicate that the hint is recharging.

<Storyboard x:Name="RechargingStoryboard">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
        Storyboard.TargetName="progressBar"
        Storyboard.TargetProperty="(RangeBase.Value)">
        <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
        <EasingDoubleKeyFrame KeyTime="00:00:10" Value="100"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

In this example, the storyboard will recharge in 10 seconds. For the game, the recharge duration should be somewhere between 30 seconds and 2 minutes.

This storyboard is started using a ControlStoryboardAction on the TextBlock:

 

The change from RechargeState to HintState is handled by the GoToStateAction and the StoryboardCompletedTrigger waiting on RechargingStoryboard. So as soon as the recharging animation ends, then the Hint button displays again.

 

Hint Overlay

Hint Overlay

The hint overlay was created in Expression Design and consists of 10 starbursts or flares set in a circular pattern. After the image is added to the project, drag it onto the LayoutRoot Canvas and locate it “off screen” (Left = 500, Top = -300). Set the ZIndex of the image to 99 so that it will be over any item on the game screen, but always under the cursor image.

 

When the hint TextBlock is clicked, two storyboards are started. The ShowHintStoryboard changes the opacity from 0% to 80% in 2 seconds and then auto reverses back to 0% over the next 2 seconds. The RotateHintStoryboard uses a RotateTransform to rotate the overlay 360 degrees over 4 seconds.

<Storyboard x:Name="ShowHintStoryboard" AutoReverse="True">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
        Storyboard.TargetName="hintFlareImage"
        Storyboard.TargetProperty="(UIElement.Opacity)">
        <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
        <EasingDoubleKeyFrame KeyTime="00:00:02" Value="0.8"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

<Storyboard x:Name="RotateHintStoryboard">
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
        Storyboard.TargetName="hintFlareImage"
        Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
        <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
        <EasingDoubleKeyFrame KeyTime="00:00:04" Value="360"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>
 

To start the storyboards, use the ControlStoryboardAction with the EventTrigger:

 

Hint Behavior

The last thing we need to do is figure out where to put the hint overlay image. To do this, we will use a behavior that exposes a ShowHint command as well as a HintItems collection and a HintOverlayName property:

In the same way that the MouseCursorBehavior exposes the CursorName property to allow selection of the cursor, the HintBehavior exposes the HintOverlayName so that we can select the hint overlay image. An EventTrigger causes the ShowHint command to fire when the HINT TextBlock is clicked.

The HintItems collection contains one HintItem object for each item that can have a hint. The HintItem object contains a TargetName property to identify the Path or object representing the item and an X and Y variance for the location of the overlay.

The HintBehavior uses two NameResolver instances (see episode 12). The first NameResolver changes the HintOverlayName into a reference to the overlay image control. The second NameResolver is used once a HintItem is randomly picked to see if the object still exists and if so gets a reference to it. Even though a HintItem exists for all clickable items in the hidden object game, it may no longer exist in the visual tree as it could have been removed by the RemoveElementAction as discussed in episode 4.

When the ShowHint command is executed, the private OnShowHint method is called. This is the heart of the HintBehavior:

private void OnShowHint()
{
  DependencyObject item = null;
 
  if (!this.IsHintOverlayNameSet)
    return;
 
  FrameworkElement hintOverlay = HintOverlay as FrameworkElement;
 
  //mix up the order of item names
  HintItems.Randomize();
 
  for (int index = 0; index < HintItems.Count; index++)
  {
    this.ItemResolver.Name = HintItems[index].TargetName;
    item = this.ItemResolver.Object;
 
    if (item != null)
    {
      double itemX = (double)item.GetValue(Canvas.LeftProperty);
      double itemWidth = (double)item.GetValue(FrameworkElement.ActualWidthProperty);
      double itemY = (double)item.GetValue(Canvas.TopProperty);
      double itemHeight = (double)item.GetValue(FrameworkElement.ActualHeightProperty);
    
      double newX = RandomWithVariance(itemX + (itemWidth / 2) - (hintOverlay.ActualWidth / 2), HintItems[index].OriginXVariance);
      double newY = RandomWithVariance(itemY + (itemHeight / 2) - (hintOverlay.ActualHeight / 2), HintItems[index].OriginYVariance);
 
      hintOverlay.SetValue(Canvas.LeftProperty, newX);
      hintOverlay.SetValue(Canvas.TopProperty, newY);

      break;
    }
  }
}

 

If the HintOverlayName is not set we exit the method, otherwise we get a reference to it. We then randomize the order of the items in the HintItems list. This is done using an extension method called Randomize(). Since some items named in the list may no longer exist on the Canvas, we do a null check after we access an item in the list and resolve it. If the item exists, then we determine the location of the item with its width and height so that we can center the overlay image over the item. The RandomWithVariance method uses the OriginXVariance and OriginYVariance values set on HintItem to make sure that the overlay image is over the item but that the item is not necessarily exactly centered.

 

The hint feature of our hidden object game is fairly simple once we break it into its three main components and work on them individually. Stay tuned for the next episode of Creating a Hidden Object Game in Silverlight 3.

Zip Source Code

silverlight Demo

Video: Creating a Silverlight 3 Casual Game using Blend 3 and Triggers, Actions, and Behaviors

November 9, 2009 in Hidden Object Game, Silverlight

On Saturday, November 7, 2009 at 9:00am I presented at the Desert Code Camp in Phoenix, AZ. The topic of my presentation was Silverlight casual game development. In less than an hour I demonstrated how to use triggers, actions, and behaviors in Blend 3 to create a hidden object game. Because all code was contained in the TABs (Triggers, Actions, and Behaviors) there was no code behind for the main UserControl.

Here is a link to the presentation video in WMV format.

If you like you can see all the episodes and follow along with the tutorial.

Hidden Object: Episode 10 – Counting Items Found with Counter Triggers, Actions & a Behavior

October 4, 2009 in Hidden Object Game, Silverlight

In episode 9 of Creating a Hidden Object Game is Silverlight 3 we added additional screens to the game. In this episode, we will add the Win screen and a collection of triggers, actions, & behaviors that work with a global counter.

The Win screen is shown after all 13 items have been clicked. What we need is an integer counter that subtracts one for each item clicked and when the count gets to zero change the state of the MainPage UserControl to show the screen.

Similar to what we did in the last episode, create a new Canvas called winCanvas and position it to the left of the UserControl on the artboard.

In the States panel, add the WinScreen state to the ScreenStates group:

With recording mode on for the WinScreen state, set the Top and Left properties of winCanvas to 0.

All we need to do is figure out a way to trigger a state change to show this screen.

The solution is a variation of the Global State Behavior sample by Christian Schormann which uses a Singleton object containing a Dictionary to hold values based on a key (or tag).

Let’s go step-by-step through the use of the GlobalCounter classes before we look at any code.

The counter store (GlobalCounterStore) has an empty dictionary keyed using a CounterKey. Some trigger invokes SetGlobalCounterAction which sets the initial values for Value, MinValue, and MaxValue in a dictionary item with a CounterKey of “HiddenItems”. In this case the trigger that sets the values is an EventTrigger on the MainPage UserControl for the Loaded event:

100409_0710_HiddenObjec3.png 

 

Any time that an entry is changed in the counter store, the Changed event is raised which passes Key, Value, MinValue, and MaxValue. Both the ShowGlobalCounterBehavior and the GlobalCounterChangedTrigger handle this event. Since every instance of the GlobalCounterChangedTrigger & ShowGlobalCounterBehavior handle the Changed event they need to have a CounterKey property to determine if they should take any action for that event. In the case of ShowGlobalCounterBehavior, the associated TextBlock’s Text property is set to Value. For the GlobalCounterChangedTrigger any actions are invoked. In this episode, the GlobalCounterChangedTrigger is not used but is included for completeness.

Now that the counter store has been initialized with values, we can use the IncrementGlobalCounterAction to increment the value. In this case we are incrementing the value by -1 which updates the value in the counter store causing the Changed event to fire which is handled by the ShowGlobalCounterBehavior which sets the text on the TextBlock.

100409_0710_HiddenObjec4.png

 

The IncrementGlobalCounterAction is invoked for each Path object clicked until the counter reaches the MinValue:

100409_0710_HiddenObjec5.png

 

The Changed event is fired to update the TextBlock, but the MinValueReached event is also fired. This is handled by the GlobalCounterMinReachedTrigger which compares its CounterKey with the Key passed in the event and if they match then the trigger invokes its actions. For this episode, there is a GoToStateAction associated with the UserControl that responds to the GlobalCounterMinReachedTrigger and sets the current state to WinScreen which shows winCanvas.

In cases where you want an incrementing counter instead of a decrementing one, pass a positive value for the IncrementCounterValueBy property of IncrementGlobalCounterAction and use the GlobalCounterMaxReachedTrigger.

The global counter classes can be used for many things including a lives counter, health, shield strength, or to keep track of a player’s score.

Here are a class diagram of the GlobalCounterStore and related classes:

Besides the implementation of the Singleton pattern, the most interesting code can be found in the SaveToStore and IncrementCounter methods of GlobalCounterStore:

internal sealed class GlobalCounterStore
{
    private Dictionary<string, GlobalCounterItem> store;

    internal void SaveToStore(string key, int value, int minValue, int maxValue)
    {
        if (key == null)
            return;
        var item = new GlobalCounterItem() {Value = value, MinValue = minValue, MaxValue = maxValue };

        if (store.ContainsKey(key))
        {
            store[key] = item;
        }
        else
        {
            store.Add(key, item);
        }

        OnChanged(key, item);
    }

    internal void IncrementCounter(string key, int incrementBy)
    {
        if ((key == null) || !store.ContainsKey(key))
            return;

        int newValue = store[key].Value + incrementBy;
        store[key].Value = newValue;

        OnChanged(key, store[key]);

        if (newValue >= store[key].MaxValue)
            OnMaxValueReached(key, store[key]);

        if (newValue <= store[key].MinValue)
            OnMinValueReached(key, store[key]);
    }
}

 

The SetGlobalCounterAction is a TriggerAction that defines four dependency properties for CounterKey, CounterValue, CounterMinValue, and CounterMaxValue. The Invoke method calls the SaveToStore method on GlobalCounterStore.

The IncrementGlobalCounterAction is a TargetedTriggerAction that defines dependency properties for CounterKey and IncrementCounterValueBy. It’s Invoke method calls the IncrementCounter method on GlobalCounterStore.

 

The three triggers are very similar in that each defines a CounterKey dependency property and each adds a handler to an event raised by GlobalCounterStore:

GlobalCounterTriggers

 

In the case of GlobalCounterMinReachedTrigger, the OnAttached method adds a handler for the GlobalCounterStore’s MinValueReached event whereas the handler is removed in the OnDetaching method:

public class GlobalCounterMinReachedTrigger : TriggerBase<DependencyObject>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        GlobalCounterStore.Instance.MinValueReached += Instance_MinValueReached;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        GlobalCounterStore.Instance.MinValueReached -= Instance_MinValueReached;
    }

    void Instance_MinValueReached(object sender, GlobalCounterEventArgs e)
    {
        if (CounterKey == null)
            return;

        if (CounterKey == e.Key)
            this.InvokeActions(e);
    }
}

 

When the MinValueReached event is raised due to a change in the GlobalCounterStore to any dictionary entry regardless of its key, the passed key (e.Key) is compared to the CounterKey property. If the values match, then the actions for this trigger are invoked.

 

ShowGlobalCounterBehavior is a behavior that can be attached to a TextBlock control and functions is a similar way as the triggers:

 

 

It defines the CounterKey property and handles the Changed event raised by GlobalCounterKey:

public class ShowGlobalCounterBehavior : Behavior<TextBlock>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        GlobalCounterStore.Instance.Changed += Instance_Changed;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        GlobalCounterStore.Instance.Changed -= Instance_Changed;
    }
 
    void Instance_Changed(object sender, GlobalCounterEventArgs e)
    {
        if (CounterKey == e.Key)
        {
            if (e.Value > e.MaxValue)
                AssociatedObject.Text = e.MaxValue.ToString();
            else if (e.Value < e.MinValue)
                AssociatedObject.Text = e.MinValue.ToString();
            else
                AssociatedObject.Text = e.Value.ToString();
        }
    }
}

 

It is possible to increment the Value in the counter store by more than 1 so I added checks so that the TextBlock will never show values greater than MaxValue or less than MinValue.

 

Now it is time to use these classes in Blend. Drop an instance of SetGlobalCounterAction on the UserControl, a GoToStateAction on LayoutRoot, and a IncrementGlobalCounterAction on a Path:

 

 

The values for SetGlobalCounterAction are set as follows:

 

 

The new GoToStateAction uses GlobalCounterMinReachedTrigger and sets the StateName to WinScreen:

 

 

Each path will have an IncrementGlobalCounterAction set with these values:

 

 

 

Finally, we add a TextBlock to our Canvas to hold the count of remaining items and add the ShowGlobalCounterBehavior to it:

 

 

 100409_0710_HiddenObjec15.png

 

 

The only property to set on the behavior is the CounterKey: 

 

 

Once we add IncrementGlobalCounterAction to each Path, we can run the game and as we find items the counter will decrement by 1. When the value reaches 0, then the winning screen is shown.

 

Source code

Demo

 

In the next episode we will update the Particles Behavior to allow particle custom shapes.

Hidden Object: Episode 9 – Add the Splash, Menu & Options Screens

September 29, 2009 in Hidden Object Game, Silverlight

This is episode 9 of Creating a Hidden Object Game is Silverlight 3. In the last episode we added background music. Now we will create a screen to set the music volume. While we are at it, we will create the splash and menu screens.

When the game is finished, it will have a screen flow as follows:

HiddenObject-ScreenFlow

 

 Currently we have been working on the Game screen which in the project is a UserControl called MainPage.xaml. We are going to create the menu, options, and splash screens as Canvas objects that are off screen of MainPage and are at the bottom of the Objects tree (or top of the z-order) so they show up over all other controls. I chose to do this since I want the background music to play during all of these screens and the MediaElement on MainPage is the one that is playing it. There are various other ways to approach this.

Let’s open Blend, add 3 Canvas controls, and name them splashCanvas, menuCanvas, and optionsCanvas. Make each Canvas have a black background and position them on the artboard so that they are to the left of MainPage:

 

On splashCanvas, add 4 TextBlock controls and enter the title information:

 

On menuCanvas, add the page title in a TextBlock and 2 Buttons:

 

On optionsCanvas, add a TextBlock for the page title, a TextBlock for the slider title, a Slider control, and a Button:

Since the silder’s Value property will be used in a Binding for the MediaElement’s Volume property, we will name the slider as sliderMusicVolume.

 

To control the screen flow, we will use the States panel to create a ScreenStates group with the following states: GameScreen, SplashScreen, MenuScreen, OptionsScreen.

On the States tab, click the Add state group button:

Then next to the ScreenStates group, click the Add state button:

As I entered the following states, I didn’t worry about setting any properties for any of the controls and after adding OptionsScreen I just turned off recording.

The GameScreen state represents when the MainPage is visible which is how it currently is shown on the artboard. No change will need to be made for this state. Click on the SplashScreen state in the States tab to turn on recording mode. Then click splashCanvas and change its Top and Left properties both to 0. Turn off recording of the SplashScreen state.

Repeat for the MenuScreen and OptionsScreen states so that when one of these states is active, MainPage will be covered by menuCanvas or optionsCanvas.

Click on the Base state at the top of the States tab to make sure you are completely out of state editing mode.

We now need to add a TextBlock with the Text property set to “MENU” and add it to LayoutRoot in MainPage. Because I added this control after the Canvas controls for the other screens, I need to change its order in the Objects tree so that it doesn’t appear above those screens. We will worry about making the MENU TextBlock into a Button at a later time.

 

Now it is time to use the GoToStateAction to connect the screen flow. Drop the first GoToStateAction onto LayoutRoot and change the EventName to Loaded and the StateName to SplashScreen:

This will cause the splash screen to show first.

NOTE: While we are still in development, we can set the loaded StateName to GameScreen to reduce testing time.

 

Drop another GoToStateAction onto splashCanvas and change the StateName to MenuScreen. When the splash screen is clicked, we will advance to the menu screen:

Drop two more GoToStateAction actions onto the Start and Options buttons on menuCanvas. Change the StateNames to GameScreen and OptionsScreen respectively.

Start button:

 

Options button:

 

Drop another GoToStateAction onto the Close button in optionsCanvas and change the StateName to MenuScreen:

 

Finally, drop the last GoToStateAction onto the MENU TextBlock in MainPage and set the StateName to MenuScreen:

 

We have just put together the majority of our screen flow with 6 instances of GoToStateAction and no code!

 

The last thing we need to do is to connect the slider value to the volume of the background music. In the Objects tree, select the musicMediaElement and in the Properties pane, click the peg to the right of the Volume property and click Data Binding to open the Create Data Binding window:

 

Select the ElementProperty tab. In the Scene elements list, open optionsCanvas and select sliderMusicVolume. In the Properties list, select Value. Now when the slider in the options screen is changed, it will set the volume on the MediaElement playing the background music.

The last thing we need to do is set some values on the slider control. Select the control and change the following properties found in the Common Properties group: LargeChange, Maximum, and Value.

 

In the next episode, we will try to figure out a way to determine when all the objects have been found so we can show a Win screen.

Demo
Source code

Hidden Object: Episode 7 – Use an Action to Toggle the Magnifier Behavior

September 26, 2009 in Hidden Object Game, Silverlight

This is episode 7 of Creating a Hidden Object Game is Silverlight 3.

In the last few posts we have been working on the magnifier feature of the game which we will finish in this episode.

Please review the following posts:

First we will add in the following files into the ClutteredCube project:

The first thing we want to do is to change MagnifierOverBehavior so that it inherits from our custom behavior base class thus inheriting the IsEnabled property:

public class MagnifierOverBehavior : BaseBehavior<FrameworkElement>

The only other change we need to make this behavior is not set the Effect property on MouseEnter but instead in MouseMove based on the value of the IsEnabled property:

private void AssociatedObject_MouseEnter( object sender, MouseEventArgs e )
{
  this.AssociatedObject.MouseMove += new MouseEventHandler( AssociatedObject_MouseMove );
  //this.AssociatedObject.Effect = this.magnifier;
}

private void AssociatedObject_MouseMove( object sender, MouseEventArgs e )
{
  if (IsEnabled)
  {
    if (this.AssociatedObject.Effect != this.magnifier)
    {
      this.AssociatedObject.Effect = this.magnifier;
    }
    ...
  }
}

In Blend, open the MainPage and select the MagnifierOverBehavior in the Object tree under magnifierCanvas. Name the behavior magnifierBehavior and set IsEnabled to false:

From the Assets tab drag two instances of SetInteractionPropertyAction onto the CheckBox magnifier control:

On the first one, set the properties as follows:

  • EventName: Checked
  • TargetName: magnifierCanvas
  • ObjectName: magnifierBehavior
  • PropertyName: IsEnabled
  • Value: true

Set the second the same as the first, except:

  • EventName: Unchecked
  • Value: false

Make sure that the CheckBox control IsChecked property is false to match the InEnabled value of false for magniferBehavior.

Run the game and verify that magnification is only turned on when the CheckBox is checked.

Let’s do one more thing to finish this episode. Notice that magnifier.png has a white background behind the glass part of the magnifying glass. The image should be on top of something white like a notepad. Import the notepad image and drop it at the bottom of LayoutRoot.

notepad

Position it how you want it and move the magnifier CheckBox on top of it.

Source Code

Demo

There are many features to add so I will keep the next episode contents a surprise.

Creating an Action to set Properties on Actions & Behaviors

September 22, 2009 in Silverlight

Behaviors and actions have properties of their own and it would be useful to be able to set those properties as a result of a trigger.

For this post, we will use the ShowMessageBox action that is part of the Expression Blend Samples CodePlex project.

To explore this, create a Silverlight project in Blend 3, drop a Rectangle on LayoutRoot, and drop a ShowMessageBox action onto the Rectangle. The ShowMessageBox action is under the Behaviors > Experimental in the Assets tab:

Change the rectangle stroke and fill so that it is easily seen.

Set the Caption and Message properties of ShowMessageBox:

When we run the application and click on the rectangle, the message box shows:

What if we wanted to change the value of the Message property when a CheckBox control is checked or unchecked?

To accomplish this, we will create SetInteractionPropertyAction that derives from TargetedTriggerAction and adds properties for the name of the action or behavior, the property on that action/behavior, and the value to set on that property:

public string ObjectName
{
  get { return (string)GetValue(ObjectNameProperty); }
  set { SetValue(ObjectNameProperty, value); }
}
public static readonly DependencyProperty ObjectNameProperty =
  DependencyProperty.Register("ObjectName",
  typeof(string), typeof(SetInteractionPropertyAction),
  new PropertyMetadata(null));</pre>

public string PropertyName
{
  get { return (string)base.GetValue(PropertyNameProperty); }
  set { base.SetValue(PropertyNameProperty, value); }
}
public static readonly DependencyProperty PropertyNameProperty =
    DependencyProperty.Register("PropertyName",
    typeof(string), typeof(SetInteractionPropertyAction),
    new PropertyMetadata(null));

public string Value
{
  get { return (string)base.GetValue(ValueProperty); }
  set { base.SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
  DependencyProperty.Register("Value",
  typeof(string), typeof(SetInteractionPropertyAction),
  new PropertyMetadata(null));

Next we need to implement the Invoke method which finds the action or behavior under the Target control that matches the ObjectName and sets its property defined by PropertyName to the value specified in Value.

To find actions and behaviors associated with a given object, we use the GetTriggers and GetBehaviors static methods defined on the Interaction class in the System.Windows.Interactivity namespace:

  public static TriggerCollection GetTriggers(DependencyObject obj);
  public static BehaviorCollection GetBehaviors(DependencyObject obj);

If we look at some sample XAML we can see that when an action is defined, it becomes part of the Triggers collection and appears under the trigger that causes the action to be invoked. If multiple actions are associated with the same object and use the same trigger, the actions appear together:

<Path>
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="MouseLeftButtonDown">
      <ic:ChangePropertyAction x:Name="changeProperty" TargetName="staplerText" PropertyName="Opacity" Value="0.4"/>
      <im:PlaySoundAction x:Name="playSoundAction" Source="/Audio/magic_wand.mp3"/>
      <ic:RemoveElementAction x:Name="removeElement" />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</Path>

Calling Interaction.GetTriggers passing a reference to the hosting Path, will return a single trigger of type EventTrigger. For that trigger, we can use its Actions property to access the three actions.

If we look at some XAML for a behavior, we notice that there is a Behaviors collection containing one or more behaviors and for each behavior there is an optional Triggers collection:

<Canvas>
  <i:Interaction.Behaviors>
    <local:ParticlesBehavior x:Name="particles">
      <i:Interaction.Triggers>
        <i:EventTrigger SourceName="staplerPath" EventName="MouseLeftButtonDown">
          <i:InvokeCommandAction CommandName="ShowParticles"/>
        </i:EventTrigger>
        <i:EventTrigger SourceName="idolPath" EventName="MouseLeftButtonDown">
          <i:InvokeCommandAction CommandName="ShowParticles"/>
        </i:EventTrigger>
      </i:Interaction.Triggers>
    </local:ParticlesBehavior>

    <Behaviors:MagnifierOverBehavior x:Name="magnifier"/>
  </i:Interaction.Behaviors>
</Canvas>

In the case, calling Interaction.GetBehaviors with a reference to the Canvas will return two behaviors: ParticlesBehavior and MagnifierOverBehavior

Just like any object you add to the Object tree, you can set the name of an action or behavior. This adds an x:Name property in the XAML and the name appears in the name textbox in the Properties panel. To access the x:Name property in code, get a reference to the behavior or action and call GetValue passing the FrameworkElement.NameProperty dependency property:

  string actionName = (string)action.GetValue(FrameworkElement.NameProperty);
  string behaviorName = (string)behavior.GetValue(FrameworkElement.NameProperty);

For SetInteractionPropertyAction we define the following methods that will try to find an action or behavior that matches the same name as the ObjectName property:

private object TryFindBehavior()
{
  var behaviors = Interaction.GetBehaviors(Target);

  foreach (Behavior behavior in behaviors)
  {
    string behaviorName = (string)behavior.GetValue(FrameworkElement.NameProperty);

    if (behaviorName == ObjectName)
    {
      return behavior;
    }
  }

return null;
}

private object TryFindAction()
{
  var triggers = Interaction.GetTriggers(Target);

  foreach (System.Windows.Interactivity.TriggerBase trigger in triggers)
  {
    foreach (System.Windows.Interactivity.TriggerAction action in trigger.Actions)
    {
      string actionName = (string)action.GetValue(FrameworkElement.NameProperty);

      if (actionName == ObjectName)
      {
        return action;
      }
    }
  }

return null;
}

Finally, the Invoke method uses the previous two methods to find a matching action or behavior and to set one of its properties. I admit that a large portion of the Invoke method in the full project source code was borrowed from the SetProperty action defined in the Expression.Samples.Interactivity assembly (thanks .NET Reflector) :

Type itemType = null;
object item = null;

item = TryFindBehavior();

if (item == null)
{
  item = TryFindAction();
}

if (item == null)
{
  return;
}
else
{
  itemType = item.GetType();
}

PropertyInfo property = itemType.GetProperty(this.PropertyName);
property.SetValue(item, this.Value, null);

After compiling the code, the SetInteractionPropertyAction will appear in the Assets panel.

Let’s add a CheckBox control to LayoutRoot and drop two instances of SetInteractionPropertyAction onto the CheckBox. Next select the ShowMessageBox action and set its name property to showMessageBox.

Click on the first instance of SetInteractionPropertyAction and set it’s EventName, TargetName, ObjectName, PropertyName, and Value properties:

For the second instance of SetInteractionPropertyAction, set the same properties as follows:

Now run the application. Initially, when you click on the rectangle, you will see a message box that says “Hello World”.

When you click on the CheckBox and then click the rectangle, the message will change to “I am checked”. Unchecking the CheckBox and clicking on the rectangle will cause the message to change to “I am unchecked”.

Install Microsoft Silverlight

 

We have successfully set the property of one action based on a trigger invoking SetInteractionPropertyAction.

If you look at the Common Properties group for our custom action, you will notice that the editors for ObjectName, PropertyName, and Value are just TextBox controls. I would like the design-time experience to be better for this action. Maybe in a future post, this will be revisited. You might have noticed that the Value property is of type string even though the data type of the property that corresponds to PropertyName might be another data type. I originally had Value as type object, but there were times when the textbox would gray out and not allow any input. This would also be solved if custom property editors were defined.

The Microsoft.Expression.Interactions assembly defines an action called ChangePropertyAction that sets the property on an object and has a much better designer experience. It provides a drop-down containing property names and when a property is selected, the editor for the Value reflects the underlying data type of the property:

I would like to include these editors plus one that would show a list of named actions and behaviors for the given Target grouped in the list under the words: Actions or Behaviors.

Source code

Hidden Object: Episode 3 – Marking Items off the List

September 17, 2009 in Hidden Object Game, Silverlight

In this episode of Creating a Hidden Object Game in Silverlight 3 we overlay the items in the image with paths and associate the ChangePropertyAction to change the Opacity of the TextBlock to indicate that the item has been found.

In order to detect when an item is clicked in the office image, we need a hotspot. A Path works great for this and odd-shaped items can be outlined using the Pen tool. Press the “P” key and the Pen tool will be selected:

Center the mouse cursor over the stapler and use the mouse wheel to zoom in. Now click various points to outline the shape of the stapler:

I started with a point in the bottom left corner and clicked a total of 7 points to outline the shape. When I got to point 5 in the top left, the path’s fill started obstructing my view of the rest of the object. Simply move over to the Appearance group and set the Opacity below 50%. Continue to draw the remaining points. When you get back to the first point, the mouse cursor will change to the pen with a loop indicating that clicking will close the path. Give the path a name (staplerPath) and set the Opacity to 0%. It is time to add the ChangePropertyAction to the path. On the Assets tab, click Behaviors and find the ChangePropertyAction.

 

 

Drag the action from the Assets tab to the Object tree and drop it on staplerPath:

 

 

This action is now associated with staplerPath object. Adding this action to our object tree caused two assemblies to be added to the References folder:

  • Microsoft.Expression.Interactions.dll
  • System.Windows.Interactivity.dll

Looking at the Properties panel you will see the properties for this action and notice that there is already an EventTrigger selected to fire when the staplerPath object gets a MouseLeftButtonDown event. That is exactly what we want. In the Common Properties group you will see TargetName and PropertyName. The TargetName is the name of the object that will have its property changed and PropertyName is the name of the property. Since we want the staplerText TextBlock to be dimmed when the staperPath object is clicked, then we set the TargetName to staplerText. An easy way to do that is to grab the bullseye (called the artboard element picker) and drag it to staplerText on either the canvas or the object tree. The Properties for this action should now look like this:

 

Press F5 to run the project and click on the stapler. The text for stapler is now dimmed.

 

You can play with the Animation Properties by setting duration for the property change and even set an easing value if you don’t want a linear change in the Opacity from 100% to 40%. Let’s make this even better by adding a sound effect when an item is clicked.  I found a nice magic wand sound on SoundBible.com and added it to the project in an Audio folder. Now drag the PlaySoundAction from the Assets list onto staplerPath and set the Properties to use the audio file:

 

 

Now repeat this same process for the other 12 items. We have the core of our very own hidden object game!

 

In our next episode, we will write a custom behavior that will shoot a spray of particles when an item is clicked.