Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Getting Started with Prism 2.1 for WPF

0.00/5 (No votes)
1 Oct 2010 1  
How to get started with Prism 2.1 for WPF, with a demo app

Introduction

This article updates and replaces my earlier article on Prism 1.0 (more formally known as the Composite Application Library, or ‘the CAL'). The major change in this article is the use of Prism 2.1, and a more detailed explanation of how the application is set up. I have left the older article in place for those still using Prism 1.0.

What is Prism?

Prism is a framework for creating composite applications using WPF or Silverlight. This article covers the creation of WPF composite applications. Silverlight composite applications share common features with their WPF counterparts, but the differences between WPF and Silverlight mean that there will be significant differences in how the two types of applications are built. Nonetheless, if you can build a WPF composite app, you are a long way toward knowing how to build a Silverlight composite app.

Why a Composite Application?

Composite apps were originally developed to help enterprise customers integrate multiple legacy apps into a single user interface, creating the appearance of an integrated system. Let's say a company has two legacy systems. They could create a composite app, similar to the demo app, that would allow both legacy systems to be shown in a single window. They would essentially have to rewrite the user interface on both systems to be displayed in the composite app main window, but if the legacy apps were written with good separation of concerns, any changes to the back-ends of the legacy apps would be minimal.

It wasn't long before architects and developers recognized that composite applications provide definite advantages in building traditional desktop applications. The composite application approach allows a development team to break an application into quasi-independent modules that are loaded into a Shell project. This ability to efficiently partition an application brings several key benefits:

  • Modules can be assigned to different development groups and developed more-or-less independently.
  • Each module can be tested separately, apart from the interactions it has with other modules.
  • Modules can be swapped in and out of the application as needed. For example, if our main menu is created as a module, it is fairly simple to swap it out for a Ribbon module.
  • Maintenance of the program becomes much easier, since changes can often be isolated to specific modules.

Those are the main reasons why the composite application model is becoming quite popular as an architectural pattern for desktop apps, and now, Silverlight apps.

About this Article

This article is written to help you get started creating WPF apps with Prism 2.1. Specifically, it explains many of the fundamental concepts behind Prism, and how to perform the following tasks:

  • Set up Prism 2.1 in a WPF application;
  • Set up communications between application modules with minimal coupling; and
  • Display workspace views on demand

Note that the third point describes displaying views, rather than loading and unloading modules. The two concepts are easy to confuse. The approach described in this article loads all modules (and their views) at launch, and then activates (shows) and deactivates (hides) views on demand.

I am not going to describe everything about Prism 2.1; the Prism documentation does a pretty good job in that department. Instead, I am going to describe a fairly narrow path through Prism 2.1—one way to get an application up and running. Once you understand that path, you can easily begin broadening your knowledge of Prism's other capabilities, to find approaches that may better suit your development needs.

This article assumes that you are familiar with WPF development and the Model-View-ViewModel (MVVM) pattern. The Prism documentation discusses a different pattern, Model-View-Presenter, that seems to be declining in popularity among WPF developers, in favor of MVVM. I have written a CodeProject article on MVVM, which may help you learn more about the MVVM pattern.

Please note that I refer to the Prism 2.0 documentation in this article, rather than the documentation for Prism 2.1. That's because the 2.0 documentation is available for download as a PDF, and the changes in Prism 2.1 are relatively minor.

Your Comments are Welcome

I wrote this article for two reasons:

  • First, I hope it will help other developers climb up the Prism 2.1 learning curve.
  • Second, I am very interested in peer review of the approach I have taken to setting up a WPF application using Prism.

There does not appear to be a general consensus yet as to the best approach for structuring Prism apps. I think I have come up with a pretty good approach to the problem, but there may be other, and better ways of structuring a Prism app and implementing a display-on-demand framework. If you have an alternative approach, or suggestions for improving my existing approach, I welcome your comments. I will update this article from time-to-time to include the best suggestions, with due credit to the author of each suggestion.

The Demo Application

Like the earlier version of this article, the demo application provided with this article, Prism2Demo, is quite simple. It was created in Visual Studio 2008, and it is easily converted to Visual Studio 2010. In this version, the application implements a Windows Explorer interface, with a Navigator pane on the left of the main window, and a Workspace pane on the right. The UI is shown in the screenshot at the top of this article.

The Navigator has two buttons, each of which displays a view from a different workspace module. The demo application performs the minimal amount of work necessary to show how to set up a Prism 2.0 app with display-on-demand. To keep things as simple as possible, the demo does not perform any data access or any real processing of any sort. Each button simply activates a view that displays the name of the module that owns it.

Creating a Prism 2.1 WPF Application

The best way to see how the demo app works is to see, step-by-step, how it was created. So, from this point forward, I will describe the procedure I used to create the demo app, with explanations of the underlying concepts, the actions taken, and the choices made.

Step 1: Create a WPF Solution

Create a WPF solution in Visual Studio, in the normal manner. The project that is created with the solution will serve as the Shell project for the solution.

Step 2: Add References to the Solution Project

Add references to the following Prism 2.1 DLLs to the Shell project:

  • Microsoft.Practices.Composite.dll
  • Microsoft.Practices.Composite.Presentation.dll
  • Microsoft.Practices.Composite.UnityExtensions.dll
  • Microsoft.Practices.Unity.dll
  • Microsoft.Practices.ObjectBuilder2.dll
  • Microsoft.Practices.ServiceLocation.dll

The services provided by each of these DLLs are explained in the Prism documentation at p. 129.

Step 3: Rename the Main Window

Rename the main window in the Shell project to ‘Shell'. This window will be the Shell window for the project. This window will contain regions that will serve as placeholders for the module views provided by the application.

Step 4: Set Up the Application's Bootstrapper

A Prism application launches differently than a regular WPF application. Normally, the App.xaml markup provides a StartupUri that specifies which window should be opened on launch. A Prism app has a more complex launch procedure, which requires the use of a Bootstrapper class.

A Prism Bootstrapper is derived from the UnityBootstrapper that ships as a part of Prism. Unity is an Inversion of Control container provided by Microsoft. It works by default with Prism, but if you prefer a different IOC container, the Prism documentation describes how to write a simple adapter to allow you to use your container of choice. In this article, we will use the default UnityBootstrapper. If you are unfamiliar with Inversion of Control or IOC containers, there are a number of good resources available on the web. Here are a couple I would recommend:

The demo app Bootstrapper overrides three methods provided by the UnityBootstrapper:

  • ConfigureContainer(): This override is used to register services with the IOC container. The demo app uses it to register a ModuleServices class that the demo app's modules will resolve from the IOC container later.
  • CreateShell(): This override is what replaces the StartupUri in App.xaml. It instantiates the Shell window, shows it, and returns it to Prism, so that Prism can work with the Shell.
  • GetModuleCatalog(): This override specifies how Prism should locate the application's modules. There are several approaches, all of which are described in the Prism documentation. The demo app uses the ‘directory search' approach, and that is what I describe in this article.

The primary reason for using the directory search method is to loosen coupling between the Shell project and the modules. Note that the Shell project does not contain references to the module projects, and nothing in the modules is referred to by the Shell project's code. In fact, the Shell project is completely ignorant of the modules it is loading. That means, we can develop and maintain modules without worrying about breaking the Shell. That is a major benefit of loose coupling.

Here is the Bootstrapper code for the demo app:

using System.Windows;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.UnityExtensions;
using Prism2Demo.Common;

namespace Prism2Demo
{
  public class Bootstrapper : UnityBootstrapper
  {
    /// <summary>
    /// Registers types with the Unity container.
    /// </summary>
    protected override void ConfigureContainer()
    {
      base.ConfigureContainer();
      Container.RegisterType<IModuleServices, ModuleServices>();
    }

    /// <summary>
    /// Creates the application shell.
    /// </summary>
    protected override DependencyObject CreateShell()
    {
      var shell = new Shell();
      shell.Show();
      return shell;
    }

    /// <summary>
    /// Populates the module catalog.
    /// </summary>
    protected override IModuleCatalog GetModuleCatalog()
    {
      /* Note that each module in this application has a post-build
       * event that copies the module to a 'Modules' subfolder in the 
       * output folder. Prism will use the DirectoryModuleCatalog that 
       * we create here to scan that folder to populate the catalog. */

      // Create a new module catalog and pass it to Prism
      var catalog = new DirectoryModuleCatalog();
      catalog.ModulePath = @".\Modules";
      return catalog;
    }
  }
}

Note particularly the GetModuleCatalog() override method. We implement the IModuleCatalog interface using a DirectoryModuleCatalog, which will search a specified directory for modules. The catalog object's ModulePath property specifies which directory should be searched, which for the demo app is a subfolder in the Shell project's application output folder. We will describe shortly how we get module assemblies to that folder.

In Prism2Demo, we use a subfolder in the Shell project's application output folder, called Modules. As will be seen below, each module has a post-build event that copies the module to this subfolder so that Prism can find it.

Step 5: Modify the Application Startup

As we noted above, we do not use the StartupUri provided by App.xaml. Instead, we will use its code-behind to call the Bootstrapper on launch. To do that, override the OnStartup() method in App.xaml.cs. The class should look like this:

using System.Windows;

namespace HelloWorld.Desktop
{
  /// <summary>
  /// Interaction logic for App.xaml
  /// </summary>
  public partial class App : Application
  {
    protected override void OnStartup(StartupEventArgs e)
    {
      base.OnStartup(e);
      var bootstrapper = new Bootstrapper();
      bootstrapper.Run();
    }
  }
}

Then, open App.xaml and remove the StartupUri reference, which is replaced by the OnStartup() override in the code-behind file.

Step 6: Create Regions in the Shell

Next, we need to define regions in the Shell window. The demo app implements a Windows Explorer interface, so it has two regions: a NavigatorRegion and a WorkspaceRegion. Here is the XAML markup that declares these regions in Shell.xaml:

<!-- Define regions-->
<DockPanel>
  <ContentControl cal:RegionManager.RegionName="NavigatorRegion" 
                  DockPanel.Dock="Left" Width="200" />
  <ContentControl cal:RegionManager.RegionName="WorkspaceRegion" />
</DockPanel>

WPF controls act as placeholders for the regions. The two that are generally used are:

  • ContentControl: Use this control when the region will hold a single view. The view (which is typically a user control) will expand to fill the region.
  • ItemsControl: Use this control when the region will hold multiple views, such as a list. The views will not expand vertically to fill the region; instead, they will generally be stacked.

In the demo app, we use ContentControls for both regions, arranged in a DockPanel set for last-child-fill. That causes the Workspace region to expand to fill the window when it is resized.

To add regions to the Shell, first add the following namespace declaration to the Shell window XAML:

xmlns:cal="http://www.codeplex.com/CompositeWPF"

Then, add regions to define the layout of the Shell, as shown above. Note that regions can be added in Visual Studio, or in Expression Blend. Layout is defined using regions in the same way as it is defined when laying out a conventional WPF window. The only twist is that each region requires a region name, which is assigned using the RegionManager.RegionName attached property.

Step 7: Create Modules

Modules are the heart of a composite application. A module may have one view in one region, several views in a region, or several views in several regions. So, how does one decide how many modules, and how many views in how many regions? I recommend thinking about how one would partition an application.

  • What are the logical points at which to sort out the various functionalities provided by an application?
  • How does its use cases group together? If the use cases can be grouped under major headings, then those headings probably provide at least initial guidance on how to partition the app.

A typical module, designed using the MVVM pattern, will have several elements:

The module has an Initializer Class, one or more views, and one or more view models. There is typically one view model per view (as in the Navigator module of the demo app), but that is not always the case. Prism calls the Initialize() method of the Initializer Class to perform initialization chores on the module, such as configuring module views and view models and adding views to regions in the Shell. The initialization is done as Prism adds modules to the Module catalog, which we discuss below.

The demo app is, by design, very simple. It has a Navigator and two Workspaces. The major problem posed by the app is that the Navigator has to direct the display of those Workspaces while knowing as little about them as possible.

We have reduced the coupling between the Navigator and the Workspaces to a single string that specifies which Workspace is to be displayed. The Navigator will fire an event that will carry that string as its ‘payload'. But more on that later. First, we have to create our modules; then, we will figure out how to implement loosely-coupled communications between them.

Each module in a Prism app is a separate class library project. Create a project for each module that the application will use. The demo app follows this convention for naming modules:

<SolutionName>.Modules.<ModuleName>

For example, the project in the Workspace A module is named ‘Prism2Demo.Modules.WorkspaceA'.

Each module should contain references to the following WPF assemblies:

  • PresentationCore.dll
  • PresentationFramework.dll
  • WindowsBase.dll

In addition, the module should contain references to the following Prism assemblies:

  • Microsoft.Practices.Composite.dll
  • Microsoft.Practices.Composite.Presentation.dll
  • Microsoft.Practices.Unity.dll

Once that is done, we are ready to create the Initializer Class for the module.

Step 8: Add a Post-Build Event to Each Module

Before we get into the details of the module, we need to make sure that we can get each module's assembly to the location where Prism can use directory search to find them. To accomplish this result, each module will need a post-build event to copy the module assembly from the module project's application output folder to the common Modules subfolder in the Shell project application output folder. The command for the event should look like this:

xcopy "$(TargetDir)<Module DLL>" 
  "$(SolutionDir)<Solution Name>\bin\$(ConfigurationName)\Modules\" /Y

where:

  • <Module DLL> = the module DLL (e.g., Prism2Demo.Modules.Navigator.dll)
  • <Solution Name> = the solution name (e.g., Prism2Demo)

To create the post-build event, open the Properties page for the module project, switch to the Build Events tab, and add the command in the Post-build event command line box. Here is the page for the Navigator project:

Step 9: Add an Initializer Class to Each Module

Now that we can copy the module assembly to the location where Prism can find it, we are ready to begin configuring the module. Each module has an Initializer Class that implements the IModule interface. The demo app follows this convention for naming Initializer Classes:

<ModuleName>Module

For example, the Initializer Class for the WorkspaceA module in the demo app is named ‘WorkspaceAModule.cs'. The simplest way to create the Initializer Class is to rename the Class1.cs class that was created with the module project, add the class attributes described below, and specify that the class derives from IModule.

The initializer class is typically used for such tasks as registering views or adding views to regions. The IModule interface specifies a single method :Initialize(). Here is the Initializer Class for the Navigator module in Prism2Demo:

using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;

namespace Prism2Demo.Modules.Navigator
{
  [Module(ModuleName = "Navigator")]
  public class NavigatorModule : IModule
  {
    #region Fields

    // Member variables
    private readonly IUnityContainer m_Container;
    private readonly IRegionManager m_RegionManager;

    #endregion

    #region Constructor

    public NavigatorModule(IUnityContainer container, 
           IRegionManager regionManager)
    {
      m_Container = container;
      m_RegionManager = regionManager;
    }

    #endregion

    #region IModule Members

    public void Initialize()
    {
      // Register view model
      this.RegisterViewsAndServices();

      /* Note that we instantiate the view directly, since we don't need to inject
       * any dependencies. We use Unity to resolve the view model, since it has a
       * dependency that needs to be injected. Then we use View Injection to add
       * the view to the Navigator region. The view model was registered with the
       * Unity container in the Bootstrapper. */

      // Set up view and view model
      var navigatorView = new MainView();
      navigatorView.DataContext = m_Container.Resolve<INavigatorViewModel>();
      m_RegionManager.Regions["NavigatorRegion"].Add(navigatorView);
    }

    #endregion

    #region Private Methods

    private void RegisterViewsAndServices()
    {
      m_Container.RegisterType<INavigatorViewModel, NavigatorViewModel>();
    }

    #endregion

  }
}

Note that a module's initializer class has one or more attributes that Prism uses when initializing the module:

  • The Module attribute is required; it specifies the name of the module. If the module is to be loaded on demand, rather than at startup, the Module attribute will also contain a second parameter that specifies on-demand loading:
  • [Module(ModuleName = "MyModule", OnDemand = true)]
  • A separate ModuleDependency attribute (not used in the Navigator Initializer Class) is used to specify any modules on which the current module depends. Prism will load the dependency modules first.
  • [ModuleDependency("SomeModule")]

Note also that the Unity container and the Prism Region Manager are constructor-injected and set as member variables. Prism registers an instance of the Region Manager with the Unity container when it starts up, so the Region Manager is immediately available for constructor injection. The initialization class in the Navigator module demo simply registers its view with the Region Manager.

The Initialize() method typically performs three tasks:

  • It registers any views and services that we may want to resolve later, using the Unity container.
  • It performs any needed creation and configuration of the module's views.
  • It adds the modules views to Shell regions.

In the Navigator module, you can see that we register the Navigator view model with the Unity container. We will use Unity to resolve the view model, rather than instantiating it ourselves, in order to take advantage of Dependency Injection. Then, we instantiate the Navigator's view directly (since we don't need to inject any dependencies), set the view model as its DataContext, and add the view to the Shell's Navigator Region. We will discuss this process further below.

