Removing a View from BackStack in MvvmCross for Windows Store Apps

July 21st 2014 MvvmCross Windows Store

Navigation in Windows Store apps is strongly based on the browser model, i.e. the application is keeping a back stack of previously shown pages which will be traversed again when navigating back. For most applications this approach works well, at least most of the time.

But there are some cases in which you don't want the user to navigate back to a specific page in the history. A typical scenario would be a starting page for creating a new instance of something, e.g. a new game session: a user would navigate to it from the main menu, set some starting parameters there and then continue to the actual game session page. When navigating back, we don't want the user to see the intermediate session setup page, but to to return directly to the main menu instead.

Desired navigation behavior

Unfortunately there's no cross-platform way to achieve the desired behavior, therefore there's also no such built-in functionality available in MvvmCross. Still, the navigation model in MvvmCross is very straight-forward and extensible, making it really simple to add such functionality, if you approach it the right way.

The key to navigation in MvvmCross are ViewPresenters. They handle two main types of messages emitted by view models: ViewModelRequests and PresentationHints. The former are used for switching between views and not applicable to our case; the latter are used for everything else related to navigation and are the ones that we will be taking advantage of.

The basic plan is as follows:

  • Define a new PresentationHint for removing the current (top) view from the navigation back stack
  • Extend the built-in platform-specific ViewPresenter to intercept this navigation hint, modify the back stack accordingly, and delegate all other navigation events to its base class.
  • Use this new ViewPresenter in our application instead of the built-in one.
  • Send the PresentationHint from the view model, we want to see removed from the back stack.

The complete process for a very similar scenario is thoroughly described in Ed Snider's blog post. I strongly encourage you to read it, as it provides some additional insight into the inner workings of MvvmCross navigation, which will prove useful when you find yourself customizing it for your needs.

As already mentioned, we'll start out with creating a new PresentationHint class:

public class RemoveTopViewFromBackStackHint
  : MvxPresentationHint
{ }

Our custom ViewPresenter will need to handle this new PresentationHint, as it will be completely ignored by the built-in ViewPresenter:

public class ExtendedViewPresenter : MvxStoreViewPresenter
{
  private readonly Frame _rootFrame;

  public ExtendedViewPresenter(Frame rootFrame)
    : base(rootFrame)
  {
    _rootFrame = rootFrame;
  }

  public override void ChangePresentation(MvxPresentationHint hint)
  {
    if (hint is RemoveTopViewFromBackStackHint)
    {
      if (_rootFrame.BackStackDepth > 0)
      {
        _rootFrame.BackStack.RemoveAt(_rootFrame.BackStack.Count - 1);
      }
    }

    base.ChangePresentation(hint);
  }
}

For this new ViewPresenter to actually be used, we need to register it instead of the built-in one in our application's setup class:

public class Setup : MvxStoreSetup
{
  protected override IMvxStoreViewPresenter CreateViewPresenter(Frame rootFrame)
  {
    var presenter = new ExtendedViewPresenter(rootFrame);
    Mvx.RegisterSingleton(presenter);
    return presenter;
  }

  // other setup code...
}

Now, everything is ready for sending our new PresentationHint from the view model. Because our code acts on the back stack, we need to wait until the page we want to remove is already in the back stack, therefore we first need to navigate to the next page, and only then send the new navigation hint:

private void OnStartSession()
{
  // code for initializing the session...

  ShowViewModel<SessionViewModel>();
  ChangePresentation(new RemoveTopViewFromBackStackHint());
}

When navigating back from SessionView, the intermediate view with the above ChangePresentation call will be skipped, and the previous page from the navigation back stack will be shown instead.

In a previous blog post I described how to unit test navigation between view models in MvvmCross. I used the following MockDispatcher class for logging navigation events and then asserting them:

public class MockDispatcher : MvxMainThreadDispatcher, IMvxViewDispatcher
{
  public readonly List<MvxViewModelRequest> Requests = 
    new List<MvxViewModelRequest>();
  public readonly List<MvxPresentationHint> Hints = 
    new List<MvxPresentationHint>();

  public bool RequestMainThreadAction(Action action)
  {
    action();
    return true;
  }

  public bool ShowViewModel(MvxViewModelRequest request)
  {
    Requests.Add(request);
    return true;
  }

  public bool ChangePresentation(MvxPresentationHint hint)
  {
    Hints.Add(hint);
    return true;
  }
}

This class doesn't store all the required information to properly test our new type of navigation with page removal from back stack: because ViewModelRequests are stored separately from PresentationHints, there is no way to check whether the order of the two calls was correct. To make that possible the MockDispatcher class needs to be changed, so that both types of events will be stored in a single collection and the order will therefore be preserved:

public class MockDispatcher : MvxMainThreadDispatcher, IMvxViewDispatcher
{
  public class NavigationEvent
  {
    public MvxPresentationHint PresentationHint { get; private set; }
    public MvxViewModelRequest ViewModelRequest { get; private set; }

    public NavigationEvent(MvxPresentationHint presentationHint)
    {
      PresentationHint = presentationHint;
    }

    public NavigationEvent(MvxViewModelRequest viewModelRequest)
    {
      ViewModelRequest = viewModelRequest;
    }
  }

  public readonly List<NavigationEvent> NavigationEvents = 
    new List<NavigationEvent>();

  public bool RequestMainThreadAction(Action action)
  {
    action();
    return true;
  }

  public bool ShowViewModel(MvxViewModelRequest request)
  {
    NavigationEvents.Add(new NavigationEvent(request));
    return true;
  }

  public bool ChangePresentation(MvxPresentationHint hint)
  {
    NavigationEvents.Add(new NavigationEvent(hint));
    return true;
  }
}

The refactoring only slightly changes the assertions for standard back and forward navigation (with and without parameters). At the same time it makes it very simple to assert for our new navigation type as well:

[TestMethod]
public void NavigationWithRemovalFromBackStackTest()
{
  var viewModel = new SetupSessionViewModel();
  viewModel.StartCommand.Execute(null);

  Assert.AreEqual(2, MockDispatcher.NavigationEvents.Count);
  Assert.AreEqual(typeof(SessionViewModel),
                  MockDispatcher.NavigationEvents[0].ViewModelRequest.ViewModelType);
  Assert.AreEqual(typeof(RemoveTopViewFromBackStackHint),
                  MockDispatcher.NavigationEvents[1].PresentationHint.GetType());
}

If the navigation type described in this blog post is not exactly what you are looking for, don't get discouraged. It shouldn't be difficult to use the same approach to achieve the behavior you require.

Copyright
Creative Commons License