Solving Circular Navigation in Windows Phone Silverlight Applications

Solving Circular Navigation in Windows Phone Silverlight Applications

  • Comments 30
  • Likes

The Nonlinear Navigation Service is the first recipe we are releasing. Windows Phone recipes are a group of open source (MSPL) projects aimed at helping the Windows Phone community with their applications. Recipes are usually common patterns for topics that require some attention. The idea is to provide a sample with some reusable code that can be reused later and modified to fit other similar scenarios. Developers are more than welcome to use them as is or to change them as they see fit.

Back to our topic – the main reason we are introducing the Nonlinear Navigation Service is to address a common pattern in Windows Phone Silverlight applications, where a given application has a circular path (also known as a loop) in its page navigation. Loops in an application navigation back-stack can be defined as “duplication” of specific pages in the application back-stack. The Windows Phone Silverlight application model uses pages as the building block of Silverlight applications. Most applications are built from multiple pages (of type WindowsPhonePage) hosted in a frame (of type WindowsPhoneFrame), and as a developer you have the ability to navigate to a new page and the user has the ability to navigate back and forth between the application’s pages. For each application, the phone maintains a page back-stack that reflects the history of pages visited by the user. With the current version of Windows Phone, you can’t manipulate the back-stack; that is, you don’t have any APIs to remove pages from the application’s back-stack.

Since developers can call the built-in NavigationServices.Navigate method to pass a page URI in order to show the desired URI, loops can become a real problem. For example if you have a wizard with a few pages, you can end up with a navigation back-stack that looks like the following figure, where you navigate from the main page to the second page to the third page, and then back again to the main page, because that is where you want to go.

From the UX perspective, the end user experiences a lack of consistency. When the user presses Navigate to Main Page on the third page, the application takes the user back to the main page. But now, what should happen when the user presses Back from the revisited main page? Normally from the main page you expect the application to exit, right? But if you have a loop, you will start to traverse back through the loop. This can result in user confusion, where the user expects a particular navigation behavior from pressing Back, while in fact, due to the navigation loop, the Back button behaves differently.

image

From a technical point of view, loops are duplications of page instances in your application’s back-stack. We want to eliminate these duplicates in the back-stack in order to avoid any navigation loops. You can read a lot more about Windows Phone navigation and circular navigation in the Non Linear Navigation documentation

Note: The UX guidelines for Windows Phone advise that you avoid creating a “home” functionality in your application since this is the number one cause of navigation loops. But even then, you might end up with navigation loops that you need to handle.

There are number of solutions. You can try avoiding navigation loops, perhaps by using a smart UI, popup, and context menus. However, sometimes you simply have no choice and you must include some navigation loops. Since you can’t manipulate the application’s page back-stack, you are forced to perform back navigation to clear the application’s page back-stack (read more on how Windows Phone navigation works).

Up until now, the only solution to this problem was to perform a recursive back navigation. The recursive back navigation automatically navigates back through the pages found in the application’s page back-stack, until the beginning of the navigation loop. For this to work, you needed to do the following:

  1. Keep track of your navigation history in order to identify potential loops.
  2. Once you identify a potential loop, start a recursive back by raising a “recursive back flag” and calling the NavigationServices.GoBack() method.
  3. For each page in your application, in the OnNavigatedTo method, check whether the “recursive back flag” is set; if so navigate back once more.

Luckily, we’ve solved that problem for you. Once it is initialized, the NonLinearNavigationService monitors the navigation in your application. When it detects a navigation loop (navigation to a page with a URI that already exists in the application’s page back-stack), it automatically performs the recursive back navigation for you. That is right--you need not do any additional work and this service will navigate back for you through the application’s page back-stack. Simply initialize the service in your application constructor using the following code snippet:

// init the non-linear nav service, make sure you call it last in the app CTOR
NonLinearNavigationService.Instance.Init(RootFrame);

From this point on, just keep on using the Navigate() method and the service will take care of everything else. You can “navigate” to any page, and the NLNS will clear the back-stack for you

There are few limitations that you need to be aware of when using the NonLinearNavigationService (NLNS):

  • NLNS will not allow you to create any navigation loops whatsoever; even if you want to create a loop, there is no way to signal to the NonLinearNavigationService that you want it--if loops are part of your application scenario, do not use the NonLinearNavigationService
  • If you are using an application bar in your application, you will have to hide it manually in the OnNavigatedFrom method to avoid flickering during the recursive back; while the NonLinearNavigationService sets the RootFrame to transparent during the recursive back navigation, the app bar requires special attention
  • A Back button bug manifests when canceling navigation requests; to work around this, you need to use the following code snippet in all of your pages if you are manually canceling the navigation event (usually in the OnNavigatedFrom method):
