>
Blog
Book
Portfolio
Search

2/25/2011

7710 Views // 0 Comments // Not Rated

Silverlight Navigation with 100% Custom Transitional Animations

Something that's always impressed me about Silverlight (and has made my life a lot easier) has been the browser integration, both in terms of the HTML bridge and the navigation history functionality. I want to focus on the latter; the former has been beaten to death, and at this point, it's hard for me to get excited about yet another way to sling JavaScript.

Clicking the "back" browser button is one of the handiest UX innovations I've ever seen. At the same time, it creates quite an onus for the web developer, as it can easily put a poorly-designed application into an indeterminate state. This can be a rude awakening (it was for me) the first time you need to start messing with Page.IsPostBack and Control.AutoPostBack. Then throw some AJAX into the mix. If you are not diligent with your page transitions, you could get a poor user experience, or worse, garbage data when the user clicks the seemingly harmless back button.

Now in Silverlight applications (verses a web page that has some Silverlight on it for purposes of sex appeal alone), the back button takes on a different role. If you aren't using the Silverlight Navigation Framework, then much like a hard page refresh, "backing" to a Silverlight page will cause the control to reload from scratch, as if it was the first time it had been visited. However, the nav framework will nestle itself into the browser's history quite nicely, using hashed "relative" URLs, making your Silverlight app behave as though it were a standard website comprised of distinct physical pages.

This enables SEO, browser forward/back navigation, true MVC-like architecture with the UriMapper, quasi-master pages, and lots of other enterprise-ish application features. Behind the scenes, all that's happening is that URLs are mapped to relative folder locations for XAML files within the XAP, and the corresponding file is loaded into a particular content frame. There are a bunch of posts out there regarding how to transition smoothly between pages while remaining within the Silverlight Navigation Framework, but none quite worked out for our situation.

We were building our second Silverlight application for a client. The first one was a SharePoint intranet, with lots of isolated Silverlight controls (ie, many XAPs) on the page. There were some major performance ramifications to this, as we had to pay a price for the spin up of each XAP's app domain. In other words, a separate version of the Silverlight runtime was loaded into memory for each control on the page. Yeah. Ouch.

So when we began architecting the second app, which was pure Silverlight, the first thing we decided upon was one XAP that contains the entire UI. This made start up times faster, which is a good first step in a development project where performance was considered a feature, not an optimization. Additionally, the app demanded very intense styling and animations, so almost every control we created has a custom template of some sort. Basically, we had to do more, and do it faster. For the navigation, pages fly in and out of the screen and the background scrolls in the opposite direction behind the "chrome" of the application. Here's a Paint masterpiece of what we had to do. (The blue rectangles represent the viewable area of the app in the browser, the solid-colored boxes are stacked images, and the back rectangles are user controls.)

navigation

As the user navigates to different "pages," the user control that corresponds to it animates "down" into view, while the appropriate background slides "up" behind it. (The word "pages" above is in quotes because, remember, everything is physically on the same page, within the same XAP.) However, the magic that happens is that browser back/forward navigation, browser tab titles, and deep linking all work!

In other words, the technique I will show allows us to essentially browser navigate around a single user control in Silverlight!

No idea what I'm talking about? Here's an example. Even though the application seems to navigate normally, notice how the pages animate feely (instead of one disappearing and another appearing, a la some cheesy PowerPoint slide transition) and smoothly. The source code will be included at the end of this post.

