Following on from that previous post I promised to make a simple example where an application would display 2 pictures and a single-finger drag would flip from one picture to another.
Here’s my example as a WPF application, a Silverlight application and a Windows Phone 7 application;
and all 3 are responding to a simple horizontal drag (or pan) gesture to change the image displayed.
I’ve used 3 mechanisms to get this done across WPF, Silverlight and Silverlight for Windows Phone 7 and all of those mechanisms were talked about in the last post.
In WPF I used the built-in system gesture support as it seemed simple.
In Silverlight for Windows Phone 7, I used the Silverlight Toolkit (for Windows Phone 7) and its GestureService/GestureListener as that had exactly what I wanted.
When it came to Silverlight itself, I was expecting to use one of the 2 libraries that I talked about in the previous post from CodePlex – the Multi-Touch Behavior for Blend and the Touch library.
However, both of these really did “too much” for what I wanted. I’m just trying to respond to a drag gesture whereas these libraries give me a whole behaviour that tries to deal with rotate/scale/translate and even to move the associated element when that gesture occurs.
I wanted less than that so I attempted to mimic the section of the API of the GestureService/GestureListener from the Phone and reproduce that in my own code from the raw touch events in order that the code for the Phone and the code for “regular” Silverlight ended up looking much the same.
I decided to try and abstract these differences behind a Trigger that would fire whenever the user did the pan gesture and I called that PanTrigger.
I ended up with 3 application projects (WPF, Silverlight, Phone) and I also ended up with a library for my behavior code.
I wanted to compile that library 3 ways (WPF, Silverlight, Phone) so I took the approach that Silverlight was the default and then created 2 more library projects – one for Phone and one for WPF and I linked the source in those library projects to the original source in the Silverlight project so that I’m now building the same code 3 ways. As in;
I have this one source file – PanTrigger.cs and it is really in the BehaviorLibrary project for Silverlight but it is also linked (flagged by the red stars above) into the WP7BehaviorLibrary project and the WpfBehaviorLibrary project where it is compiled according to those local project settings (and library references) for those 3 different frameworks.
I wanted to have a simple class to represent my data ( yes, 2 images ) and so I wrote that and used the same technique to compile it 3 different ways for the 3 environments. In the picture above that is done in the DataModelLibrary (Silverlight) project and then the source is linked into the WP7DataModelLibrary project and the WpfDataModelLibrary project;
namespace DataModelLibrary { using System.Windows.Input; using Microsoft.Expression.Interactivity.Core; public class DataModel : BaseEntity { public DataModel(params string[] imageUris) { this.imageUris = imageUris; moveToNextImageCommand = new ActionCommand(() => { this.index++; if (this.index >= this.imageUris.Length) { this.index = 0; } RaisePropertyChanged("ImageUri"); }); } public string ImageUri { get { return (this.imageUris[this.index]); } } public ICommand MoveToNextImageCommand { get { return (moveToNextImageCommand); } } string[] imageUris; int index; ICommand moveToNextImageCommand; } }
You can see that this is very simple in that it can be constructed with any number of image URIs and it surfaces the current URI via its ImageUri property and it has an ICommand called MoveToNextImageCommand which just moves the ImageUri property on to the next URI in the array (or back to the beginning of the array if necessary).
Note – the EntityBase base class just implements property change notification for me.
The WPF Version
The WPF version was pretty easy to put together. I have a project which references my WpfBehaviorLibrary and my WpfDataModelLibrary and then I sketched out a quick UI;
<Window x:Class="WpfApplication.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication" xmlns:behaviors="clr-namespace:BehaviorLibrary;assembly=WpfBehaviorLibrary" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" Title="MainWindow" Height="600" Width="800"> <Window.DataContext> <local:MainWindowViewModel /> </Window.DataContext> <Grid> <Image Margin="12" Stretch="Fill" Source="{Binding DataModel.ImageUri}"> <i:Interaction.Triggers> <behaviors:PanTrigger> <behaviors:PanTrigger.Actions> <i:InvokeCommandAction Command="{Binding DataModel.MoveToNextImageCommand}"/> </behaviors:PanTrigger.Actions> </behaviors:PanTrigger> </i:Interaction.Triggers> </Image> </Grid> </Window>
you can see that I have an Image whose Source is bound to DataModel.ImageUri and I am using Expression’s behaviors/triggers in order to set up my PanTrigger such that it fires the DataModel.MoveToNextImageCommand.
The DataContext for this window is just set to this tiny little view model;
namespace WpfApplication { using DataModelLibrary; class MainWindowViewModel { public MainWindowViewModel() { this.DataModel = new DataModel( "/WpfApplication;component/img7.jpg", "/WpfApplication;component/img12.jpg"); } public DataModel DataModel { get; set; } } }
and so how does the WPF version of the PanTrigger look? Well, here’s the code for all of the PanTrigger.cs file with its conditional compilation;
#if !SILVERLIGHT && !WINDOWS_PHONE #define WPF #endif namespace BehaviorLibrary { using System.Windows; using System.Windows.Controls; using System.Windows.Interactivity; using System.Windows.Input; #if WINDOWS_PHONE using Microsoft.Phone.Controls; #endif // WINDOWS_PHONE public partial class PanTrigger : TriggerBase<UIElement> { protected override void OnAttached() { base.OnAttached(); if (base.AssociatedObject != null) { AttachHandlers(); } } protected override void OnDetaching() { if (base.AssociatedObject != null) { DetachHandlers(); } base.OnDetaching(); } #if WPF void AttachHandlers() { base.AssociatedObject.StylusSystemGesture += OnSystemGesture; } void DetachHandlers() { base.AssociatedObject.StylusSystemGesture -= OnSystemGesture; } void OnSystemGesture(object sender, StylusSystemGestureEventArgs e) { if (e.SystemGesture == SystemGesture.Drag) { e.Handled = true; base.InvokeActions(null); } } #endif // WPF #if SILVERLIGHT void AttachHandlers() { this.listener = GestureService.GetGestureListener(base.AssociatedObject); this.listener.DragCompleted += OnDragCompleted; } void OnDragCompleted(object sender, DragCompletedGestureEventArgs e) { if (e.Direction == Orientation.Horizontal) { e.Handled = true; base.InvokeActions(null); } } void DetachHandlers() { if (this.listener != null) { this.listener.DragCompleted -= OnDragCompleted; this.listener = null; } } GestureListener listener; #endif // SILVERLIGHT } }
and you’ll notice that the WPF version is attaching to the StylusSystemGesture event and then handling that event to see if we get a Drag operation and it is firing its associated Actions whenever we see that Drag ( note – it doesn’t check orientation on that drag ).
The Windows Phone 7 Version
What about the WP7 version? I referenced my WP7BehaviorLibrary project and my WP7DataModelLibrary project and that was all fine and I wrote a local view model class just like the one I wrote for WPF but when I came to build my UI I remembered something;
<!--ContentPanel - place additional content here--> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Image Stretch="Fill" Source="{Binding DataModel.ImageUri}"> <i:Interaction.Triggers> <behaviors:PanTrigger> <behaviors:PanTrigger.Actions> <ebs:InvokeDataCommand Command="{Binding DataModel.MoveToNextImageCommand}" /> </behaviors:PanTrigger.Actions> </behaviors:PanTrigger> </i:Interaction.Triggers> </Image> </Grid>
In the snippet above, on line 9 – the Action that I am using is not the same as the one in the WPF sample. Instead of using an InvokeCommandAction I’ve had to bring in a different Action. This one is InvokeDataCommand and it come from the Expression Blend Samples on CodePlex
Why did I need this? Because the InvokeCommandAction is present in the Windows Phone 7 libraries from Expression Blend but it does not have a Command property that can be bound to. Why not? Because in Silverlight 3 on Windows Phone 7 you can’t do that kind of binding unless the target property is on an object derived from FrameworkElement and that’s not the case for InvokeCommandAction.
That means that I ended up including the source for the InvokeDataCommand and its friend BindingListener from the Blend samples in my project.
What about the WP7 version of my PanBehavior? You can see from the previous lump of source that it uses the GestureService in order to get a GestureListener for the object that the trigger is attached to and it then syncs up to the DragCompleted event on that GestureListener in order to know when to InvokeActions().
The Silverlight Version
The Silverlight version was a bit trickier. I referenced my BehaviorLibrary and my DataModelLibrary and once again I wrote a local view model class just like the one I wrote for WPF and my UI ended up looking just the same in terms of XAML;
<UserControl x:Class="SLApplication.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SLApplication" xmlns:behaviors="clr-namespace:BehaviorLibrary;assembly=BehaviorLibrary" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <UserControl.DataContext> <local:MainWindowViewModel /> </UserControl.DataContext> <Grid x:Name="LayoutRoot" Background="White"> <Image Name="image1" Source="{Binding DataModel.ImageUri}"> <i:Interaction.Triggers> <behaviors:PanTrigger> <behaviors:PanTrigger.Actions> <i:InvokeCommandAction Command="{Binding DataModel.MoveToNextImageCommand}" /> </behaviors:PanTrigger.Actions> </behaviors:PanTrigger> </i:Interaction.Triggers> </Image> </Grid> </UserControl>
and, because Silverlight 4 has improved binding ,the InvokeCommandAction that comes from Blend does allow me to bind to my MoveToNextImageCommand so that’s all good.
However, how to build the PanTrigger? I really wanted something that looked a lot like the GestureService/GestureListener from the Window Phone 7 Toolkit but that sits on top of some XNA bits and bobs and so there isn’t a version for Silverlight on the desktop.
I couldn’t really find anything that I could re-use from the 2 CodePlex libraries which offer more of a rotate/scale/translate solution that you can just attach to an element so that it can be dragged around ( on a Canvas ).
So I figured that I’d write a cut-down version of the GestureService/GestureListener for Silverlight that suited my needs in that it only knows how to do a Drag gesture. It’s not been tested very much so apply a large pinch of salt to it;
namespace BehaviorLibrary { using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; #if SILVERLIGHT && !WINDOWS_PHONE public class DragCompletedGestureEventArgs : EventArgs { public Orientation Direction { get; set; } public bool Handled { get; set; } } public class GestureListener { public event EventHandler<DragCompletedGestureEventArgs> DragCompleted; internal void RaiseDragCompleted(Orientation orientation) { if (DragCompleted != null) { DragCompleted(this, new DragCompletedGestureEventArgs() { Direction = orientation }); } } } public class GestureService { private class CurrentTouchData { Point primary; IEnumerable<UIElement> AffectedElements; int MaxPointsSeen; public void StartTouchOperation(TouchPoint primary) { this.primary = primary.Position; this.AffectedElements = VisualTreeHelper.FindElementsInHostCoordinates( this.primary, null); } public void ProcessNewPoints(TouchPointCollection points) { this.MaxPointsSeen = Math.Max(this.MaxPointsSeen, points.Count); } public void EndTouchOperation(TouchPointCollection endPoints) { if (this.MaxPointsSeen == 1) { Rect rect = new Rect(this.primary, endPoints[0].Position); if (rect.Width > tolerance) { RaiseEvent(Orientation.Horizontal); } else if (rect.Height > tolerance) { RaiseEvent(Orientation.Vertical); } } } void RaiseEvent(Orientation orientation) { foreach (var element in this.AffectedElements) { GestureListener listener = (GestureListener)element.GetValue(GestureListenerProperty); if (listener != null) { listener.RaiseDragCompleted(orientation); } } } const int tolerance = 96 * 2; } public static readonly DependencyProperty GestureListenerProperty; static GestureService() { GestureListenerProperty = DependencyProperty.RegisterAttached( "GestureListener", typeof(GestureListener), typeof(GestureService), new PropertyMetadata(null)); } static void EnsureTouchEvents() { if (!catchingTouchEvents) { catchingTouchEvents = true; Touch.FrameReported += OnTouchFrameReported; } } static void OnTouchFrameReported(object sender, TouchFrameEventArgs e) { TouchPoint point = e.GetPrimaryTouchPoint(Application.Current.RootVisual); TouchPointCollection points = e.GetTouchPoints(Application.Current.RootVisual); if ( (currentTouchData == null) && (point != null) && (points.Count > 0) && (points != null)) { currentTouchData = new CurrentTouchData(); currentTouchData.StartTouchOperation(point); } if ( (points != null) && (points.Count > 0) && (currentTouchData != null) && (points[0].Action == TouchAction.Up)) { currentTouchData.EndTouchOperation(points); currentTouchData = null; } else if ((currentTouchData != null) && (points != null)) { currentTouchData.ProcessNewPoints(points); } } public static void SetGestureListener(DependencyObject theObject, GestureListener listener) { EnsureTouchEvents(); theObject.SetValue(GestureListenerProperty, listener); } public static GestureListener GetGestureListener(DependencyObject theObject) { GestureListener listener = (GestureListener)theObject.GetValue(GestureListenerProperty); if (listener == null) { listener = new GestureListener(); SetGestureListener(theObject, listener); } return (listener); } static CurrentTouchData currentTouchData; static bool catchingTouchEvents; const int dragTolerance = 96 * 2; } #endif // SILVERLIGHT && !WINDOWS_PHONE }
The essence of this is;
- The GestureService defines an attached property via which instances of GestureListener can be associated with objects.
- The GestureService listens for touch events and tries to spot a “touch interaction” between the first time it sees a touch event and the time when it sees the last touch point come “Up”.
- When the interaction has finished, the service tries to determine whether the points (in terms of where they started, where they ended and how many points of touch in total were seen) represent a drag.
- If so, the service tries to call all GestureListeners that are attached to UI elements that were hit-tested by the service when the first touch point went down.
- The GestureListener is told the orientation of the drag as Horizontal/Vertical and fires a DragCompleted event back to my PanTrigger which then calls InvokeActions().
Note – it’s not meant to be complete. It’s just meant to “fake up” enough of what’s there in the Windows Phone 7 Toolkit for my particular needs and (on first pass) it seems to work reasonably although I’m sure that there are ways to fool it.
Here’s all the source code for download – note that the Expression Blend sample code is under the MS Permissive License as the source code states.