Introduction
It’s like the punch line of an old joke: “What, you again?” Yes, it’s Prism update time again, and this time we’re up to Version 4, which brings Prism in line with the current numbering of the .NET Framework. The good news is that this upgrade is pretty worthwhile, with improved navigation, MVVM guidance, and a Service Locator that allows us to use either Unity or the Managed Extensibility Framework incorporated into .NET 4.0.
This article is an update to my earlier article, Getting Started with Prism 2.1 for WPF. Prism 4 includes pretty good documentation and a number of QuickStarts, so I won’t spend a lot of time explaining Prism’s background and theory. The article will focus on how to get a Prism 4 line-of-business application up and running, using WPF and the Unity dependency injection (DI) container. If you need a tutorial before diving into this article, try the Hands-On Lab included with Prism 4.
This article goes a little further than my previous Prism article. The earlier article presented a strictly bare-bones UI—you couldn’t really use it in a production app. This article demonstrates a more sophisticated UI, complete with:
- A Ribbon at the top of the application; and
- Outlook-style TaskButtons in the lower left corner.
You might ask “What’s the big deal?” After all, it’s fairly easy to add a Ribbon and a couple of buttons to the Shell. If the controls are being added by the developer at design-time, that’s true. However, this first-pass approach will result in tight coupling between the Shell and its modules.
Consider the following: If we wanted to add modules to that first-pass application down the road, here is what we would have to do:
- Open the Shell;
- Add a new
TaskButton
control to the Shell;
- Change the
Ribbon
in the Shell;
- Recompile the Shell; and
- Retest the Shell.
And that really defeats the purpose of using Prism, which is designed to keep modules as loosely coupled as possible. Ideally, to add a new module later, we should be able to simply drop it into a designated folder, where Prism will discover it and load it into the Shell, complete with its own TaskButton
and RibbonTab
, along with its views.
That’s what the demo app does—it provides commercial-grade user interaction without coupling the Shell to its modules. That keeps the modules as independent and isolated as possible, so that we can develop each one separately from all of the others. The trick is to have each module load its own TaskButton
and its own RibbonTab
.
This article is a companion to another CodeProject article that I have written, A Prism 4 Application Checklist, which presents a fairly detailed checklist of steps to follow in building a Prism 4 application. I used the checklist to structure the demo app, so the checklist should provide a good walkthrough for the demo, in addition to general guidance on setting up a Prism 4 application. But, before turning to the checklist, let’s take a look at the general structure of the demo app.
The Parts of the Demo App
The UI of the demo app is modeled on Outlook 2010. It uses a custom TaskButton
control, which is documented in my article Create a WPF Custom Control. This control mimics the behavior of the Mail, Calendar, Contacts, and Tasks buttons in the lower-left corner of Outlook’s main window.
An Outlook-style interface offers a great deal of flexibility, and is adaptable to a wide variety of applications. It is particularly useful for applications that need to switch between several modules, working with only one at a time, the way Outlook switches between mail, a calendar, contacts, and tasks. Most business users are familiar with the Outlook UI and should find an app built on it fairly easy to learn.
The Shell: The Shell has four named regions:
The regions behave similarly to their counterparts in Outlook 2010:
- Ribbon Region: This region contains the application Ribbon. The Ribbon itself and its Home tab are hard-coded into the Shell.
- TaskButton Region: This region is used to switch modules. The buttons in this region behave similarly to Outlook’s Mail, Calendar, Contacts, and Tasks buttons.
- Navigator Region: This region is used to navigate among views within the active module. It behaves similarly to Outlook’s Navigator Region. For example, in Outlook’s Mail module, the Navigator Region contains a folder list for various email folders, such as Inbox, Sent Items, and Deleted Items. For simplicity, the demo app loads a view with a
TextBlock
that merely identifies itself.
- Workspace Region: This region contains the views where the actual work is done. For simplicity, the module’s views for this region simply identify themselves, the same as Navigator views.
Modules: The demo app has two modules, Module A and Module B. Each module loads its TaskButton
, its RibbonTab
, and simple views for its Navigator and Workspace regions. Modules are loaded using Module Discovery, which minimizes coupling to the Shell. If you look at the references for the Shell project, you will see that it does not contain references to the module projects. In other words, the Shell has no knowledge of the modules that it hosts.
The TaskButton
controls are loaded and activated at application startup, when modules are discovered and loaded by Prism. Each module’s RibbonTab
and its views are registered with the Unity container, but are not loaded until the user navigates to the module. As controls are loaded for one module (which I refer to as ‘activating’ the module), the controls for the other module are unloaded (the module is ‘deactivated’). All TaskButton
controls remain loaded and active at all times.
Bootstrapper: The third piece to the puzzle is the Bootstrapper, which controls the process of configuring the application at initial startup. The demo app’s Bootstrapper is conventional, and should be self-explanatory.
DI Container: The final element in a Prism app is a Dependency Injection (DI) container, also referred to simply as a Container. A DI container is essentially a factory that can create objects for any type that is registered with the container. If you aren’t familiar with containers, spend a little time learning how they work before continuing on. If you have never used a container before, you will be amazed at the extent to which they simplify creating complex objects.
Prism 4 natively supports two DI containers: Unity 2.0, and the Managed Extensibility Framework that ships with .NET 4. However, Prism is container-agnostic—it can support other containers (such as Windsor Castle), but you will need to find or write an adapter class so that Prism can communicate with the container.
There is a lot of debate over which container is best—I think they are all pretty good, and the choice is largely one of personal preference. I have used Unity for a while now, so the demo app uses Unity 2.0. The demo app should be adaptable to another container without too much difficulty.
MVVM pattern: The demo app follows the MVVM pattern. If you aren’t familiar with the pattern, see Chapter 5 of the Developer’s Guide to Microsoft Prism. The demo app uses the view-first approach to MVVM. The nomenclature is a bit confusing, because in this approach, the View is created first, and either it instantiates its View Model, or another component injects the View Model into the View. In either event, the View must have knowledge of its View M, but the View Model is totally ignorant of the View that uses it. The result is that the View has a dependency on the View Model.
Here is why I prefer the View-first approach: The View Model creates an API that defines a contract between the View Model and any View that uses it. So long as the View complies with this contract, one can change the View without re-opening the View Model. Designers (and clients) are notorious for playing around with Views, and as a result, Views are highly volatile. If we follow the principle that the more volatile component should depend on the less volatile component, then the View should depend on the View Model.
Stated another way, once the API is settled, the client and designer can play with the UI as much as they want without disrupting the rest of the application, so long as they adhere to the contract.
Note that the demo app does not implement View Models for every module view. The Workspace and Navigator views don’t do anything that requires a View Module, and the Ribbon isn’t wired up. So, the only View Models in the demo app are very simple ones for the modules’ TaskButton
controls.
The Application Checklist
If you would like a step-by-step description of how to set up a Prism application, you can turn to the companion article, A Prism 4 Application Checklist. As was noted above, I used the checklist to set up the demo app, so it will give you a good walkthrough of how it was developed.
If you don’t need the walkthrough, then you can continue here, where we will turn our consideration to the specific issues presented in the demo app.
The TaskButtons
The TaskButton
controls are actually fairly straightforward. Each module loads its TaskButton
into the Task Button Region at startup, when the Bootstrapper populates the module catalog and loads the application’s modules. The TaskButton
controls for all modules are available immediately, and they remain activated regardless of which module is active.
TaskButton XAML: Each module’s TaskButton
is defined as a View. The TaskButton
is wrapped in a UserControl, which facilitates adding margins between buttons in the Shell. The UserControl markup is fairly simple:
<UserControl x:Class="Prism4Demo.ModuleA.Views.ModuleATaskButton"
... >
<fsc:TaskButton x:Name="TaskButton"
Command="{Binding ShowModuleAView}"
IsChecked="{Binding IsChecked}"
MinWidth="150"
Foreground="Black"
Image="Images/module_a.png"
Text="Module A"
Margin="5,2,5,2"
Background="{Binding Path=Background, RelativeSource={RelativeSource
FindAncestor, AncestorType={x:Type Window}}}" />
</UserControl>
The UserControl contains a TaskButton
control, a custom control derived from a RadioButton
. The TaskButton
is bound to a couple of View Model command properties:
- The
Command
property manages the navigation that the TaskButton
invokes when clicked. It is bound to an ICommand
object and is discussed in detail later in this article.
- The
IsChecked
property controls whether the button is selected.
It is worth noting at this point that I prefer to use fully-articulated ICommand
objects, rather than the DelegateCommand
feature provided by Prism. That means that each of the commands in my Prism application is contained in a separate ICommand
class, which I store in a separate Commands folder in each project. I like the way that ICommand
classes segregate my command code, and I find that the approach keeps my View Models relatively uncluttered.
Task Button View Models: You can see an example of this approach in the commands for the two modules of the demo project. Each View Model’s constructor calls an Initialize()
method for the View Model. The Initialize()
method instantiates all command properties in the View Model to corresponding ICommand
objects, like this:
#region Command Properties
public ICommand ShowModuleAView { get; set; }
#endregion
#region Private Methods
private void Initialize()
{
this.ShowModuleAView = new ShowModuleAViewCommand(this);
this.IsChecked = false;
...
}
#endregion
And as we saw above, the TaskButton
that uses the View Model is bound to the ShowModuleAView
command property.
Registering the Task Buttons with Prism: Now let’s switch to the module initializer classes. The Initialize()
method for each initializer class registers its module’s Views with Prism. The Task Button View is registered with the Prism Region Manager, so that it will load and become available immediately and remain available throughout the application’s lifetime:
#region IModule Members
public void Initialize()
{
var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
regionManager.RegisterViewWithRegion("TaskButtonRegion", typeof(ModuleATaskButton));
...
}
#endregion
The Shell’s task button region: In the Shell, TaskButtonRegion
is simply a StackPanel
with a BorderControl
to provide the horizontal divider at the top of the region, and an ItemsControl to hold the TaskButton
controls.
The Ribbon Control
The Ribbon
control presents some interesting problems. The Quick Access Toolbar and the Application and Home tabs of a Ribbon
control typically provide functionality that is available across the entire application. To avoid duplication, this functionality should be housed in the Shell. However, modules frequently need to add their own functionality to the Ribbon
, and to avoid tight coupling, this functionality should be located in each module. The solution is to have each module add its own RibbonTab
control to the Ribbon
.
The RibbonRegionAdapter: Unfortunately, the Ribbon
cannot natively host a Prism region. Prism only defines a few region controls, most notably the ContentControl
(for individual views) and the ItemsControl
(for multiple controls). The good news is that Prism can be extended to allow other controls to host regions through the use of Region Adapters. The RibbonRegionAdapter
performs this function for the Ribbon. Region Adapters are documented in Appendix E of the Developer’s Guide to Microsoft Prism. The code for the RibbonRegionAdapter
is included as Appendix A to this article.
The Bootstrapper registers the Region Adapter in a ConfigureRegionAdapterMappings()
override:
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var mappings = base.ConfigureRegionAdapterMappings();
if (mappings == null) return null;
mappings.RegisterMapping(typeof(Ribbon),
ServiceLocator.Current.GetInstance<RibbonRegionAdapter>());
return mappings;
}
Once the Bootstrapper has done its work, Prism can use the Ribbon
as a region. The region is declared as an attached property of the Ribbon
:
<ribbon:Ribbon x:Name="ApplicationRibbon"
Grid.Row="0"
Background="Transparent"
prism:RegionManager.RegionName="RibbonRegion">
Setting up the Ribbon: Note that the Ribbon
’s Application Menu, Quick Access Toolbar, and Home tab are defined in the Shell. In a production application, the Ribbon items would be wired up to ICommand
properties in the ShellWindow
View Model, in the same manner as the TaskButton
objects. To keep things simple, the demo app’s Ribbon
items aren’t wired to anything.
Each module defines a View for the RibbonTab
that it loads:
<ribbon:RibbonTab x:Class="Prism4Demo.ModuleA.Views.ModuleARibbonTab"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ribbon="clr-namespace:Microsoft.Windows.
Controls.Ribbon;assembly=RibbonControlsLibrary"
mc:Ignorable="d"
Header="Module A">
-->
<ribbon:RibbonGroup Header="Group A1">
<ribbon:RibbonButton LargeImageSource="Images\LargeIcon.png" Label="Button A1" />
<ribbon:RibbonButton SmallImageSource="Images\SmallIcon.png" Label="Button A2" />
<ribbon:RibbonButton SmallImageSource="Images\SmallIcon.png" Label="Button A3" />
<ribbon:RibbonButton SmallImageSource="Images\SmallIcon.png" Label="Button A4" />
</ribbon:RibbonGroup>
</ribbon:RibbonTab >
Note that we don’t wrap the RibbonTab
in a UserControl. The View class derives from RibbonTab
, instead of UserControl
, as shown below:
public partial class ModuleARibbonTab : RibbonTab, IRegionMemberLifetime
{
#region Constructor
public ModuleARibbonTab()
{
InitializeComponent();
}
#endregion
#region IRegionMemberLifetime Members
public bool KeepAlive
{
get { return false; }
}
#endregion
}
This approach is not optional. If we were to wrap the RibbonTab
in a UserControl, it wouldn’t appear in the Ribbon when Prism loads it.
The IRegionMemberLifetime interface: Note also that the ModuleARibbonTab
class implements the IRegionMemberLifetime
interface. This interface is provided by Prism, and it controls whether a View is removed from a region when the user navigates away from the View. For example, the RibbonRegionAdapter
acts like an ItemsControl
, in that it can display multiple RibbonTab
controls at one time. Without the IRegionMemberLifetime
interface, it would load the RibbonTab
for Module B, without unloading the tab for Module A, when the user navigates from Module A to Module B. As a result, the user would see both modules’ RibbonTab
controls.
But the behavior we want is for Module A’s RibbonTab
to be unloaded when we navigate to Module B so that only the active module’s RibbonTab
is shown at any time. That’s the task that the IRegionMemberLifetime
interface performs. The interface consists of a single property, KeepAlive
. If we set this property to false
, then the implementing View is unloaded when the user navigates away from it.
As a result, when we click on the Module A Task Button, Module A’s Ribbon Tab appears, and when we click on the Task Button for Module B, the Ribbon Tab is replaced with that of Module B.
The IRegionMemberLifetime
interface can be implemented on a View or on a View Model. In the demo app, I implemented the interface on the View, because the KeepAlive
property value is hard-coded, and we don’t have to interact with the property in code. If we did have code interaction (if we needed to change the value of the KeepAlive
property at run-time), then the interface would be implemented on the View Model.
Registering the RibbonTab controls: The RibbonTab
controls are registered with Prism differently than the TaskButton
controls. We don’t want the RibbonTab
controls to be available until the user navigates to the host module, so we register the RibbonTab
controls with the Unity container, rather than with the Region Manager:
#region IModule Members
public void Initialize()
{
...
var container = ServiceLocator.Current.GetInstance<IUnityContainer>();
container.RegisterType<Object, ModuleARibbonTab>("ModuleARibbonTab");
...
}
#endregion
Note that if you are using the Unity 2.0 container, it has a quirk that you need to deal with. By default, Unity resolves all requests as System.Object
types. To get the correct type when a View is requested, you have to register the type using a type-mapping overload, with System.Object
as the TFrom
type parameter, and the actual type of the View as the TTo
type parameter:
container.RegisterType<Object, ModuleARibbonTab>("ModuleARibbonTab");
Otherwise, your module will load, but at most it will only show the string “System.Object”.
The Workspace and Navigator Views
As I mentioned above, the Workspace and Navigator Views for both modules are simple placeholders. The Workspace and Navigator regions in the Shell are declared as ContentControl
objects, so we could easily skip implementing the IRegionMemberLifetime
interface on these Views—only one View can be shown at a time in a ContentControl
. To make it clear that these Views should be removed when they are not active, I went ahead and implemented the interface on the Workspace and Navigator Views.
Navigation
Prism 4 added a new Navigation API that by itself would justify the hassle of an upgrade:
The demo app uses Prism 4 navigation to load and unload the Views of its modules. The navigation code is triggered when the user clicks a TaskButton
, which is bound to a command property in its own View Model. This View Model is contained in the same module as the TaskButton
, and it can be found in the module’s ViewModels folder.
Here is the XAML that binds the TaskButton
:
<fsc:TaskButton Command="{Binding ShowModuleAView}"
... />
As we noted earlier, the Command
property is initialized in the View Model as an instance of an ICommand
class contained in the module’s Commands folder. The ICommand
class contains the actual navigation code, in its Execute()
method:
public void Execute(object parameter)
{
var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
var moduleARibbonTab = new Uri("ModuleARibbonTab", UriKind.Relative);
regionManager.RequestNavigate("RibbonRegion", moduleARibbonTab);
var moduleANavigator = new Uri("ModuleANavigator", UriKind.Relative);
regionManager.RequestNavigate("NavigatorRegion", moduleANavigator);
var moduleAWorkspace = new Uri("ModuleAWorkspace", UriKind.Relative);
regionManager.RequestNavigate("WorkspaceRegion",
moduleAWorkspace, NavigationCompleted);
}
The navigation requests are relatively straightforward. We simply call IRegionManager.RequestNavigate()
and pass it a region name and a URI object containing the name of the View we want to load.
Communication Between Modules
The app’s TaskButton
s should be single-select. That is, when one in selected, the others should be de-selected. Normally, this would be handled automatically by the TaskButton
object (since it is derived from RadioButton
), but unfortunately, that feature doesn’t work when buttons are inserted into a Prism region. So, we will have to code the task.
The navigation callback method: Note that in the last navigation request, we pass an additional parameter, NavigationCompleted
. This parameter is the name of a callback method that Prism should invoke when navigation is completed. The demo app uses this callback method, together with a Composite Presentation Event (CPE), to implement single-select behavior on the TaskButton
s.
Composite Presentation Events: CPEs are the key to loosely-coupled communication between Prism modules. They enable any Prism component (the Shell, modules) to communicate with any other component, without having direct knowledge of it. CPEs use a Publish/Subscribe model based on event objects that are derived from the CompositePresentationEvent<T>
class. Here is the declaration for the NavigationCompletedEvent
, which the demo app uses to trigger single-select behavior:
using Microsoft.Practices.Prism.Events;
namespace Prism4Demo.Common.Events
{
public class NavigationCompletedEvent : CompositePresentationEvent<string>
{
}
}
CPEs are designed to cross assembly boundaries, which normal .NET events can’t do. Prism’s Event Aggregator provides an event registry to make that possible:
- When a CPE is declared, it is added to the Event Aggregator.
- Prism components that wish to be notified of an event subscribe to its CPE in the Event Aggregator.
- A Prism component raises a CPE by publishing it to the Event Aggregator, which notifies all subscribers to the event.
Since a CPE is used across the entire application, its class is located in the demo app’s Common project, in the Events folder. Each module has a reference to this project, but the Common project knows nothing of the modules that subscribe to it. Accordingly, we can add and remove modules without reopening the Common project, maintaining loose coupling in the application.
A CPE class declaration simply assigns a type to the CompositePresentationEvent<T>
class. The type indicates the ‘payload’ that the event will carry when it is published. In the NavigationCompletedEvent
, the type is a string—we only need to communicate the name of the event’s publisher. But in a more complex app, we could pass a custom type that contained whatever data that needs to be passed with the event.
Implementing single-select behavior: Let’s return to the task at hand. We need to implement single-select behavior on the demo app’s Task Buttons. Here is how we accomplish that task:
First, we declare the CPE, as shown above. Note that we do not have to explicitly register the CPE with the Event Aggregator.
Next, components that wish to be notified when the event is raised subscribe to the event in the Event Aggregator. In the demo app, it is the View Model associated with each Task Button that needs to be notified. So, the View Model subscribes to the CPE in its Initialize()
method:
#region Private Methods
private void Initialize()
{
...
var eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
var navigationCompletedEvent = eventAggregator.GetEvent<NavigationCompletedEvent>();
navigationCompletedEvent.Subscribe(OnNavigationCompleted, ThreadOption.UIThread);
}
#endregion
As we saw above, clicking a Task Button triggers a series of navigation requests in an associated ICommand
object. The last of these requests passes the name of a callback method, NavigationCompleted()
:
var moduleAWorkspace = new Uri("ModuleAWorkspace", UriKind.Relative);
regionManager.RequestNavigate("WorkspaceRegion",
moduleAWorkspace, NavigationCompleted);
Publishing the CPE: The callback method is invoked when the navigation request has been completed, and it raises the CPE by publishing it with the Event Aggregator:
#region Private Methods
private void NavigationCompleted(NavigationResult result)
{
if (result.Result != true) return;
var eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
var navigationCompletedEvent = eventAggregator.GetEvent<NavigationCompletedEvent>();
navigationCompletedEvent.Publish("ModuleA");
}
#endregion
The CPE event handler: When the CPE is published, the Event Aggregator notifies all subscribers to the event—in this case, the Task Button View Models—and those subscribers handle the event. Here is the event handler in Module A:
#region Event Handlers
private void OnNavigationCompleted(string publisher)
{
if (publisher == "ModuleA") return;
this.IsChecked = false;
}
The event handler checks first to see which module published the event. If the event was published from its host module, then the handler does nothing. Its Task Button was clicked, and it needs no changes. But if the event was published by another module, then its Task Button should have its IsChecked
state set to false
. The event handler sets this property in the View Model. The IsChecked
property of the module’s Task Button is bound to this property, so the button deselects. The net result is that all Task Buttons, except the one that was clicked, are deselected.
Is it worth it?: We use the CPE communication model to perform a relatively simple task. But the model is highly scalable, and it can be used to perform tasks of nearly any complexity by developing the appropriate data class to act as the CPE’s payload. If it seems like an overly-complex series of steps to solve a simple problem, consider the benefits that the approach provides:
- First, events can be passed across assembly boundaries.
- Second, and most importantly, Prism components can communicate with each other with minimal knowledge of the other component required. In most cases, project references can be reduced to a single reference to a Common project, which has no knowledge on the modules that depend upon it.
Close attention to the direction of dependencies will preserve loose coupling of all of the components of a Prism application, allowing them to be developed and tested independently of each other. And it is well-known that it is far easier to develop a set of small projects than a single large one.
Conclusion
Hopefully, this article, along with its companion piece, will prove helpful in creating the essential plumbing for Prism View-switching applications. As always, I welcome the peer review of other CodeProject users. Please let me know of any errors that you may find, or any suggestions you would like to make, by posting in the Comments section at the end of this article.
Appendix A: The RibbonRegionAdapter
The code for the RibbonRegionAdapter
used in the demo app is shown below. The class can be found in the Utility folder of the Shell project.
I want to thank Scott, from La Crosse, Wisconsin, who posted his code for a Ribbon Region Adapter on the Code Review web site. This RibbonRegionAdapter
in the demo app is derived from his work.
using System.Collections.Specialized;
using System.Windows;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Windows.Controls.Ribbon;
namespace PrismRibbonDemo
{
public class RibbonRegionAdapter : RegionAdapterBase<Ribbon>
{
public RibbonRegionAdapter(IRegionBehaviorFactory behaviorFactory)
: base(behaviorFactory)
{
}
protected override void Adapt(IRegion region, Ribbon regionTarget)
{
region.Views.CollectionChanged += (sender, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (FrameworkElement element in e.NewItems)
{
regionTarget.Items.Add(element);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (UIElement elementLoopVariable in e.OldItems)
{
var element = elementLoopVariable;
if (regionTarget.Items.Contains(element))
{
regionTarget.Items.Remove(element);
}
}
break;
}
};
}
protected override IRegion CreateRegion()
{
return new SingleActiveRegion();
}
}
}