Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF
Article

Scribble: WPF InkCanvas Application Using PRISM, MVVM

Rate me:
Please Sign up or sign in to vote.
4.90/5 (29 votes)
25 Jul 2013CPOL14 min read 81.6K   4.1K   60   19
Scribble is a simple WPF InkCanvas sample application built using PRISM framework that follows the MVVM pattern.

Contents

Scribble

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

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.

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

Application strucure

  • 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, Region, View, ViewModel

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:

  1. Open Visual Studio and create a new WPF Application Project
  2. Add reference to Prism libraries
  3. Rename MainWindow.xaml to Shell.xaml
  4. In App.xaml, remove the startupUri
  5. Create a Bootstrapper class and override the following methods of UnityBootstrapper:
    • CreateShell
    • InitializeShell
  6. Override this method in the App.xaml file:
    • OnStartup

Bootstrapper.cs

C#
/// <summary>
/// Bootstrapper class for initialization of application
/// </summary>
public class Bootstrapper : UnityBootstrapper
{
    /// <summary>
    /// Method to create a shell
    /// </summary>
    /// <returns>An instance of shell class</returns>
    protected override DependencyObject CreateShell()
    {
        return this.Container.Resolve<Shell>();
    }

    /// <summary>
    /// Method to initialize the shell as the main window
    /// </summary>
    protected override void InitializeShell()
    {
        base.InitializeShell();
        App.Current.MainWindow = (Window)this.Shell;
        App.Current.MainWindow.Show();
    }
}

App.xaml.cs

C#
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    /// <summary>
    /// Application startup method
    /// </summary>
    /// <param name="e">event args</param>
    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.

C#
Microsoft.Practices.Prism.Regions

Shell.xaml

XML
<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

Infrastructure class

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.

C#
/// <summary>
/// BaseModule Abstract class implements IModule interface
/// </summary>
public abstract class BaseModule : IModule
{
    /// <summary>
    /// Abstract method to register types
    /// </summary>
    protected abstract void RegisterTypes();
 
    /// <summary>
    /// Gets or set Unity container
    /// </summary>
    protected IUnityContainer Container { get; set; }
 
    /// <summary>
    /// Gets or sets region manager
    /// </summary>
    protected IRegionManager RegionManager { get; set; }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="BaseModule" /> class.
    /// </summary>
    /// <param name="container">unity container</param>
    /// <param name="regionManager">region manager</param>
    public BaseModule(IUnityContainer container, IRegionManager regionManager)
    {
        this.Container = container;
        this.RegionManager = regionManager;
    }
 
    /// <summary>
    /// Method to initialize the module
    /// </summary>
    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.

C#
/// <summary>
/// DrawingMode enums
/// </summary>
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.

C#
/// <summary>
/// StrokeChangedEvent class
/// </summary>
public class StrokeChangedEvent : CompositePresentationEvent<DrawingMode> { }
C#
/// <summary>
/// ToolChangedEvent class
/// </summary>
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.

Menubar

Class Diagram

Menubar

Menu: Module

The initializer class registers the MenuView with the container and adds the MenuView to the MenuRegion.

C#
/// <summary>
/// MenubarModule class
/// </summary>
[Module(ModuleName = "MenubarModule")]
public class MenubarModule : BaseModule
{
    /// <summary>
    /// Initializes a new instance of the <see cref="MenubarModule" /> class.
    /// </summary>
    /// <param name="regionManager">region manager</param>
    /// <param name="container">unity container</param>
    public MenubarModule(IUnityContainer container, IRegionManager regionManager)
        : base(container, regionManager) { }

