Update - 9/6/2008 : Updated for Silverlight 2 Beta 2!
One of the problems I've run across while playing around with the Beta is the lack of support for any easy way to navigate around you application by moving between Xaml pages. The problem really stems from this little piece of generated code in your App.xaml.cs file.
private void Application_Startup(
object sender,
StartupEventArgs e)
{
// Load the main control
this.RootVisual = new Page();
}
Looking at the docs for the RootVisual property of Application class, we're told that RootVisual can only be set once. My limited testing shows that this isn't actually true, and that it's possible to set LayoutRoot as many times as you'd like but only in the Application_Startup handler, which is obviously of very little use!
My solution is actually really very simple and can be summed up with one short snippet of code...
private void Application_Startup(object sender, StartupEventArgs e)
{
// Load the main control
Grid root = new Grid();
root.Children.Add(new Page());
this.RootVisual = root;
}
As you can see, here I am setting the RootVisual of the application as a new Grid and adding my Page usercontrol as a child of the grid. All we need to do now when we want to switch out Xaml pages is to manipulate the Children collection of our Grid! Here, check out my demo to see how it looks in action, this is a little two page application which allows you to switch back and forth between the pages in a number of different ways.
Woah! Hang on, where did all those cool transition effects come from? It just so happens that I've knocked together a few classes (I'm not going to use the word 'framework'!) that make these navigation issues and transition effects, even making your own transitions, really really easy. So... On to the code.
First up we'll take a look at the transitions. Every transition is implemented as a class deriving from the abstract TransitionBase, it's a really tiny little class which provides the implementation for a completed event, for when the transition has finished, and the contract for a single method which must be implemented by derived transitions.
public abstract class TransitionBase
{
public event EventHandler<TransitionCompletedEventArgs> TransitionCompleted;
protected void OnTransitionCompleted(UserControl newPage, UserControl oldPage)
{
if (TransitionCompleted != null)
TransitionCompleted(this, new TransitionCompletedEventArgs() { NewPage = newPage, OldPage = oldPage });
}
public abstract void PerformTranstition(UserControl newPage, UserControl oldPage);
}
We have a PerformTransition method here which takes a reference to the two pages, the current and the one we'd like to change it with. This method is obviously implemented by our derived transitions to manipulate and animate the pages in some pleasing way, one thing to note here is that when the transition has completed its shuffling of the pages around, it must fire the completed event as we will see shortly.
Next up lets look at NavigationHelper, this is a static class which contains methods which can be called to switch pages.
public static class NavigationHelper
{
private static Grid root;
static NavigationHelper()
{
root = Application.Current.RootVisual as Grid;
}
public static void Navigate(TransitionBase transition, UserControl newPage)
{
UserControl oldPage = root.Children[0] as UserControl;
root.Children.Insert(0, newPage);
transition.TransitionCompleted += transition_TransitionCompleted;
transition.PerformTranstition(newPage, oldPage);
}
public static void Navigate(UserControl newPage)
{
UserControl oldPage = root.Children[0] as UserControl;
root.Children.Add(newPage);
root.Children.Remove(oldPage);
}
private static void transition_TransitionCompleted(object sender, TransitionCompletedEventArgs e)
{
root.Children.Remove(e.OldPage);
}
}
Ok, so here in our static constrcutor we grab a reference to our root visual from the application object. This is cast to a Grid so we can manipulate the Children collection. Now this is not really 100% safe because our static constructor will run the first time and member of our NavigationHelper class is accessed and this could be before the application's root visual has been set as a Grid. In practice though this is very unlikely to ever happen and it would make no sense at all for someone to call our navigation methods before Application_Startup so I'm going to let it go...
We also have two methods here, one which take a TransitionBase and a UserControl and one which just takes a UserControl. The method taking just a UserControl simply switches out the Xaml pages with no other action. The method first gets a reference to the current page which should be at the top of the children collection, then it inserts our new page at the top, this will cause it to be visible and hide the previous page as we have dropped our new one on top, the final step is to pull out the old page from underneath. This is how the "Switch" button in the demo above is working...
The other Navigate overload is more interesting, first we get a reference to the old page but this time we insert the new page behind it so it isn't immediately visible. Next we subscribe to the completed event for the passed in transition object and fire it off to let it do it's thing. We can see that when the transition completes and fires it's event, we simply remove the old page.
Looks great! Now we'll check out how a transition is implemented. There are a few transitions included with the code, but it's really easy for you to write your own and pass them to the NavigationHelper.Navigate method, just by inheriting from TransitionBase. Here is my FadeTransition.
public class FadeTransition : TransitionBase
{
private UserControl newPage;
private UserControl oldPage;
private TimeSpan time;
public FadeTransition(TimeSpan duration)
{
time = duration;
}
public FadeTransition() : this(TimeSpan.FromSeconds(2))
{ }
public override void PerformTranstition(UserControl newPage, UserControl oldPage)
{
this.newPage = newPage;
this.oldPage = oldPage;
Duration duration = new Duration(time);
DoubleAnimation animation = new DoubleAnimation();
animation.Duration = duration;
animation.To = 0;
Storyboard sb = new Storyboard();
sb.Duration = duration;
sb.Children.Add(animation);
sb.Completed += sb_Completed;
Storyboard.SetTarget(animation, oldPage);
Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
sb.Begin();
}
void sb_Completed(object sender, EventArgs e)
{
OnTransitionCompleted(newPage, oldPage);
}
}
This first thing to notice is that we overload the constructor so that we can pass in timespan, this lets us lengthen or shorten the transition as we see fit. Next is the PerformTransition implementation, this method just steps through and constructs a Storyboard object in code then sets it to run. When the Storyboard has finished its work we fire the TransitionCompleted event which lets NavigationHelper clear up out old page. I won't step through how the Storyboard is created as it's well documented on MSDN and I pretty much just copied if from there anyway, so if you want to know more, go check it out.
There is one other transition I'd like to show you, which is called CompositeTransition. This is how I achieve multiple effects at once, for example fading and sliding. I create a fade transition and a wipe transition, wrap them in a CompositeTransition and pass that to the NavigationHelper.
public class CompositeTransition : TransitionBase
{
private TransitionBase transitionOne;
private TransitionBase[] transitions;
private Int32 count;
private UserControl newPage;
private UserControl oldPage;
public CompositeTransition(TransitionBase transitionOne, params TransitionBase[] transitions)
{
this.transitionOne = transitionOne;
this.transitions = transitions;
}
public override void PerformTranstition(UserControl newPage, UserControl oldPage)
{
this.newPage = newPage;
this.oldPage = oldPage;
transitionOne.TransitionCompleted += transition_TransitionCompleted;
transitionOne.PerformTranstition(newPage, oldPage);
foreach (TransitionBase transition in transitions)
{
transition.TransitionCompleted += transition_TransitionCompleted;
transition.PerformTranstition(newPage, oldPage);
}
}
private void transition_TransitionCompleted(object sender, TransitionCompletedEventArgs e)
{
count++;
if (count == transitions.Length + 1)
OnTransitionCompleted(newPage, oldPage);
}
}
There is nothing particularly clever going on here, we have a constructor which take a variable number of TransitionBase instances. When PerformTransition is called we fire off all the transitions at once. When each one of them has completed we fire the TransitionCompleted event. Nice, now we can create loads of cool stuff! One thing worth noting though when stringing transition together like this, in my implementation there is a limitation in that my transtions set the UserControl's RenderTransform property and animate on this. If you chain multiple transitions which work like this they will over write the UserControls RenderTransform and only the last transition will do anything. The fix for this I guess would be to pass a TransformGroup into each transition so they're all animating against the same instance, but I'll leave that bit of fun and games as an excercise for the reader!
Just quickly, here is a snipet showing what a call to NavigationHelper.Navigate looks like, and then I'll leave you with the code download.
TransitionBase transition = new CompositeTransition(
new FadeTransition(TimeSpan.FromSeconds(1)),
new WipeTransition(WipeTransition.WipeDirection.LeftToRight));
NavigationHelper.Navigate(transition, new AnotherPage());
Enjoy!
FlawlessCode.Navigation.zip (144.27 kb)