Adapting Silverlight Navigation to MVVM

25 בינואר 2011

 

Adapting Silverlight Navigation to MVVM

The Navigation feature in Silverlight is pretty awesome. It adds support for two very critical issues:

1. Real web-compliancy (Browser Address changes, which adds support for: SEO, Deep Linking, Browser Journal)

2. View-Switching navigation (switching views easily to solve the ever-lasting navigation problem)

It’s important to notice that these two issues are actually completely different problems, which for some reason were bundled together!

Many times you would want to switch views in different scenarios, but you might not want to change the Url, and even more often than that, you’d want to change the Url, but remain in the same view (e.g. going from a page the shows Product #5 to Product #7 – you’d want to use the same view, but to change the url to enjoy browser integration)

 

 

The Problems

1. Those two issues should not be bundled together.

2. The view navigation support is naïve at best. Most real world scenarios are more complex and the navigation framework was not built to allow such scenarios.

In real world I would not suggest using this for view-navigation. Either go light-weight with the MVVMLight way, or heavy weight with Prism navigation (which is IMHO the best and handiest feature of the entire Prism toolset).

3. The browser features are amazing and extremely important, but I would like that to be stripped away from the view-navigation completely, since it’s not really related to actual view-navigation.

and most importantly –

4. I should be able to control the browser features from the View-Model, allowing for clean MVVM solution.

Specifically here, the navigation controls (Frame & UriMapper) are not MVVM-ready, and it’s rather annoying. I would expect the P&P team to work closer to the .Net teams so that the gap between them both would be smaller. But that’s really a story for another post.

 

Silverlight Navigation – The way it is now

Using the Navigation Application Template in Visual Studio offers a really fast Quick Start, and it’s pretty easy to understand how the different parts are working.

Navigation Template in Visual Studio

The main page now have the following Frame control:

 

   1: <navigation:Frame x:Name="ContentFrame" Style="{StaticResource ContentFrameStyle}" 

   2:                   Source="/Home" Navigated="ContentFrame_Navigated" NavigationFailed="ContentFrame_NavigationFailed">

   3:     <navigation:Frame.UriMapper>

   4:       <uriMapper:UriMapper>

   5:         <uriMapper:UriMapping Uri="" MappedUri="/Views/Home.xaml"/>

   6:         <uriMapper:UriMapping Uri="/{pageName}" MappedUri="/Views/{pageName}.xaml"/>

   7:       </uriMapper:UriMapper>

   8:     </navigation:Frame.UriMapper>

   9: </navigation:Frame>

The frame control is the only place we can get Navigation-related events, and the Source property is not data-boundable.

The Uri-Mapper, allowing the mapping of custom uris to actual files is very cool, but isn’t very helpful in our MVVM-quest.

In order to navigate from one view to another, we use the hyper-link button:

   1: <HyperlinkButton x:Name="Link1" Style="{StaticResource LinkStyle}" 

   2:                  NavigateUri="/Home" TargetName="ContentFrame" Content="home"/>

Out of the box, the way to control Navigation controls comes in two flavors.

1. Manipulating the Frame directly via code behind.

2. Using HyperLinks to Navigate to different Uris.

 

 

 

The way it should be, MVVM Style

I would really like to be able to use the Navigation feature in an MVVM-like way, namely:

1. I would like to have a NavigationHelper that allows me to change the Uri of my application without actually loading another view, or loading my current view again from scratch.

2. I would like the NavigationHelper to be available from the ViewModel, so I could control the deeplinking feature from the ViewModel layer, the way it should be done, and not automatically from the view itself.

I’m not interesting in the view-switching capabilities at-all. In real world scenarios, I would need more control over the navigation process than these controls allows me to begin with.

All is not lost though Smile

Although out-of-the-box it’s not working the way you’d want it to be, it is, however, quite easy to customize to our needs.

 

 

 

Creating MVVM-Ready NavigationHelper

Our first task is to create a NavigationHelper class that would be easy to handle from the view-model layer. 

NavigationHelper class

 

The Navigation Helper should expose the following:

1. Navigate(uri): changes the uri

2. GoBack & GoForward methods

3. GetQueryStringParameter method which would allow me to easily extract parameters from the uri

4. Events exposing the navigation (Navigated, Navigation, NavigationFailed & NavigationStopped) so that other ViewModels can “listen” to Navigational events.

 

(full code below)

 

This would allow us to change the uri from the ViewModel (without actually switching views if we don’t want to) like this:

   1: // Change the url to url#/Product=7 (while staying in the same view)

   2: navigationHelper.Navigate("/Product=7");

   3:  

   4: // Change the url to url#/Product=20 (while staying in the same view)

   5: navigationHelper.Navigate("/Product=20");

   6:  

   7: // Get the current Product

   8: var Product = navigationHelper.GetQueryStringParameter("Product");

   9:  

  10: // register to navigation event

  11: navigationHelper.Navigated += ...

 

In all of the scenarios, we would like to remain in the same view hence as we said before – for actual view-switching it’s best to use other solutions. However, it turns out that by default the Frame control will refresh it’s contents even if we’re navigating to the same uri. it will always create the give view from scratch, which is bad because we would probably like to use the same view (and view model) and just change some of the data.

The default behavior of the Frame Control can be extended though:

The Frame Control exposes a Frame.ContentLoader property which accepts type of INavigationContentLoader which we can extend.

The default INavigationContentLoader is of type PageResourceContentLoader, which is what causing the problem – this is the class that is in charge of loading content, and here is where it’s refreshing our views instead of recycling them.

All we need to do is to create an INavigationContentLoader of ourselves, which will recycle views if we’re navigating to the same uri. It’s actually very easy to create since we can copy most of the functionality from PageResourceContentLoader:

   1: public class RecyclingContentLoader : INavigationContentLoader

   2: {

   3:     private PageResourceContentLoader _loader = new PageResourceContentLoader();

   4:  

   5:     private bool isNavigatingToSameView;

   6:  

   7:     public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri,

   8:         AsyncCallback userCallback, object asyncState)

   9:     {

  10:         isNavigatingToSameView = false;

  11:         if (currentUri != null)

  12:         {

  13:             var file1 = Path.GetFileNameWithoutExtension(targetUri.OriginalString);

  14:             var file2 = Path.GetFileNameWithoutExtension(currentUri.OriginalString);

  15:  

  16:             isNavigatingToSameView = (file1 == file2);

  17:         }

  18:         

  19:  

  20:         return _loader.BeginLoad(targetUri,

  21:             currentUri, userCallback, asyncState);

  22:     }

  23:  

  24:     public bool CanLoad(Uri targetUri, Uri currentUri)

  25:     {

  26:         return _loader.CanLoad(targetUri, currentUri);

  27:     }

  28:  

  29:     public void CancelLoad(IAsyncResult asyncResult)

  30:     {

  31:         _loader.CancelLoad(asyncResult);

  32:     }

  33:  

  34:     UserControl currentView; 

  35:  

  36:     public LoadResult EndLoad(IAsyncResult asyncResult)

  37:     {

  38:         if (isNavigatingToSameView)

  39:             return new LoadResult(currentView);

  40:         else

  41:         {

  42:             var loadResult = _loader.EndLoad(asyncResult);

  43:  

  44:             currentView = loadResult.LoadedContent as UserControl;

  45:             return loadResult;

  46:         }

  47:         

  48:     }

  49:  

  50: }

