You are browsing the archive for 2009 October.

Hidden Object: Episode 12 – Custom Mouse Cursor Behavior

October 27, 2009 in Hidden Object Game, Silverlight

This is episode 12 of Creating a Hidden Object Game is Silverlight 3. In this episode, we will create a behavior that allows us to set the shape of the mouse cursor to any Image or Path we desire.

Let start this tutorial in Expression Design. Create a new document that is 25×25 pixels and on the single layer add the shapes shown:

 

102709_1407_HiddenObjec1.png 

Export the document as a PNG making sure that both Transparency and Antialias are checked.

 

If you don’t have a copy of Expression Design, you can use a vector graphics program like Inkscape.

 

Add the image to the Visual Studio project and drag it onto the Canvas. Let’s position the cursor image off screen at: Top = -50 and Left = -5. The -5 positions the cursor so the tip of the arrow is right in the corner and the -50 just to get it off screen. Since this image needs to be over all other objects including the screen Canvas objects, let’s set the ZIndex property to 1000.

We will compensate for the Top value with the OffsetY value in the MouseCursorBehavior that we will create:

 

The two offset values are dependency properties of type Double whereas CursorName is a string. You will notice that the cursor name has the artboard element picker (target symbol) that allows you to pick an object from the artboard.

Under the Interactivity folder, create a folder called MouseCursor and add three class files: MouseCursorBehavior, NameResolvedEventArgs, and NameResolver.

To get NameResolver and NameResolvedEventArgs, I admit that I used .NET Reflector to understand how the TargetedTriggerAction class was able to get a reference to an instance of the Target class using the string dependency property, TargetName.

Here is a sample usage of the NameResolver class as it relates to the MouseCursorBehavior:

NameResolver cursorResolver = new NameResolver();
cursorResolver.NameScopeReferenceElement = AssociatedObject;
cursorResolver.Name = "cursorArrow";
DependencyObject cursor = cursorResolver.Object;
 

After creating an instance of the NameResolver, we must assign an object as the NameScopeReferenceElement and set the name of the element that will be our cursor. Calling the Object property on the resolver will return a reference to the object specified by the Name property. The key method in NameResolver is UpdateObjectFromName:

private void UpdateObjectFromName(DependencyObject oldObject)
{
    DependencyObject resolvedObject = null;
    this.ResolvedObject = null;
 
    if (this.NameScopeReferenceElement != null)
    {
        if (!IsElementLoaded(this.NameScopeReferenceElement))
        {
            this.NameScopeReferenceElement.Loaded += new RoutedEventHandler(this.OnNameScopeReferenceLoaded);
            this.PendingReferenceElementLoad = true;
            return;
        }
        if (!string.IsNullOrEmpty(this.Name))
        {
            FrameworkElement actualNameScopeReferenceElement = this.ActualNameScopeReferenceElement;
            if (actualNameScopeReferenceElement != null)
            {
                resolvedObject = actualNameScopeReferenceElement.FindName(this.Name) as DependencyObject;
            }
        }
    }
    this.HasAttempedResolve = true;
    this.ResolvedObject = resolvedObject;
    if (oldObject != this.Object)
    {
        this.OnObjectChanged(oldObject, this.Object);
    }
}

 

Line 19 shows how NameScopeReferenceElement and Name are used to resolve the actual object.

The MouseCursorBehavior class defines three dependency properties with their corresponding .NET properties:

public static readonly DependencyProperty CursorNameProperty =
     DependencyProperty.Register("CursorName", typeof(string), typeof(MouseCursorBehavior),
     new PropertyMetadata(new PropertyChangedCallback(OnCursorNameChanged)));
 
public static readonly DependencyProperty OffsetXProperty =
    DependencyProperty.Register("OffsetX", typeof(double), typeof(MouseCursorBehavior), null);
    
public static readonly DependencyProperty OffsetYProperty =
    DependencyProperty.Register("OffsetY", typeof(double), typeof(MouseCursorBehavior), null);
 

