Introduction

This article was written as a companion piece to Creating View-Switching Applications with Prism 4, also published on CodeProject. However, it can be used as a guide to setting up almost any Prism 4 application. The article provides a step-by-step explanation of how to set up a Prism application.

The checklist in this article assumes that the developer will use Unity 2.0 as the Dependency Injection (DI) container for the application, and that logging will be performed using the log4net framework. But the checklist does not depend heavily on these components, and it should be easily adaptable for use with other DI containers and logging frameworks.

The checklist includes the steps needed to set up a Prism application for View-switching, and the demo app included with the article is a View-switching app. For more information on View-switching, see the companion article. Note that the demo app does not implement logging.

Step 1: Create a Prism Solution

The first step in setting up a Prism application is to create and configure the solution and Shell project.

Step 1a – Create a solution: Create a new WPF project, making sure that the Solution drop-down in the New Project dialog is set to ‘Create new solution’. The Name field should contain the name for your solution. If you are going to use the WPF Ribbon in your app, be sure to select WPF Ribbon Application as the project type, since its window differs somewhat from the standard WPF window.

Step 1b – Add DLLs to the solution library: Create a folder at the root level of your solution called [Solution Name].Library. In that folder, create a subfolder named Prism. Add the following DLLs to that folder:

  • Microsoft.Practices.Prism.dll (and the corresponding XML file)
  • Microsoft.Practices.Prism.UnityExtensions.dll (and the corresponding XML file)
  • Microsoft.Practices.ServiceLocation.dll (and the corresponding XML file)
  • Microsoft.Practices.Unity.dll (and the corresponding XML file)

The XML files provide Intellisense for the DLL files.

We copy the Prism DLLs to a solution library folder so that we can use a private, or side-by-side installation of Prism for our solution. This approach eliminates some of the versioning problems that can occur when Prism is updated. For example, I have several Prism 2.x solutions that I maintain. Since each solution uses its own Prism DLLs, the older version does not interfere with the newer version, and vice versa.

Step 1c – Set references to library DLLs: In the References section of Solution Explorer, add references to the DLLs added to the solution library in the preceding step.

Step 1d – Rename the Shell window: First, rename MainWindow.xaml to ShellWindow.xaml in Solution Explorer. Then, open Shell.xaml.cs in VS. The class will still be named MainWindow. Rename the class using the Visual Studio refactoring tools; this will change all references to the class, as well.

Step 1e – Add a namespace definition to the Shell window: Add a XAML namespace definition to ShellWindow.xaml:

xmlns:prism=”http://www.codeplex.com/prism”

Step 1f – Lay out Shell window: ShellWindow.xaml will contain controls that are declared as regions, typically ContentControl or ItemsControl objects.  Each of these controls represents a region in the Shell window. These regions will hold the content that is loaded into the Shell window.

  • ContentControl regions are suitable for regions that will display one View at a time.
  • ItemsControl regions are suitable for regions that will display several Views at once, such as the task buttons in the screenshot above.

ShellWindow.xaml is laid out in much the same way as any other window, using grids, splitters, and other layout controls. The only difference is that the window contains few if any real controls. Instead, it contains placeholder controls that are declared as regions. Prism inserts content from regions into these placeholders later.

Each region control needs both a x:Name and a RegionName. The regular x:Name is used by your code to refer to the control as needed. Prism’s RegionManager uses the RegionName to load content later. So, a region declaration based on an ItemsControl looks like this:

<ItemsControl x:Name=”MyRegion” prism:RegionManager.RegionName=”MyRegion” />

Note that both names can be the same.

Step 1g – Create a Bootstrapper: The bootstrapper initializes a Prism app. Most of the heavy lifting is done by the Prism library, and the main task of the bootstrapper class created by the developer is to invoke this functionality. To set up the bootstrapper, first create a class in the Shell project called Bootstrapper.cs. Update the class signature to inherit from the UnityBootstrapper class.

A sample Bootstrapper class is included in the download file. The sample class includes the method overrides discussed below.

