A Framework to Animate WPF and Silverlight Pages Similar to the PowerPoint Slides

WPF and Silverlight have many things to say in terms of features and capabilities, but unlike many Microsoft products, they are not some kind of Rapid Application Development (RAD) framework. In this paper, I developed a plug & play framework to animate page entrance and page exit in the WPF and Silverlight applications similar to the Powerpoint slides. The advantages of the framework are plug & play deployment and its capability of extensions. In fact, the framework can be used in a RAD manner.

Introduction

Windows Presentation Foundation (WPF) supports browser-style navigation that can be used in two types of applications: standalone applications and XAML browser applications (XBAPs). To package content for navigation, WPF provides the Page class [MSDN]. The WPF support of page navigation is very similar to the page navigation in the web. Each page has its own content and each page can contain hyperlinks to point to other pages similar to the web. But unlike the web, WPF has great build in support for animations. It would be a good idea if the WPF pages could be animated; similar to the slides of the MS PowerPoint. In the MS PowerPoint, user just selects an animation for the slide entrance and an animation for the slide movement. Although, one can easily define Storyboards and Transforms and then assign them to the pages, but it makes the XAML code too crowded. The problem is the fact that the stupid Storyboard and its target have to be in the same NameScope. In practical, many developers prefer to don’t have such fancy graphic feature in case it makes their codes complicated. The ideal is a framework that can be easily deployed without any risk and whenever the developer doesn’t like it, he/she could remove it without any cost; here is the main requirement we wanted from the framework.

1- A plug and play deployment that can be used in both WPF and Silverlight.

2- The ability to animate page entering and page moving.

3- Containing many animations.

4- The ability of extension. It should not be difficult for graphic designers to extend the codes.

In this paper, I develop such framework. Using the framework is simple and needs adding one attached property to the navigation frame.

<Frame local:VisualNavigation.HavingAnimatedNavigation="True" ></Frame>

And in the pages, developer just needs to add two attached attributes about the type of animation.

<Page x:Class="ICP.NavigationFramework.Demo.Page2"

local:VisualNavigation.PageEntrance="FlyingEnter"

local:VisualNavigation.PageExit="FlyingExit"

>

In the above code, the PageEntrance animation has been set to FlyingEnter. It means that when user wants to see the page, it flies to the screen. The framework contains dozen of build in animations and the developer can easily assign them to the pages, or add his own page animations.

WPF Version

The following class diagram represents the main classes of the framework in the WPF.

PageAnimationFramework

PageAnimationFramework is the main class of the framework. It has attached properties that can be used to enable/disable the framework, and also specifying the type of animations per page. Here is the list of its attached properties.

HavingAnimatedNavigation: This attached property used by the Frame and NavigationWindows to enable/ disable the framework. The framework can gain access to the NavigationService using this attached property. NavigationService encapsulates the ability to download content within the context of a browser-style navigation [ MSDN]. Both the Frame and NavigationWindow have instances of it.

It has methods like Navigate, GoForward and GoBack that can be used to do navigation. Its Source property points to the current loaded page and it also has the Navigating and Navigated events. The Navigating even occurs when a new navigation is requested. Its handler is the place that the “Page Exit” animation code should be put. The Navigated event occurs when the content that is being navigated to has been found, and is available from the Content property, although it may not have completed loading [MSDN]. Its handler is the place that the “Page Enter” animation code should be put.

The framework hooks its event handlers of the navigating and navigated events in the onHavingAnimatedNavigationChanged method.

PageEntrance attached property: This attached property specifies the page entering animation.

PageExit attached property: This attached property specifies the page exit animation.

Here is the code of the class:

public class PageAnimationFramework : DependencyObject

{

#region Private members

private static Uri uri;

private static bool animated = false;

private static NavigationService navigation;

private static ResourceDictionary pageNavigationDictionary;

#endregion

#region Frame & Navigation Events

private static void onHavingAnimatedNavigationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)