In the constructor of the behavior, a NameResolver called cursorResolver is created. In the OnAttached method, the resolver’s NameScopeReferenceElement is set to the AssociatedObject. And in the OnCursorChanged method, the Name property is set to the name of the object to be used as the cursor:

private static void OnCursorNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    MouseCursorBehavior behavior = (MouseCursorBehavior)obj;
    behavior.CursorResolver.Name = (string)args.NewValue;
}

To get the artboard element picker to show in the Properties panel for the CursorName, add the CustomPropertyValueEditor attribute to the CursorName property and specify the editor for an Element:

[CustomPropertyValueEditor(CustomPropertyValueEditor.Element)]
public string CursorName
{
    get
    {
        return (string)base.GetValue(CursorNameProperty);
    }
    set
    {
        base.SetValue(CursorNameProperty, value);
    }
}
 

The Cursor property is where the CursorName value is resolved by the NameResolver and the object reference is returned.

Now the behavior has a reference to the image that is being used as a cursor, now it needs to use that instead of the default cursor. In the OnAttached method, the behavior registers to handle the MouseEnter and MouseLeave events of the AssociatedObject. In our case, the MouseCursorBehavior will be attached to the MainPage UserControl which becomes the AssociatedObject. So whenever the mouse enters the area of the UserControl, it will be changed to the arrow:

private void AssociatedObject_MouseEnter(object sender, MouseEventArgs e)
{
    if (!this.IsCursorNameSet)
        return;
 
    FrameworkElement cursor = Cursor as FrameworkElement;
 
    cursor.Visibility = Visibility.Visible;
    AssociatedObject.Cursor = Cursors.None;
    cursor.IsHitTestVisible = false;
 
    this.AssociatedObject.MouseMove += new MouseEventHandler(AssociatedObject_MouseMove);
}

The cursor variable has a reference to the resolved object which is the Image named cursorArrow. We make the image visible and hide the real cursor. We must make the image “invisible” to mouse events so we set the IsHitTestVisible property to false. Finally, we register for the MouseMove event which is responsible for positioning the image wherever the mouse should be:

 

private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
{
    if (!this.IsCursorNameSet)
        return;
 
    FrameworkElement cursor = Cursor as FrameworkElement;
 
    Point mousePosition = e.GetPosition(null);
    cursor.Margin = new Thickness(mousePosition.X + OffsetX, mousePosition.Y + OffsetY, 0, 0);
 
}

To position the image, we use the trick of specifying the top and left values of its Margin based on the current mouse postion and the OffsetX and OffsetY dependency properties we defined.

 

In the MouseLeave event handler, we simply undo what we set in the MouseEnter handler.

 

 

I’ve seen a similar approach to custom cursors various places on the Internet. One requirement that I had was the need to specify a cursor at the UserControl level as shown, but then to have a child (or grandchild, etc.) object of that UserControl also have a unique cursor. If you move the mouse over a hidden rectangle on the left side of the screen the cursor will change to a left-facing arrow. Clicking the arrow will take you to another screen which has a hidden rectangle on the right that displays a right-facing arrow to get you pack to the original screen.

 

 

In the code, I created two path objects for the arrows just to show that any element could be used for a cursor. If I wanted, I could even have a cursor with multiple objects grouped in a Canvas that use animation storyboards.

 

When you look at the complete source for this project, you will notice that I use a Stack data structure to keep track of the nested cursors. I am not completely satisfied with the code as it stands, but it works for the present situation. Two things to note. First, the hidden rectangle must have space all around it so that the cursor properly changes from the left arrow to the cursorArrow. Second, I had to do a workaround because the shapes generated from the ParticlesBehavior was interfering with the custom cursor as sometimes a MouseEnter event was being fired with the star shapes but no MouseLevent event.

 
Check out the code and let me know if you come up with a better solution.

Zip Source Code

silverlight Demo

In the next episode, we will add a hint feature that allows the player to see an area of the screen that still has an object to find.

Hidden Object: Episode 11 – Add Custom Shapes to the Particles Behavior

October 5, 2009 in Hidden Object Game, Silverlight

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