protected override void OnBackKeyPress
                      (System.ComponentModel.CancelEventArgs e)
{
    base.OnBackKeyPress(e);
 
    if (NavigationService.CanGoBack)
    {
        e.Cancel = true;
        NavigationService.GoBack();
    }
}

I strongly recommend that you to read the documentation that comes with the NonLinearNavigationService to better understand the problem and its solution. The document also includes some background information about how this recipe was created.

Download the Non-Linear Navigation Service

30 Comments
You must be logged in to comment. Sign in or Join Now
  • I've found a bug when dealing with the NonLinearNavigationService in the following scenario:

    1) MainPage.xaml -> (navigates) -> Page2.xaml -> (navigates) -> MainPage.xaml

    Obviously, the service resolves the above navigation scenario. But suppose that I'm on the MainPage.xaml, press the Start button and press Back to return to the application. Now, when running the scenario 1) again, the navigation from Page2 to MainPage is not handled as a circular navigation anymore.

    The problem is that when in the MainPage and then pressing the Start button, a navigation event is raised and the object representing the navigation (_NavHelper) in the NonLinearNavigationService is created with CurrentUri = "MainPage.xaml", NavMode = "New", TargetUri = "app://external/". Until now everything is ok, with the History of URIs containing only the MainPage. But then, if I press the Back button to return to the application, a navigation event is raised and the object representing the navigation (_NavHelper) is created with CurrentUri = "MainPage.xaml", NavMode = "Back", TargetUri = "MainPage.xaml" (note that CurrentUri should be "app://external/", but since the CurrentUri is taken from the _AppRootFrame.CurrentSource, it is still the MainPage.xaml). Then, when executing the following switch statement in the NonLinearNavigationService_Navigated handler, the MainPage.xaml is removed from the History (but it never should be):

    switch (_NavHelper.NavMode)

    {

                   case NavigationMode.Back:

                      // remove previous page from stack

                      _History.Remove(_NavHelper.CurrentUri);

                      (more code)...

    }

    I've fixed the issue by comparing the CurrentUri with the TargetUri and only removing if they are different (since is a bad navigation logic if navigating from one page to the same):

    switch (_NavHelper.NavMode)

    {

                   case NavigationMode.Back:

                          if (_NavHelper.CurrentUri != _NavHelper.TargetUri)

                          {

                                 // remove previous page from stack

                                 _History.Remove(_NavHelper.CurrentUri);

                          }

                      (more code)...

    }

    Hope this helps...

  • Is there any way of disabling using previous state data in the NonLinearNavigationService.cs?

    So that even if we go back to the inital instance of MainPage, the most recent saved data is used.

  • For this issue about backstack being persisted properly, it's important to make sure your own persisting code isn't interfering with the NLNS code. In my case, I was clearing the state after restoration in Application_Activated, so there was no backstack for the NLNS to restore. (I think that was the problem.)

  • @dt008 Yes, I see the same problem in my app. The backstack history is not always saved on tombstoning, for some reason.

  • dt008
    2 Posts

    @nitro52: thanks, I *should* have written the backstack state. Most of the times, when I tombstone, I see that the NLNS preserves the backstack state. There are a couple of times I have not seen this happening, though

  • nitro52
    28 Posts

    @dt008 you need to manually persist any application state when your application is tombstoned. You should be able to find heaps of samples for this.

  • dt008
    2 Posts

    Sometimes, when my app returns from tombstoning, the state is not preserved. Has anybody else encountered this or should I look for a bug in my code?

  • GFR2011
    5 Posts

    Yochay,

    Thanks for the heads-up. I look forward for a revised edition of this recipe.

    Gus

  • @GFR2011 - there are two main limitations with this version of the service.

    1. The service doesn’t work well with transition’s, mainly because we cancel the navigation when we find a loop

    2. The service will not let you create loops (at all) therefore there is a hidden back navigation that the service is executing behind the scenes

    We are working on a revised version of this service that will give you better control over your back navigation - stay tuned.

  • Actually, these errors seem to come in more frequently, so its not rare.  Any advice?

  • I get the following error reported from the rare user, I have no idea what they are doing or how to reproduce the error.

    NullReferenceException

      at WindowsPhoneRecipes.NonLinearNavigationService.LoadServiceHistory()

      at WindowsPhoneRecipes.NonLinearNavigationService.root_Navigated(Object sender, NavigationEventArgs e)

      at System.Windows.Navigation.NavigationService.RaiseNavigated(Object content, Uri uri, PhoneApplicationPage existingContentPage, PhoneApplicationPage newContentPage)

      at System.Windows.Navigation.NavigationService.Journal_NavigatedExternally(Object sender, JournalEventArgs args)

      at System.Windows.Navigation.Journal.OnNavigatedExternally(String name, Uri uri, NavigationMode mode)

      at System.Windows.Navigation.Journal.ShellPage_NavigatedAway(Object sender, NavigateAwayEventArgs args)

      at Microsoft.Phone.Shell.Interop.ShellPageCallback.FireOnNavigateAway(IntPtr thisPage, Direction direction, IntPtr pageNext)

  • @Morten Nielsen - can you please provide a little bit more context and or repro to the problem you descirbed?

  • @GFR2011 - the nonlineaer navigation performs a hiden navigation in the background, cleaning the stack. Your solutions are to either use a global model-view to save the information between the pages, or simple manage your own navigation flow and your own back stack to tailor the solution to your needs.

  • Often when I resume my app, I get a null-ref exception in NonLinearNavigationService line 246 (NavigationMode.New):

      at WindowsPhoneRecipes.NonLinearNavigationService.NonLinearNavigationService_Navigated(Object sender, NavigationEventArgs e)

      at System.Windows.Navigation.NavigationService.RaiseNavigated(Object content, Uri uri, PhoneApplicationPage existingContentPage, PhoneApplicationPage newContentPage)

      at System.Windows.Navigation.NavigationService.CompleteNavigation(DependencyObject content)

      at System.Windows.Navigation.NavigationService.ContentLoader_BeginLoad_Callback(IAsyncResult result)

      at System.Windows.Navigation.PageResourceContentLoader.BeginLoad_OnUIThread(AsyncCallback userCallback, PageResourceContentLoaderAsyncResult result)

      at System.Windows.Navigation.PageResourceContentLoader.<>c__DisplayClass4.<BeginLoad>b__0(Object args)

      at System.Reflection.RuntimeMethodInfo.InternalInvoke(RuntimeMethodInfo rtmi, Object obj, BindingFlags invokeAttr, Binder binder, Object parameters, CultureInfo culture, Boolean isBinderDefault, Assembly caller, Boolean verifyAccess, StackCrawlMark& stackMark)

      at System.Reflection.RuntimeMethodInfo.InternalInvoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, StackCrawlMark& stackMark)

      at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)

      at System.Delegate.DynamicInvokeOne(Object[] args)

      at System.MulticastDelegate.DynamicInvokeImpl(Object[] args)

      at System.Delegate.DynamicInvoke(Object[] args)

      at System.Windows.Threading.DispatcherOperation.Invoke()

      at System.Windows.Threading.Dispatcher.Dispatch(DispatcherPriority priority)

      at System.Windows.Threading.Dispatcher.OnInvoke(Object context)

      at System.Windows.Hosting.CallbackCookie.Invoke(Object[] args)

      at System.Windows.Hosting.DelegateWrapper.InternalInvoke(Object[] args)

      at System.Windows.RuntimeHost.ManagedHost.InvokeDelegate(IntPtr pHandle, Int32 nParamCount, ScriptParam[] pParams, ScriptParam& pResult)

  • GFR2011
    5 Posts

    Hi there,

    I've also found that if the pageto has a nav query, coming from the pagefrom..well, the service creates a true mess!

    Not forgetting tha the service creates somer rendering problems with page transitions.

    So far, I think this recipe has too many limitations to be considered useful to deal with the circualr navigation issue.

    Is it possible, that since MS owns the OS, to implement in it a way to handle this issue or even better, to provide some kind of application exit mechanism?

    Things will be much better for all of us.

  • GFR2011
    5 Posts

    Well, I've been investigationg the issue (posted before) and have found that the NonLinearNav only works (well) with 3 pages.

    In my case, I have a 4 pages navigation.

    Any ideas on how to solve this?

  • GFR2011
    5 Posts

    I''d ported the solution to VB and while it works well, I found a rather weird side-effect.

    In one of the app pages, a query is created when going form this page to a second one, which supplies a listbox with items. Once an item is selected, it goes back to the calling page that uses that value to execute a query,

    So far, so good.

    Problem is, when I hit back on this page (the one with the query) since it goes back to a "blank" screen...!!! insted of going back to the previous page.

    This was NOT happening without the nonlinear service.

    This only happens when the query was executed.

    The sequence is

    MainPage -> P1 -> P2 -> P3

    P2 contains the query. P3 the listbox with items.

    So, when going back from P3 to P2 no issue at all,...but when going back from P2 to P1...well, a "blank" screen

    Moreover, if in that "blank" (should be P1) I press the BackButton it goes back to the MainPage, as expected.

    P1, P2 and P3 have the BackKeyPress navigating to the specific page. (P3 to P2, P2 to P1, P1 to MainPage)

    Any ideas ? Again only happens now, with the nonlinear service.

  • @mikeloaf The sample provided with the recipe works well and return from tombstone correctly. Can you please send a repro?

  • Hi!

    I found an interesting issue with this NonLinearNavigationService.

    If you have two pages like Main and Other

    and you navigate with a button from Main -> Other

    then you press the start button to do something else

    then back button to get back to the application

    back button to get back to main page

    and press the navigate button to go to the Other page again, and you will get az InvalidOperationException saying you

    "Cannot go back when CanGoBack is false."

    without the NonLinearNavigationService it works fine.

    Is this something with the NonLinearNavigationService, or something else?

  • @drswoboda the Logger dll should be in the 3rd party lib folder...

  • @diomedtmc currently this library doesnt work well with page transition, mainly out such as in your case. We are publishing an update to the library (or should I say a different library) that solve this problem.

  • @Hosam Aly Currently, the  Back button doesn't rout events (like other SL events), therefore canceling the navigation event in the OnNavigatingFrom will result in a condition where your application can navigate at all. If you enable in your application to cancel navigation in the OnNavigatingFrom, you'll need to use the workaround introduced in the post.

  • @Odegaard Yes we are

  • Happy New Year.

    First, thank you for this great class.

    Concerning the bug, does it apply when the e.Cancel is inside the PhoneApplicationPageBackKeyPress?

    private void PhoneApplicationPageBackKeyPress(object sender, System.ComponentModel.CancelEventArgs e)

           {

               if (MessageBox.Show("Are you sure to leave this current game?", "Leaving Game", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel)

               {

                   e.Cancel = true;

               }

               else

               {

                   NavigationService.Navigate(new Uri("/Pages/MainPage.xaml", UriKind.Relative));    

               }

           }

    Thanks

  • When I download and run the sample project, I am getting an error at statup:

    Warning 1 The referenced component 'WindowsPhoneLogger' could not be found.

    When I rebuild the project, I get 24 Errors. Here is one:

    Error 1 The type or namespace name 'Logger' does not exist in the namespace 'WindowsPhoneRecipes' (are you missing an assembly reference?) C:\WP7TrainingKit\NonLinearNavigationService\WindowsPhoneNonLinearNavigationServiceSample\CategoriesListingPage.xaml.cs 29 33 WindowsPhoneNonLinearNavigationServiceSample

    In Solution Explorer, under references, there is a yellow warning for WindowsPhoneLogger.

    Is this broken. I have not been able to reassign the reference.

    Again, all I did was unRAR the files and run the project.

    Thanks.

    -David

  • I should clarify...transitionframe with TransitionOut:

    <toolkit:TransitionService.NavigationOutTransition>

           <toolkit:NavigationOutTransition>

               <toolkit:NavigationOutTransition.Backward>

                   <toolkit:TurnstileTransition Mode="BackwardOut"/>

               </toolkit:NavigationOutTransition.Backward>

               <toolkit:NavigationOutTransition.Forward>

                   <toolkit:TurnstileTransition Mode="ForwardOut"/>

               </toolkit:NavigationOutTransition.Forward>

           </toolkit:NavigationOutTransition>

       </toolkit:TransitionService.NavigationOutTransition>

  • Has anyone gotten this type of solution to work with a TransitionFrame?

  • Could you please elaborate on the Back button bug? Where can I find more information about it?

  • igoros
    1 Posts

    Will this work if one of the pages in the stack is a camera capture task? I recently had a problem with the fact that the camera capture task puts itself on the page history and I didn't come up with a good workaround.

  • I assume you guys are considering, no... make that planning, to improve the navigation service soon, so we don't have to do these "hacks" ?