Step 10: Add Views to Each Module

Module views are typically WPF user controls. Views are designed like any other WPF user control, using Visual Studio and Expression Blend. Modules may contain multiple views, although each module in the demo app contains a single view. Here is the Navigator module view from the Prism2Demo app:

<UserControl x:Class="Prism2Demo.Modules.Navigator.MainView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Background="Beige">
 <Grid>
  <StackPanel VerticalAlignment="Center">
   <TextBlock Text="Navigator" Foreground="Green" HorizontalAlignment="Center"
           VerticalAlignment="Center" FontFamily="Calibri" 
           FontSize="24" FontWeight="Bold" />
   <Button Command="{Binding ShowWorkspaceA}" 
           Margin="5" Width="125">Show Workspace A</Button>
   <Button Command="{Binding ShowWorkspaceB}"  
           Margin="5" Width="125">Show Workspace B</Button>
  </StackPanel>
 </Grid>
</UserControl>

There are two methods for getting views into regions:

  • View Discovery: We simply register views with Prism's Region Manager and let Prism take care of instantiating and displaying the views.
  • View Injection: We write code to explicitly instantiate and initialize views, and to add them to regions.

The demo app uses view injection, for the following reasons: in the Initialize() method, we use the Unity container to resolve (instantiate) a view model for the Navigator view. As I noted above, the demo app shows, in the Navigator module, an implementation of the MVVM pattern. The Prism documentation is geared more toward the MVP pattern, which seems to be losing favor in the WPF community as MVVM gains prominence.