Step 1h – Override the CreateShell() method: Create an override for the CreateShell() method so that it looks like this:

protected override System.Windows.DependencyObject CreateShell()
{
    /* The UnityBootstrapper base class will attach an instance 
     * of the RegionManager to the new ShellWindow. */

    return new ShellWindow ();
}

This override creates a new ShellWindow and sets it as the Prism Shell.

Step 1i – Override the InitializeShell() method:  Create an override for the InitializeShell() method of the UnityBootstrapper class. The override method will set the base class Shell property to the Shell window for the app. The override should look like this:

protected override void InitializeShell()
{
    base.InitializeShell();

    App.Current.MainWindow = (Window)this.Shell;
    App.Current.MainWindow.Show();
}

Step 1j – Override the CreateModuleCatalog () method: This step assumes you want to use Module Discovery to populate the module catalog, where application modules are simply placed in a specified folder, where Prism will discover and load them. Create an override for the CreateModuleCatalog() method of the UnityBootstrapper class. The override method should look like this:

protected override IModuleCatalog CreateModuleCatalog()
{
    var moduleCatalog = new DirectoryModuleCatalog();
    moduleCatalog.ModulePath = @".\Modules";
    return moduleCatalog;
}

All we need to do is specify the folder where modules will be placed when compiled. Here, we specify a Modules subfolder in the Shell project bin folder.

Step 1k – Override the ConfigureRegionAdapterMappings() method:  If your application uses any Region Adapters, create an override for the ConfigureRegionAdapterMappings()  method of the UnityBootstrapper class.  Region Adapters are used to extend Prism region capabilities to controls (such as the WPF Ribbon) that cannot support Prism regions out-of-the-box. See Appendix E to the Developer's Guide to Microsoft Prism.

The override method should look like this:

protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
    // Call base method
    var mappings = base.ConfigureRegionAdapterMappings();
    if (mappings == null) return null;

    // Add custom mappings
    mappings.RegisterMapping(typeof(Ribbon), 
       ServiceLocator.Current.GetInstance<RibbonRegionAdapter>());

    // Set return value
    return mappings;
}

Step 1l – Modify the App class: The App class represents the application—it is called when the application starts. We will use the App class to set up our Prism application, by configuring our logging framework and calling the Bootstrapper we created in the preceding steps. So, open App.xaml.cs, and add the following override method to the class:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    // Configure Log4Net
    XmlConfigurator.Configure();

    // Configure Bootstrapper
    var bootstrapper = new Bootstrapper();
    bootstrapper.Run();
}

The logger configuration call assumes we are using the log4net framework, and that it is being configured in an App.config file. See the How-To document Configuring log4net for more information on configuring log4net in this manner.

Step 1m – Modify App.xaml: WPF applications are normally initialized by the StartupUri attribute in App.xaml. This attribute simply points to the main window as the window to load on startup. Since we are taking control of initialization in the App class, we don’t need the XAML attribute. So, open App.xaml and delete that attribute.

Step 1n – Create a custom logger class: Create a new custom logger class, implementing ILoggerFacade, to enable Prism to log using your logging framework. A sample custom logger is attached to this document as Appendix B. It enables Prism logging with the log4net framework.

Step 1o – Initialize the custom logger: In the application bootstrapper, add an override to the CreateLogger() method. The override simply needs to create and return an instance of your custom logger, like this:

protected override Microsoft.Practices.Prism.Logging.ILoggerFacade CreateLogger()
{
    return new Log4NetLogger();
}

Step 2: Add Modules to the Application

Modules are logical units in Prism applications. Each module is set up as a separate Class Library (DLL) project within the Prism solution. A code listing for a sample module initializer class is attached to this document as Appendix B.

Step 2a – Create a module: Add a WPF User Control Library project to your solution. Name the module according to this pattern: [SolutionName] .Module[Module Name]. For example, if the solution is named MySolution, and the module represents a folder navigator, you might name it MySolution.ModuleFolderNavigator.