Explanation:

1. All of the public methods are methods we are required to implement for INavigationContentLoader.

2. The only interesting methods are BeginLoad, where we check whether the navigation request is for the current view, or for a new one, and saves the result in a private flag, and

3. EndLoad, which, based on the aforementioned flag chooses to either continue and load a new view, or to recycle the current one.

4. The _loader field of type PageResourceContentLoader is what’s doing most of the actual work – we’re actually using 99% of it’s implementation, only deciding for ourselves if we want to recycle the view.

 

All we need now it to plug it in to the frame, like so:

   1: <navigation:Frame x:Name="MyFrame" >

   2:     <navigation:Frame.ContentLoader>

   3:         <helpers:RecyclingContentLoader />

   4:     </navigation:Frame.ContentLoader>

   5: ...

   6: </navigation:Frame>

 

Our Frame is now ready to be controlled by the NavigationHelper, the only thing that’s missing is to register the Frame with the NavigationHelper, so we should introduce another method to the Navigation Helper, InitializeFrame(Frame theFrame) so we could catch all of the Navigation events and allow us complete control over the frame from the ViewModel.

The initialization of the NavigationHelper should be done in the code behind of the Frame’s UserControl:

   1: public partial class NavigationShell : UserControl

   2: {

   3:     public NavigationShell()

   4:     {

   5:         InitializeComponent();

   6:  

   7:         NavigationHelper.Instance.InitializeFrame(MyFrame);

   8:     }

   9: }

 