MVP assumes that either the Presenter will create the View, or that the View will create the presenter. Either approach creates an explicit dependency between the two classes. MVVM assumes a third class will instantiate both the view and the view model, and that the same class will set the view model as the view's DataContext. This results in looser coupling, with only an implicit dependency running from the view to the view model.

View discovery works well with the MVP pattern, where the presenter will create the view, or vice-versa. The Prism documentation contains examples of both approaches. But, it does not work as well with the MVVM pattern, where we need a third class to instantiate the view and view model and bolt them together. So, the demo app uses view injection in its Initialize() method.

The Navigator's Initialize() method directly instantiates the Navigator's view, since we do not need to inject any dependencies into it. Then, it uses the Unity container to resolve the view model (which was registered with Unity in the Bootstrapper), in order to inject the container into the view model. The view model and its components (commands and services) will use the container for various operations later. The view model is set as the view's DataContext, and the view is added to the Navigator Region of the Shell.

At this point, we have a basic Prism application, but it has no real functionality. Now, we need to begin adding that functionality. We will start by creating a project where we can put common elements that will be used across modules.

Step 11: Add a Common Project to the Solution

A functional Prism app will have a number of base classes and interfaces. We centralize dependencies and avoid duplication by placing the base classes and interfaces in a common project. Modules will contain a reference to this project, rather than to other projects or the Shell project. This project is referred to as the Infrastructure project in the Prism documentation. I prefer to call it the Common project, and that is the name of the project in the demo app.

