22 September, 2010

WP7: NavigationService support when using MVVM

Update (30.03.2011): See this post for a MVVM-framework with built-in support for navigation based on the below: http://blog.clauskonrad.net/2011/03/simplemvvmtoolkit-in-improved-version.html

The MVVM pattern has an inherent problem when it comes to navigation. Should the ViewModel (VM) be responsible for navigation to and from the neighboring Views (and thereby know about them!) or should we have a dedicated class responsible for this navigation? With Separation of Concern (SoC) in mind; I would advocate for using a dedicated ApplicationController to navigate between the different Views in the application.

As an example; let’s pretend that a User clicks a button in say View1 to navigate to View2; should the View1 know where the View2 exists? Or even that it exists? I don’t see that it is a problem for the Views to know what neighboring views that might exist in an application ? This is where the ApplicationController comes into play. This dedicated class has the single responsibility of knowing all views and where they reside in this context.

View
Responsible for how to display data (rendering of UI). Does not know anything about the ViewModel.

ViewModel
Responsible for what data to display (interacts with the business layer for data). Does not know anything about the Views.

ApplicationController
Absolutely part of the UI-layer, but knows how to navigate between the different Views. This controller knows the whereabouts of the different Views in the Application.

As seen from the below figure – a single ViewModels exists for each View (1:1). These VM’s delegates the task of navigating from say View1 –> View2 to the ApplicationController and thereby leaves responsibility to this controller.

image

Conceptual view of architecture.

That’s all grand – so where is the problem here?
The problem lies in the way Silverlight and MVVM works together. The NavigationService is only known to the View as it is a property of PhoneApplicationPage from which the Views inherit, and the ViewModel has no concept of this NavigationService (the ViewModels are just plain clr-classes). Due to this reason, the ViewModel nor the ApplicationController can perform navigation on it’s own. We need some way of informing the ApplicationController of the NavigationService provided by the Views. How to do this in a neat way?

This is the ApplicationController implementation. As seen in the NavigateTo method, the ApplicationController takes out the NavigationService from the Application.Current.RootVisual class. It now has access to the NavigationService and can call .Navigate on this service.

public enum ViewType{Home, Session, Statistics}

public class ApplicationController
{
static ApplicationController m_instance;
Dictionary<ViewType, Uri> m_Views;

ApplicationController()
{
m_Views = new Dictionary<ViewType, Uri>();

//register views with controller
Register(ViewType.Home, new Uri("/", UriKind.Relative));
Register(ViewType.Session, new Uri("/ExerciseView.xaml", UriKind.Relative));
Register(ViewType.Statistics, new Uri("/StatisticsView.xaml", UriKind.Relative));
}

public static ApplicationController Default
{
get
{
if(m_instance == null)
m_instance = new ApplicationController();

return m_instance;
}
}

void Register(ViewType type, Uri address)
{
if (m_Views.ContainsKey(type)) //update
{
m_Views[type] = address;
return;
}

m_Views.Add(type, address); //add
}

void UnRegister(ViewType type)
{
if (m_Views.ContainsKey(type))
m_Views.Remove(type);
}

public void NavigateTo(ViewType type)
{
if(!m_Views.ContainsKey(type))
return;

Uri address = m_Views[type];

//magic code here!
PhoneApplicationFrame root = Application.Current.RootVisual as PhoneApplicationFrame;
Debug.Assert(root != null, "Root is null");
root.Navigate(address);
}



The ViewModels interact with the ApplicationController in this way:

public class MainViewModel : ViewModelBase
{

/// <summary>
///
Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel()
{
if (IsInDesignMode)
{
// Code runs in Blend --> create design time data.
}
else
{
//bind Command to action
NavigateToNewSession = new RelayCommand(() => SendNavigateToSessionCmd());
NavigateToStatistics = new RelayCommand(() => SendNavigateToStatCmd());
}
}


public string ApplicationTitle
{
get
{
return Constants.APPNAME;
}
}

public string PageName
{
get
{
return "Welcome";
}
}

public ICommand NavigateToNewSession { get; set; }

void SendNavigateToSessionCmd()
{
//move to session page
ApplicationController.Default.NavigateTo(ViewType.Session);
}

public ICommand NavigateToStatistics { get; set; }

void SendNavigateToStatCmd()
{
//move to statpage
ApplicationController.Default.NavigateTo(ViewType.Statistics);
}
}



And finally the XAML from the View binding the View to the ViewModel is seen here:

            <Button Content="Start" Height="72" HorizontalAlignment="Left" Margin="100,156,0,0" VerticalAlignment="Top" Width="255" >
<
Custom:Interaction.Triggers>
<
Custom:EventTrigger EventName="Click">
<
GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding NavigateToNewSession}"/>
</
Custom:EventTrigger>
</
Custom:Interaction.Triggers>
</
Button>



Note: See this post for how to make ICommand support in SL3 and WP7: http://blog.clauskonrad.net/2010/09/wp7-and-missing-icommand-support-on.html


As seen from the above – the View is bound to the ViewModel’s ICommand property (called ‘NavigateToNewSession’). Now the ViewModel is invoked when ever the Button is clicked and the ViewModel asks the ApplicationController to move to another View (where ever that might reside!).


To put it in another way: nobody knows anything about anything ;-), but only knows about what they need to do. This is really SoC in a nutshell: “I’m only interesting in knowing that I need, to be able to fulfill my job in the world”.

Clean and (not so) simple!



8 comments:

Anonymous said...

Thanks for a very nice article. It really presents a nice approch with Separation Of Concern and spliting the UI and UIlogic in a smart way.

Anonymous said...

Thanks for the article it solved my problem in a very nice way. The thing i like is that the navigation doesn't use strings (at least from the ViewModels) so there's less chance of breakage if you rename something.

Nitro52 said...

Do you know a good way that you could pass generic objects between views? I'm tried modifying your sample by adding json serialization, the problem is knowing what type to deserialize it as. Then I tried storing it in the ApplicationController but had similar issues

The idea of course being that you can pass any type of object between views.

Claus Konrad said...

I completely see what you are attempting; I too had the same "concerns" during development.
The thing is though; that the native SilverLight NavigationService implementation lends itself towards the "query-string" pattern (if such exists;-)), which advocates that you only pass simple entities (like strings and id's) as parameters.
You have never seen any webpage pass large complex objects in the querystring? If you have - I would say it is a very bad practise. The URL you can send has a limitation (2083 chars) so it is in all respects a bad idea to use the querystring as vehicle of objects (other than simple strings and ids).

So what to do???
I'm using a single instance (singleton) for storing my state during program execution. This object holds all relevant state I need to be able to operate from each View (they all have a common Initialize-method). In addition - you need to remember that when the user flips out of your application you would like to be able to return to the state he left the application in. This makes him perceive that the application is always running, even it isn't. When the application is deactivated - store this state object on disk and reload it when you are activated again. In this way - the user sees the application as running all the time (even it isn't).

The stateobject is a simple class which is stored in a table in the JubbaDB-database (see elsewhere for info).
It works quite well I should say ;-)

Nitro52 said...

Hmm good point, I wasn't aware of the character limit but it makes sense, really all i would be using it for is to pass small objects that a user has selected. Storing it to disk does have advantages when managing state. The question is how does the view your navigating to know that the object stored on the disk is actually from this session and isn't from the user doing something strange.

The scenario i have is that i am trying to pass some server details to another view, that view will then use this to connect to a WCF Service. In this case you probably wouldn't want it to show the connection screen again when if the user leaves and comes back to the application.

Really i'm just trying to think of the most maintainable way to do this. query strings don't really seem maintainable when they are scattered throughout your program

Mauri said...

Thanks a lot for this post. I have a silly question though: how can I simulate the NavigateBack function?
Adding a "Home" ViewType doesn't help cause the root.Navigate will just add the Home page as new item in the navigation history breaking the use of the back hardware button...

Claus Konrad said...

The built-in NavigationService exposes a method called 'GoBack'. This method should do the trick for you...?

Anonymous said...

It's really helpful. Thanks a lot.

InRiver: Not loading your extensions?

(You really need to in the loop to appreciate the issue this post addresses). Man, I've been fighting this problem for hours before I ...