And the full code of the NavigationHelper:

   1: public class NavigationHelper

   2: {

   3:     private static NavigationHelper instance = null;

   4:     public static NavigationHelper Instance

   5:     {

   6:         get { return instance; }

   7:     }

   8:  

   9:     private NavigationHelper()

  10:     { }

  11:  

  12:     static NavigationHelper()

  13:     {

  14:         instance = new NavigationHelper();

  15:     }

  16:  

  17:     public void InitializeFrame(Frame frame)

  18:     {

  19:         _theFrame = frame;

  20:  

  21:  

  22:         theFrame.Navigated += new NavigatedEventHandler(theFrame_Navigated);

  23:         theFrame.Navigating += new NavigatingCancelEventHandler(theFrame_Navigating);

  24:         theFrame.NavigationFailed += new NavigationFailedEventHandler(theFrame_NavigationFailed);

  25:         theFrame.NavigationStopped += new NavigationStoppedEventHandler(theFrame_NavigationStopped);

  26:     }

  27:  

  28:     void theFrame_NavigationStopped(object sender, NavigationEventArgs e)

  29:     {

  30:         if (NavigationStopped != null)

  31:             NavigationStopped(this, e);

  32:     }

  33:  

  34:     void theFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)

  35:     {

  36:         if (NavigationFailed != null)

  37:             NavigationFailed(this, e);

  38:     }

  39:  

  40:     void theFrame_Navigating(object sender, NavigatingCancelEventArgs e)

  41:     {

  42:         if (Navigating != null)

  43:             Navigating(this, e);

  44:     }

  45:  

  46:     void theFrame_Navigated(object sender, NavigationEventArgs e)

  47:     {

  48:         if (Navigated != null)

  49:             Navigated(this, e);

  50:     }

  51:  

  52:     public string GetQueryStringParameter(string parameterName)

  53:     {

  54:         var pList = theFrame.CurrentSource.ToString().Split('/');

  55:  

  56:         foreach (var parameter in pList)

  57:         {

  58:             if (!parameter.Contains("="))

  59:                 continue;

  60:  

  61:             var pLeft = parameter.Split('=')[0];

  62:             var pRight = parameter.Split('=')[1];

  63:  

  64:             if (pLeft.ToLower() == parameterName.ToLower())

  65:                 return pRight;

  66:         }

  67:  

  68:         return String.Empty;

  69:     }

  70:  

  71:     public void Navigate(String uri)

  72:     {

  73:         theFrame.Navigate(new Uri(uri, UriKind.Relative));

  74:     }

  75:  

  76:     public void GoBack()

  77:     {

  78:         theFrame.GoBack();

  79:     }

  80:  

  81:     public void GoForward()

  82:     {

  83:         theFrame.GoForward();

  84:     }

  85:  

  86:     public event NavigatedEventHandler Navigated;

  87:     public event NavigatingCancelEventHandler Navigating;

  88:     public event NavigationFailedEventHandler NavigationFailed;

  89:     public event NavigationStoppedEventHandler NavigationStopped;

  90:  

  91:     private Frame _theFrame = null;

  92:     private Frame theFrame

  93:     {

  94:         get

  95:         {

  96:             if (_theFrame == null)

  97:                 throw new Exception("The Frame was not initialized. This can be fixed by calling to 'NavigationService.InitializeFrame'");

  98:  

  99:             return _theFrame;

 100:         }

 101:     }

 102:  

 103: }

 

And voila – the Navigation is now fully MVVM- enabled Smile

The navigation helper is coupled to the view, but the ViewModels can now hold reference to INavigationHelper, and so be decoupled from it for UnitTesting etc.

Also, the one line of code behind is not pretty, but I don’t think it’s too bad. In any case it’s easy to introduce an attached property that does exactly that, but IMHO it’s an overkill since we’re not going to see more that one line of code behind, in only one “Navigation-Shell” file that we’re not going to touch to much.

 

Attached is the source code for everything plus 2 views & viewModels that shows it all glued together. It demonstrates very simply how to use it in several scenarios.

 

Happy MVVMing!

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים

4 תגובות

  1. Vina4 במרץ 2011 ב 8:43

    Could you please provide source code, it will be much appreciated.

    להגיב
  2. Thomas20 באוקטובר 2011 ב 20:52

    Great thanks – it worked first time !!, still missing out to find out how and why it works :-)

    להגיב
  3. sunfun21 באוקטובר 2011 ב 18:15

    Could you possibly provide an updated version that shows using the id value on one (or both) of the pages? I'm still trying to figure out how to implent the query string in the page binding.

    Thanks in advance

    להגיב