One problem encountered in designing Prism apps is where to locate resource dictionaries that are used by multiple modules. The usual approach of locating them app level of the shell won't work by itself, since Prism modules are more-or-less independent of the Shell. The modules will have to reach across shell boundaries to reach common resource dictionaries. One can use 'pack URLs' to cross these boundaries.

A pack URL is simply a URL that contains a reference to another assembly on the user's machine, typically another project in the same solution. They are rather odd-looking, but they get the job done. I won't spend much time on them here, since they are well-documented on MSDN. Here is an example of a pack URL that references a resource dictionary called Styles.xaml in a Common project of a Prism app. The dictionary is being referenced at the application level of another project in the solution:

<Application.Resources>
    <ResourceDictionary>
       <!-- Resource Dictionaries -->
       <ResourceDictionary.MergedDictionaries>
           <ResourceDictionary Source="pack://application:,,,/Common;
		component/Dictionaries/Styles.xaml"/>
       </ResourceDictionary.MergedDictionaries>
   </ResourceDictionary>
</Application.Resources>

Note that we don't use a resource dictionary in the demo app, so it doen't contain any references of this sort.

Obviously, one could store solution-wide resource dictionaries in the Shell project, and use pack URLs to reference them, in the manner shown above. However, I prefer to keep my dependencies between modules and the shell to a minimum, since the Shell is one of the most-frequently changed objects as I develop my apps. For that reason, I prefer to put solution-wide resource dictionaries in the Common project, which changes less frequently.