{

bool newValue = (bool)e.NewValue;

if (pageNavigationDictionary == null)

{

Assembly assembly = Assembly.GetExecutingAssembly();

Stream resourceStream = assembly.GetManifestResourceStream("ICP.NavigationFramework.PageNavigationDictionary.xaml");

pageNavigationDictionary = (ResourceDictionary)XamlReader.Load(resourceStream);

Application.Current.Resources.MergedDictionaries.Add(pageNavigationDictionary);

}

if (newValue)

{

if (obj is Frame)

{

Frame frame = obj as Frame;

frame.Navigating += new NavigatingCancelEventHandler(frame_Navigating);

frame.Navigated += new NavigatedEventHandler(frame_Navigated);

navigation = frame.NavigationService;

}

else if (obj is NavigationWindow)

{

NavigationWindow navigationWindow = obj as NavigationWindow;

navigationWindow.Navigated += new NavigatedEventHandler(navigationWindow_Navigated);

navigationWindow.Navigating += new NavigatingCancelEventHandler(navigationWindow_Navigating);

navigation = navigationWindow.NavigationService;

}

}

else

{

if (obj is Frame)

{

Frame frame = obj as Frame;

frame.Navigating -= new NavigatingCancelEventHandler(frame_Navigating);

frame.Navigated -= new NavigatedEventHandler(frame_Navigated);

navigation = frame.NavigationService;

}

else if (obj is NavigationWindow)

{

NavigationWindow navigationWindow = obj as NavigationWindow;

navigationWindow.Navigated -= new NavigatedEventHandler(navigationWindow_Navigated);

navigationWindow.Navigating -= new NavigatingCancelEventHandler(navigationWindow_Navigating);

navigation = navigationWindow.NavigationService;

}

}

}

private static void navigationWindow_Navigating(object sender, NavigatingCancelEventArgs e)

{

NavigationWindow navigationWindow = sender as NavigationWindow;

Page page = navigationWindow.Content as Page;

Context.CurrentContext.FrameSize = new Rect(0, 0, navigationWindow.ActualWidth, navigationWindow.ActualHeight);

Context.CurrentContext.CurrentPage = page;

showExitAnimation(page, e);

}

private static void showExitAnimation(Page page, NavigatingCancelEventArgs e)

{

if (page != null && !animated)

{

string PageExit = PageAnimationFramework.GetPageExit(page);

if (PageExit.Length > 0)

{

animated = true;

PageAnimation p = ((PageAnimation)Application.Current.FindResource(PageExit)).Copy();

page.RenderTransform = p.MainTransform as Transform;

p.MainStoryboard.Completed += new EventHandler(animation_Completed);

p.MainStoryboard.RepeatBehavior = new RepeatBehavior(1);

p.MainStoryboard.Begin(p);

uri = e.Uri;

e.Cancel = true;

}

}

else

{

animated = false;

}

}

private static void navigationWindow_Navigated(object sender, NavigationEventArgs e)

{

NavigationWindow navigationWindow = sender as NavigationWindow;

Page page = navigationWindow.Content as Page;

Context.CurrentContext.FrameSize = new Rect(0, 0, navigationWindow.ActualWidth, navigationWindow.ActualHeight);

Context.CurrentContext.CurrentPage = page;

showEnterAnimation(page, e);

}

private static void showEnterAnimation(Page page, NavigationEventArgs e)

{

if (page != null)

{

// means the main window does not laoded

if (Application.Current.MainWindow.ActualWidth == 0)

{

return;

}

string pageEntranceAnimation = PageAnimationFramework.GetPageEntrance(page);

if (pageEntranceAnimation.Length > 0)

{

PageAnimation p = ((PageAnimation)Application.Current.FindResource(pageEntranceAnimation)).Copy();

page.RenderTransform = p.MainTransform as Transform;

p.MainStoryboard.RepeatBehavior = new RepeatBehavior(1);

p.MainStoryboard.Begin(p);

}

}

}

private static void frame_Navigated(object sender, NavigationEventArgs e)

{

Frame frame = sender as Frame;

Page page = frame.Content as Page;

Context.CurrentContext.FrameSize = new Rect(0, 0, frame.ActualWidth, frame.ActualHeight);

Context.CurrentContext.CurrentPage = page;

showEnterAnimation(page, e);

}

private static void animation_Completed(object sender, EventArgs e)

{

navigation.Navigate(uri);

}

private static void frame_Navigating(object sender, NavigatingCancelEventArgs e)

{

Frame frame = sender as Frame;

Page page = frame.Content as Page;

Context.CurrentContext.FrameSize = new Rect(0, 0, frame.ActualWidth, frame.ActualHeight);

Context.CurrentContext.CurrentPage = page;

showExitAnimation(page, e);

}