Growing up our family didn’t splurge on Lucky Charms cereal, but I remember the TV commercials with Lucky the leprechaun talking about all the fun marshmallow shapes: pink hearts, yellow moons, orange stars, green clovers, and blue diamonds. In later years other shapes appeared such as purple horseshoes, red balloons, rainbows, and pots of gold.

 

What does this have to do with our hidden object game? Currently the ParticleControl creates only one shape. Circles. Wouldn’t it be great if you could specify the shape of the particles? That’s what I thought and I knew it could easily be done once I saw this discussion post on the CodePlex site for the Silverlight String-To-PathGeometry Converter.

To the existing ParticlesBehavior, we will add two dependency properties with corresponding property get/set blocks:

 

public static readonly DependencyProperty ParticleShapeProperty =
    DependencyProperty.Register("ParticleShape", typeof(ParticleShape),
    typeof(ParticlesBehavior), null);

public static readonly DependencyProperty CustomShapePathDataProperty =
    DependencyProperty.Register("CustomShapePathData", typeof(string),
    typeof(ParticlesBehavior), null);

 

The ParticleShape property is an enum with a list of possible shapes and a custom option:

public enum ParticleShape
{
    Circle,
    Square,
    Star4,
    Star5,
    Star8,
    Custom
}

 

The CustomShapePathData property is a string that holds a special XAML Path markup syntax and is used when the ParticleShape property is set to Custom. Here is an example of a path definition for a triangle:

 

<Path Data="F1 M 50,7.62939e-006L -1.02561e-005,100L 100,100L 50,7.62939e-006 Z "/>

 

The value of the Data property is what is set on CustomShapePathData.

 

The only other change we need to make to ParticlesBehavior is in the OnShowParticles method where we set the ParticleShape and CustomShapePathData on the instance of ParticleControl.

 

In ParticleControl, we add the same two dependency properties for ParticleShape and CustomShapePathData with their corresponding property get and set.

 

The Ellipse, Rectangle, and Path all inherit from the Shape base class. So we change all the methods in ParticleControl that worked on an Ellipse to a Shape. In the SpawnParticle method, we call the newly created CreateShape method:

 

private Shape CreateShape()
{
    string pathData = "";

    switch (ParticleShape)
    {
        case ParticleShape.Circle:
            return new Ellipse();
 
        case ParticleShape.Square:
            return new Rectangle();

        case ParticleShape.Star4:
            pathData = star4;
            break;

        case ParticleShape.Star5:
            pathData = star5;
            break;

        case ParticleShape.Star8:
            pathData = star8;
            break;

        case ParticleShape.Custom:
            if (string.IsNullOrEmpty(CustomShapePathData))
                return new Ellipse();
            else
                pathData = CustomShapePathData;
            break;

        default:
            return new Ellipse();
    }

    string xamlPath = string.Format("<Path xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' " +
             "xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' " +
             "Data='{0}' Stretch='Fill'/>", pathData);

    Path path = (Path)System.Windows.Markup.XamlReader.Load(xamlPath);

    return path;
}

 

If the ParticleShape property is set to Circle or Square, then a new Ellipse or Rectangle is created and returned. If the selected shape is a 4, 5, or 8-pointed star then a string constant containing the Path data syntax is set and a new Path is created using the XamlReader.Load method. If it is a custom shape, then the data is set based on the value of the CustomShapePathData property. If there is no custom path data, then an Ellipse is created.

 

When we compile the project and open Blend, we will now see the additional properties:

 

 

Here is what the custom triangle particles look like as well as the built-in shapes:

 

 

One of the best ways to create these custom shapes is in Expression Design. Create a new document that is 100×100 pixels. Use the Pen, Polygon, or Polyline tools to create a path to your desired shape. Or create multiple paths and use Path operations (Unite, Front Minus Back, etc.) to create a single path.

 

Select the Path and on the Edit menu select Copy XAML. Paste the XAML into Notepad and copy the Data property string and paste it as the CustomShapePathData value:

 

<Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Layer_1_4" Width="100" Height="100" Canvas.Left="0" Canvas.Top="0">

    <Path Width="99.7292" Height="100.79"
        Canvas.Left="-1.84403" Canvas.Top="-1.13409"
        Stretch="Fill" StrokeLineJoin="Round" Stroke="#FF000000"

        Data="F1 M 31.5808,99.156C 34.4917,98.2985 37.4026,97.441 40.1509,96.3542C 42.8991,95.2675 45.4846,93.9515 47.874,92.4426C 50.2634,90.9336 52.4566,89.2317 54.4271,87.3777C 56.3976,85.5237 58.1453,83.5175 59.6511,81.4035C 61.1568,79.2895 62.4206,77.0676 63.4311,74.7844C 64.4415,72.5011 65.1986,70.1565 65.699,67.7977C 66.1994,65.439 66.4431,63.0661 66.6868,60.6933C 67.6642,61.334 68.6416,61.9748 69.581,62.7785C 70.5203,63.5821 71.4217,64.5487 72.2503,65.6721C 73.0789,66.7954 73.8349,68.0754 74.4845,69.4997C 75.1342,70.9239 75.6777,72.4923 76.0837,74.1863C 76.4897,75.8803 76.7583,77.6999 76.8615,79.6207C 76.9647,81.5416 76.9025,83.5637 76.6513,85.6577C 76.4,87.7517 75.9597,89.9176 75.5194,92.0835C 76.6639,89.273 77.8084,86.4624 78.6723,83.6362C 79.5361,80.81 80.1193,77.9681 80.4293,75.1592C 80.7393,72.3503 80.7762,69.5744 80.5552,66.8778C 80.3342,64.1813 79.8555,61.564 79.1415,59.0687C 78.4275,56.5734 77.4784,54.2 76.3232,51.9865C 75.1681,49.7729 73.8071,47.7191 72.2749,45.8572C 70.7427,43.9953 69.0395,42.3253 67.3363,40.6554C 68.4466,40.2907 69.557,39.926 70.771,39.6927C 71.985,39.4594 73.3028,39.3573 74.6976,39.4098C 76.0925,39.4624 77.5646,39.6695 79.0832,40.0495C 80.6018,40.4296 82.1669,40.9826 83.7444,41.7214C 85.322,42.4601 86.912,43.3846 88.4781,44.5016C 90.0443,45.6185 91.5865,46.9279 93.067,48.4299C 94.5475,49.9319 95.9663,51.6266 97.3851,53.3212C 95.9014,50.6741 94.4176,48.027 92.7466,45.5895C 91.0756,43.152 89.2173,40.9241 87.2145,38.9304C 85.2117,36.9367 83.0644,35.1771 80.8184,33.6686C 78.5723,32.1601 76.2276,30.9026 73.8315,29.905C 71.4354,28.9074 68.9881,28.1697 66.5372,27.6927C 64.0864,27.2156 61.632,26.9993 59.2211,27.0363C 56.8101,27.0733 54.4425,27.3637 52.075,27.6541C 52.4822,26.5587 52.8893,25.4632 53.4638,24.3686C 54.0383,23.2739 54.7801,22.18 55.6909,21.1222C 56.6017,20.0644 57.6814,19.0426 58.9254,18.0923C 60.1693,17.142 61.5775,16.2632 63.1387,15.4904C 64.6999,14.7176 66.414,14.0509 68.2638,13.5228C 70.1135,12.9948 72.0987,12.6054 74.1961,12.3844C 76.2935,12.1634 78.5031,12.1107 80.7127,12.058C 77.7179,11.5676 74.7232,11.0772 71.7757,10.8639C 68.8281,10.6506 65.9276,10.7144 63.1202,11.0372C 60.3127,11.36 57.5982,11.9418 55.0184,12.7573C 52.4386,13.5727 49.9936,14.6219 47.7197,15.8732C 45.4458,17.1245 43.3432,18.578 41.4421,20.1967C 39.5411,21.8155 37.8416,23.5994 36.3674,25.5075C 34.8931,27.4156 33.644,29.4477 32.3949,31.4798C 31.7923,30.4784 31.1897,29.4771 30.6921,28.3454C 30.1944,27.2137 29.8017,25.9518 29.5425,24.5802C 29.2834,23.2085 29.1577,21.7273 29.1903,20.1622C 29.223,18.5971 29.4138,16.9482 29.783,15.2458C 30.1522,13.5434 30.6997,11.7875 31.4402,10.0121C 32.1806,8.23676 33.114,6.44186 34.2489,4.66424C 35.3838,2.88662 36.7202,1.12626 38.0567,-0.634094C 35.8061,1.40149 33.5555,3.4371 31.5509,5.60862C 29.5464,7.78015 27.7879,10.0876 26.2898,12.4839C 24.7918,14.8801 23.5542,17.3651 22.5833,19.8904C 21.6124,22.4158 20.9082,24.9816 20.4687,27.5396C 20.0293,30.0976 19.8547,32.6477 19.935,35.1433C 20.0153,37.6388 20.3505,40.0798 20.9231,42.4221C 21.4956,44.7643 22.3056,47.0079 23.1156,49.2515C 21.957,49.0983 20.7984,48.9451 19.6033,48.6286C 18.4083,48.3121 17.1768,47.8323 15.9428,47.1797C 14.7089,46.5272 13.4724,45.7019 12.2692,44.7006C 11.0659,43.6993 9.89571,42.522 8.79491,41.1719C 7.69411,39.8218 6.66265,38.299 5.73625,36.6131C 4.80985,34.9273 3.9885,33.0785 3.3063,31.0828C 2.6241,29.0872 2.08106,26.9447 1.53801,24.8023C 1.72628,27.8311 1.91455,30.8598 2.36251,33.781C 2.81047,36.7021 3.51811,39.5156 4.45754,42.1809C 5.39696,44.8461 6.56816,47.3631 7.93723,49.6967C 9.30631,52.0304 10.8733,54.1807 12.5992,56.1191C 14.3251,58.0575 16.2101,59.784 18.2112,61.2772C 20.2124,62.7704 22.3298,64.0303 24.5181,65.043C 26.7063,66.0557 28.9654,66.8213 31.2246,67.5869C 30.3824,68.3972 29.5402,69.2075 28.5477,69.9445C 27.5551,70.6815 26.4122,71.3452 25.1326,71.903C 23.8531,72.4609 22.4369,72.913 20.9038,73.2295C 19.3707,73.546 17.7207,73.7268 15.9788,73.7457C 14.237,73.7646 12.4033,73.6215 10.5076,73.2947C 8.61196,72.9679 6.65438,72.4573 4.66878,71.7464C 2.68319,71.0355 0.669576,70.1243 -1.34403,69.2131C 1.14133,70.9543 3.62669,72.6955 6.18983,74.1666C 8.75297,75.6376 11.3939,76.8386 14.0634,77.7659C 16.7329,78.6932 19.4309,79.3468 22.1091,79.7314C 24.7872,80.116 27.4453,80.2316 30.037,80.0908C 32.6286,79.95 35.1537,79.5528 37.5688,78.9192C 39.9839,78.2856 42.2891,77.4156 44.4452,76.3362C 46.6014,75.2568 48.6085,73.9679 50.6156,72.6789C 50.724,73.8426 50.8324,75.0063 50.7898,76.2418C 50.7472,77.4773 50.5534,78.7847 50.1918,80.1329C 49.8302,81.4811 49.3007,82.8702 48.5923,84.2661C 47.8838,85.6621 46.9964,87.0649 45.9251,88.4385C 44.8539,89.8121 43.5987,91.1566 42.1613,92.4349C 40.7239,93.7132 39.1042,94.9254 37.3104,96.0345C 35.5166,97.1437 33.5487,98.1498 31.5808,99.156 Z "/>
</Canvas>

 

It’s that easy:

 


 
Source code
Demo

In the next episode, we will create a custom mouse cursor and manage its use with a behavior.

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.