Step 2b – Add Prism references: Add references to the following Prism and Unity assemblies to your new module project:

  • Microsoft.Practices.Prism.dll
  • Microsoft.Practices.ServiceLocation.dll
  • Microsoft.Practices.Unity.dll
  • Microsoft.Practices.Prism.UnityExtensions.dll

The Microsoft.Practices.ServiceLocation reference is required for the Prism Service Locator, which is used to resolve objects from the DI container (see ConfigureRegionAdapterMappings() in Appendix A to this article).

Step 2c – Rename the Module Initializer class: Rename the Class1.cs file in the new module project to [Module Name]. For example, if the module project is named MySolution.ModuleFolderNavigator, rename Class1 to ModuleFolderNavigator.

Step 2d – Derive from the IModule interface: Change the class signature of the Module Initializer class to derive from the IModule interface. VS will show error squiggles under the Interface name (because it is not yet implemented) and will offer to implement the interface. Tell VS to do so, and it will add the Initialize() method required to implement IModule. Leave the Initialize() method as-is, for now.

Step 2e – Add folders to the module project: Add the following folders to the modules project to help organize the module code:

  • Commands: Contains ICommand objects used by local View Models.
  • Services: Contains local services invoked by the commands.
  • ViewModels: View Models for the module’s Views.
  • Views: The module’s Views.

You may or may not use all of these folders, depending on how you structure your applications. For example, I use a Commands folder, because I prefer to encapsulate each ICommand object in a separate class, rather than using Prism’s DelegateCommand objects. I find that it separates code better and keeps my View Models from becoming cluttered.

Step 2f– Add a post-build event to each module: Since the whole point of Prism is to eliminate dependencies between the Shell and its modules, the VS compiler won’t be able to detect the app’s modules as dependencies of the Shell project, which means the modules won’t get copied to the Shell’s application output folder. As a result, each module will need a post-build-event command to copy the module to the Modules subfolder in the Shell project application output folder. That’s the folder Prism will search for application modules.

You can access the post-build command in the project's Properties pages, under the Build Events tab:

The command for the event should look like this:

xcopy /y "$(TargetPath)" "$(SolutionDir)<ShellProjectName>\$(OutDir)Modules\"

Where <ShellProjectName> = the solution name (e.g., “Prism4Demo.Shell”). Here is an example:

xcopy /y "$(TargetPath)" "$(SolutionDir)Prism4Demo.Shell\$(OutDir)Modules\"

Note also that if the module’s Views use any third-party or custom controls, the VS compiler won’t detect the third-party control as a dependency of the Shell, for the same reason it can’t detect the module. As a result, the module will need a separate post-build-event command to copy the third-party control to the Shell project application output folder. Here is an example:

xcopy /y "$(TargetDir)FsTaskButton.dll" "$(SolutionDir)Prism4Demo.Shell\$(OutDir)"

Note the use of the $(TargetDir) macro, followed by the name of the DLL to be copied to the Shell app output directory. The macro points VS to the output folder for the module’s assembly, which will contain the third-party control, since VS will detect the control as a dependency of the module.

Note also that we copy the third-party control to the root level of the output directory for the Shell assembly, not to its Modules sub-directory. If multiple modules use the same third-party control, we only need to include a post-build macro in one of those modules.

Finally, note the following with respect to the post-build command:

  • The command needs to be contained in a single line. For example, if there is a line break between the source and destination paths, the command will exit with an error.
  • There can be no spaces between macros and hard-coded portions of the command. For example, $(SolutionDir) Prism4Demo.Shell\$(OutDir)Modules\ will fail, because there is a space between (SolutionDir) and Prism4Demo.Shell\$(OutDir)Modules\.
  • Macros contain their own terminating backslash, so you don’t need to add a backslash between a macro and a subfolder.
  • Xcopy will create the Modules folder in the destination path, if it doesn’t already exist.
  • The /y parameter suppresses the overwrite prompt for the Xcopy command. If this param is omitted, Windows will attempt to show the prompt after the first compile, which will cause Visual Studio to abort the command and exit with Code 2.

