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

Creating View-Switching Applications with Prism 4

0.00/5 (No votes)
6 Mar 2011 8  
How to get a Prism 4 line-of-business application up and running, using WPF and the Unity Dependency Injection (DI) container.

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

/// <summary>
/// Loads the view for Module A.
/// </summary>
public ICommand ShowModuleAView { get; set; }   

#endregion

#region Private Methods

/// <summary>
/// Initializes the view model.
/// </summary>
private void Initialize()
{
    // Initialize command properties
    this.ShowModuleAView = new ShowModuleAViewCommand(this);

    // Initialize administrative properties
    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

/// <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));

    ...
}

#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:

/// <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
    mappings.RegisterMapping(typeof(Ribbon), 
        ServiceLocator.Current.GetInstance<RibbonRegionAdapter>());

    // Set return value
    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">

    <!-- See code-behind for implementation 
       of IRegionMemberLifetime interface. This interface
       causes the RibbonTab to be unloaded from the Ribbon when we switch views. -->

    <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

/// <summary>
/// Initializes the module.
/// </summary>
public void Initialize()
{
    ...

    /* 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");

    ...
}

#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:

  • Navigation has been simplified by the addition of the RequestNavigate() method.
  • Parameters can now be passed during navigation.
  • The RequestNavigate() method can specify a callback method to be invoked when navigation has completed.
  • Prism can now inform a View that the user is navigating to it or away from it.
  • It is now much easier to re-use an existing View to display new information.

    I won’t explain Prism 4 navigation in detail, since The Developer’s Guide to Microsoft Prism has an entire chapter on the subject.

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:

/// <summary>
/// Executes the ShowModuleAViewCommand
/// </summary>
public void Execute(object parameter)
{
    // Initialize
    var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();

    // Show Ribbon Tab
    var moduleARibbonTab = new Uri("ModuleARibbonTab", UriKind.Relative);
    regionManager.RequestNavigate("RibbonRegion", moduleARibbonTab);

    // Show Navigator
    var moduleANavigator = new Uri("ModuleANavigator", UriKind.Relative);
    regionManager.RequestNavigate("NavigatorRegion", moduleANavigator);

    /* We invoke the NavigationCompleted() callback method in the next  
     * navigation request since it is the last request we have to make. */

    // Show Workspace
    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 TaskButtons 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 TaskButtons.

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
{
    /// <summary>
    /// A composite Presentation event 
    /// </summary>
    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

/// <summary>
/// Initializes the view model.
/// </summary>
private void Initialize()
{
    ...

    // Subscribe to Composite Presentation Events
    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():

/* We invoke the NavigationCompleted() callback 
 * method in our final  navigation request. */

// Show Workspace
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

/// <summary>
/// Callback method invoked when navigation has completed.
/// </summary>
/// <param name="result">Provides information
///        about the result of the navigation.</param>
private void NavigationCompleted(NavigationResult result)
{
    // Exit if navigation was not successful
    if (result.Result != true) return;

    // Publish ViewRequestedEvent
    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

/// <summary>
/// Sets the IsChecked state of the Task Button when navigation is completed.
/// </summary>
/// <param name="publisher">The publisher of the event.</param>
private void OnNavigationCompleted(string publisher)
{
    // Exit if this module published the event
    if (publisher == "ModuleA") return;

    // Otherwise, uncheck this button
    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
{
    /// <summary>
    /// Enables use of a Ribbon control as a Prism region.
    /// </summary>
    /// <remarks> See Developer's Guide to Microsoft Prism (Ver. 4), p. 189.</remarks>
    public class RibbonRegionAdapter : RegionAdapterBase<Ribbon>
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="behaviorFactory">Allows the registration
        /// of the default set of RegionBehaviors.</param>
        public RibbonRegionAdapter(IRegionBehaviorFactory behaviorFactory)
            : base(behaviorFactory)
        {
        }

        /// <summary>
        /// Adapts a WPF control to serve as a Prism IRegion. 
        /// </summary>
        /// <param name="region">The new region being used.</param>
        /// <param name="regionTarget">The WPF control to adapt.</param>
        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();
        }
    }
}

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