Andrej Tozon's blog

In the Attic

NAVIGATION - SEARCH

Reactive.buffering.from event.

Reactive?

This post is continuing the series about Reactive Extensions, a powerful little framework currently available for .NET 3.5 & 4, Silverlight 3/4, Windows Phone 7 and JavaScript,  that Microsoft’s Dev Labs are working on.

Buffering?

In this post, I’ll look into two reactive operators that will help me shape my Windows Phone 7 app into something that could eventually be called a  game, even. Those operators are:

BufferWithTime. BufferWithTime() will buffer values, produced in a specified time interval, to publish them all at once when that interval ends. Take, for example, a logger service, where you don’t need to log all events at once; instead, you would check your log queue every minute or so and log all queued events in a batch.

BufferWithCount. BufferWithCount() does a similar thing, but instead of a Timespan value, it takes an integer that specifies the number of values to buffer, before publishing all buffered values.

From event?

We know the FromEvent() construction operator already – it-s used to observe the sequence of specified events, the observation starting upon subscribing to that sequence.

So?

Right… Ok, let’s take the code from the previous post and build on that. Where I left it off was having an ItemsControl with nine tiles in it. What I wanted to do next was make those tiles react to user mouse clicks…

So let’s go and do that, taking small steps.

The source code for this blog post is available from here.

To be able to subscribe to tiles’ mouse events, we first need to find the visual elements representing them. This may seem a difficult task at first, because ItemsControl’s Items property holds a collection of data items (in this case, those are letters), not their visual representations. It’s not, really. Actually, this is a Linq query that will project the collection of letters (data) to a collection of tiles (visuals) through the item container:

IEnumerable<DependencyObject> c = from i in list.Items
                                  select list.ItemContainerGenerator.ContainerFromItem(i);

We’re going to observe these tiles so we’ll turn them to an observable collection:

IObservable<DependencyObject> oc = c.ToObservable();

Now, here’s the trick. We’re going to use a single Linq expression that will notify the observer whenever a tile (any tile!) has been clicked with a mouse, handing it a reference to that tile:

IObservable<Control> ee = from i in oc
    from e in Observable.FromEvent<MouseButtonEventArgs>(i, "MouseLeftButtonDown")
    select VisualTreeHelper.GetParent(((Grid)e.EventArgs.OriginalSource).Parent) as Control;