Step 3: Add Views to Modules

The next step in setting up a Prism application is to add Views to the application’s modules. The application UI is composed of module Views that Prism loads into the Shell window regions, which means that each View represents a part of the overall UI. Accordingly, Prism Views are generally contained in WPF UserControls.

Step 3a – Create the View: Create a View, which will usually take the form of a WPF User Control. You don’t need to do anything special to the View to enable its use with Prism. Note that the View may be a composite of several controls, as in a UI form, or it may consist of a single control, such as an Outlook-style Task Button or an application Ribbon Tab. In some cases, a View composed of a single control may derive from the control, rather than from UserControl. For an example, see the Ribbon Tab Views in the demo app modules.

Step 3b – Register the View: In the module’s Initialize() method, register the View with either the Prism Region Manager, or the Unity container. The choice of registry depends on the behavior desired from the View:

  • If the View will be loaded when the module is loaded, and if it will last for the lifetime of the application, register the View with the Prism Region Manager. An example of this type of View would be an Outlook-style Task Button.
  • If the View will be loaded and unloaded as the user navigates within the application, register the View with the Unity container—we will use RequestNavigate() later to load the View. Examples of this type of View would be a UI form, or an application Ribbon Tab.

The Initialize() method in the module’s initializer class should look like this:

#region IModule Members

/// <summary>
/// Initializes the module.
/// </summary>
public void Initialize()
{
    /* We register always-available controls with the Prism Region Manager, and on-demand 
     * controls with the DI container. On-demand controls will be loaded when we invoke
     * IRegionManager.RequestNavigate() to load the controls. */

    // Register task button with Prism Region
    var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
    regionManager.RegisterViewWithRegion("TaskButtonRegion", typeof(ModuleATaskButton));

    /* View objects have to be registered with Unity using the overload shown below. By
     * default, Unity resolves view objects as type System.Object, which this overload 
     * maps to the correct view type. See "Developer's Guide to Microsoft Prism" (Ver 4), 
     * p. 120. */

    // Register other view objects with DI Container (Unity)
    var container = ServiceLocator.Current.GetInstance<IUnityContainer>();
    container.RegisterType<Object, ModuleARibbonTab>("ModuleARibbonTab");
    container.RegisterType<Object, ModuleANavigator>("ModuleANavigator");
    container.RegisterType<Object, ModuleAWorkspace>("ModuleAWorkspace");
}

#endregion

Here are the method parameters for IRegionManager.RegisterViewWithRegion():

  • Region name: The first parameter specifies the name of the region into which the View will be loaded.
  • Type to register: The second parameter specifies the type of the View to be loaded into that region. Prism takes care of the actual loading of the View.

And here are the parameters for IUnityContainer.RegisterType<TFrom, TTo>():

  • TFrom: The first type parameter should always be System.Object, because this is how Prism will initially resolve the View object.
  • TTo: The second type parameter should match the actual type of the View object. Unity will map the resolved object from System.Object to this type.
  • View name: The method parameter specifies the name of the View to be registered, as a string value.

By default, Prism creates a new instance of the View object every time a resolution is requested. You can override this behavior, and register a class as a singleton, by passing a second method parameter, in the form of a new ContainerControlledLifetimeManager().

Note that there are several other overloads for IUnityContainer.RegisterType(), including an overload that allows the developer to specify an interface or base class to resolve, and the concrete type to return when a resolution is requested.

Step 3c – Implement the IRegionMemberLifetime interface: If the View should be unloaded when its host module is deactivated, implement the IRegionMemberLifetime interface on either the View or its View Model. The interface consists of a single property, KeepAlive. Setting this property to false will cause the View to be unloaded when the user navigates to a different module. Here is sample code to implement the interface:

using Microsoft.Practices.Prism.Regions;
using Microsoft.Windows.Controls.Ribbon;

