ViewModel backstack
This article explains how to navigate to the same page with different data while maintaining back button functionality
Windows Phone 8
Contents |
Introduction
When working on a comic application for Windows Phone I encountered a problem that I’ve had in the past and have heard others ran into as well. When on the detailspage of a comic character I have a list of enemies of that character. Those enemies are clickable to load their details. Nothing hard there, but both the first character and its enemies are of the same type and they use the same view and viewmodel to show their data. This isn’t hard to do, the difficult part is using the phone’s back button. After navigating to the same CharacterDetailPage 4 times I would expect the back button to take me back through all the characters I’ve viewed. I've created a solution for this problem and poured it into a Nuget package, hoping that I can help others to solve this problem. In this article I'll explain how to use the package. The demo application described here is included in the GitHub repository together with the package itself.
Adding the package
I'm assuming that you're using some form of MVVM, the sample project uses MVVM Light. In your Windows Phone 8 solution, use the following command in the Package Manager Console or search for ViewModelBackstack in the Nuget GUI
Install-Package ViewModelBackstack
Congratulations, you just did the hardest part of this setup!
The sample application
The sample application is a basic one, it has two pages, a MainPage and a GuidPage. The MainPage only contains some text and a button to navigate to the second page. The GuidPage contains a textblock that is bound to a property on the viewmodel, and a button that simulates navigating to the same page again but loading in different data.
The scenario is this:
MainPage > GuidPage > GuidPage > GuidPage > …
MainVM > GuidVM > GuidVM > GuidVM > …
Follow it the other way around to know how the back button will respond.
The ViewModelBackstack class
ViewModelBackstack is a static class, and not a very big one.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace ViewModelBackstack
{
public static class ViewModelBackStack
{
private static Dictionary<string, string> _viewModelStack;
public static void Add(string key, object value)
{
if (_viewModelStack == null)
_viewModelStack = new Dictionary<string, string>();
_viewModelStack.Add(key, JsonConvert.SerializeObject(value));
}
public static object Take<T>(string key)
{
string toReturn = _viewModelStack[key];
Delete(key);
return JsonConvert.DeserializeObject<T>(toReturn);
}
public static bool TryTake<T>(string key, out T value)
where T : class
{
try
{
value = JsonConvert.DeserializeObject<T>(_viewModelStack[key]);
Delete(key);
return true;
}
catch (Exception)
{
value = null;
return false;
}
}
public static bool ContainsKey(string key)
{
if (_viewModelStack == null)
return false;
return _viewModelStack.ContainsKey(key);
}
public static void Delete(string key)
{
_viewModelStack.Remove(key);
}
public static void Replace(string key, object newValue)
{
_viewModelStack[key] = JsonConvert.SerializeObject(newValue);
}
public static bool CanGoBack()
{
if (_viewModelStack == null)
return false;
return _viewModelStack.Count > 0;
}
public static T GoBack<T>()
{
var toReturn = _viewModelStack.Last();
_viewModelStack.Remove(toReturn.Key);
return JsonConvert.DeserializeObject<T>(toReturn.Value);
}
}
}
It contains a Dictionary<string, string> that will hold the instances of the viewmodels. The instances are serialized into JSON strings with Json.net. This to save memory and avoid reference issues. There are some methods in there to manually take out a specific instance or to delete one. But more importantly are the CanGoBack() and GoBack() methods. Let’s have a look at how to use this.
Usage
In the GuidViewModel’s constructor we start listening for a message, when that message arrives we load in new data (in this case, generate a new GUID) GuidString is a normal property that calls RaisePropertyChanged from the setter.
public GuidViewModel()
{
Messenger.Default.Register<GenerateNewGuidMessage>(this, msg => GenerateGuid());
}
private void GenerateGuid()
{
GuidString = Guid.NewGuid().ToString();
}
Next is the command that is bound to the button on the page, this is a RelayCommand that will call the LoadNewData method
private void LoadNewData()
{
if (ViewModelBackStack.ContainsKey(GuidString))
ViewModelBackStack.Replace(GuidString, this);
else
ViewModelBackStack.Add(GuidString, this);
Messenger.Default.Send(new GenerateNewGuidMessage());
}
The LoadNewData method will check if the ViewModelBackStack already contains the key we use (each instance needs a unique key, we’re using the GUID in this case). If it’s already there, replace it, if not add it to the backstack. After that, send the message to generate new data.
Note that we’re not actually navigating away from the page, since the NavigationService doesn’t actually navigate when you try going to the same page there’s really no use in trying.
The final step is intercepting the back button press and using it load in a previous instance of the GuidViewModel. We need to do this in the code-behind of the page, since we need to cancel the navigation there (by default, when pressing the back button here it would just take us back to MainPage, so navigation needs to be cancelled).
protected override void OnBackKeyPress(CancelEventArgs e)
{
if (ViewModelBackStack.CanGoBack())
{
DataContext = ViewModelBackStack.GoBack<GuidViewModel>();
e.Cancel = true;
return;
}
base.OnBackKeyPress(e);
}
OnBackKeyPress can be overriden from PhoneApplicationPage base class. If the ViewModelBackStack can go back we take out the most recent record in the dictionary, deserialize it to T, set that result as DataContext and we’re done. We can cancel the navigation by setting e.Cancel to true. Once the ViewModelBackStack is empty the app will return to MainPage.
Summary
In this article I've explained how you can leverage the ViewModelBackstack package on Nuget to easily navigate to the same page with different data over and over again while maintaining a correct backstack for the phone's backbutton. I've also explained the inner workings of the package.
Contents
Depechie -
Few questions ( not tried this implementation yet, I'm doing something similar with the ioc container of MVVMLight - maybe for another WIKI ;) )
Depechie (talk) 13:00, 27 November 2013 (EET)
NicoVermeir -
Navigating back: because there is no actual navigation, would this mean page transition effects will not be visible? that's right, you can do an actual page navigation by adding a parameter as querystring to the page URI, if that parameter is unique every navigation the page will actually navigate
Thombstombing: is the ViewModelStack stored to local storage for thombstoning scenario's? no it isn't, you'll need to do this manually (or implement it and send me a pull request on github ;-) )
Depechie -
Thanks for the extra info :)Depechie (talk) 13:50, 27 November 2013 (EET)
Hamishwillee - Still in progress but
Hi
Did you see How to pass a complex object to a page by extending NavigationService on Windows Phone - which is looking at some of the same problems and a very similar solution.
Thoughts?
Regards
Hhamishwillee (talk) 09:13, 2 December 2013 (EET)
Hamishwillee - PS
Er, I guess I should add that the linked article doesn't contain all our thinking - in the latest discussion I've had it is even more similar. In that version of the code we use GUID for key as you do so that no key needs to be explicitly passed by the user (though I'm arguing still that a simple counter would be fine on every navigate operation). The main question was cleanup - we thought perhaps the right way to do this would be to delete the dictionary when on the "main page" IFF the backstack is empty.
We didn't sort out tombstoning yet either though that shouldn't be hard. We didn't use the JSON serialiser either which is a nice refinement.
What I like about Kunal's approach is the use of an extension function so that navigation is almost the same as usual for the developer.
Anyway, I'm interested in whether we can/should merge the articles.hamishwillee (talk) 09:19, 2 December 2013 (EET)
Hamishwillee - From another developer
I got this comment offline - thoughts?
I also don't know how it will handle several pages that require their own backstacks. The example given has two pages: MainPage and a DetailPage called GuidPage. So it handles MainPage => GuidPage(0) => GuidPage(1) => GuidPage(0) without issues (I'm using the numbers to show a circular connection).
Does it deal with MainPage => GuidPage(0) => AnotherPage => GuidPage(1) as that would create a second instance of GuidPage due to the necessary page navigation that isn't expected. Will it work with multiple Pages that need to be managed in that way, e.g. MainPage => GuidPage(0) => GuidPage(1) => AnotherPage(0) => AnotherPage(1).
Looking at the code inside the article after navigating from AnotherPage(1) to AnotherPage(0) the backstack would not be empty (the GuidPage navigations are still on the backstack) so it wouldn't fall back onto the system backstack to properly navigate back to GuidPage.hamishwillee (talk) 03:45, 4 December 2013 (EET)