    /// <summary>
    /// Method for registering menubar types with the container
    /// </summary>
    protected override void RegisterTypes()
    {
        // Register the MenuView with the container
        this.Container.RegisterType<MenuView>();

        // Add the MenuView to the MenuRegion
        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.

C#
/// <summary>
/// MenuViewModel class
/// </summary>
public class MenuViewModel
{
    /// <summary>
    /// Initializes a new instance of the <see cref="MenuViewModel" /> class.
    /// </summary>
    public MenuViewModel()
    {
        IEventAggregator eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
        this.MenuItemCommand = new DelegateCommand<DrawingMode?>(param => 
        {   
            eventAggregator.GetEvent<ToolChangedEvent>().Publish(param.Value); 
        });
    }

    /// <summary>
    /// Gets MenuItemCommand
    /// </summary>
    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.

XML
<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 class

Canvas: Module Initialize

C#
/// <summary>
/// CanvasModule class
/// </summary>
[Module(ModuleName = "CanvasModule")]
public class CanvasModule : BaseModule
{
    /// <summary>
    /// Initializes a new instance of the <see cref="CanvasModule" /> class.
    /// </summary>
    /// <param name="regionManager">region manager</param>
    /// <param name="container">unity container</param>
    public CanvasModule(IRegionManager regionManager, IUnityContainer container)
        : base(container, regionManager) { }

    /// <summary>
    /// Method for registering canvas types with the container
    /// </summary>
    protected override void RegisterTypes()
    {
        // Register the CanvasView with the container
        this.Container.RegisterType<CanvasView>();

        // Add the CanvasView to the CanvasRegion
        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.

C#
/// <summary>
/// Drawing attributes for Pen (Black, Blue, Red, Green) and Highlighter (Yellow, Pink)
/// </summary>
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, Highl<code>ighter) is selected, the converter is called to return the corresponding drawing attributes.

DrawingAttributes: Value Converter

C#
/// <summary>
/// DrawingAttributesConverter class
/// </summary>
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:

EraserShape

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.

C#
/// <summary>
/// InkCanvasControl class extending the InkCanvas class
/// </summary>
public class InkCanvasControl : InkCanvas
{
    /// <summary>
    /// Gets or set the eraser shape
    /// </summary>
    new public StylusShape EraserShape
    {
        get { return (StylusShape)GetValue(EraserShapeProperty); }
        set { SetValue(EraserShapeProperty, value); }
    }

    // Using a DependencyProperty as the backing store for EraserShape.  
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty EraserShapeProperty =
        DependencyProperty.Register("EraserShape", typeof(StylusShape), 
        typeof(InkCanvasControl), 
        new UIPropertyMetadata(null, OnEraserShapePropertyChanged));

    /// <summary>
    /// Event to handle the property change
    /// </summary>
    /// <param name="d">dependency object</param>
    /// <param name="e">event args</param>
    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.

C#
/// <summary>
/// CanvasViewModel class
/// </summary>
public class CanvasViewModel : NotificationObject
{
    private InkCanvasEditingMode editingMode;
    private DrawingMode resourceKey;
    private StylusShape stylusShape;
    private StrokeCollection strokes;
    private IEventAggregator eventAggregator;

    /// <summary>
    /// Initializes a new instance of the <see cref="CanvasViewModel" /> class.
    /// </summary>
    public CanvasViewModel()
    {
        // Get instance of event aggregator
        this.eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();

        // Initializes new instance of StrokeCollection class
        this.strokes = new StrokeCollection();

        // On Stroke change publish StrokeChangedEvent to update the count in statusbar
        (this.strokes as INotifyCollectionChanged).CollectionChanged += delegate
        {
            this.eventAggregator.GetEvent<StrokeChangedEvent>().Publish(this.strokes.Count);
        };
 
        // Subscribe to tool changed event
        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; }
    }        

    /// <summary>
    /// Method to perform the tool change operation
    /// </summary>
    /// <param name="drawingMode"></param>
    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:

XML
<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".

C#
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 DrawingModes which can be used to identify the drawing attributes and the other is an array of Points 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.

C#
/// <summary>
/// CanvasModel class
/// </summary>
[Serializable]
public sealed class CanvasModel
{
    /// <summary>
    /// Initializes a new instance of the <see cref="CanvasModel" /> class.
    /// </summary>
    public CanvasModel() { }
    
    /// <summary>
    /// Variable for Modes array
    /// </summary>
    public DrawingMode[] Modes { get; set; }

    /// <summary>
    /// Variable for point array
    /// </summary>
    public Point[][] Points { get; set; }
}

Presentation Logic

C#
/// <summary>
/// Method to save the canvas strokes to a scribble file
/// </summary>
private void Save()
{
    CanvasModel canvasModel = new CanvasModel();

    // Call the extension method to convert the strokes in to point array
    canvasModel.Points = this.Strokes.GeneratePointArray();

    // Call the extension method to get the drawing modes of strokes
    canvasModel.Modes = this.Strokes.GetDrawingModes();

    // create a instance of file dialog box to specify the file name and location for save
    Microsoft.Win32.SaveFileDialog saveFileDialog = new Microsoft.Win32.SaveFileDialog();

    // Set the filter that determines the file type
    saveFileDialog.Filter = CanvasConstants.FileType;

    if (saveFileDialog.ShowDialog() == true)
    {
        DataService.CanvasService canvasService = new DataService.CanvasService();

        // call the service method to serialize and save the the contents
        canvasService.Write(saveFileDialog.FileName, canvasModel);              
    }
}

/// <summary>
/// Method to open a scribble file
/// </summary>
private void Open()
{
    // create a instance of file dialog box to specify the file
    Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog();
    openFileDialog.Filter = CanvasConstants.FileType;

    // call showDialog to display the file dialog
    if (openFileDialog.ShowDialog() == true)
    {
        // Call service method to deserialize the file contents into Model
        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)
            {
                // Call the extension method to convet the points array to storke
                var strokes = canvasModel.Points[i].GenerateStroke((DrawingMode)canvasModel.Modes[i]);

                // add the stroke to the collection
                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.

C#
/// <summary>
/// Method to read and deserialize the data
/// </summary>
/// <param name="fileName">file name</param>
/// <returns>canvas model</returns>
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;
}

/// <summary>
/// Method to serialize and save to the given location
/// </summary>
/// <param name="fileName">file Name</param>
/// <param name="canvasModel">canvas model</param>
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.

statusbar

Class Diagram

Statusbar class

Statubar: Initialize

The initializer class registers the StatusbarView with the container and adds the view to the region.

C#
/// <summary>
/// Method for registering statusbar types with the container
/// </summary>
protected override void RegisterTypes()
{
    // Register the StatusView with the container
    this.Container.RegisterType<StatusbarView>();

    // Add the StatusbarView to the StatusbarRegion
    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).

C#
/// <summary>
/// Gets or sets the strokes
/// </summary>
public string Strokes
{
    get
    {
        return this.strokes;
    }

    set
    {
        this.strokes = value;
        this.RaisePropertyChanged(() => this.Strokes);
    }
}

/// <summary>
/// Gets or sets the selected tool
/// </summary>
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.

C#
<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.

C#
/// <summary>
/// Method to load the modules from the directory
/// </summary>
/// <returns>The ModuleCatalog</returns>
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.

C#
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.

Publishing and subcribing to 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

C#
this.MenuItemCommand = new DelegateCommand<DrawingMode?>(param => 
{
    eventAggregator.GetEvent<ToolChangedEvent>().Publish(param.Value); 
});

Canvas ViewModel Subscribes to ToolChangedEvent

C#
eventAggregator.GetEvent<ToolChangedEvent>().Subscribe(param =>
{
    this.OnToolChanged(param);
});

Similarly, Statusbar ViewModel Subscribes to ToolChangedEvent

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

  1. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Ireland Ireland
Many years of experience in software design, development and architecture. Skilled in Microsoft .Net technology, Cloud computing, Solution Design, Software Architecture, Enterprise integration, Service Oriented and Microservices based Application Development. Currently, focusing on .Net Core, Web API, Microservices, Azure

Comments and Discussions

 
QuestionThanks for article! (small issue though) Pin
JohnKEA16-Jun-16 0:19
JohnKEA16-Jun-16 0:19 
AnswerRe: Thanks for article! Pin
JohnKEA16-Jun-16 4:03
JohnKEA16-Jun-16 4:03 
GeneralMy vote of 5 Pin
Ehsan Sajjad29-Apr-16 8:34
professionalEhsan Sajjad29-Apr-16 8:34 
QuestionException at startup Pin
Andrew Phillips17-Sep-14 21:55
Andrew Phillips17-Sep-14 21:55 
QuestionNot Working This Article Pin
LakDinesh31-Mar-14 19:00
professionalLakDinesh31-Mar-14 19:00 
GeneralExcellent article Pin
njohnstone17-Feb-14 11:27
njohnstone17-Feb-14 11:27 
GeneralRe: Excellent article Pin
John-ph2-Mar-14 23:56
John-ph2-Mar-14 23:56 
QuestionAh I recall the InkCanvas Pin
Sacha Barber9-Aug-13 2:49
Sacha Barber9-Aug-13 2:49 
AnswerRe: Ah I recall the InkCanvas Pin
John-ph9-Aug-13 3:08
John-ph9-Aug-13 3:08 
SuggestionGreat article with minor flaws Pin
Monte Christo25-Jul-13 4:37
professionalMonte Christo25-Jul-13 4:37 
GeneralRe: Great article with minor flaws Pin
John-ph25-Jul-13 7:59
John-ph25-Jul-13 7:59 
GeneralMy vote of 5 Pin
benmiao16-Jul-13 2:11
benmiao16-Jul-13 2:11 
GeneralGreat article. Pin
charlybones14-Jul-13 23:18
charlybones14-Jul-13 23:18 
GeneralRe: Great article. Pin
John-ph14-Jul-13 23:29
John-ph14-Jul-13 23:29 
QuestionExcellent example but is it really loosely coupled? Pin
Chris Marassovich10-Jul-13 19:12
Chris Marassovich10-Jul-13 19:12 
Excellent example. I have been trying to introduce myself to PRISM and so far I have found the examples either too simple (demonstrate nothing at all) or jump into the advanced topics so quickly that I am lost.

Yours is a perfect balance of simple concepts yet complete and complicated enough to demonstrate why and how PRISM is useful. Thank you.

Now to my question.
One point of PRISM is to build loosely coupled modules. Your example is excellent and I certainly see that demonstrated except for one thing. Your modules are aware of the region names of the shell. In my mind that is not loosely coupled.

Each module has its initialize code and within the RegisterTypes method you register the view with the regions name. So your module must be aware of the detail of the shell.

Would it not be better to perform this work within the bootstrapper? This would then be loosely coupled as your modules have no knowledge of the shell.

Or am I way off in my understanding?

I hope you understand my point and are able to respond.

Thank you once again for a fantastic example.
AnswerRe: Excellent example but is it really loosely coupled? Pin
John-ph10-Jul-13 23:49
John-ph10-Jul-13 23:49 
AnswerRe: Excellent example but is it really loosely coupled? Pin
njohnstone17-Feb-14 11:34
njohnstone17-Feb-14 11:34 
GeneralMy vote of 5 Pin
Dennis Dykstra10-Jul-13 10:37
Dennis Dykstra10-Jul-13 10:37 
GeneralRe: My vote of 5 Pin
John-ph11-Jul-13 2:16
John-ph11-Jul-13 2:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.