namespace Prism4Demo.ModuleA.Views
{
    /// <summary>
    /// Interaction logic for ModuleARibbonTab.xaml
    /// </summary>
    public partial class ModuleARibbonTab : RibbonTab, IRegionMemberLifetime
    {
        #region Constructor

        public ModuleARibbonTab()
        {
            InitializeComponent();
        }

        #endregion

        #region IRegionMemberLifetime Members

        public bool KeepAlive
        {
            get { return false; }
        }

        #endregion
    }
}

If KeepAlive will always have a value of false, then you can safely implement the interface on the View class and hard-code the value (as shown above). However, if the value of the property will be changed at run-time, implement the interface on the corresponding View Model instead, in order to avoid having the program’s back end interacting directly with the View.

Step 3d – Add the ViewSortHint Attribute: If the View will be loaded into an ItemsControl region, or any other region that holds multiple items, you may want to add the ViewSortHint attribute to the View. This attribute specifies the sort order of the View when it is loaded into the region. Here is an example for an Outlook-style Task Button:

[ViewSortHint("01")]
public partial class ModuleATaskButton : UserControl
{
    public ModuleATaskButton()
    {
        InitializeComponent();
    }
}

Note that the control declared as a Prism region must support sorting for the ViewSortHint attribute to have any effect.

Step 4: Add a Common Project to the Application

A Prism production app will have numerous base classes, interfaces, CompositePresentationEvent classes, and resource dictionaries. We can centralize dependencies and avoid duplication by placing all of these application resources in a single project. The modules contain a reference to this project, which enables them to invoke base and other classes directly, and to access resource dictionaries using ‘pack URLs’.

The Shell project can serve as the repository of these application-wide resources. However, that approach tightens coupling between the modules and the Shell, since modules can no longer be tested without the Shell. For that reason, I prefer to locate the resources in a class library designated as the Solution’s Common project.

I mentioned above that modules can use pack URLs to access resource dictionaries across assembly 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 doesn't contain any references of this sort.

I typically add the following folders to the Common project:

  • BaseClasses
  • Interfaces
  • Events (for CompositePresentationEvent classes)
  • ResourceDictionaries

You can use a Common project as the repository for your business model and data access classes as well, but I prefer to put these classes in separate projects. I find that approach better for separating code and managing dependencies between layers of my application.

Conclusion

The preceding steps will produce the basic framework for a Prism application. There are a number of other elements involved in a production app, such as a View Model for each View defined in the application’s modules, and the back-end logic that implements the application’s use cases. You can find additional documentation for the demo app, as well as a discussion of loosely-coupled communication between modules, in the companion article cited above.

As always, I welcome peer review by other CodeProject users. Please flag any errors you may find and add any suggestions for this article in the Comments section at the end of the article. Thanks.

Appendix A: Sample Bootstrapper Class

The following listing shows the code for the Bootstrapper class in the demo app:

using System.Windows;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Prism.UnityExtensions;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Windows.Controls.Ribbon;
using Prism4Demo.Shell.Views;
using PrismRibbonDemo;

namespace Prism4Demo.Shell
{
    public class Bootstrapper : UnityBootstrapper
    {
        #region Method Overrides

        /// <summary>
        /// Populates the Module Catalog.
        /// </summary>
        /// <returns>A new Module Catalog.</returns>
        /// <remarks>
        /// This method uses the Module Discovery method
        /// of populating the Module Catalog. It requires
        /// a post-build event in each module to place
        /// the module assembly in the module catalog
        /// directory.
        /// </remarks>
        protected override IModuleCatalog CreateModuleCatalog()
        {
            var moduleCatalog = new DirectoryModuleCatalog();
            moduleCatalog.ModulePath = @".\Modules";
            return moduleCatalog;
        }

        /// <summary>
        /// Configures the default region adapter
        /// mappings to use in the application, in order 
        /// to adapt UI controls defined in XAML
        /// to use a region and register it automatically.
        /// </summary>
        /// <returns>The RegionAdapterMappings instance
        /// containing all the mappings.</returns>
        protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
        {
            // Call base method
            var mappings = base.ConfigureRegionAdapterMappings();
            if (mappings == null) return null;

            // Add custom mappings
            var ribbonRegionAdapter = 
              ServiceLocator.Current.GetInstance<RibbonRegionAdapter>();
            mappings.RegisterMapping(typeof(Ribbon), ribbonRegionAdapter);

            // Set return value
            return mappings;
        }