private static void onPageExit(DependencyObject obj, DependencyPropertyChangedEventArgs e)

{

}

private static void onPageEntranceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)

{

}

#endregion

#region Attached Properties

public static string GetPageEntrance(DependencyObject obj)

{

return (string)obj.GetValue(PageEntranceProperty);

}

public static void SetPageEntrance(DependencyObject obj, string value)

{

obj.SetValue(PageEntranceProperty, value);

}

// Using a DependencyProperty as the backing store for PageEntrance. This enables animation, styling, binding, etc...

public static readonly DependencyProperty PageEntranceProperty =

DependencyProperty.RegisterAttached("PageEntrance", typeof(string), typeof(PageAnimationFramework), new UIPropertyMetadata("", onPageEntranceChanged));

public static string GetPageExit(DependencyObject obj)

{

return (string)obj.GetValue(PageExitProperty);

}

public static void SetPageExit(DependencyObject obj, string value)

{

obj.SetValue(PageExitProperty, value);

}

// Using a DependencyProperty as the backing store for PageExit. This enables animation, styling, binding, etc...

public static readonly DependencyProperty PageExitProperty =

DependencyProperty.RegisterAttached("PageExit", typeof(string), typeof(PageAnimationFramework), new UIPropertyMetadata("", onPageExit));

public static string GetName(DependencyObject obj)

{

return (string)obj.GetValue(NameProperty);

}

public static void SetName(DependencyObject obj, string value)

{

obj.SetValue(NameProperty, value);

}

// Using a DependencyProperty as the backing store for Name. This enables animation, styling, binding, etc...

public static readonly DependencyProperty NameProperty =

DependencyProperty.RegisterAttached("Name", typeof(string), typeof(PageAnimationFramework), new UIPropertyMetadata(""));

public static bool GetHavingAnimatedNavigation(DependencyObject obj)

{

return (bool)obj.GetValue(HavingAnimatedNavigationProperty);

}

public static void SetHavingAnimatedNavigation(DependencyObject obj, bool value)

{

obj.SetValue(HavingAnimatedNavigationProperty, value);

}

public static readonly DependencyProperty HavingAnimatedNavigationProperty =

DependencyProperty.RegisterAttached("HavingAnimatedNavigation", typeof(bool), typeof(PageAnimationFramework), new UIPropertyMetadata(false, onHavingAnimatedNavigationChanged));

#endregion

}

The following flowchart illustrates the main process when a page has been loaded.

The following flowchart illustrates the main process when a page has been unloaded

PageAnimation

This class is used to define the custom animations. It has two main properties: MainStoryboard and MainTransform. The MainStoryBoard contains custom animations that target the transformations of the MainTransform. The MainTransform is a transformation that is used to change the appearance of the Page. In the Navigating and Navigated events, the MainTransform of the loaded PageAnimation has been assigned to the Page and then the Storyborad of the PageAnimation has been started. The main question here is: “Why we need such class? We can easily define Storyboards and transformations in the ResourceDictionaries.” The philosophy of existence the class relies on the accessing of the animation targets. Animations inside a Stoaryboard have to specify the target of animation using an attached property of the Storyboard called TargetName. Unfortunately, one cannot set the target of animation directly using the Target property. A Storyboard can be used to animate dependency properties of animatable classes (for more information about what makes a class animatable, see the Animation Overview). However, because storyboarding is a framework-level feature, the object must belong to the NameScope of a FrameworkElement or a FrameworkContentElement.

Like it or not, a Storyboard should be inside the NameScope of its target. A NameScope is a class that implements the INameScope interface. WPF uses the NameScopes to enforce the uniqueness of names. All of the controls inside a NameScope must have unique names. The following classes in the WPF have implemented the INameScope: Window, Page, UserControl, Style, Template, NameScope and ResourceDictionary. Although ResourceDictionary implements the INameScope, but it does not allow controls to have x:Name attribute. NameScope is a non UI implementation of the INameScope.

According to the above notations, the Storyboards cannot be located in the ResourceDictionaries due to the problem of NameScopes. I struggled with the problem a couple of hours and finally find two solutions.

1- Do not define the Transformation and Storyboards in the XAML files and instead create them from the scratch in the code of navigated and navigating events.

