Practical .NET
Navigating Between Views in WPF and Prism
Prism's Navigation API makes it a lot easier to swap Views in and out of regions in your Window -- assuming you give Prism enough information to do the job right.
If you're creating loosely-coupled composable applications with Microsoft's WPF, Prism and Unity frameworks, as I described in my column for this month [ref: Managing Modules in WPF/Prism Applications], "navigation" means any change to the UI beyond updating the data being display. With the MVVM model, a lot of what counts as "navigation" can be done just by binding XAML controls in the View to ViewModel properties and then updating those properties from code in the ViewModel.
But what if you want to completely replace the View in a Region with a different View? In my column, I provided a low-level way of doing that by adding Views to Regions and using the Region's Activate method to make a View available (there's also a Deactivate method to make a View unavailable.) Writing the code to ensure that the right Views are added to the right region, and the right View is active, would be taxing -- to say the least.
Fortunately, the Region's RequestNavigate method provides an easier way to replace a View: just select the Region you want to update and pass the name of the View you want to have displayed in the Region.
For instance, I might want to have a Button that will allow the user to switch between two Views: a terse list of customer names and a more verbose list that includes additional data. To support that button using the MVVM approach, I add a property of type ICommand to the Window's ViewModel, and in the ViewModel's constructor, stuff a Command class into that property. Then in my XAML, I bind the Button's Command attribute to the property. When the user clicks the button, the Command's Execute method will automatically be run.
In that Execute method, I'd check to see which View is currently being displayed by checking a field which I've called currentView (the following code assumes that the default View is called ShortCustomerView). This code would then toggle currentView between the two View names:
private string currentView = " CustListViewLong";
public void Execute(object CustomerID)
{
if (currentView == " CustListViewLong")
{
currentView = " CustListViewShort";
}
else
{
currentView = " CustListViewLong";
}
Changing Views
Now that I've loaded currentView with the View I want to display next, I need to retrieve the application's RegionManager using Prism's ServiceLocator, and use it to retrieve the Region in my UI that I want to change. This code retrieves the Region called CustListRegion:
Microsoft.Practices.Prism.Regions.IRegionManager rm =
Microsoft.Practices.ServiceLocation.ServiceLocator.
Current.GetInstance<Microsoft.Practices.Prism.Regions.IRegionManager>();
Microsoft.Practices.Prism.Regions.IRegion rgn = rm.Regions["CustListRegion"];
I'm now ready to replace the View in the selected Region. First, I create a Uri object using the name of the View I want to put in the View. I then call the selected Region's RequestNavigate method, passing the Uri I created and providing a callback function that will be executed after the navigation's complete:
Uri vu = new Uri(currentView, UriKind.Relative);
rgn.RequestNavigate(vu, CheckForError);
There's quite a lot going on here: Prism will find the View with the name specified, instantiate it, hook it up with its ViewModel, add the View to the Region, deactivate the View currently in the Region, and load the new View into the Region.
It wouldn't be surprising if something went wrong, which is where that callback function comes in handy. The callback function is passed a NavigationResult object as its only parameter. If the parameter's Result property is false, it indicates that something went wrong in the navigation. The parameter's Error property will contain the Exception object relating to the error. A typical callback function might look like this:
void CheckForError(Microsoft.Practices.Prism.Regions.NavigationResult nr)
{
if (nr.Result == false)
{
throw new Exception(nr.Error.Message);
}
}
To make this work you must register your View with Unity in the IModule class that contains your View, providing enough information to let Prism resolve the View from the name used in the call to RequestNavigate. For instance, when Prism retrieves the View, it will retrieve it as type Object; you need to tell Prism what type your View really is.
The first step in that process is to use the RegisterType method to register your View. When registering your View, you must specify the "from type" as Object because that's what Prism will use to find the View. For the "to type," you must specify your View's actual type (i.e., the name of the UserControl that defines your View). You'll also need to pass a LifetimeManager as part of registering your View.
However, a View isn't much good without its ViewModel; you need to ensure that the View's DataContext property is set to its ViewModel. When registering a type, you can use an InjectionProperty object to specify a property to be set when the object is created, and provide a value for that property.
The first thing I do is create my ViewModel (called CustListVM in the following code), and use it to create an InjectionProperty specifying that this ViewModel is to be used to set the DataContext property of whatever object it's associated with:
CustListVM clvm = new CustListVM();
Microsoft.Practices.Unity.InjectionProperty ip =
new Microsoft.Practices.Unity.InjectionProperty("DataContext", clvm);
With that done, I can register my View (called CustListView) using the same name I used in the RequestNavigate method (the cont variable is a reference to the Unity container that is passed into the IModule class' constructor):
Microsoft.Practices.Unity.TransientLifetimeManager tlm =
new Microsoft.Practices.Unity.TransientLifetimeManager();
cont.RegisterType(typeof(object), typeof(CustListViewShort),
"CustListViewShort", tlm, ip);
So now you don't have to determine what View is currently in the Region or ensure that the right Views are loaded; just register the Views as shown, and use RequestNavigate to get the View you want.
There's more here to talk about: Adding the IConfirmNavigationRequest interface to a ViewModel will let you catch any Navigation request and check to see if the user should be allowed to move to another View, and accessing the NavigationJournal makes it easier to implement a "Return to previous view" button. It's a powerful API, one worth investigating.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.