Contents
Introduction
Scribble is a simple WPF InkCanvas sample application built using PRISM framework that follows the MVVM pattern. With the sample, the article discusses about building a PRISM application that includes creating the shell, separating the functional parts into independent modules, inject dependency through Unity container, and enable communication between modules through EventAggregator. Further, the sample includes some code snippets on how to program the InkCanvas
control the MVVM way, which is also discussed in the related section.
Software Environment
The initial version of application is developed in the following environment:
- Development environment: Visual Studio .NET 2010
- Framework: .NET Framework 4.0
- User Interface: WPF
- Programming language: C# .NET
Pre-requisite
You need Microsoft Visual Studio 2010 / 2012 installed and the following PRISM 4 libraries.
- Microsoft.Practices.Prism.dll
- Microsoft.Practices.Prism.UnityExtensions.dll
- Microsoft.Practices.ServiceLocation.dll
- Microsoft.Practices.Unity.dll
PRISM Libraries are available in CodePlex. You can download it from here http://compositewpf.codeplex.com/[^]
To understand and start building a PRISM application you need some hands-on experience working on WPF or Silverlight (both uses XAML) with a good understanding on the basic concepts: Data binding, Dependency properties, Value Converters, Commands, User controls.
PRISM: An Outline
PRISM is a framework that contains a set of libraries used to design and build rich WPF based desktop applications. PRISM comes from the Microsoft Patterns and Practices team. The primary advantage of this framework is that we can build loosely coupled components/modules which can be independently developed and integrated into the application.
In a Prism application, Modules are often separated into functional units that are independently developed. These modules contain View, View Model, Services, Models (DataModels) all related to its functionality.
Bootstrapper, Shell, Regions, Views, Modules, Module catalog are some of the key concepts of PRISM. Shell is the main startup application where these modules are loaded. It defines the layout and structure of the application through Prism Regions. Prism takes care of loading these modules into the shell. Prism's Module catalog holds the information (type, name, and location) of modules used by the application. A bootstrapper is a class that is responsible for the initialization of an application. The bootstrapper class is defined inside the Shell application. It takes care of registering the Prism libraries, creating and initializing the shell, and configuring the module catalog. Prism 4 Documentation[^] has the complete details on this.
MVVM Pattern
Model
Model represents the domain object which is the actual data. In the sample application the actual data is the canvas strokes saved in the form of a point array (x and y coordinates). The model contains properties that are relevant to the canvas strokes. This data is saved to a data source (XML file) and retrieved. Save and retrieve logic is kept separate in a service class that uses the model to hold the information.
View
View is the user interface of the application that defines the layout and appearance of the screen. View does not contain UI logic in its code-behind. It contains a constructor that calls the
InitalizeComponent
method. In addition to that, a reference of the ViewModel is assigned to the
DataContext
property of the View as shown below.
view.DataContext = viewModel
This way the view keeps a reference to the ViewModel.
ViewModel
View model contains the presentation logic and data for the view by implementing Properties and Commands. It is like an abstract layer that separates the Model and View. Controls in the View are bound to the ViewModel through data binding and ViewModel notifies the view of any changes through change notification events. Ideally, all logical behavior of the application is implemented in the viewModel.
Application Structure
A PRISM application typically consists of a shell project and multiple module projects. The basic idea is to build a desktop application with the following functional units.
- A
MenuBar
which contains a list of tools like pen, highlighter, eraser.
- A
Canvas
(drawing) region to draw with the selected tool.
- A
StatusBar
region that indicates the selected tool from the menu and number of strokes on the canvas region.
These functional units are developed as independent modules and integrated into the main application through Prism. The sample application can be divided into three parts as shown in the figure below:
- Main application which contains Shell
- PRISM and Infrastructure framework acts as a bridge between the shell and the modules.
- Modules that contains the functional units which are -
MenuBar
,
Canvas
, and
StatusBar
Building the Application
Important note
The code blocks included throughout the article is with reference to the initial version VS 2010. If you happen to work through it using VS 2012, then please refer to the source code when required. 2012 implementation has important changes which are addressed at the end of the article (see update in the history section).
The diagram below shows the key parts of the application that needs to be built. The outermost part is the shell which is the core application. The shell contains regions which determine the layout of the application. Regions act as a container for Views which can be loaded into them through PRISM. Each view is wired to a ViewModel that contains the presentation logic.
Shell: The Main Application
Shell is the starting point of the application. Generally in a WPF application, the startup URI is specified in the
App.xaml file which launches the main window. In a PRISM application, launching the main window is achieved by creating and initializing the shell. This is done through the Bootstrapper. Bootstrapper is a class that is responsible for initializing the application. The Prism library includes default Bootstrapper abstract base classes. To create the shell, follow these steps:
- Open Visual Studio and create a new WPF Application Project
- Add reference to Prism libraries
- Rename MainWindow.xaml to Shell.xaml
- In App.xaml, remove the startupUri
- Create a Bootstrapper class and override the following methods of UnityBootstrapper:
CreateShell
InitializeShell
- Override this method in the App.xaml file:
Bootstrapper.cs
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return this.Container.Resolve<Shell>();
}
protected override void InitializeShell()
{
base.InitializeShell();
App.Current.MainWindow = (Window)this.Shell;
App.Current.MainWindow.Show();
}
}
App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Bootstrapper bootstrapper = new Bootstrapper();
bootstrapper.Run();
}
}
Regions: The placeholders
Regions are the placeholders defined inside the shell application that determines the layout of the application. Common controls that can act as a container control can be used as a region. Views from other modules / components are loaded into the Regions. Other modules can locate the Regions inside the shell by their names through the
RegionManager
component. In the sample application, three regions are defined in the Shell View, namely:
MenuRegion
CanvasRegion
StatusbarRegion
Each region is loaded with its respective view. And the views are connected to their respective View Model. The Views are created in separate modules and are loaded into the shell Regions through PRISM. To name the regions in the shell view through
RegionManager
, we need to import the following namespace in XAML.
Microsoft.Practices.Prism.Regions
Shell.xaml
<Window x:Class="John.Scribble.Shell.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Regions="clr-namespace:Microsoft.Practices.Prism.Regions;
assembly=Microsoft.Practices.Prism"
Title="Scribble" Height="700" Width="800"
WindowStyle="ToolWindow" MaxHeight="700" MaxWidth="800">
<DockPanel>
<ContentControl x:Name="MenuRegion" Regions:RegionManager.RegionName="MenuRegion"
DockPanel.Dock="Top" />
<ContentControl x:Name="CanvasRegion" Regions:RegionManager.RegionName="CanvasRegion"
HorizontalAlignment="Center"
VerticalAlignment="Center" DockPanel.Dock="Top"/>
<ContentControl x:Name="StatusbarRegion"
Regions:RegionManager.RegionName="StatusbarRegion"
DockPanel.Dock="Bottom"/>
</DockPanel>
</Window>
Infrastructure: The Common Framework
The Infrastructure module is a common library that contains the logic that is application specific and acts as a bridge between the shell and the modules. In the sample, the Infrastructure library has an abstract base class, composite events, enumerators, interfaces. These are used in other modules. Having a common library in an application increases the reusability.
Class Diagram
BaseModule
BaseModule
is an abstract class that implements the IModule
interface and declares an abstract method RegisterTypes
. The constructor of the class keeps the reference of
UnityContainer
and RegionManager
that will be passed to it by Prism. This class acts as a base class to initialize a module.
Unity Container
PRISM applications rely on dependency injection containers for managing dependencies between the components. The PRISM library provides two options for containers: Unity or MEF. In the sample I have used Unity container. The container is primarily used for injecting dependencies between modules, registering and resolving views, and other services such as region manager, event aggregator.
public abstract class BaseModule : IModule
{
protected abstract void RegisterTypes();
protected IUnityContainer Container { get; set; }
protected IRegionManager RegionManager { get; set; }
public BaseModule(IUnityContainer container, IRegionManager regionManager)
{
this.Container = container;
this.RegionManager = regionManager;
}
public void Initialize()
{
this.RegisterTypes();
}
}
DrawingMode: Enum
A list of modes that demonstrates the state of the application is required which can be used in all modules to identify the application state. For this, an enumeration list named
DrawingMode
is defined in the library. It includes the needed InkCanvasEditingModes
,
EraserShapes
, FileModes
.
public enum DrawingMode
{
BlackPen,
BluePen,
RedPen,
GreenPen,
YellowHighlighter,
PinkHighlighter,
EraseByPointSmall,
EraseByPointMedium,
EraseByPointLarge,
EraseByStroke,
Select,
None,
Clear,
Save,
Open,
Exit
}
Presentation Events
The library defines two common events specific to the application, namely
ToolChangedEvent
and StrokeChangedEvent
. These events are used by the EventAggregator Service to enable communication between the modules.
EventAggregator
is discussed in a later section.
public class StrokeChangedEvent : CompositePresentationEvent<DrawingMode> { }
public class ToolChangedEvent : CompositePresentationEvent<string> { }
Modules: The Functional Parts
Modules are the functional parts of the application that contains Models, Views, ViewModels and classes that implement module specific logic and business functionality. A module is implemented in a separate class library. In the sample application I have used a separate class library project for each of my modules. Each module consists of an
Initializer
class. The initializer class is a class that implements the
IModule
interface and is decorated with the Module
attribute which specifies the Module’s name.
Menubar
The MenuBar module is for creating a simple menu for tool selection. A module has an
initializer class, a View, and a View Model. It doesn’t have a Model as there is no need for it. The View is designed using the Menu
control and the ViewModel defines a
MenuItemCommand
which is bound to the View through data-binding.
Class Diagram
Menu: Module
The initializer class registers the MenuView
with the container and adds the
MenuView
to the MenuRegion
.
[Module(ModuleName = "MenubarModule")]
public class MenubarModule : BaseModule
{
public MenubarModule(IUnityContainer container, IRegionManager regionManager)
: base(container, regionManager) { }
protected override void RegisterTypes()
{
this.Container.RegisterType<MenuView>();
this.RegionManager.Regions["MenuRegion"].Add(this.Container.Resolve<MenuView>());
}
}
Menu: ViewModel
The ViewModel implements a command property that takes the DrawingMode
as input parameter and publishes the
TooChangedEvent
. Other modules which are subscribed to this event will be notified of the tool change when the command is executed.
public class MenuViewModel
{
public MenuViewModel()
{
IEventAggregator eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
this.MenuItemCommand = new DelegateCommand<DrawingMode?>(param =>
{
eventAggregator.GetEvent<ToolChangedEvent>().Publish(param.Value);
});
}
public ICommand MenuItemCommand { get; private set; }
}
Menu: View
View uses the Menu
control and each menu item's Command
property is bound to the ViewModels's
MenuItemCommand
, and CommandParameter
is assigned with the related drawing mode.
<Menu>
<MenuItem Header="Tools" Height="25">
<MenuItem Header="Pen" Foreground="Black">
<MenuItem Header="Black"
Command="{Binding MenuItemCommand}"
CommandParameter="{x:Static enums:DrawingMode.BlackPen}"/>
<MenuItem Header="Blue"
Command="{Binding MenuItemCommand}"
CommandParameter="{x:Static enums:DrawingMode.BluePen}"/>
<MenuItem Header="Red"
Command="{Binding MenuItemCommand}"
CommandParameter="{x:Static enums:DrawingMode.RedPen}"/>
<MenuItem Header="Green"
Command="{Binding MenuItemCommand}"
CommandParameter="{x:Static enums:DrawingMode.GreenPen}"/>
</MenuItem>
</Menu>
Canvas
The Canvas module is the core part in the sample that is designed using the InkCanvas
control and it implements the MVVM pattern. It contains the code (initializer class, Model, View, ViewModel, Data Service, Value converter, Extension methods, Constants) that delivers the presentation logic.
Class Diagram
Canvas: Module Initialize
[Module(ModuleName = "CanvasModule")]
public class CanvasModule : BaseModule
{
public CanvasModule(IRegionManager regionManager, IUnityContainer container)
: base(container, regionManager) { }
protected override void RegisterTypes()
{
this.Container.RegisterType<CanvasView>();
this.RegionManager.RegisterViewWithRegion("CanvasRegion", typeof(CanvasView));
}
}
Drawing attributes
Drawing attributes such as Color
, Width
, Height
,
StylusTip
needed for Pen
and Highlighter
are stored in a static variable.
public static readonly DrawingAttributes[] DrawingAttributes = new DrawingAttributes[]
{
new DrawingAttributes()
{
Color = Colors.Black,
StylusTip = StylusTip.Rectangle,
Height = 1.8,
Width = 1.8,
IsHighlighter = false
},
new DrawingAttributes()
{
Color = Colors.Blue,
StylusTip = StylusTip.Rectangle,
Height = 1.8,
Width = 1.8,
IsHighlighter = false
},
new DrawingAttributes()
{
Color = Colors.Red,
StylusTip = StylusTip.Rectangle,
Height = 1.8,
Width = 1.8,
IsHighlighter = false
},
new DrawingAttributes()
{
Color = Colors.Green,
StylusTip = StylusTip.Rectangle,
Height = 1.8,
Width = 1.8,
IsHighlighter = false
},
new DrawingAttributes()
{
Color = Colors.Yellow,
StylusTip = StylusTip.Rectangle,
Height = 32.4,
Width = 8.67,
IsHighlighter = true
},
new DrawingAttributes()
{
Color = Colors.Pink,
StylusTip = StylusTip.Rectangle,
Height = 32.4,
Width = 8.67,
IsHighlighter = true
}
};
DrawingAttributes
are bound to the canvas view through a MultiBinding
converter. When a particular drawing tool (Pen
,
Highli
ghter
) is selected, the converter is called to return the corresponding drawing attributes.
DrawingAttributes: Value Converter
public class DrawingAttributesConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
DrawingMode targetMode = (DrawingMode)values[1];
DrawingAttributes[] drawingAttributesList = (DrawingAttributes[])
(CanvasConstants.DrawingAttributes);
return drawingAttributesList[(int)targetMode];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
InkCanvasControl: InkCanvas
The Canvas ViewModel implements the necessary properties that specify the editing mode of the pointing device to interact with the
InkCanvas
through the Pen
, Highlighter
, and Eraser
tools. But we need to write some extra code for the eraser mode. The Eraser tool provides an option for selecting Eraser shapes of different sizes (small, medium, large).
The InkCanvas
control provides an EraserShape
property to achieve this. But binding cannot be set on the
EraserShape
property. It is not a dependency property and cannot be used in XAML. If we do so, it throws an exception as shown below:
It is not a good practice to write the logic in the code-behind. The logic should go into the ViewModel and get it working in view through data-binding. To have the provision of data binding for this property,
the InkCanvas
control is extended by implementing a dependency property for
EraseShape
. The code sample is given below.
public class InkCanvasControl : InkCanvas
{
new public StylusShape EraserShape
{
get { return (StylusShape)GetValue(EraserShapeProperty); }
set { SetValue(EraserShapeProperty, value); }
}
public static readonly DependencyProperty EraserShapeProperty =
DependencyProperty.Register("EraserShape", typeof(StylusShape),
typeof(InkCanvasControl),
new UIPropertyMetadata(null, OnEraserShapePropertyChanged));
private static void OnEraserShapePropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var uie = (System.Windows.Controls.InkCanvas)d;
uie.EraserShape = (StylusShape)e.NewValue;
uie.RenderTransform = new MatrixTransform();
}
}
Canvas: ViewModel
The ViewModel implements the editing mode, stylus shape, and strokes property that are bound to the
CanvasView
through data-binding. PRISM’s NotificationObject
class is the base class for the ViewModel which implements the
INotifyPropertyChanged
interface. An event RaisePropertyChanged
of the
NotificationObject
class is called on each property of the bound type. This is to notify the view when the binding property value changes. This is how the View and ViewModel communicates.
public class CanvasViewModel : NotificationObject
{
private InkCanvasEditingMode editingMode;
private DrawingMode resourceKey;
private StylusShape stylusShape;
private StrokeCollection strokes;
private IEventAggregator eventAggregator;
public CanvasViewModel()
{
this.eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
this.strokes = new StrokeCollection();
(this.strokes as INotifyCollectionChanged).CollectionChanged += delegate
{
this.eventAggregator.GetEvent<StrokeChangedEvent>().Publish(this.strokes.Count);
};
eventAggregator.GetEvent<ToolChangedEvent>().Subscribe(param =>
{
this.OnToolChanged(param);
});
}
public InkCanvasEditingMode EditingMode
{
get { return this.editingMode; }
set {
this.editingMode = value;
this.RaisePropertyChanged(() => this.EditingMode);
}
}
public DrawingMode ResourceKey
{
get { return resourceKey; }
set {
resourceKey = value;
this.RaisePropertyChanged(() => this.ResourceKey);
}
}
public StylusShape StylusShape
{
get { return this.stylusShape; }
set {
this.stylusShape = value;
this.RaisePropertyChanged(() => this.StylusShape);
}
}
public StrokeCollection Strokes
{
get { return strokes; }
set { strokes = value; }
}
private void OnToolChanged(DrawingMode drawingMode)
{
switch (drawingMode)
{
case DrawingMode.BlackPen:
case DrawingMode.BluePen:
case DrawingMode.RedPen:
case DrawingMode.GreenPen:
case DrawingMode.YellowHighlighter:
case DrawingMode.PinkHighlighter:
this.EditingMode = InkCanvasEditingMode.Ink;
this.ResourceKey = drawingMode;
break;
case DrawingMode.EraserByPointSmall:
this.EditingMode = InkCanvasEditingMode.EraseByPoint;
this.StylusShape = new RectangleStylusShape(6, 6);
break;
case DrawingMode.EraserByPointMedium:
this.EditingMode = InkCanvasEditingMode.EraseByPoint;
this.StylusShape = new RectangleStylusShape(18, 18);
break;
case DrawingMode.EraserByPointLarge:
this.EditingMode = InkCanvasEditingMode.EraseByPoint;
this.StylusShape = new RectangleStylusShape(32, 32);
break;
case DrawingMode.EraseByStroke:
this.EditingMode = InkCanvasEditingMode.EraseByStroke;
break;
case DrawingMode.Select:
this.EditingMode = InkCanvasEditingMode.Select;
break;
case DrawingMode.Clear:
this.Strokes.Clear();
break;
case DrawingMode.Save:
this.Save();
break;
case DrawingMode.Open:
this.Strokes.Clear();
this.Open();
break;
case DrawingMode.Exit:
System.Windows.Application.Current.Shutdown();
break;
default:
this.EditingMode = InkCanvasEditingMode.None;
break;
}
}
}
Canvas: View
The Canvas view uses the InkCanvasControl
control that is defined from
InkCanvas
with support for the EraserShape
property. The drawing attributes are
assigned through a MultiBinding
converter by passing the resource key (DrawingMode
) as the input parameter:
<Control:InkCanvasControl x:Name="MyInkCanvas"
Background="{StaticResource RuleLines}"
EditingMode="{Binding EditingMode}"
EraserShape="{Binding StylusShape}"
Strokes="{Binding Strokes}">
<InkCanvas.DefaultDrawingAttributes>
<MultiBinding Converter="{StaticResource DrawingAttributesConverter}">
<MultiBinding.Bindings>
<Binding RelativeSource="{RelativeSource Self}" />
<Binding Path="ResourceKey"/>
</MultiBinding.Bindings>
</MultiBinding>
</InkCanvas.DefaultDrawingAttributes>
</Control:InkCanvasControl>
File Save and Open
Canvas strokes can be saved into a data store. To implement this option, the module contains a Model which is used to write and read canvas strokes to and from a file. The ViewModel implements the presentation logic through private methods for file save and file open. This logic in turn calls data service methods to serialize the data in XML format to save and deserialize the data from XML format back to Model.
File Type
The file type/extension that is associated with the sample is ".scrib".
public static readonly string FileType = "scribble files (*.scrib)|*.scrib";
Canvas: Model
The drawing modes and canvas strokes are the actual data to be stored. To describe this data in a Model, the
CanvasModel
is designed using two properties: one is an array of DrawingMode
s which can be used to identify the drawing attributes and the other is an array of Point
s containing the (x, y) coordinates to represent the strokes. This Model is used as a data transfer object between the ViewModel and the Data Service.
[Serializable]
public sealed class CanvasModel
{
public CanvasModel() { }
public DrawingMode[] Modes { get; set; }
public Point[][] Points { get; set; }
}
Presentation Logic
private void Save()
{
CanvasModel canvasModel = new CanvasModel();
canvasModel.Points = this.Strokes.GeneratePointArray();
canvasModel.Modes = this.Strokes.GetDrawingModes();
Microsoft.Win32.SaveFileDialog saveFileDialog = new Microsoft.Win32.SaveFileDialog();
saveFileDialog.Filter = CanvasConstants.FileType;
if (saveFileDialog.ShowDialog() == true)
{
DataService.CanvasService canvasService = new DataService.CanvasService();
canvasService.Write(saveFileDialog.FileName, canvasModel);
}
}
private void Open()
{
Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog();
openFileDialog.Filter = CanvasConstants.FileType;
if (openFileDialog.ShowDialog() == true)
{
DataService.CanvasService canvasService = new DataService.CanvasService();
CanvasModel canvasModel = canvasService.Read(openFileDialog.FileName);
for (int i = 0; i < canvasModel.Points.Length; i++)
{
if (canvasModel.Points[i] != null)
{
var strokes = canvasModel.Points[i].GenerateStroke((DrawingMode)canvasModel.Modes[i]);
this.Strokes.Add(strokes);
}
}
}
}
Service Logic
This is just a simple code to save and restore. It's not implemented in a very fine way. The code doesn't validate XML contents. The logic just works to save as and open ".scrib" files.
public CanvasModel Read(string fileName)
{
FileStream fs = new FileStream(fileName, FileMode.Open);
XmlSerializer serializer = new XmlSerializer(typeof(CanvasModel));
StreamReader reader = new StreamReader(fs);
CanvasModel canvasModel = (CanvasModel)serializer.Deserialize(reader);
fs.Close();
return canvasModel;
}
public void Write(string fileName, CanvasModel canvasModel)
{
FileStream fs = new FileStream(fileName, FileMode.Create);
XmlSerializer serializer = new XmlSerializer(canvasModel.GetType());
StreamWriter writer = new StreamWriter(fs);
serializer.Serialize(writer, canvasModel);
fs.Close();
}
Statusbar
The Statusbar Module is to show the strokes count and the selected tool at the bottom of the window.
Class Diagram
Statubar: Initialize
The initializer class registers the StatusbarView
with the container and adds the view to the region.
protected override void RegisterTypes()
{
this.Container.RegisterType<StatusbarView>();
this.RegionManager.RegisterViewWithRegion(
"StatusbarRegion", typeof(StatusbarView));
}
Statusbar: ViewModel
The ViewModel implements two properties that are bound to the view through binding; one to display the strokes count and the other to display the selected tool. Like
the Canvas
ViewModel,
the NotificationObject
class is the base class for the StatusBar
ViewModel to notify the View of any changes to strokes count and the selected tool. The values of strokes count
and selected tool are updated through the EventAggregator
service by subscribing to
ToolChangedEvent
and StrokeChangedEvent
(EventAggregator
discussed in a later section).
public string Strokes
{
get
{
return this.strokes;
}
set
{
this.strokes = value;
this.RaisePropertyChanged(() => this.Strokes);
}
}
public string SelectedTool
{
get
{
return this.selectedTool;
}
set
{
this.selectedTool = value;
this.RaisePropertyChanged(() => this.SelectedTool);
}
}
Statusbar: View
This is designed using the StatusBar
control and TextBlock
.
It simply data-binds the View-Model properties SelectedTool
and
Strokes
.
<StatusBar Background="Gray">
<TextBlock Text="{Binding SelectedTool}" Height="20" Foreground="White" FontWeight="Bold"/> |
<TextBlock Text="{Binding Strokes}" Height="20" Foreground="White" FontWeight="Bold"/>
</StatusBar>
PRISM: The Manager
PRISM is the overall manager.
- Manages the bootstrapping process by creating and initializing the shell through the Bootstrapper class
- Manages dependencies between the components through dependency injection container: Unity
- Registers the views to the region through region manager
- Locates the modules for the application
- Enables communication between modules through Event Aggregation
Loading the Modules
PRISM uses an IModuleCatalog
instance to locate the modules available to the application. In a PRISM application there are different ways of loading the modules. It can be done through code, using XAML, through a configuration file, or from a directory location. In the Scribble sample, Modules are loaded from the directory. To load the modules from the directory, first create a directory in the application start up path “\bin” folder with the name Modules. Then add the following code in the Bootstrapper class that tells PRISM the location of the modules.
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}
Finally, you’ll need to add a post-build event activity to copy the DLL generated by the project to the directory you specified as your ModuleCatalog. For example, here is the post-build event command line added to the canvas module. You will find this in Project -> Properties -> Build Events.
xcopy /y "$(TargetPath)" "$(SolutionDir)John.Scribble.Shell\$(OutDir)Modules\"
EventAggregator
PRISM provides a way for loosely coupled modules to communicate through the EventAggregator service. The aggregator service provides two functionalities: Publish and Subscribe. Communication between the modules can be enabled by publishing an event from one module and subscribing to the event from another module. The service allows multiple publish and subscribe of the events across modules. It also allows sending a message when publishing an event. The below diagram shows communication between the modules through the EventAggregator’s Subscribing and Publishing of events.
Menubar
publishes ToolChangedEvent
which is subscribed by
Canvas
and Statusbar
Canvas
publishes StrokeChangedEvent
which is subscribed by
Statusbar
Sample Code
Menubar ViewModel Publishes ToolChangedEvent
this.MenuItemCommand = new DelegateCommand<DrawingMode?>(param =>
{
eventAggregator.GetEvent<ToolChangedEvent>().Publish(param.Value);
});
Canvas ViewModel Subscribes to ToolChangedEvent
eventAggregator.GetEvent<ToolChangedEvent>().Subscribe(param =>
{
this.OnToolChanged(param);
});
Similarly, Statusbar ViewModel Subscribes to ToolChangedEvent
eventAggregator.GetEvent<ToolChangedEvent>().Subscribe(param =>
{
this.SelectedTool = string.Format("{0} {1}", "Selected Tool = ", param);
});
Conclusion
PRISM makes it a lot easier to build independent modules through the Unity Container. I would
recommend reading the PRISM documentation for complete details on all of the key concepts of PRISM. Hope you enjoyed reading this article. Maybe, learned something from my scribbling on WPF, MVVM, PRISM using InkCanvas sample.
Reference
- Prism 4 - Developer's Guide to Microsoft Prism[^]
History
- Inital post included code for Visual Studio 2010
Update
- Added source code for Visual studio 2012
- Code includes the following changes
- Added IView interface for View injection
- Registering view with region is done from Bootstrapper class
- BaseModule modified to contain only the instance of unity container for dependency injection
- RegionManager instance removed from BaseModule and Module Initializer classes updated accordingly