        /// <summary>
        /// Instantiates the Shell window.
        /// </summary>
        /// <returns>A new ShellWindow window.</returns>
        protected override DependencyObject CreateShell()
        {
            /* This method sets the UnityBootstrapper.Shell property to the ShellWindow
             * we declared elsewhere in this project.
             * Note that the UnityBootstrapper base 
             * class will attach an instance 
             * of the RegionManager to the new Shell window. */

            return new ShellWindow();
        }

        /// <summary>
        /// Displays the Shell window to the user.
        /// </summary>
        protected override void InitializeShell()
        {
            base.InitializeShell();

            App.Current.MainWindow = (Window)this.Shell;
            App.Current.MainWindow.Show();
        }

        #endregion
    }
}

Appendix B: Sample Module Class

The following listing shows the code for the ModuleA class in the demo app:

using System;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Practices.Unity;
using Prism4Demo.ModuleA.Views;

namespace Prism4Demo.ModuleA
{
    /// <summary>
    /// Initializer class for Module A of the Prism 4 Demo.
    /// </summary>
    public class ModuleA : IModule
    {
        #region IModule Members

        /// <summary>
        /// Initializes the module.
        /// </summary>
        public void Initialize()
        {
            /* We register always-available controls 
             *  with the Prism Region Manager, and on-demand 
             * controls with the DI container.
             * On-demand controls will be loaded when we invoke
             * IRegionManager.RequestNavigate() to load the controls. */

            // Register task button with Prism Region
            var regionManager = 
                ServiceLocator.Current.GetInstance<IRegionManager>();
            regionManager.RegisterViewWithRegion("TaskButtonRegion", 
                          typeof(ModuleATaskButton));

            /* View objects have to be registered 
             *  with Unity using the overload shown below. By
             * default, Unity resolves view objects as 
             * type System.Object, which this overload 
             * maps to the correct view type. See "Developer's 
             * Guide to Microsoft Prism" (Ver 4), p. 120. */

            // Register other view objects with DI Container (Unity)
            var container = ServiceLocator.Current.GetInstance<IUnityContainer>();
            container.RegisterType<Object, ModuleARibbonTab>("ModuleARibbonTab");
            container.RegisterType<Object, ModuleANavigator>("ModuleANavigator");
            container.RegisterType<Object, ModuleAWorkspace>("ModuleAWorkspace");
        }

        #endregion
    }
}

Appendix C: Sample Custom Logger Class

The following listing shows the code for a sample custom logger:

using log4net;
using Microsoft.Practices.Prism.Logging;

namespace FsNoteMaster3
{
    class Log4NetLogger : ILoggerFacade
    {
        #region Fields

        /* Note that the ILog interface and
         * the LogManager object are Log4Net members.
         * They are used to instantiate the Log4Net 
         * instance to which we will log. */

        // Member variables
        private readonly ILog m_Logger = 
                LogManager.GetLogger(typeof(Log4NetLogger)); 

        #endregion

        #region ILoggerFacade Members

        /// <summary>
        /// Writes a log message.
        /// </summary>
        /// <param name="message">The message to write.</param>
        /// <param name="category">The message category.</param>
        /// <param name="priority">Not used by Log4Net; pass Priority.None.</param>
        public void Log(string message, Category category, Priority priority)
        {
            switch (category)
            {
                case Category.Debug:
                    m_Logger.Debug(message);
                    break;
                case Category.Warn:
                    m_Logger.Warn(message);
                    break;
                case Category.Exception:
                    m_Logger.Error(message);
                    break;
                case Category.Info:
                    m_Logger.Info(message);
                    break;
            }
        }

        #endregion
    }
}
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"