Since we're not actually moving to different pages or loading different controls, the sky is the limit for animated transitions; we don't need to rely on any third party controls or limited OOTB functionality. Instead all we need is the chrome (the main user control; the RootVisual of the Silverlight application itself), the child user controls that act as "pages" (but aren't Pages a la Silverlight 3 Navigation), and a custom Navigation Loader that does the magic of making the browser think it's going to different pages when it's really staying in the same place.

The "chrome" of the application is more or less like a master page: it contains all the UI that remains stationary while the content pages animate around it. This includes menus, tab strips, footer links, mastheads, etc. It also includes the Silverlight Navigation Framework pieces: the aforementioned UriMapper and Navigation Loader, as well as the Frame, which, in conventional Silverlight navigation terms, is the area where the content for each actual page is loaded inside the chrome. Think of it just like an HTML iframe whose source is set with JavaScript.

Inside the chrome is some sort of Panel that contains all of the controls that act as our pages. This panel can be as simple as a Canvas wherein its children have their Y coordinates animated, or a custom panel (as was our case) that arranged pages all over the place and used interfaces and attributes to facilitate communication and positioning. This detail, however, is out of the scope of this post. What's important is that all the pages live inside a single panel in the chrome. Alongside this panel is the navigation frame, which, as its children, has the UriMapper busily mapping URIs to XAML file relative paths, a navigation loader, and the content of the page being loaded.

I hope that last sentence sounded strange. If we have a panel that's animating child controls around to show different pages, then what content are we loading into the frame itself? The answer: nothing. The things we need the frame for are to fire navigation events and have its Navigate method called. This method allows, for example, a button click on one page to kick off a nav to another. But there's no rule that the frame's Content property ever needs to be set! That's right: we basically have a collapsed frame on the page that does nothing except listens for URL changes and fires events. It doesn't have to display anything!

How can this be so? Because frames have these navigation loader things we've been talking about. Their actual job is to asynchronously turn a URI into, essentially, a UserControl. Frames have a default navigation loader: an instance of PageResourceContentLoader. This puppy cranks out actual Silverlight pages reflectively instantiated from the passed in Uri, and sets them to the content of the frame. Here is where conventional frame transition animations will be implemented. However, like I said, the fact that these animations are linked to the frame, and not the pages themselves, makes them not flexible enough. So my thinking: simply don't show the frame at all and handle the UI myself.

(Besides, if I'm animating a transition from one page to another, it makes my head hurt conceptualizing that fact that I only have one page at time in the traditional transition model: the current page is destroyed and then the new page is materialized. With my technique, we actually have all of the physical page UserControls sitting next to each other; it's much easier to conceive and work with.)

The only problem is that it doesn't work. That is, we need to do a little more than simply ignore the frame's content. Without a custom loader in place, Silverlight's default frame loader will still "try" to navigate around, and with no content, blank pages will be displayed. So what I did was create a custom loader that EXPLICITLY loads nothing. It's sort of like the different between a null string and an empty string where nulls are not supported. Leaving the frame "null" didn't work; using a custom loader to ensure the frame that it was okay to be "empty" did the trick! Here's what the loader looks like:

Code Listing 1

  1. using System;
  2. using System.Threading;
  3. using System.Windows.Controls;
  4. using System.Windows.Navigation;
  5. namespace UI.Infrastructure
  6. {
  7. public class NavigationListener : INavigationContentLoader
  8. {
  9. #region Members
  10. private class LoadAsyncResult : IAsyncResult
  11. {
  12. #region Members
  13. public object AsyncState { get; set; }
  14. public WaitHandle AsyncWaitHandle
  15. {
  16. get { return null; }
  17. }
  18. public bool CompletedSynchronously
  19. {
  20. get { return true; }
  21. }
  22. public bool IsCompleted
  23. {
  24. get { return true; }
  25. }
  26. #endregion
  27. }
  28. #endregion
  29. #region Public Methods
  30. public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
  31. {
  32. //oblige callback
  33. userCallback(new LoadAsyncResult() { AsyncState = asyncState });
  34. //fake result
  35. return null;
  36. }
  37. public bool CanLoad(Uri targetUri, Uri currentUri)
  38. {
  39. //TODO: security?
  40. return true;
  41. }
  42. public void CancelLoad(IAsyncResult asyncResult)
  43. {
  44. //TODO: cancellation?
  45. }
  46. public LoadResult EndLoad(IAsyncResult asyncResult)
  47. {
  48. //fake result
  49. return new LoadResult(new Page());
  50. }
  51. #endregion
  52. }
  53. }

As you can tell, this is more or less a fake class. The only notable lines are #35, where we actually return null for the result (which is okay) and #49, where we return a harmless new Page as our content. You'll see why we need something there later, even though the frame in XAML is collapsed. But that's it. The rest of it is fluff to embody the interface. Even our implementation of IAsyncResult with LoadAsyncResult (Line #'s 10-27) is fake; it's the bare minimum we need to get the meaningless IAsyncResult we need to pass our fake Page. It's actually kind of depressing how abusive I am to this sub-system; all this work to nullify its functionality.

So right now, we have a frame that's properly updating the browser's history as we nav around the app, but isn't showing anything. Next we need to wire in the event handlers so that we can tell our panel how to animate our pages. On your frame, handle any of the following events, per your requirements:

  • Navigating
  • Navigated
  • NavigationFailed
  • NavigationStopped

The first two adhere to the familiar .NET event "-ing" and "-ed" paring pattern. Navigating fires first, and like other "-ing" events, gives you the opportunity to cancel the navigation. The event args (NavigatingCancelEventArgs) contain the new Uri. If you keep track of the current Uri in your code behind (or custom navigation panel, as I did) and have a mechanism to associate your page user controls to URIs (via attribution on the user control itself, as I did), you can pretty easily come up with a nice little "page lifecycle" model for your controls. In fact, all of my pages (which, as I will keep reminding you, are normal UserControls, not Silverlight Pages) have a custom attribute that provides metadata containing which URL each responds to, an ordinal to make calculating coordinates easier, etc.

The code behind of each such page control looks like this:

Code Listing 2

  1. [NavPage("/NewCustomerForm", 1)]
  2. public partial class NewCustomerPage : UserControl
  3. {
  4. }

So when Navigating fires, I will use the current Uri to pull the corresponding UserControl from my panel via reflection. (Basically, find everything decorated with "NavPage" in my panel, and give me the one whose Uri matches.) Then I could do things like error validation, clean up, etc. on the current control, and, using the Uri from the event args, pull the destination control and start loading data. Finally, we kick off the animation.

Here's what the frame looks like in XAML:

Code Listing 3

  1. <navigation:Frame
  2. x:Name="frmContent"
  3. Visibility="Collapsed"
  4. Navigating="frmContent_Navigating"
  5. NavigationFailed="frmContent_NavigationFailed">
  6. <navigation:Frame.ContentLoader>
  7. <infrastructure:NavigationListener />
  8. </navigation:Frame.ContentLoader>
  9. <navigation:Frame.UriMapper>
  10. <uriMapper:UriMapper>
  11. <uriMapper:UriMapping Uri="" MappedUri="/Views/Home.xaml" />
  12. <!-- other page mappings here -->
  13. </uriMapper:UriMapper>
  14. </navigation:Frame.UriMapper>
  15. </navigation:Frame>
  16. <Canvas x:Name="canNavPanel">
  17. <Canvas.Clip>
  18. <!-- something here to hide controls that are "off the screen" -->
  19. </Canvas.Clip>
  20. <!-- page here at (X,Y) -->
  21. <!-- page here at (X,Y + 500 or however much needed to be off screen) -->
  22. </Canvas>

Line #'s 6 - 8 tell the frame to use our custom navigation content loader (an instance of "NavigationListener" defined arbitrarily with the XAML namespace "infrastructure"). And notice Line #3 that makes the frame itself invisible, allowing the Canvas to take its place in the UI.

It should be pointed out that I didn't handle the frame's Navigated event in this case. (Note that this is what fires the OnNavigatedTo event in standard Silverlight Pages.) If you do use this event, make sure that any initialization work you do in your destination control isn't intense enough to make your transition animation choppy. It's for this reason that I hooked the Completed event of my animation's Storyboard and used that to tell the destination control that it's currently on stage.

Now that all of this is wired up, when the URL changes (including deep linked, SEO-happy URLs) the navigation logic will flow as follows:

The frame detects the URL change and gets a relative path to the destination XAML file from the UriMapper. It then forwards this information to its INavigationContentLoader and raises the Navigating event. The navigation content loader has a few opportunities to cancel the navigation, and if it does, the NavigationStopped event is fired. (This will also fire if the user changes the URL again during the processing of the new page.) Otherwise, it will asynchronously load the new page, and the frame will raise Navigated when the new content is ready. Finally, if anything is amiss, the NavigationFailed event occurs.

So that's how browser integration works. What if we wanted one page to be able to navigate to another? No sweat. The navigation frame has a lot of HyperlinkButton behavior built into it, most notably, its Navigate method. Pass this puppy a Uri that the UriMapper can deal with and our navigation logic works as advertised. All we have to do is expose this as a utility method either directly in chrome (hacky) or have it all abstracted into a navigation behavior (elegant). I went with the hacky approach, as this entire mess evolved over time in Visual Studio as requirements changed on us, instead of being born fully mature on a whiteboard.

Code Listing 4

  1. public bool NaviateToPage(string relativeUri)
  2. {
  3. //navigate
  4. return this.frmContent.Navigate(new Uri(relativeUri, UriKind.Relative));
  5. }

Just one problem: as all of this browser integration goodness was happening, I found a bug; the browser tab's title was being set to the relative path of the page! Since most users won't notice the URL changing (no doubt transfixed on the beautiful page transitions) then we really lose the UX of being on a different page, browser-wise, after each animation. Even the clicking sound your browser makes when a link is followed adds to this experience. I was able to trace the problem down to the fake Page we were returning from the custom nav content loader. Silverlight Pages (which extend UserControl directly) add a Title property, which ultimately has the same functionality as the "title" tag in an HTML page's "header." The fake page I returned to the navigation frame had a blank title, which the Silverlight Navigation Framework must default to the URL. So the solution was simple: grab the frame, then grab the frame's content (which is the instantiated fake page), and then grab the frame's content's title and set it. Set it to what? Whatever you want. You can add the title of each page to the custom attribute decorating our user controls, infer it from the Uri, etc.

Here's a hypothetical idea of what the frame's Navigating event could look like, assuming all of our page user controls implement some interface named "ISomePageUserControlInterface," for example. (This is another option you can do to go along with the custom attribution of these controls we discussed earlier.)

Code Listing 5

  1. private void frmContent_Navigating(object sender, NavigatingCancelEventArgs e)
  2. {
  3. //[pseudo-code example validation logic of current page]
  4. ISomePageUserControlInterface currentPage = this.MethodToGetPageUserControlByURL(this._variableHoldingCurrentURL);
  5. if (!currentPage.Validate())
  6. {
  7. //cancel
  8. e.Cancel = true;
  9. return;
  10. }
  11. //...
  12. //[pseudo-code example load logic of new page]
  13. ISomePageUserControlInterface newPage = this.MethodToGetPageUserControlByURL(e.Uri.ToString());
  14. newPage.Load();
  15. //...
  16. //begin invoke this so that the content is loaded before we nav to it (needed when app doesn't start on home page (if we have been deep linked))
  17. this.Dispatcher.BeginInvoke(() =>
  18. {
  19. //set new page title on content of frame
  20. ((Page)this.frmContent.Content).Title = newPage.Title;
  21. });
  22. }

Additionally, you'd kick off your animation in this event handler, as well as perform any other cleanup / maintenance tasks (like setting "this._variableHoldingCurrentURL" to "e.Uri.ToString()," etc.).

Well that's about it: 100% custom animations for transitions between pages navigated to both internally and externally. And it's really not all that much work; just a lot if indirection. It always amazes me how much a layer of abstraction can affect any system. The Silverlight Navigation Framework fools the browser into thinking it's loading different pages. My custom navigation content loader fools the page into thinking it's loading different controls. At this point, the poor user has no idea what's going on behind the scenes (which is a good thing, probably, as lengthy and stunning animations can buy us up to two seconds (anything longer won't fly) to load data). A colleague of mine always quotes the famous David Wheeler saying "All problems in computer science can be solved by another level of indirection." This is good stuff; it is why, for example, I named my navigation content loader "NavigationListner" - it does just that: listens. It uses the navigation frame as a puppet to fire the events our UI needs to animate to new controls. It abstracts a layer of abstraction.

Have fun navigating! (Or, more accurately, "navigating.")

No Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.


Loading...