Preface
A little over a year ago I made DragFlickBehavior, a behavior for Windows Phone that makes essentially anything draggable and ‘flickable’, that is, you can drag a GUI element along with your finger and it seems to have a little inertia when you let it go. In my previous post, I described the basics of how to make a behavior at all for Windows 8 Metro style. The testing of this was done using a ported version of the DragFlickBehavior. I’ve retraced my steps to how I got it to work, and will describe the process of porting an existing behavior here.
For the DragFlickBehavior to work, some groundwork needed to be layed first. For Windows Phone, I made a couple of extension methods for both FrameworkElement and StoryBoard first. To make matters worse, one of those extension methods in FrameworkElementExtensions used yet another extension method – GetVisualParent in VisualTreeHelperExtensions from Phone7.Fx… Nil desperandum… I’ll start at the beginning
Porting VisualTreeHelperExtensions
I created a class library Win8nl.External, copied VisualTreeHelperExtensions.cs from it’s codeplex location, and opened the it the editor. And then the process was pretty simple:
- The namespace System.Windows.Media is gone. So I deleted it’s using.
- I basically clicked every red line, hit Control-. (that’s Control-dot) and in most cases the editor would suggest a name space to add
In the end I seemed to have added
using Windows.UI.Xaml; using Windows.UI.Xaml.Media;
And then there was this slight matter of 2 places where the author calls VisualStateManager.GetVisualStateGroups and expects the result to be an IList. Now it’s an IEnumerable. Anyway, I solved this by changing the
IList groups = VisualStateManager.GetVisualStateGroups(root);
into
var groups = VisualStateManager.GetVisualStateGroups(root);
on both occasions. One file done. I won’t even pretend I understand what all those methods in this file are actually doing. I just ported them.
Porting FrameworkElementExtensions
I then created a library Win8nl, added references to Win8nl.External and WinRtBehaviors, and started on the FrameworkElementExtensions . This proved to be a pretty trivial matter. I needed to remove
using System.Windows.Controls; using System.Windows.Media; using System.Linq;
using Phone7.Fx;
And add after Control-dotting trough the errors I found I had added
using Windows.UI.Xaml.Media; using Windows.UI.Xaml; using Windows.Foundation using Windows.UI.Xaml.Controls
Two files done!
Porting StoryboardExtensions
Routine starts to settle in. Remove
using System.Windows.Media; using System.Windows.Media.Animation;Control-dot around, and you will see you've added
using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation;
But then we hit our first snag. Two methods use a parameter of type IEasingFunction, that does no longer exist. But that can be fixed, by changing it into EasingFunctionBase.
Then I found out that Storyboard.SetTargetProperty apparently no longer wants to have a PropertyPath – which can be made from a DependencyProperty object – but a string. So method
public static void AddAnimation(this Storyboard storyboard, DependencyObject item, Timeline t, DependencyProperty p) { if (p == null) throw new ArgumentNullException("p"); Storyboard.SetTarget(t, item); Storyboard.SetTargetProperty(t, new PropertyPath(p)); storyboard.Children.Add(t); }Need to be changed to
public static void AddAnimation(this Storyboard storyboard, DependencyObject item, Timeline t, string property) { if (string.IsNullOrWhiteSpace(property)) throw new ArgumentNullException("property"); Storyboard.SetTarget(t, item); Storyboard.SetTargetProperty(t, property); storyboard.Children.Add(t); }This is bad news, since it breaks the public interface. And it breaks even more, namely another public extension method
public static void AddTranslationAnimation(this Storyboard storyboard, FrameworkElement fe, Point from, Point to, Duration duration, EasingFunctionBase easingFunction) { storyboard.AddAnimation( fe.RenderTransform, storyboard.CreateDoubleAnimation(duration, from.X, to.X, easingFunction), CompositeTransform.TranslateXProperty); storyboard.AddAnimation(fe.RenderTransform, storyboard.CreateDoubleAnimation(duration, from.Y, to.Y, easingFunction), CompositeTransform.TranslateYProperty); }Needs to be changed to
public static void AddTranslationAnimation(this Storyboard storyboard, FrameworkElement fe, Point from, Point to, Duration duration, EasingFunctionBase easingFunction) { storyboard.AddAnimation(fe.RenderTransform, storyboard.CreateDoubleAnimation(duration, from.X, to.X, easingFunction), "TranslateX"); storyboard.AddAnimation(fe.RenderTransform, storyboard.CreateDoubleAnimation(duration, from.Y, to.Y, easingFunction), "TranslateY"); }
I must honestly say I find the apparent need to specify storyboard target properties verbatim, as in strings, quite peculiar, but apparently this is the way it needs to be done. I am only the messenger here.
Porting DragFlickBehavior
Here we go again. Deleteusing System.Windows.Interactivity; using System.Windows.Media; using System.Windows.Media.Animation; using Wp7nl.Utilities;And Control-dotting learns you the following needs to be added:
using Win8nl.Utilities; using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation; using WinRtBehaviors;Soon after that, you'll learn that the second parameter of a ManipulationDelta event is no longer of type ManipulationDeltaEventArgs but of ManipulationDeltaRoutedEventArgs, and that it does no longer have a “DeltaManipulation” property but a plain “Delta” property. So the AssociatedObjectManipulationDelta method capturing the event was this:
void AssociatedObjectManipulationDelta(object sender, ManipulationDeltaEventArgs e) { var dx = e.DeltaManipulation.Translation.X; var dy = e.DeltaManipulation.Translation.Y; var currentPosition = elementToAnimate.GetTranslatePoint(); elementToAnimate.SetTranslatePoint(currentPosition.X + dx, currentPosition.Y + dy); }and now needs to be this
void AssociatedObjectManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) { var dx = e.Delta.Translation.X; var dy = e.Delta.Translation.Y; var currentPosition = elementToAnimate.GetTranslatePoint(); elementToAnimate.SetTranslatePoint(currentPosition.X + dx, currentPosition.Y + dy); }
No rocket science in there, right? And almost identical set of rework needs to be done to the method capturing ManipulationCompleted. Its second parameter was of type ManipulationCompletedEventArgs and is now – you’ve probably guessed it – ManipulationCompletedRoutedEventArgs. And that does no longer have a property e.FinalVelocities.LinearVelocity.X and Y but is does have a Velocities.Linear.X and Y.
For some reason though, those properties return values that are somewhere between 0 and 1, or at least it seems so. So I made a rule-of-thumb conversion multiplying them by 1000. Wrapping that up: AssociatedObjectManipulationCompleted used to be
private void AssociatedObjectManipulationCompleted(object sender, ManipulationCompletedEventArgs e) { // Create a storyboard that will emulate a 'flick' var currentPosition = elementToAnimate.GetTranslatePoint(); var velocity = e.FinalVelocities.LinearVelocity; var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd }; var to = new Point(currentPosition.X + (velocity.X / BrakeSpeed), currentPosition.Y + (velocity.Y / BrakeSpeed)); storyboard.AddTranslationAnimation(elementToAnimate, currentPosition, to, new Duration(TimeSpan.FromMilliseconds(500)), new CubicEase {EasingMode = EasingMode.EaseOut}); storyboard.Begin(); }and it now is
private void AssociatedObjectManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) { // Create a storyboard that will emulate a 'flick' var currentPosition = elementToAnimate.GetTranslatePoint(); var xVelocity = e.Velocities.Linear.X * 1000; var yVelocity = e.Velocities.Linear.Y * 1000; var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd }; var to = new Point(currentPosition.X + (xVelocity / BrakeSpeed), currentPosition.Y + (yVelocity / BrakeSpeed)); storyboard.AddTranslationAnimation(elementToAnimate, currentPosition, to, new Duration(TimeSpan.FromMilliseconds(500)), new CubicEase { EasingMode = EasingMode.EaseOut }); storyboard.Begin(); }
The final thing you will need to take into consideration when you port behaviors is the fact that you used to have an OnAttached and Loaded event. You still have those, but by the very nature I implemented behaviors everything that happened in OnAttached and OnLoaded needs to be in OnAttached. Same goes for Unloaded and OnDetaching – the last one is fired by the first one. So follow the pattern I set out: initialize in OnAttached only, and only clean up in OnDetached.
So, the behavior used to have a setup like this:
protected override void OnAttached() { base.OnAttached(); AssociatedObject.Loaded += AssociatedObjectLoaded; AssociatedObject.ManipulationDelta += AssociatedObjectManipulationDelta; AssociatedObject.ManipulationCompleted += AssociatedObjectManipulationCompleted; } void AssociatedObjectLoaded(object sender, RoutedEventArgs e) { elementToAnimate = AssociatedObject.GetElementToAnimate(); if (!(elementToAnimate.RenderTransform is CompositeTransform)) { elementToAnimate.RenderTransform = new CompositeTransform(); elementToAnimate.RenderTransformOrigin = new Point(0.5, 0.5); } }And that should now be
protected override void OnAttached() { elementToAnimate = AssociatedObject.GetElementToAnimate(); if (!(elementToAnimate.RenderTransform is CompositeTransform)) { elementToAnimate.RenderTransform = new CompositeTransform(); elementToAnimate.RenderTransformOrigin = new Point(0.5, 0.5); } AssociatedObject.ManipulationDelta += AssociatedObjectManipulationDelta; AssociatedObject.ManipulationCompleted += AssociatedObjectManipulationCompleted; AssociatedObject.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY; base.OnAttached(); }Notice a couple of interesting things:
- The capture of “Loaded” is gone. We don’t need that any longer
- There is an extra last line, setting the “ManipulationMode”. Apparently you need to set that up to make ManipulationDelta and ManipulationCompleted happen at all. It accidently stumbled upon that
Finally, the last part: OnDetaching. It used to be
protected override void OnDetaching() { AssociatedObject.Loaded -= AssociatedObjectLoaded; AssociatedObject.ManipulationCompleted -= AssociatedObjectManipulationCompleted; AssociatedObject.ManipulationDelta -= AssociatedObjectManipulationDelta; base.OnDetaching(); }And the only thing that needs to be changed to use that is the removal of the first line: AssociatedObject.Loaded -= AssociatedObjectLoaded;;
And then we’re done. If you add this behavior to any object on the screen, like I showed in the previous post:
<Page x:Class="Catchit8.BlankPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Catchit8" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Win8nl_Behaviors="using:Win8nl.Behaviors" xmlns:WinRtBehaviors="using:WinRtBehaviors" mc:Ignorable="d"> <Grid Background="{StaticResource ApplicationPageBackgroundBrush}"> <TextBlock HorizontalAlignment="Left" Margin="503,213,0,0" TextWrapping="Wrap" VerticalAlignment="Top" FontSize="18" Text="Drag me"> <WinRtBehaviors:Interaction.Behaviors> <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/> </WinRtBehaviors:Interaction.Behaviors> </TextBlock> <Button Content="Drag me too!" HorizontalAlignment="Left" Margin="315,269,0,0" VerticalAlignment="Top" > <WinRtBehaviors:Interaction.Behaviors> <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/> </WinRtBehaviors:Interaction.Behaviors> </Button> </Grid> </Page>
You will get an effect like this (I added a slider just for kicks)
Conclusion
At first glance, Windows 8 development does not seem to differ that much from Windows Phone development. After I made my behavior framework, porting a fairly complex behavior like this was pretty easy, so I’d say that holds true at second glance as well. Sure, some things are different – mostly namespaces and some property names. The XAML is a wee bit different as well. As to why Microsoft have decided to change namespaces, rename properties or methods or even let return values be a bit different – I don’t know. What I do know is that bitching about it will probably raise your blood pressure but it won’t help you very much as a developer. Just think of this: Microsoft sold 450 million copies of Windows 7. I don’t think those will all be Windows 8 next year, but I think the 100 million mark will be hit pretty soon. The choice is yours – either you are spending time and energy on getting angry that Microsoft moved your cheese (or actually, only some of it) or you can go out and find new and probably a bloody lot of cheese.
Well, I’ve made my choice
As usual, a complete demo solution for those who, like me, are too lazy to do all the typing themselves, can be found here. So you can get started even faster. ¡Arriba! ¡Andale! ;-)