This is part 4 in the series Working with Prism 4.
Introduction
At Microsoft ASP.NET Connections March 26- 29 2012 I’ll be presenting a session on Prism development that includes the capabilities covered in this article – the URI-based navigation capabilities of Prism.
In this article I am going to right some wrongs in the code from the past couple articles that I put there to keep things simple and focused on other things for the first few articles. When using the MVVM pattern, one of the major goals is to maintain loose coupling and separation of concerns between views and their view models. But even more important is that different view models should not be coupled to each other unless it is a parent-child kind of relationship, and view models should not be coupled to other views or their own view either. When I set up the code in the previous article to load and switch CustomerEditView instances and their view models, I did it all from the CustomerListViewModel. That code involved using the Prism RegionManager to do the view loading and switching, but it required me to create instances of CustomerEditView and CustomerEditViewModel in the code of CustomerListViewModel. That is a violation of the decoupling goals of MVVM.
To fix this I am going to leverage one of the other features of Prism 4 – view switching navigation. In the previous articles, you have seen two parts of the API of the RegionManager service. One involved the Add and Activate methods of the IRegionManager interface. These allow you to explicitly add a view to a region and activate (or show) it at the appropriate times. You also saw in the last article that the AddToRegion method simplifies the process of adding a view if it will be the only. However, these approaches require you to have a view instance available to pass to the methods. That means the code that makes those calls is inherently coupled to the view instances. If you also need to initialize some state of the view model at the point where you are adding the views, that means you might also be coupled to the view model class in that same code. This is the code I placed in the CustomerListViewModel that really should not have been there.
The sample code for this article builds on the code from the last article. You can download the completed code from Part 3 here. You can download the completed code for this article here.
View Switching Navigation
In Prism 4 the capability was added to have a more implicit and loosely coupled form of view switching that mimics the URI-based navigation of the web. If you think about what a user is doing when they navigate to a particular page on a web site by putting in an address, they are not really requesting a specific implementation of a page, they are requesting a named view that is the relative portion of the URI (minus the site address at the root). Through mechanisms like the ASP.NET Routing Service, that URI could map to any kind of implementation behind the scenes, and the requester need not know what or where it is. They just need to know the logical name of the view they are trying to get to in the form of a URI. Additionally, the web addressing scheme allows you to pass additional information as part of the URI, including parameters to be passed to the receiver of the request.
If a view model or other code is going to cause views to switch out in the user interface, it would be nice if that code had the same degree of decoupling from the implementation of the target views as well as a similar logical addressing mechanism. Prism added a great capability that mimics this to load and switch views within a region based on an extension method that was added to the IRegionManager interface, called RequestNavigate. This method has several overloads, but they all take two key parameters: the region name in which you want navigation to happen (similar to the site root address in the web browser analogy) and the logical name of the view you want to navigate to. The caller of the method does not have to know what view type or instance the request for navigation maps to, it just knows there is a logical view out there that is wants to see.
This capability includes more than just the ability to load a view into a region. You can pass parameters to its view or view model as part of the navigation request, and the view or view model can use that information to initialize itself or to refresh its state. The view or view model can participate in the decision of whether to create a whole new view or to reuse one that has been shown before. The view can also participate in deciding if and when the navigation can be completed, so that it could for example prompt the user to save changes or to allow the view switching. I’ll be showing you how to leverage all of this in this article.
Step 1: Export the views by logical name
The first modification to make is to declare the logical name of the view as it will be requested by a piece of code wanting to navigate to it. The way this is done with MEF is by using the [Export] attribute with a contract name. When a view is first requested within a region, Prism will create a new instance of it, add it to the region, and activate it. Subsequent requests to navigate to that view type will either get a new instance or will reuse the existing instance, base on some interfaces I’ll discuss a little later in the article.
The modifications to the views look like this:
1: [Export("CustomerListView")]
2: [PartCreationPolicy(CreationPolicy.NonShared)]
3: public partial class CustomerListView : UserControl
4: {
5: ...
6: }
Notice the association of the logical name with the class. In this case, they happen to be the same. But there is no requirement for them to match. The PartCreationPolicy is required because by default, MEF creates singleton instances of any class it exports unless you change the CreationPolicy to NonShared as shown. This allows us to have multiple instances of the view if desired.
Do the same for the CustomerEditView:
1: [Export("CustomerEditView")]
2: [PartCreationPolicy(CreationPolicy.NonShared)]
3: public partial class CustomerEditView : UserControl
4: {
5: public CustomerEditView()
6: {
7: InitializeComponent();
8: }
9: }
Step 2: Switch to “View First” construction of the CustomerEditViewModel
In the previous article’s code, the CustomerEditViewModel and CustomerEditView were being constructed and married together by code in the CustomerListViewModel. That is part of what we are trying to fix. It is much easier and better from a development tools perspective (Visual Studio and Blend designers – aka “Blendability”) and code simplicity perspective if the view creates the view model declaratively in the XAML. The CustomerListView and OrdersView were already doing this, but I needed to switch the CustomerEditViewModel so that it is structured the same so that when the view gets constructed (which will happen in the Prism framework with the navigation features we are looking at), the view model is also created and wired up as the view’s DataContext.
1: <UserControl x:Class="Prism101.Modules.Core.CustomerEditView"
2: xmlns:local="clr-namespace:Prism101.Modules.Core"
3: ...>
4: <UserControl.DataContext>
5: <local:CustomerEditViewModel />
6: </UserControl.DataContext>
7: </UserControl>
Step 3: Request navigation to the CustomerListView on initial load
This step is not explicitly required, but just want to show an alternative to the initial adding of the CustomerListView to the main region. In the previous few articles, the Core module’s IModule.Initialize code looked like this:
1: public void Initialize()
2: {
3: var view = new CustomerListView();
4: RegionManager.AddToRegion("MainContent", view);
5: }
Instead, using the navigation features, we could do this:
1: public void Initialize()
2: {
3: RegionManager.RequestNavigate("MainContent", "CustomerListView");
4: }
Not a huge savings there, but at least this code no longer has to be coupled to the CustomerListView type and worry about its instance lifetime. In the next few steps you will see how it can really start to shine in other areas.
Step 4: Remove the view model coupled code
In the last article, I had the following code invoked when the Edit button was pressed in the CustomerListViewModel:
1: private void OnEditCustomer()
2: {
3: IRegion secondaryContentRegion = RegionManager.Regions["SecondaryContent"];
4: bool alreadyExists = false;
5: foreach (var view in secondaryContentRegion.Views)
6: {
7: var custView = view as CustomerEditView;
8: var custViewModel = custView.DataContext as CustomerEditViewModel;
9: if (custViewModel.Customer == SelectedCustomer)
10: {
11: secondaryContentRegion.Activate(view);
12: alreadyExists = true;
13: }
14: }
15: if (!alreadyExists)
16: {
17: CustomerEditView editView = new CustomerEditView();
18: CustomerEditViewModel viewModel = new CustomerEditViewModel { Customer = SelectedCustomer };
19: editView.DataContext = viewModel;
20: secondaryContentRegion.Add(editView);
21: secondaryContentRegion.Activate(editView);
22: }
23: }
This is the code that not only has coupling to the CustomerEditView and CustomerEditViewModel types, it is also doing instance management of those objects, which is really just wrong. Remove all the code in this method.
Step 5: Request navigation to the CustomerEditView for the right customer
Replace the code that was handling editing with the following code:
1: private void OnEditCustomer()
2: {
3: var uriQuery = new UriQuery();
4: if (SelectedCustomer != null)
5: {
6: uriQuery.Add("customerId", SelectedCustomer.CustomerID);
7: }
8:
9: var uri = new Uri("CustomerEditView" + uriQuery.ToString(), UriKind.Relative);
10:
11: RegionManager.RequestNavigate("SecondaryContent", uri);
12: }
Not only is this code much simpler, you can see that there is no type coupling in there at all. The first part of the method uses a helper object from Prism called UriQuery to formulate a query string with a name of customerId and the value from the selected customer. It then concatenates this with the logical view name it wants to request navigation to, and then calls the RegionManager.RequestNavigate extension method, identifying in which region it wants the view to be shown (SecondaryContent in this case). Could not be simpler, right?
Because SecondaryContent happens to be a tab control, it means that each time navigation happens, it could do one of several things:
- Add a new tab to the control on every navigation, regardless of what customer is selected
- Add only one tab and replace its content with the information for the customer identified in the query string (in which case there would not be much point in having a tab control)
- Add a new tab when the customer identified does not already have an active tab, and if it does just activate it
The last option is the one that will be most intuitive for the user, so that is the one we will go with here. But just realize with the mechanisms I’ll be showing shortly, you could do any of these with the exact same RequestNavigate call.
Step 6: Let the CustomerEditViewModel control its own initialization and instancing
In the code that I stripped out of the CustomerListViewModel, the code was not only managing the instancing of the edit view and view model, it was also initializing the state of the view model by setting the Customer property to the SelectedCustomer. Now that Prism is going to take care of creating the view (and implicitly the view model with the view-first construction), you still need a way to initialize the state of the view model. This can easily be done with an interface defined in Prism called INavigationAware. This interface has three members which allow your view or view model to
- Known when it has been navigated to and get access to the query string parameters to initialize state
- Known when it is being navigated away from (to do something like persist its state to a cache, service, or DB)
- Influence whether the view is the target of the navigation or should be reused for navigating to that view type but with different contents
When RequestNavigate is called for a region, the navigation service that does the handling will check the views that are currently contained by that region. If one matches the URI for the view being requested, it will invoke the IsNavigationTarget method to allow that instance to decide whether it should be activated for the navigation to complete. If the view or view model returns true from that, it will be activated if its URI path matches the one being requested. If it returns false, Prism will create a new instance of the view type and place it in the region and activate it. The method is passed a NavigationContext object that contains the URI and parameters for the request to help guide it in its decision to answer the question “are you the one?”
To accommodate the MVVM pattern but not force you to adopt it, whenever Prism looks for an interface implementation on a view, it also checks to see if the DataContext of the view (which should be the view model if doing MVVM) implements it. This allows you to put the logic that supports the interface only on the view model and leave the view as just the structural definition of the visual aspects of the view.
After IsNavigationTarget is called, if it returns true, then the OnNavigatedTo method is called with the same NavigationContext object. This is the chance for the view or view model to initialize itself (possibly based on URI parameters).
The implementation of the INavigationAware interface in the CustomerEditViewModel looks like this:
1: bool INavigationAware.IsNavigationTarget(NavigationContext navigationContext)
2: {
3: string custId = navigationContext.Parameters["customerId"];
4: if (!string.IsNullOrWhiteSpace(custId) && Customer != null && Customer.CustomerID == custId)
5: return true;
6: return false;
7: }
8:
9: void INavigationAware.OnNavigatedTo(NavigationContext navigationContext)
10: {
11: // Choosing to not refresh the customer every time the view is loaded
12: if (Customer != null) return;
13: // Initial load - Load customer based on ID passed in
14: string custId = navigationContext.Parameters["customerId"];
15: if (string.IsNullOrWhiteSpace(custId)) return;
16: CustomersDomainContext context = new CustomersDomainContext();
17: EntityQuery<Customer> custQuery = context.GetCustomersQuery().Where(c => c.CustomerID == custId);
18: context.Load(custQuery, OnCustomerLoadCompleted, null);
19: }
20:
21: private void OnCustomerLoadCompleted(LoadOperation<Customer> obj)
22: {
23: if (obj.HasError)
24: {
25: MessageBox.Show("Error loading customer: " + obj.Error.Message);
26: obj.MarkErrorAsHandled();
27: return;
28: }
29: Customer = obj.Entities.FirstOrDefault();
30: }
31:
32: void INavigationAware.OnNavigatedFrom(NavigationContext navigationContext)
33: {
34: }
You can see that the logic for IsNavigationTarget checks to see if the query string parameter customerId matches the CustomerID property of the Customer that the view model is managing. If you simply wanted to reuse the view and view model instance and only have one instance of that view type (for containment in a ContentControl for example), you could just hardcode a true return value from the IsNavigationTarget. But in our case we are going to have multiple instances of this view type in a tab control, so we want to use the IsNavigationTarget to drive the logic that if the navigation request is for a customer for which there is already a view, just activate that view. If not, create a new instance of that view type and initialize it based on the customerId.
So the OnNavigatedTo method checks to see if the Customer is already set, this view would not be navigated to unless the IsNavigationTarget had found that the Customer matched the request, so there is no initialization needed (unless you wanted to always refresh the customer data from the back end for example). But if the Customer is null, it is the first time activating this view for a given CustomerId, so the code uses RIA Services to load a Customer from the back end. This approach has the additional advantage that the views are more self-deterministic – instead of requiring some other code to know how to get a Customer and shove the right one into the view, the view (or better yet its view model) knows how to go get the right state based on nothing more than a state identifier of some sort that is placed in the query string. This allows you to move the views around within the app, change who their parent view/view model is, and not have to change anything about how the view gets initialized.
If you are concerned that this pattern will make you make more round trips to the back end, all you need to do is adopt the Repository pattern and have your view models call the repository with their state identifiers, and have the Repository cache the data client side and encapsulate what the cache invalidation scheme is so the views don’t have to worry about it and can just request their current state at any time.
Finally, in this case, we have no logic to perform in the OnNavigatedFrom method so we can just leave that one blank.
Step 7: Let the view decide when navigation happens
There is a very important but subtle point in the way the RequestNavigate method is named. It is a request for something to happen, but it doesn’t say when it should happen or make the implication that navigation will be complete when the method returns. Navigation needs to be a non-blocking request because the view that is currently presented might need to do something (such as prompt the user to save) before allowing the navigation to complete. That is inherently a non-deterministic and potentially long-running block (the user might be distracted tweeting in another window for example). Additionally, a view needs to be able to reject navigation because it may be in the middle of an operation it cannot cancel immediately or you may want to let the user decide whether to dismiss a view for example.
So the RequestNavigate method is a non-blocking asynchronous call. If the caller needs to know when navigation is complete, there is an overload that takes a callback reference that will be invoked by the Prism navigation service once the navigation happens or is rejected. If the view does want to decide if and when navigation should occur, it (or its view model) implements an interface called IConfirmNavigationRequest. This interface has one method, ConfirmNavigationRequest, which is passed a NavigationContext like the INavigationAware methods, and a callback for the method to invoke when it has decided to either allow the view to be deactivated (navigated away from) or to reject the navigation.
In the same application, I chose to demonstrate this by using the IsDirty state of the edit views (the same one that is enabling and disabling Save commands as discussed in the last article). If the view state IsDirty, then the view model will prompt the user with a message box to let them confirm or deny navigation with an OK/Cancel action.
The implementation of this interface is shown below. You can see that is IsDirty is true, it prompts the user. If they press OK, the callback is invoked with a true argument, meaning navigation can proceed. If Cancel, then it gets invoked with false.
1: void IConfirmNavigationRequest.ConfirmNavigationRequest(NavigationContext navigationContext,
2: Action<bool> continuationCallback)
3: {
4: if (IsDirty)
5: {
6: string prompt = "The view's state has changed and has not been saved, do you want to allow view switching?";
7: var result = MessageBox.Show(prompt,"Confirmation",MessageBoxButton.OKCancel);
8: if (result == MessageBoxResult.OK)
9: {
10: continuationCallback(true);
11: return;
12: }
13: else
14: {
15: continuationCallback(false);
16: return;
17: }
18: }
19: continuationCallback(true);
20: }
Now the one hitch in the sample application is that because these views are presented in a tab control, the user can click on a tab header at any time and cause view switching to occur and the Prism navigation service would not be involved at all, so your view implementations of the INavigationAware and IConfirmNavigationRequest interfaces would not be called. To intercept and prevent tab switching would require a lot lower interception of events on the tab control. So this mechanism for preventing navigation is better suited for containment in a ContentControl where the user does not have a direct user interface mechanism to cause views to swap. But as long as the changing occurs through the navigation controls you put in place that call RequestNavigate, life will be good.
Summary
In this article, you learned how to leverage the navigation capabilities of Prism in a more loosely coupled and flexible way with the IRegionManager.RequestNavigate method, INavigationAware and IConfirmNavigationRequest interfaces. These capabilities allow you to compose a much more loosely coupled application where no code is explicitly tied to view or view model types. The view models can be designed to operate independently of their containers and the code that causes navigation or view switching to happen does not have to be coupled to what the specific implementation of the view is or how it needs to be initialized. This approach still gives you control over when views are created and added to regions and activated, but puts the control in the hands of the views themselves instead of the code that triggers the navigation.
This capability can also be mixed in with the Silverlight navigation framework as shown in this article by Karl Shifflet.
You can download the completed code for this article here.
About the Author
Brian Noyes is Chief Architect of IDesign, a Microsoft Regional Director, and Silverlight MVP. He is a frequent top rated speaker at conferences worldwide including Microsoft TechEd, DevConnections, VSLive!, DevTeach, and others. Brian worked directly on the Prism team with Microsoft patterns and practices and co-authored the book Developers Guide to Microsoft Prism 4. He is also the author of Developing Applications with Windows Workflow Foundation, Smart Client Deployment with ClickOnce, and Data Binding in Windows Forms 2.0. Brian got started programming as a hobby while flying F-14 Tomcats in the U.S. Navy, later turning his passion for code into his current career. You can contact Brian through his blog at http://briannoyes.net/ or on Twitter @briannoyes.