Again I am going to defer my plans for this next article, I was going to cover implementing a wizard using NavFx but I have had several people contact me asking about creating custom transition handlers so will cover that topic first.
To support this article I have created the FadeTransitor, which fades out the current page then fades in the new page. It does so by applying a DoubleAnimation to the Opacity property of the target pages.
Background
The NavFx Navigator does a lot of work for you, and makes it easy to navigate from one page to the next, loading them from external libraries if necessary, but it doesn’t actually do the work of hiding or removing the current page and displaying the next. Navigator leaves this final step to a transition handler or Transitor, that is a class that implements ITransitor.
To navigate with Navigator you call one of the many GoToPage overloads. Within these methods Navigator manages the page cache, maintains forward and backward history and validates your requests, but, when it comes to making the transition from one page to the next it delegates to the TransitionPage method of the current ITransitor interface on it’s Transitor property. This must be set either before the call to GoToPage or you must use one of the overloads that set it for you.
ITransitor
The ITransitor interface requires the implementation of two things, a Target property and a TransitionPage method that accepts a reference to the current page and the next page to be displayed. Navigator provides these references as the final step in a call to GoToPage.
Target
The Target property of a transition handler is of type object, this effectively means it can be set to any .NET type, but the actual types it can be set to are dependent on the actual transition process so your ITransitor implementation should include validation of the type assigned to this property.
The SimpleTransitor included with NavFx supports System.Windows.Application, UserControl or Panel as a Target. If Target is set to a System.Windows.Application SimpleTransitor expects RootVisual to be an implementation of IHostPage. If Target is set to a UserControl SimpleTransitor expects it to be an implementation of IHostPage. In both of these cases SimpleTransitor relies on the SetContent method of IHostPage to display the next page. If Target is set to a Panel SimpleTransitor replaces the current Children with the next page.
In my early attempts at a FadeTransitor I tried to support the same types for Target but found it difficult to support anything other than Panel because I felt it needed to directly manipulate the children and I was not able to quickly find a way to get an interface on Application.RootVisual or UserControl that allowed me to do so. So rather than impose any requirements on your pages I limited the Target property to a Panel, which includes any subclass of Panel such as Grid or StackPanel. I hope to expand on this in the future, but for now it serves the purpose of demonstrating an implementation of ITransitor. The Target property of FadeTransitor is implemented as follows:
public object Target
{
get
{
return this.target;
}
set
{
if(value is Panel)
{
this.target = value;
}
else
{
throw (new TargetNotSupportedException("…"));
}
}
}
TransitionPage
The TransitionPage method is responsible for the actual transition from the current page to the next page to be displayed. Within this method you can do pretty much anything you like with the page references provided by Navigator. Navigator takes no further interest in the pages once it has called TransitionPage other than maintaining them in the page cache and history stacks.
Although ITransitor requires the implementation of a Target property TransitionPage does not have to use it. SimpleTransitor and FadeTransitor both do but this is not enforced, your implementation of TransitionPage can be rely on knowledge of the host page or pages it will handle to carry out the transition.
If TransitionPage is reliant on the Target property then the first step should be to validate that it is set, something like this:
if(this.Target == null)
{
//No target set to contain the page control
throw (new TargetNotSetException());
}
Here you can see that a custom TargetNotSetException is thrown if the Target property is not set. Both SimpleTransitor and FadeTransitor validate the Target property in this way as the first step of their TransitionPage implementations.
I am not an expert in animation by any means, so I went through a couple of implementations before I settled on the one documented in this article. What has remained constant is the basics of the requirements.
1. The total duration of the transition needs to be configurable
2. A DoubleAnimation is used to gradually decrease the Opacity of the current page to zero over half of the configured duration, the fade out step.
3. A DoubleAnimation is used to initially set the Opacity of the next page to zero then gradually increase it to 100 over the remaining half of the configured duration, the fade in step.
4. A StoryBoard is used to run the animations.
Initially I tried to do this with a single StoryBoard, but the effect I got was more of a morphing or blending effect, not distinct fade out and fade in steps. I tried offsetting the start of the fade in animation to get the effect I wanted but still couldn’t get the effect I wanted (I said I am no expert, I am sure this can be achieved but I wanted to get something to support this article so didn’t spend a lot of time on it).
So in the end I settled on an implemenation that does the first animation in TransitionPage then the second is handled in an event handler for the Completed event of the StoryBoard.
FadeTransitor implements a Duration property, which can be set directly or through the following constructor, which is the recommended approach.
public FadeTransitor(Panel target, TimeSpan duration)
{
this.Target = target;
this.Duration = duration;
}
Notice you can also set the target with this constructor.
The following is the full implemenation of the TransitionPage method for FadeTransitor:
public void TransitionPage(IPage previousPage, IPage nextPage)
{
if(this.Target == null)
{
//No target set to contain the page control
throw (new TargetNotSetException());
}
if(nextPage is UserControl)
{
//get the usercontrol interface of the previous page
this.nextPage = nextPage as UserControl;
this.currentPage = previousPage as UserControl;
//set up the fade out animation
TimeSpan fadeOutDuration = TimeSpan.FromSeconds(this.Duration.TotalSeconds / 2);
Duration duration = new Duration(fadeOutDuration);
DoubleAnimation fadeOutAnimation = new DoubleAnimation();
fadeOutAnimation.Duration = duration;
fadeOutAnimation.To = 0;
//prepare a story board to do the fade out
Storyboard fadeOutStoryBoard = new Storyboard();
fadeOutStoryBoard.Duration = duration;
fadeOutStoryBoard.Children.Add(fadeOutAnimation);
fadeOutStoryBoard.Completed += new EventHandler(fadeOut_Completed);
Storyboard.SetTarget(fadeOutAnimation, this.currentPage);
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath("Opacity"));
//start the fade out
fadeOutStoryBoard.Begin();
}
else
{
throw (new PageTypeNotSupportedException("…"));
}
}
First validation that Target is set takes place
Next validation that the next page is a UserControl, if not an exception is thrown.
If the next page is valid then references to both next and previous pages are captured in instance variable to make them available to events handlers later.
Next half the total configured duration is calculated and a Duration is prepared for use with the animation and story board.
Next a DoubleAnimation is prepared to fade the current page out by reducing the target property value to zero of the calculated duration.
Next a StoryBoard is prepared to run the animation and call an event handler on completion
The static SetTarget and SetTargetProperty methods of StoryBoard are used to finish preparing the animation setting the element and property it should animate.
Finally the animation is started with a call to the Begin method of the story board.
When the story board finishes running the animation its Completed event is fired and the following handler is executed:
private void fadeOut_Completed(object sender, EventArgs e)
{
Storyboard storyBoard = sender as Storyboard;
storyBoard.Stop();
((Panel)this.Target).Children.Remove(this.currentPage);
this.currentPage = null;
//set up the fade in animation
TimeSpan fadeInDuration = TimeSpan.FromSeconds(this.Duration.TotalSeconds / 2);
Duration duration = new Duration(fadeInDuration);
DoubleAnimation fadeInAnimation = new DoubleAnimation();
fadeInAnimation.Duration = duration;
fadeInAnimation.From = 0;
fadeInAnimation.To = 100;
//prepare a story board to do the animation
Storyboard fadeInStoryBoard = new Storyboard();
fadeInStoryBoard.Duration = duration;
fadeInStoryBoard.Children.Add(fadeInAnimation);
fadeInStoryBoard.Completed += fadeIn_Completed;
Storyboard.SetTarget(fadeInAnimation, this.nextPage);
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath("Opacity"));
((Panel)this.Target).Children.Add(this.nextPage);
fadeInStoryBoard.Begin();
}
First the story board that triggered the event is explicitly stopped (this isn’t strictly necessary, but I did it anyway)
Next the current page is removed from the Children collection of the target Panel
The next sections of code are almost identical to the main code in TransitionPage with the exception that it sets the From property of the animation to 0, this causes the Opacity property of the target to be set to zero when the animation starts before gradually being increased to 100.
The other difference is that the next page is added as a child of the target Panel before the story board is started.
And that is it, the implemenation of the fadeIn_Completed handler simply stops the story board using the same code as shown above, so I have not included it in this article.
Using the FadeTransitor is pretty simple, just set the Transitor property of Navigator to an instance of FadeTransitor and call one of the GoToPage methods. The following is a snippet from the NavFx C# demo that demonstrates the use of both ITransitor implementations provided by NavFx.
if(((bool)this.simpleTransitor.IsChecked))
{
//Make sure the Transitor target is set to the application
//and display the home page
this.Navigator.Transitor = new SimpleTransitor(Application.Current);
}
else
{
this.Navigator.Transitor = new FadeTransitor(this.contentPanel, TimeSpan.FromSeconds(2));
}
this.Navigator.GoToPage("Home");
There you have it, the simplest of transition handlers that fades out the current page then fades in the next page. The fade in is not as good as the fade out, but again the main purpose at this stage was to support this article and show how an implementation of ITransitor might animate the transition between pages. If you are more experienced with animation than I and can suggest ways to improve this simple implementation please feel free to comment. Likewise if you feel inclined to create one or more ITransitor implementations and want to share them, let me know and I can either include them in NavFx or make them available for download as a Release on the CodePlex project site.