Windows Phone: Page navigation with parameters
When I was developing a Windows phone app I needed a way to navigate between different pages. The out-of-the-box way of navigating from one page to another is quite simple, however it did not suit my requirements. MSDN suggests to do the following:
Add a “Click” handler for a XAML button and implement the Navigation in the code behind file of the page:
XAML:
CodeBehind:
private void OnEditClicked(object sender, RoutedEventArgs e) { NavigationService.Navigate(newUri("/EditPage.xaml")); }
A full example can be found on MSDN: http://msdn.microsoft.com/en-us/library/windowsphone/develop/ff626521(v=vs.105).aspx
While this works and is straightforward to implement, I looked for a different solution because I was concerned about the following issues:
- Coupling: The view needs to “know” the URI of the target view
- Code behind: I tend to avoid code behind as much as possible and try to do everything in view models and commands.
- Parameters: In my project I want to pass a parameter to the edit view in order to initialize my edit view model. (Windows Phone 7 SDK does allow adding URL parameters, however it would also require code behind to parse the parameter)
Looking for a solution, I’ve stumbled over MVVM Light’s Messenger. I was already using MVVM light as a lightweight IOC container, so using the messenger seemed logical.
- MVVMLight? http://www.galasoft.ch/mvvm/
- Need a good Intro? http://jesseliberty.com/2011/01/04/wpfs-mvvm-light-toolkit-soup-to-nuts-part-i
Using Mvvm Light Messenger for Navigation In short, the Messenger allows you to register an object to receive messages that are sent to the messenger. This functionality will be used to provide a central point where viewmodels can send their navigation requests to. A navigation service will be implemented that registers itself for navigation messages and performs the navigation. Additionally parameters can be passed in the navigation message and any interested ViewModels can register themselfes and receive the parameter as well.
Step 1: Create a Navigation Message
This message will be sent to the messenger and will be received by the NavigationService for navigation as well as by the ViewModel to receive parameters
public class PageNavigationMessage { public PageNavigationMessage(Uri pageUri) { PageUri = pageUri; } public PageNavigationMessage(Uri pageUri, object parameter) : this(pageUri) { Parameter = parameter; } public Uri PageUri { get; private set; } public object Parameter { get; private set; } }
Step 2: Create a Navigation Service
public class NavigatorService : INavigatorService { private PhoneApplicationFrame _mainFrame; public event NavigatingCancelEventHandler Navigating; public NavigatorService() { Messenger.Default.Register(this, message => NavigateTo(message.PageUri)); } public void NavigateTo(Uri pageUri) { if (EnsureMainFrame()) { _mainFrame.Navigate(pageUri); } } public void GoBack() { if (EnsureMainFrame() && _mainFrame.CanGoBack) { _mainFrame.GoBack(); } } public void RemoveBackEntry() { if (_mainFrame.BackStack != null && _mainFrame.BackStack.Any()) { _mainFrame.RemoveBackEntry(); } } private bool EnsureMainFrame() { if (_mainFrame != null) { return true; } _mainFrame = Application.Current.RootVisual as PhoneApplicationFrame; if (_mainFrame != null) { // Could be null if the app runs inside a design tool _mainFrame.Navigating += (s, e) => { if (Navigating != null) { Navigating(s, e); } }; return true; } return false; } }
The service does nothing than navigating, the “magic” part is in the constructor, where it registers itself on the messenger to receive navigation messages.
Step 3: Register the Target View model to receive Messages
In the target view model we’ll also register to navigation messages, however only to receive any navigationparameters:
public EditFeedEntryViewModel(IFeedEntryService feedEntryService) { _feedEntryService = feedEntryService; MessengerInstance.Register(this, ReceiveNavigationMessage); _addFeedEntryCommand = new RelayCommand(ExecuteAddFeedEntry); } //"ReceiveNavigationMessage" also has to be implemented to deal with the parameters correctly: private void ReceiveNavigationMessage(PageNavigationMessage message) { if (message != null && message.PageUri == ViewModelLocator.EditFeedEntryPageUri) { if (message.Parameter != null) { FeedEntry = message.Parameter as IFeedEntrySettings; } else { FeedEntry = _feedEntryService.CreateNewEntry(); } RaisePropertyChanged("FeedEntry"); } }
Clarifications about the names in my project: IFeedEntrySettings is the business object I want to edit. IFeedEntryService is the injected service providing these entries. The view binds to the property called “FeedEntry” and is therefore updated whenever a navigation message is received.
Step 4: Wiring everything up
In my simple project, MVVM Lights ViewModelLocator is used to set up the IOC container. Make sure to register the new NavigatorService. Additionally I’ve exposed the URIs to my pages in the ViewModelLocator as well, in order not to hard-code them in the commands.
Step 5: Add a Command in the XAML
I’ll add the following XAML in the mainPage where I want to have a button to open the edit view:
CurrentFeedEntry is the object in the datacontext that I’ want to edit. I’ll add it as command parameter that will be sent to the edit view model in the end.
Step 6: Prepare the ViewModel:
public class MainViewModel : ViewModelBase { private readonly RelayCommand _editFeedEntryCommand; // Class constructor, create the data context object. public MainViewModel() { _editFeedEntryCommand = new RelayCommand(ExecuteEditFeedEntry); } public IFeedEntrySettings CurrentFeedEntry { get { return ...} } public RelayCommand EditFeedEntryCommand { get { return _editFeedEntryCommand; } } private void ExecuteEditFeedEntry(IFeedEntrySettings feedEntrySettings) { MessengerInstance.Send(new PageNavigationMessage(ViewModelLocator.EditFeedEntryPageUri, feedEntrySettings)); } }
“ExecuteEditFeedEntry” is sending the navigation message that is received by the navigation service as well as the viewModel.
Please note:
I’ve noticed that the target view model did not always receive the parameter. The reason was simple: It has to be initialized before it receives navigation messages. so when navigating to the edit page the first time, it creates the view model just then. Because the navigation message already was handled by the Messenger, the newly created viewmodel does not receive it. The workaround as simple, but ugly: Create and register instances of the viewmodels that you want to navigate to in the ViewModelLocator.
Conclusion: This approach allows me to navigate between views and initialize them with parameters, without having the Views knowing each other. Everything is wired up using commands inside the view models. What I don’t like is to create instances of the view models that I want to navigate to when registering them in the ViewModelLocator. Perhaps I’ll find a a better solution for this in near future.