2- Define a class that implements the INameScope that has two main properties: A StoryBoard and a Transformation. Each instance of the class represents a custom page navigation animation. Since the class implements the INameScope, so there is no problem in assigning the TargetName properties. (This class is PageAnimation).

Among the above solution, number 2 is clearly superior. Its main advantage is the fact that it doesn’t hardcode the animations which is very useful for the ones that want to add their own custom animations. Here is the code of PageAnimation.

public class PageAnimation : FrameworkElement, INameScope

{

#region NameScope

private NameScope nameScope = new NameScope();

public object FindName(string name)

{

return nameScope.FindName(name);

}

public void RegisterName(string name, object scopedElement)

{

nameScope.RegisterName(name, scopedElement);

}

public void UnregisterName(string name)

{

nameScope.UnregisterName(name);

}

#endregion

#region Properties

public Storyboard MainStoryboard

{

get

{

return mainStrotyborard;

}

set

{

mainStrotyborard = value;

}

}

private Storyboard mainStrotyborard;

public Transform MainTransform

{

get

{

return mainTransform;

}

set

{

mainTransform = value;

string name = PageAnimationFramework.GetName(mainTransform);

if (this.FindName(name) == null && name.Length > 0)

{

this.RegisterName(name, mainTransform);

}

if (mainTransform is TransformGroup)

{

TransformGroup transformGroup = mainTransform as TransformGroup;

foreach (Transform transform in transformGroup.Children)

{

name = PageAnimationFramework.GetName(transform);

if (this.FindName(name) == null && name.Length > 0)

{

this.RegisterName(name, transform);

}

}

}

}

}

private Transform mainTransform;

#endregion

#region Methods

public PageAnimation Copy()

{

PageAnimation value = (PageAnimation)this.MemberwiseClone();

value.MainStoryboard = this.MainStoryboard.Clone();

return value;

}

#endregion

}

Here is a custom implementation of the PageAnimation in a ResourceDictionary.

<local:PageAnimation x:Key="SkewEnter">

<local:PageAnimation.MainStoryboard>

<Storyboard x:Name="storyBoard">

<DoubleAnimation Duration="0:0:1" From="90" To="0" Storyboard.TargetName="skew" Storyboard.TargetProperty="AngleX"></DoubleAnimation>

</Storyboard>

</local:PageAnimation.MainStoryboard>

<local:PageAnimation.MainTransform>

<TransformGroup>

<TransformGroup.Children>

<SkewTransform local:PageAnimationFramework.Name="skew"

CenterX="{Binding Source={x:Static local:Context.CurrentContext}, Path=FrameSize.Width, Mode=TwoWay}"

CenterY="{Binding Source={x:Static local:Context.CurrentContext}, Converter={StaticResource divideConverter}, ConverterParameter=2, Path=FrameSize.Height, Mode=TwoWay}"

></SkewTransform>

</TransformGroup.Children>

</TransformGroup>

</local:PageAnimation.MainTransform>

</local:PageAnimation>

Context Class

Using the PageAnimation class, one can define the Storyboards and Transformations in a systematic way. But the story is not over. An animation inside a Stoaryboard or a Transformation definitely needs to have access the Page and binds its values to the Page value. Basically they need the boundaries of the page. The Context class is used to hold the current page temporary. Here is its code.

public class Context : INotifyPropertyChanged

{

#region INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

public void notifyPropertyChanged(string name)

{

if (PropertyChanged != null)

PropertyChanged(this, new PropertyChangedEventArgs(name));

}

#endregion

public Rect FrameSize

{

get

{

return frameSize;

}

set

{

frameSize = value;

notifyPropertyChanged("FrameSize");

}

}

private static Rect frameSize;

public Page CurrentPage

{

get

{

return currentPage;

}

set

{

currentPage = value;

notifyPropertyChanged("CurrentPage");

}

}

private static Page currentPage;

public static Context CurrentContext = new Context();

}

The static instance of the class insures that the PageAnimation can access it. Here is an example of how the PageAnimations bind their values to the Context class.

<SkewTransform local:PageAnimationFramework.Name="skew"

CenterX="{Binding Source={x:Static local:Context.CurrentContext}, Path=FrameSize.Width, Mode=TwoWay}"

