Practical .NET
Managing WPF and Prism Modules
Windows Presentation Foundation with Prism and Unity makes assembling applications at runtime from loosely coupled Modules easy -- provided you don't have competing Modules and don't need to communicate between them. Here's how to solve those two problems.
In last month's column I showed how the combination of Microsoft Windows Presentation Foundation (WPF) and the Unity and Prism frameworks allowed you to build applications as a set of loosely coupled Modules that assembled themselves at runtime.
That article concentrated on implementing the application's UI using a view and view model (as defined by the Model-View-ViewModel, or MVVM, pattern). In that article I let the view (a WPF UserControl) specify where it would be loaded into the main application's window.
There are still two critical topics left to address: How the application can manage the UI, instead of letting the individual views decide where they'll appear, and how Modules can communicate with each other.
While letting the views specify where in the UI they should appear is certainly easy, the safer way is to let the application manage it. If a particular installation of the application has two Modules that request the same spot in the UI, it's not obvious what the result will be. In my case study, I have two versions of a view that lists customers: one view provides a simple list of customer names, and the other provides more detail about each customer. By having the application manage the UI, users can be given the choice of which view they want.
Once the user selects a customer, a second view displays a list of orders for that customer. As a result, I need the view model behind the customer view to communicate to the order's view model that the customer has changed, as well as pass the Id of the selected customer (see Figure 1). This allows the order's view model to update its view with the appropriate orders.
[Click on image for larger view.] |
Figure 1. The sample application juggles three independently developed Modules (two different customer views at the top and the order view at the bottom), while allowing them to communicate with each other. |
Registering the Views
In a WPF-Prism-Unity application, the Shell divides its UI up into regions. A region is any one of the XAML controls that implements the IRegion interface and is tagged with the RegionName attribute from the codeplex.com/prism namespace. Listing 1 uses two ContentControls to define two regions: one called CustListRegion and the other called OrdListRegion.
With the regions defined, the Shell can now use the Prism-provided RegionManager to retrieve views (WPF UserControls defined in a Module) from the Unity container and load them into the UI.
The first step in that process is to register your views with the Unity container in the IModule class in the view's project (I always call this class Initializer). The IModule class's constructor can be passed references to many Prism objects, including the Unity container, provided you specify the right parameters to the constructor. For instance, to be passed a reference to the Unity container, you only need to specify a parameter of type IUnityContainer in the class's constructor. Typically, in the constructor I save a reference to the container to a field so that I can use it later:
private Microsoft.Practices.Unity.IUnityContainer cont;
public Initializer(Microsoft.Practices.Unity.IUnityContainer container
)
{
cont = container;
}
Prism will automatically call the class's Initialize method (part of the IModule interface) with the expectation that you'll register your Module's classes in that method. To make your Module's view available to the Shell you'll need to do several things.
First, you'll need to instantiate your view and its related view model (in this example I've called the view CustListView and the view model CustListVM). Then you'll need to set your view's DataContext property to your view model. That's what this code does:
public void Initialize()
{
CustListView cl = new CustListView();
cl.DataContext = new CustListVM();
The next step is to register this instance of your view with Unity. You need to create a LifetimeManager to use as part of the registration process. When you register your view you pass your view's type, the name you want to use to refer to this instance of the view, the actual instance of the view and the LifetimeManager. This code registers an instance of my view as a UserControl with the name CustListView:
Microsoft.Practices.Unity.ContainerControlledLifetimeManager cclm2 =
new Microsoft.Practices.Unity.ContainerControlledLifetimeManager();
cont.RegisterInstance(
typeof(System.Windows.Controls.UserControl),
"CustListView", cl, cclm);
For the type parameter, I could've used the view's actual type (CustListView). However, as I'll show later, that would require the Shell to have a reference to my Module project. To keep my application and its Modules loosely coupled, I've chosen to use the more generic UserControl. Also, I could've used a different name than my class name (CustListView), but that can confuse Prism.
Retrieving the View
Now, in the code file for the window that forms your Shell, you can retrieve the view you want to display in your UI and attach it to the region you want it displayed in. The first step is to retrieve a reference to the instance of the view you registered with Unity using the Prism ServiceLocator. The ServiceLocator has a static method called GetInstance that finds and returns instances from the Unity container. You need to specify the type or interface the instance was registered with (in my example, UserControl) and the name (CustListView), as this code does:
UserControl clv = Microsoft.Practices.ServiceLocation.ServiceLocator.
Current.GetInstance<UserControl>("CustListView");
Had I registered my view under its actual type (CustListView), I would've had to use CustListView as the type passed to GetInstance. That, in turn, would force me to add a reference to my view project to my Shell project's References list. By using UserControl, I don't need to add any references to my application Shell and I can leave the Shell and the Module loosely coupled.
The next step in adding a view to the Shell's window is to retrieve a reference to the Prism RegionManager. Unlike the IModule class in a Module project, the Shell isn't passed a reference to the RegionManager. But the good news is that the RegionManager is automatically added to the application's Unity container, so you can retrieve the RegionManager using GetInstance.
Once you've retrieved the RegionManager, you can use its regions collection to retrieve the region to which you want to add the view (in my case, as specified back in Listing 1, the region is called CustListRegion). You can then add your view to the region using the region's Add method, specifying a name for the view to be used in the collection.
Finally, you activate the view within the region. In order for all of this to work, of course, you need to put the code that adds the view in an event that fires after all of the Modules have been loaded into the Unity container -- the window's ContentRendered event works well:
Microsoft.Practices.Prism.Regions.IRegionManager rm =
Microsoft.Practices.ServiceLocation.ServiceLocator.
Current.GetInstance<Microsoft.Practices.Prism.Regions.IRegionManager>();
Microsoft.Practices.Prism.Regions.IRegion clr =
rm.Regions["CustListRegion"];
clr.Add(clv, "CustListView");
clr.Activate(clv);
You have some options here. If you need the ability to mix and match views and view models, you don't have to attach your view model to your view in the Module's IModule class. There's nothing stopping you from registering the view and view model separately in the Module's IModule class. You could then retrieve both the view and the view model from the Unity container and put them together in the application's Shell.
If you're letting the views control which region they're displayed in (as I did in last month's article) and end up with multiple views in the same region, you don't need to do anything -- the region will display the first view added to it. While I've used a ContentControl to define my region in my case study, you can also use an ItemsList to let your region display all the views added to a region in the order they were added.
If, in your code, you add multiple views to a region and activate several of them, you can set a precedence among your views by adding the ViewSortHint attribute to the XAML code file. The ViewSortHint accepts a single string, which Prism uses in a simple alphabetic sort to order the views: The region will display the first view in the sort order or, in an ItemsList, display the views in that order. This example adds a ViewSortHint to the class for a XAML file named CustListView (ViewSortHint doesn't seem to work if the views add themselves to a region):
[Microsoft.Practices.Prism.Regions.ViewSortHint("10")]
public partial class CustListView : UserControl
{
If you want to have the application control which of several different views to display in a region, you can add several views to a region and then retrieve the view you want to display and activate it. That name of the view to display could be read from a configuration file or set through some user action. The code here adds two different views under different names, then retrieves one of them and activates it:
Microsoft.Practices.Prism.Regions.IRegion clr = rm.Regions["CustListRegion"];
UserControl clvs = Microsoft.Practices.ServiceLocation.ServiceLocator.
Current.GetInstance<UserControl>("CustListViewShort");
clr.Add(clvs, "CustListViewShort");
UserControl clvl = Microsoft.Practices.ServiceLocation.ServiceLocator.
Current.GetInstance<UserControl>("CustListViewLong");
clr.Add(clvl, "CustListViewLong");
UserControl vw = (UserControl)clr.GetView("CustListViewLong");
clr.Activate(vw);
In this scenario, however, you should consider using the Prism Navigation API, which simplifies managing multiple views in a region. I'll look at the Navigation API in an online column this month.
Communicating Between Modules
One problem remains in implementing my case study: When the user selects a customer in either of the CustomerListViews, I need to run the GetOrders command in the OrdListVM. Prism provides several mechanisms for implementing this feature, including using WPF Routed and Delegate Commands.
Prism also includes Composite Commands, which combine several Commands into one: When the Composite Command or any of its subsidiary Commands is executed, all of the subsidiary Commands are executed. Prism also provides loosely coupled events as a way of communicating between Modules, which is what I'll use here (you can also use standard events, but that would require references between the Modules and the Shell).
The first step in creating a loosely coupled event is to define your event by creating a class that inherits from the Prism CompositePresentationEvent (the base class takes care of all the work involved in integrating the event with Prism). The CompositePresentationEvent is a generic type that allows you to specify the kind of information passed in the event. This code defines a class called CustomerSelectedEvent and specifies that the event will pass a string:
public class CustomerSelectedEvent : Microsoft.Practices.Prism.
Events.CompositePresentationEvent<string> {}
This event must be defined in a project that's referenced by all the Modules that use the event. To avoid tightly coupling the Shell and its Modules, create a separate project to hold these kinds of shared resources. You can then have your Shell project and all your Modules reference that project (I think of the project with these shared resources as the application's "infrastructure" project).
In my case study, when the user selects a customer in the CustListView, I execute a WPF command. It's the Execute method of that command that needs to publish this event, passing the CustomerId of the currently selected customer. Publishing the event requires access to the Prism EventAggregator.
You have two ways of acquiring a reference to the EventAggregator. First, as with the UnityContainer, you can pass a reference to the EventAggregator to the constructor for the Module's IModule class -- just specify IEventAggregator as one of the parameters to the class's constructor. However, in the case study, accepting the reference in the IModule class forces me to pass the reference to the UserControl's ViewModel class, because that's where I instantiate the command. Then, in the view model, I'd have to pass the EventAggregator reference to the command when it's instantiated.
It's a lot easier (though less efficient) to use the ServiceLocator GetInstance method to retrieve the EventAggregator by adding this code to the command's Execute method:
public void Execute(object CustomerID)
{
Microsoft.Practices.Prism.Events.IEventAggregator ea =
Microsoft.Practices.ServiceLocation.ServiceLocator.
Current.GetInstance<Microsoft.Practices.Prism.Events.IEventAggregator>();
Whichever way you get the EventAggregator, you then use its GetEvent method to retrieve the event. Once you have the event, you call its Publish method, passing any information, to notify other Modules that something interesting has happened. This code retrieves the CustomerSelectedEvent and publishes it, passing the CustomerID value that was passed into the Execute method:
CustomerSelectedEvent cse = ea.GetEvent<CustomerSelectedEvent>();
cse.Publish(CustomerID.ToString());
Modules interested in the event must subscribe to it. Again, you need to use the EventAggregator object and its GetEvent method to retrieve the event. This time, though, you use the event's Subscribe method, specifying the method to be called when the event is published. In addition, if you intend to update the UI, you need to specify that the event is to be received on the UI thread (the default is to receive the event on the publisher's thread, which won't permit you to update the UI). The method tied to the event must accept the parameter used when the event was defined.
This code, in the constructor for the orders view, retrieves the event and ties it to a method called GetOrders (because the CustomerSelectedEvent definition specified that it will pass a string, the GetOrders event accepts a string). As I'll be updating the UI, I've specified the UI thread:
CustomerSelectedEvent cse = ea.GetEvent<CustomerSelectedEvent>();
cse.Subscribe(GetOrders,
Microsoft.Practices.Prism.Events.ThreadOption.UIThread);
public void GetOrders(string CustomerID)
{
WPF-Prism-Unity enables two key requirements for modern applications: It lets multiple teams work on the same application and also supports test-driven development. In other words, it lets you deliver more-reliable applications earlier.
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/.