Step 12: Add a View Model to the Shell

If the Shell needs a view model, add one. Most of the communication should be between modules, so the module view models will receive most attention. However, the Shell may need a view model for a menu bar, a ribbon, or for similar controls. Note that the demo app doesn't use a Shell view model.

Step 13: Create Composite Presentation Events

The whole point of using Prism is to keep modules as independent and isolated from each other as possible. This goal is in direct conflict with the need for modules to communicate with one another, which tends to couple modules together.

One way to reconcile the conflict is through the use of Composite Presentation Events (‘CPEs', also known as ‘Aggregate Events'). Prism contains an Event Aggregator (‘the EA') that serves as a registry for CPEs, which are classes derived from the CompositePresentationEvent class. The EA is in charge of instantiating CPEs and managing event publications and subscriptions. The result is that modules need have no knowledge of each other. They only need to know about the EA.

The Navigator module in the Prism2Demo app contains two buttons that activate and deactivate the views of the two workspace modules. The Navigator module performs this task by firing a CPE that carries as its payload the name of the module whose view is requested. If this were a normal .NET event, the Workspace modules would need to know about the Navigator module in order to subscribe to its event. But, a CPE lives as a separate class in the EA, which means we can cut the cord between the Navigator module and the Workspace modules. Instead, all modules know about the EA.

CPEs are simple classes that do no more than inherit from the CompositePresentationEvent base class, specifying the type of payload they carry. Here is the class declaration for the ViewRequestedEvent used by the demo app:

using Microsoft.Practices.Composite.Presentation.Events;

namespace Prism2Demo.Common.Events
{
    public class ViewRequestedEvent : CompositePresentationEvent<string>
    {
    }
}

In this case, the event will carry a string that specifies which view has been requested by the Navigator, which publishes the event. The event will be subscribed to by each Workspace module, which will inspect the string to determine whether it needs to display its view.

In general, you will need a CPE for each element of communication from module to module, and between modules and the Shell.

Step 14: Publish CPEs

The Navigator publishes the ViewRequestedEvent in ICommands that are bound to the buttons in its view. ICommands are commonly used as part of the MVVM pattern. They are popular because they provide easy binding to WPF controls, such as buttons and menu items, through the Command property.

ICommands also provide an easy way to enable or disable controls, through the CanExecute() method provided by the ICommand interface. Once a WPF control has been bound to an ICommand, it will be automatically enabled and disabled, in accordance with logic provided by the developer in the CanExecute() method.

The Navigator's view model class contains an ActiveWorkspace property that the view model's ICommands use to determine whether each button on the Navigator should be enabled. The ICommands delegate the task of publishing the ViewRequestedEvent to a service class, CommandServices, an approach that eliminates duplication between the ICommand classes. Here is the code that actually publishes the event:

// Publish ViewRequestedEvent
var eventAggregator = viewModel.Container.Resolve<IEventAggregator>();
var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
viewRequestedEvent.Publish(workspaceName);

The Navigator simply gets the event from the Event Aggregator, and then it publishes it with a string that specifies which view has been requested. The call to GetEvent<T>() will cause the event object to be instantiated, if it doesn't already exist, so there is no need to check for the existence of the event by either publishers or subscribers. In this example, the workspace name is the payload of the event object, so we simply publish the event and pass the payload that it is to deliver. The EA takes care of the rest.

Step 15: Subscribe to CPEs

The procedure for subscribing to the event is very similar. We call the EA to get the event, and then we subscribe to it. Here is the code that the Navigator module uses (in its Initialize() method) to subscribe to the ViewRequestedEvent:

// Subscribe to ViewRequestedEvent
var eventAggregator = m_Container.Resolve<IEventAggregator>();
var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
viewRequestedEvent.Subscribe(this.ViewRequestedEventHandler, 
                   ThreadOption.PublisherThread, true);

The Subscribe() overload that we have used creates a strong reference to the delegate we are using as an event handler—the third param of the overload specifies a strong reference as a boolean. I haven't had much luck getting CPE subscriptions to work with weak references, and I'm not sure why. It appears that the delegate is being garbage collected too soon, but that isn't certain. If anyone can shed additional light on this problem, please leave a comment.

In any event, the use of strong delegate references means that each module must unsubscribe from its CPEs before it closes. Therefore, each module with CPE subscriptions should implement a destructor that un-subscribes from all subscribed CPEs. Here is the code used in the Workspace modules:

#region Destructor

/// <summary>
/// Releases resources and references before this object is destroyed.
/// </summary>
~WorkspaceAModule()
{
  // Initialize
  var eventAggregator = m_Container.Resolve<IEventAggregator>();
  var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();

  // Unsubscribe from ViewRequestedEvent
  viewRequestedEvent.Unsubscribe(ViewRequestedEventHandler);
}

#endregion

At this point, all the basic functionality of a Prism 2.0 app, as exemplified by the demo app, has been implemented. In a real app, of course, you would have more modules, and more functionality in each one.

The most obvious question is how to integrate a data access layer into a composite application. Generally, I give each module its own data access layer. That approach preserves the independence and isolation of each module, but it can lead to a lot of code duplication. I avoid that problem by having the modules call on data-access services in the Common project, or in a separate DataAccess project. That way, data access can be easily mocked, and individual modules can be data-tested by wiring them only to the DataAccess project.

Creating a Setup Project

The demo app includes a Setup project that shows the basics of setting up an installer for Prism 2.1 apps. The main issue that one has to deal with is getting the module assemblies to the right place in the Setup project. Remember, the goal of Prism development is to minimize coupling. As a result, the demo project's Shell application is completely ignorant of the modules that it hosts. That means, the .NET compiler will not find the modules as dependencies of the Shell project when it builds the Setup project.

The solution to the problem is to manually copy the modules from the Modules folder in the Shell project's output folder to the Application folder in the Setup project. We will assume you have created a Setup project for your Prism app in Visual Studio, and have gone through the normal steps to populate the project with your project output. Once that is done, open the File System Editor for the Setup project, and add a Modules subfolder to the Application folder. Then, use the Add File option on the context menu to select all files in the Modules subfolder of the Shell project's output folder and add them to the Modules subfolder you created in the Setup project.

Note that any time you add or remove modules from your application, you will need to update your Setup project by performing these steps again. Since there are no dependencies between the Shell project and the modules, Visual Studio will remain ignorant of the modules when it builds the Setup project, which means it will miss any modules added or deleted.

Where to Go from Here?

At this point, you should have a basic framework for a Prism 2.1 app that can display module views on demand. This article should give you enough background to dig into the Prism documentation and learn the details of the library. The Composite Application Guidance for WPF and Silverlight (Version 2.1, October 2009) contains a wealth of hands-on-labs and quick-starts, as well as a detailed reference app.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here