The expression is three part:

  1. Take all tiles and
  2. subscribe to their MouseLeftButtonDown event and
  3. when that event happens, return a reference to the tile that raised that event (using VisualTreeHelper to get tile’s parent element, as for the item's data template shown below).

Subscribing to this observable sequence of tiles will enable us to react to tile MouseLeftButtonDown events:

ee.Subscribe(Reveal);

In reaction to the mouse down event, we’ll make the tile go into a “Selected” state:

private static void Reveal(Control control)
{
    VisualStateManager.GoToState(control, "Selected", true);
}

But for this, we need to adjust the tile’s data template first. This is a simple “two-sided element” template that flips slides when going between Selected and Unselected sides. Visual states are used to represent the state on each of the side and transitions between those states. I also added the third state – the Matched state will make the matched tiles disappear:

<DataTemplate x:Key="TileTemplate">
    <ContentControl>
        <ContentControl.Template>
            <ControlTemplate>
                <Grid x:Name="TileRoot"
                      RenderTransformOrigin="0.5,0.5"
                      Opacity="0"
                      Width="142"
                      Height="142"
                      Margin="5">
                    <Grid.Projection>
                        <PlaneProjection CenterOfRotationX="0" 
                                         RotationY="-90" />
                    </Grid.Projection>
                    <Grid.RenderTransform>
                        <ScaleTransform />
                    </Grid.RenderTransform>
                    <i:Interaction.Behaviors>
                        <ib:LoadedBehavior />
                    </i:Interaction.Behaviors>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="SelectionStates">
                            <VisualState x:Name="Unselected">
                                <Storyboard>
                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)"
                                                                   Storyboard.TargetName="front">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="180" />
                                        <EasingDoubleKeyFrame KeyTime="0:0:1" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)"
                                                                   Storyboard.TargetName="back">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="180" />
                                        <EasingDoubleKeyFrame KeyTime="0:0:1" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Canvas.ZIndex)"
                                                                   Storyboard.TargetName="back">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <System:Int32>1</System:Int32>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                        <DiscreteObjectKeyFrame KeyTime="0:0:0.5">
                                            <DiscreteObjectKeyFrame.Value>
                                                <System:Int32>0</System:Int32>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                        <DiscreteObjectKeyFrame KeyTime="0:0:1">
                                            <DiscreteObjectKeyFrame.Value>
                                                <System:Int32>0</System:Int32>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Selected">
                                <Storyboard>
                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)"
                                                                   Storyboard.TargetName="front">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="0" />
                                        <EasingDoubleKeyFrame KeyTime="0:0:1" Value="180" />
                                    </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)"
                                                                   Storyboard.TargetName="back">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="0" />
                                        <EasingDoubleKeyFrame KeyTime="0:0:1" Value="180" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Canvas.ZIndex)"
                                                                   Storyboard.TargetName="back">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <System:Int32>0</System:Int32>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                        <DiscreteObjectKeyFrame KeyTime="0:0:0.5">
                                            <DiscreteObjectKeyFrame.Value>
                                                <System:Int32>1</System:Int32>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                        <DiscreteObjectKeyFrame KeyTime="0:0:1">
                                            <DiscreteObjectKeyFrame.Value>
                                                <System:Int32>1</System:Int32>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Matched">
                                <Storyboard>
                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="UIElement.Opacity"
                                                                   Storyboard.TargetName="TileRoot">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="1" />
                                        <EasingDoubleKeyFrame KeyTime="0:0:1" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="UIElement.Opacity"
                                                                   Storyboard.TargetName="front">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
                                                                   Storyboard.TargetName="TileRoot">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="1" />
                                        <EasingDoubleKeyFrame KeyTime="0:0:1" Value="1.5" />
                                    </DoubleAnimationUsingKeyFrames>
                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
                                                                   Storyboard.TargetName="TileRoot">
                                        <EasingDoubleKeyFrame KeyTime="0" Value="1" />
                                        <EasingDoubleKeyFrame KeyTime="0:0:1" Value="1.5" />
                                    </DoubleAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid x:Name="back"
                          HorizontalAlignment="Left"
                          Height="142"
                          Margin="0"
                          VerticalAlignment="Bottom"
                          Width="142"
                          Background="{StaticResource PhoneForegroundBrush}">
                        <Grid.Projection>
                            <PlaneProjection RotationY="180" />
                        </Grid.Projection>
                        <TextBlock Text="{Binding}"
                                   FontSize="{StaticResource PhoneFontSizeExtraExtraLarge}"
                                   Foreground="{StaticResource PhoneAccentBrush}"
                                   VerticalAlignment="Bottom"
                                   HorizontalAlignment="Left"
                                   IsHitTestVisible="False"
                                   Margin="20,0,0,10" />
                    </Grid>
                    <Grid x:Name="front"
                          HorizontalAlignment="Left"
                          Margin="0"
                          VerticalAlignment="Bottom"
                          Background="{StaticResource PhoneAccentBrush}"
                          Width="142"
                          Height="142">
                        <Grid.Projection>
                            <PlaneProjection />
                        </Grid.Projection>
                        <TextBlock Text="{Binding}"
                                   FontSize="{StaticResource PhoneFontSizeExtraExtraLarge}"
                                   Foreground="{StaticResource PhoneForegroundBrush}"
                                   VerticalAlignment="Bottom"
                                   IsHitTestVisible="False"
                                   HorizontalAlignment="Left"
                                   Margin="20,0,0,10" />
                    </Grid>
                </Grid>
            </ControlTemplate>
        </ContentControl.Template>
    </ContentControl>
</DataTemplate>

The game

Great, now we’ve got ourselves a very simple game of revealing tiles. Sounds familiar? Yes, it looks a lot like a Memory  game.

In a Memory game, we’re dealing with pairs, right? Selecting two of the tiles makes up for a single turn in the game. If we take the existing observable collection and  additionally make it buffer to two elements, the observer will only going get notified when the user selects two tiles. But we also need to let the revealing animation to finish (lasts 1 second), so we have to delay the event publication for one second.

var ff = ee.BufferWithCount(2).Delay(TimeSpan.FromSeconds(1));

The OnNext handler will get both data items associated with the tiles, we just need to subscribe:

ff.ObserveOnDispatcher().Subscribe(OnPairRevealed);

The handler’s implementation at this point is this:

private void OnPairRevealed(IList<Control> tiles)
{
    string state = (char) tiles[0].DataContext == (char) tiles[1].DataContext ? "Matched" : "Unselected";
    VisualStateManager.GoToState(tiles[0], state, true);
    VisualStateManager.GoToState(tiles[1], state, true);
}

Matched and Unselected states are the only valid states to transition from the Selected state. Target state is determined by comparing letters on the tiles – if both letters are the same, it’s a match!

Making it work

There’s a tiny small thing left to make this all work. The previous version of this app was showing the initial tile animation only, and now we need to wire in the logic described in this post. Nothing easier – with existing animation sequence subscription, we only need to call the function when the initial sequence ends. So this:

.Subscribe(AddLetter);

becomes this:

.Subscribe(AddLetter, ObserveClicks);

Video and source code

See the application in action…

… and download source code.

Finishing

Reactive Extensions sure are a fun way to program – take a quick run through the source code to see how little code was required to get things moving as shown in the video.

In future posts I’ll shape up this app into more functional Memory game. Possibly end up wanting publishing it on the marketplace…

Oh, and Happy New Year everybody!