CenterY="{Binding Source={x:Static local:Context.CurrentContext}, Converter={StaticResource divideConverter}, ConverterParameter=2, Path=FrameSize.Height, Mode=TwoWay}"

></SkewTransform>

Using the Code

As I mentioned in the Introduction section, using the framework is simple and just needs two lines of code. In the Navigation Frame, the Framework should be enabled using one line of code.

<Frame local:VisualNavigation.HavingAnimatedNavigation="True" ></Frame>

And in the Pages, the PageEntance and PageExit animation should be specified.

<Page x:Class="ICP.NavigationFramework.Demo.Page2"

local:VisualNavigation.PageEntrance="FlyingEnter"

local:VisualNavigation.PageExit="FlyingExit"

>

The attached code contains two demo projects: One is a standalone navigation application and one is a Navigation project hosted in the IE. The code of WPF version can be downloaded here.

Silverlight Version

Unfortunately, Silverlight applications cannot access NameScopes and on the other hand, Storyboards and Transformations on the silverlight do not support binding. It means that Storyboards and Transformation should be static and being on the same NameScope of their targets. But fortunately, Storyboards in the Silverlight can use the SetTarget property which is a great achievement towards WPF. Using the SetTarget property, we could easily create Storyboards in the code behind. The Silverlight version of the framework is simpler than its WPF version. There are no PageAnimation classes and the Storyboards & Transformation have been created in the code behind. Here are the Navigating and Navigated event handlers of the PageAnimationFramework class.


private
static void showExitAnimation(Page page, NavigatingCancelEventArgs e)

{

if (page != null && !animated)

{

string PageExit = PageAnimationFramework.GetPageExit(page);

if (PageExit.Length > 0)

{

animated = true;

Storyboard storyBoard = null;

Transform transform = null;

if (PageExit == "FlyingExit")

{

storyBoard = PageAnimations.FlyingExit(currentFrame, out transform);

}

else if (PageExit == "SkewExit")

{

storyBoard = PageAnimations.SkewExit(currentFrame, out transform);

}

else

{

storyBoard = PageAnimations.FlyingExit(currentFrame, out transform);

}

page.RenderTransform = transform;

storyBoard.Completed += new EventHandler(animation_Completed);

storyBoard.Begin();

uri = e.Uri;

e.Cancel = true;

}

}

else

{

animated = false;

}

}

private static void showEnterAnimation(Page page, NavigationEventArgs e)

{

if (page != null)

{

// means the main window does not laoded

if (firstTime)

{

firstTime = false;

return;

}

string pageEntranceAnimation = PageAnimationFramework.GetPageEntrance(page);

if (pageEntranceAnimation.Length > 0)

{

Storyboard storyBoard = null;

Transform transform = null;

if (pageEntranceAnimation == "FlyingEnter")

{

storyBoard = PageAnimations.FlyEnter(currentFrame, out transform);

}

else if (pageEntranceAnimation == "SkewEnter")

{

storyBoard = PageAnimations.SkewEnter(currentFrame, out transform);

}

else

{

storyBoard = PageAnimations.FlyEnter(currentFrame, out transform);

}

page.RenderTransform = transform;

storyBoard.Begin();

}

}

}


And here is an instance of how creating Storyboards in code behind.


public
static Storyboard SkewExit(Frame currentFrame, out Transform transform)

{

Storyboard storyBoard = new Storyboard();

TransformGroup mainTransform = new TransformGroup();

SkewTransform skewTransform = new SkewTransform();

skewTransform.CenterX = currentFrame.ActualWidth / 2;

skewTransform.CenterY = currentFrame.ActualHeight / 2;

mainTransform.Children.Add(skewTransform);

DoubleAnimation skewTransformAnimation = new DoubleAnimation();

skewTransformAnimation.Duration = new Duration(new TimeSpan(0, 0, 1));

skewTransformAnimation.From = 0;

skewTransformAnimation.To = 90;

Storyboard.SetTarget(skewTransformAnimation, skewTransform);

Storyboard.SetTargetProperty(skewTransformAnimation, new PropertyPath("SkewTransform.AngleX"));

storyBoard.Children.Add(skewTransformAnimation);

transform = mainTransform;

return storyBoard;

}

The code of Silverlight version can be downloaded from here.
By Siyamand Ayubi   Popularity  (5157 Views)