65.9K
CodeProject 正在变化。 阅读更多。
Home

Scribble: 使用 PRISM、MVVM 的 WPF InkCanvas 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (29投票s)

2013 年 7 月 9 日

CPOL

14分钟阅读

viewsIcon

86730

downloadIcon

4219

Scribble 是一个简单的 WPF InkCanvas 示例应用程序,使用遵循 MVVM 模式的 PRISM 框架构建。

目录

Scribble

引言

Scribble 是一个简单的 WPF InkCanvas 示例应用程序,使用遵循 MVVM 模式的 PRISM 框架构建。本文通过该示例,讨论了如何构建一个 PRISM 应用程序,包括创建 Shell、将功能部分分离为独立的模块、通过 Unity 容器注入依赖关系以及通过 EventAggregator 实现模块间的通信。此外,该示例还包含了一些关于如何以 MVVM 的方式对 InkCanvas 控件进行编程的代码片段,这些内容也在相关章节中进行了讨论。

软件环境

应用程序的初始版本是在以下环境中开发的

  • 开发环境:Visual Studio .NET 2010
  • 框架:.NET Framework 4.0
  • 用户界面:WPF
  • 编程语言:C# .NET

先决条件

您需要安装 Microsoft Visual Studio 2010 / 2012 以及以下 PRISM 4 库。

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

PRISM 库可在 CodePlex 上找到。您可以从此处下载 http://compositewpf.codeplex.com/[^]

要理解并开始构建 PRISM 应用程序,您需要具备 WPF 或 Silverlight(都使用 XAML)的实践经验,并对以下基本概念有良好理解:数据绑定、依赖属性、值转换器、命令、用户控件。

PRISM:概述

PRISM 是一个包含一组库的框架,用于设计和构建富 WPF 桌面应用程序。PRISM 来自 Microsoft Patterns and Practices 团队。该框架的主要优势在于我们可以构建松耦合的组件/模块,这些模块可以独立开发并集成到应用程序中。

在 Prism 应用程序中,模块通常被划分为功能单元,这些单元可以独立开发。这些模块包含与特定功能相关的视图、视图模型、服务、模型(数据模型)。

Bootstrapper、Shell、Regions、Views、Modules、Module catalog 是 PRISM 的一些关键概念。Shell 是加载这些模块的主启动应用程序。它通过 Prism Regions 定义应用程序的布局和结构。Prism 负责将这些模块加载到 Shell 中。Prism 的 Module catalog 存储应用程序使用的模块的信息(类型、名称和位置)。Bootstrapper 是负责初始化应用程序的类。 Bootstrapper 类定义在 Shell 应用程序内部。它负责注册 Prism 库、创建和初始化 Shell,以及配置 Module catalog。 Prism 4 文档[^] 提供了详细信息。

MVVM 模式

MVVM pattern

模型

Model 代表领域对象,即实际数据。在示例应用程序中,实际数据是以点数组(x 和 y 坐标)形式保存的画布笔触。Model 包含与画布笔触相关的属性。这些数据被保存到数据源(XML 文件)并检索。保存和检索逻辑被保存在一个使用 Model 来存储信息的服务类中。

视图

View 是应用程序的用户界面,它定义了屏幕的布局和外观。View 的代码隐藏中不包含 UI 逻辑。它包含一个调用 InitalizeComponent 方法的构造函数。此外,ViewModel 的引用被分配给 View 的 DataContext 属性,如下所示。

view.DataContext = viewModel

这样,View 就保留了对 ViewModel 的引用。

ViewModel

ViewModel 通过实现属性和命令来包含 View 的表示逻辑和数据。它是一个将 Model 和 View 分离的抽象层。View 中的控件通过数据绑定绑定到 ViewModel,ViewModel 通过更改通知事件向 View 通知任何更改。理想情况下,应用程序的所有逻辑行为都在 ViewModel 中实现。

应用程序结构

PRISM 应用程序通常由一个 Shell 项目和多个模块项目组成。基本思想是构建一个具有以下功能单元的桌面应用程序:

  • 一个 MenuBar,其中包含笔、荧光笔、橡皮擦等工具列表。
  • 一个 Canvas(绘图)区域,用于使用所选工具进行绘图。
  • 一个 StatusBar 区域,指示菜单中选择的工具以及 Canvas 区域中的笔触数量。

这些功能单元被开发为独立的模块,并通过 Prism 集成到主应用程序中。示例应用程序可以分为三部分,如下图所示

Application strucure

  • 主应用程序,包含 Shell
  • PRISM 和基础设施框架充当 Shell 和模块之间的桥梁。
  • 模块,包含功能单元,即 MenuBarCanvasStatusBar

构建应用程序

重要提示

本文中的代码块均参考初始版本的 VS 2010。如果您使用 VS 2012 进行操作,请在需要时参考源代码。2012 年的实现有一些重要更改,这些更改将在文章末尾(请参见历史记录部分中的更新)进行介绍。

下图显示了需要构建的应用程序的关键部分。最外层是 Shell,即核心应用程序。Shell 包含定义应用程序布局的区域。区域充当 Views 的容器,这些 Views 可以通过 PRISM 加载到其中。每个 View 都连接到一个包含表示逻辑的 ViewModel。

Shell, Region, View, ViewModel

Shell:主应用程序

Shell 是应用程序的起点。通常在 WPF 应用程序中,启动 URI 在 App.xaml 文件中指定,用于启动主窗口。在 PRISM 应用程序中,通过创建和初始化 Shell 来启动主窗口。这通过 Bootstrapper 实现。Bootstrapper 是负责初始化应用程序的类。Prism 库包含默认的 Bootstrapper 抽象基类。要创建 Shell,请执行以下步骤:

  1. 打开 Visual Studio 并创建一个新的 WPF 应用程序项目
  2. 添加 Prism 库的引用
  3. MainWindow.xaml 重命名为 Shell.xaml
  4. App.xaml 中,删除 startupUri
  5. 创建一个 Bootstrapper 类,并重写 UnityBootstrapper 的以下方法:
    • CreateShell
    • InitializeShell
  6. App.xaml 文件中重写此方法:
    • OnStartup

Bootstrapper.cs

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

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

区域:占位符

区域是定义在 Shell 应用程序内部的占位符,它们决定了应用程序的布局。可以用常见的控件作为容器控件来充当区域。来自其他模块/组件的 Views 会被加载到区域中。其他模块可以通过 RegionManager 组件按名称查找 Shell 中的区域。在示例应用程序中,在 Shell View 中定义了三个区域,分别是:

  • MenuRegion
  • CanvasRegion
  • StatusbarRegion

每个区域都加载了其相应的视图。视图与其相应的 ViewModel 相连接。视图在单独的模块中创建,并通过 PRISM 加载到 Shell 区域中。要在 XAML 中通过 RegionManager 为 Shell View 中的区域命名,我们需要导入以下命名空间:

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 模块是一个通用库,其中包含应用程序特定的逻辑,并充当 Shell 和模块之间的桥梁。在示例中,Infrastructure 库包含一个抽象基类、复合事件、枚举、接口。这些在其他模块中使用。在应用程序中拥有通用库可以提高可重用性。

类图

Infrastructure class

BaseModule

BaseModule 是一个实现 IModule 接口的抽象类,并声明了一个抽象方法 RegisterTypes。该类的构造函数保留 UnityContainerRegionManager 的引用,它们将由 Prism 传递给它。此类充当初始化模块的基类。

Unity Container

PRISM 应用程序依赖于依赖注入容器来管理组件之间的依赖关系。PRISM 库提供了两种容器选项:Unity 或 MEF。在示例中,我使用了 Unity 容器。容器主要用于在模块之间注入依赖关系、注册和解析视图,以及区域管理器、事件聚合器等服务。

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

需要一个模式列表来演示应用程序的状态,该列表可用于所有模块以识别应用程序状态。为此,在库中定义了一个名为 DrawingMode 的枚举列表。它包括所需的 InkCanvasEditingModesEraserShapesFileModes

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

该库定义了两个特定于应用程序的通用事件,即 ToolChangedEventStrokeChangedEvent。事件聚合器服务使用这些事件来实现模块之间的通信。EventAggregator 将在后面的章节中讨论。

/// <summary>
/// StrokeChangedEvent class
/// </summary>
public class StrokeChangedEvent : CompositePresentationEvent<DrawingMode> { }
/// <summary>
/// ToolChangedEvent class
/// </summary>
public class ToolChangedEvent : CompositePresentationEvent<string> { }

模块:功能部分

模块是应用程序的功能部分,包含模型、视图、视图模型以及实现模块特定逻辑和业务功能的类。模块实现在单独的类库中。在示例应用程序中,我为每个模块都使用了一个单独的类库项目。每个模块都包含一个 Initializer 类。Initializer 类是实现 IModule 接口并带有 Module 属性的类,该属性指定了模块的名称。

菜单栏

MenuBar 模块用于创建简单的工具选择菜单。一个模块有一个初始化类、一个视图和一个视图模型。它没有模型,因为没有必要。视图使用 Menu 控件设计,视图模型定义了一个 MenuItemCommand,该命令通过数据绑定绑定到视图。

Menubar

类图

Menubar

Menu:模块

初始化类将 MenuView 注册到容器,并将 MenuView 添加到 MenuRegion

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

ViewModel 实现一个命令属性,该属性以 DrawingMode 作为输入参数,并发布 TooChangedEvent。订阅此事件的其他模块将在执行命令时收到工具更改的通知。

/// <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 使用 Menu 控件,并且每个菜单项的 Command 属性都绑定到 ViewModel 的 MenuItemCommandCommandParameter 被赋值为相关的绘图模式。

<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

Canvas 模块是示例的核心部分,它使用 InkCanvas 控件设计并实现了 MVVM 模式。它包含(初始化类、Model、View、ViewModel、Data Service、Value converter、Extension methods、Constants)代码,这些代码提供了表示逻辑。

类图

Canvas class

Canvas:模块初始化

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

绘图属性

用于 PenHighlighterColorWidthHeightStylusTip 等绘图属性存储在一个静态变量中。

/// <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 通过 MultiBinding 转换器绑定到 canvas 视图。当选择特定的绘图工具(PenHighlighter)时,会调用转换器来返回相应的绘图属性。

DrawingAttributes:值转换器

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

Canvas ViewModel 实现必要的属性,这些属性指定了指向设备与 InkCanvas 交互的编辑模式,通过 PenHighlighterEraser 工具。但是,我们需要为橡皮擦模式编写一些额外的代码。橡皮擦工具提供了选择不同尺寸(小、中、大)橡皮擦形状的选项。InkCanvas 控件提供了 EraserShape 属性来实现这一点。但是,不能为 EraserShape 属性设置绑定。它不是依赖属性,也不能在 XAML 中使用。如果这样做,将会引发如下所示的异常。

EraserShape

在代码隐藏中编写逻辑不是一个好的做法。逻辑应该放在 ViewModel 中,并通过数据绑定在视图中实现。为了支持此属性的数据绑定,通过实现 EraseShape 的依赖属性来扩展 InkCanvas 控件。代码示例如下。

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

ViewModel 实现编辑模式、笔尖形状和笔触属性,这些属性通过数据绑定绑定到 CanvasView。PRISM 的 NotificationObject 类是 ViewModel 的基类,它实现了 INotifyPropertyChanged 接口。在绑定类型上的每个属性上都会调用 NotificationObject 类的 RaisePropertyChanged 事件。这是为了在绑定属性值更改时通知 View。这就是 View 和 ViewModel 之间通信的方式。

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

Canvas 视图使用 InkCanvasControl 控件,该控件是从 InkCanvas 定义的,支持 EraserShape 属性。通过 MultiBinding 转换器设置绘图属性,将资源键(DrawingMode)作为输入参数传递。

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

文件保存和打开

Canvas 笔触可以保存到数据存储中。为了实现此选项,该模块包含一个 Model,用于将 Canvas 笔触写入文件或从文件中读取。ViewModel 通过私有方法实现文件保存和文件打开的表示逻辑。此逻辑进而调用数据服务方法,将数据序列化为 XML 格式进行保存,并将数据从 XML 格式反序列化回 Model。

文件类型

与示例关联的文件类型/扩展名是“.scrib”。

public static readonly string FileType = "scribble files (*.scrib)|*.scrib";

Canvas:Model

绘图模式和 Canvas 笔触是需要存储的实际数据。为了在 Model 中描述这些数据,CanvasModel 使用两个属性进行设计:一个是由 DrawingMode 组成的数组,用于标识绘图属性;另一个是由 Point 组成的数组,包含 (x, y) 坐标来表示笔触。此 Model 用作 ViewModel 和 Data Service 之间的数据传输对象。

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

表示逻辑

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

服务逻辑

这只是一个简单的保存和恢复代码。它没有以非常精细的方式实现。代码不验证 XML 内容。该逻辑仅用于保存和打开“.scrib”文件。

/// <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 模块用于在窗口底部显示笔触计数和选定的工具。

statusbar

类图

Statusbar class

Statubar:初始化

初始化类将 StatusbarView 注册到容器,并将视图添加到区域。

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

ViewModel 实现两个属性,这些属性通过绑定绑定到视图;一个用于显示笔触计数,另一个用于显示选定的工具。与 Canvas ViewModel 一样,NotificationObject 类是 StatusBar ViewModel 的基类,用于通知 View 笔触计数和选定工具的任何更改。笔触计数和选定工具的值通过订阅 ToolChangedEventStrokeChangedEventEventAggregator 将在稍后讨论)来更新。

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

这使用 StatusBar 控件和 TextBlock 进行设计。它只是简单地对 ViewModel 的 SelectedToolStrokes 属性进行数据绑定。

<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:管理器

PRISM 是整体管理器。

  • 通过 Bootstrapper 类管理引导过程,创建和初始化 Shell
  • 通过依赖注入容器(Unity)管理组件之间的依赖关系
  • 通过区域管理器将视图注册到区域
  • 定位应用程序的模块
  • 通过事件聚合实现模块之间的通信

加载模块

PRISM 使用 IModuleCatalog 实例来查找应用程序可用的模块。在 PRISM 应用程序中,有多种加载模块的方式。可以通过代码、使用 XAML、通过配置文件或从目录位置进行加载。在 Scribble 示例中,模块是从目录加载的。要从目录加载模块,首先在应用程序启动路径的“\bin”文件夹中创建一个名为 Modules 的目录。然后在 Bootstrapper 类中添加以下代码,告知 PRISM 模块的位置。

/// <summary>
/// Method to load the modules from the directory
/// </summary>
/// <returns>The ModuleCatalog</returns>
protected override IModuleCatalog CreateModuleCatalog()
{
    return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}

最后,您需要添加一个生成后事件活动,将项目生成的 DLL 复制到您指定的 ModuleCatalog 目录。例如,这是添加到 canvas 模块的生成后事件命令行。您可以在 Project -> Properties -> Build Events 中找到它。

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

事件聚合器

PRISM 提供了一种方法,使松耦合的模块能够通过 EventAggregator 服务进行通信。聚合器服务提供发布和订阅两种功能。可以通过一个模块发布事件,另一个模块订阅该事件来启用模块之间的通信。该服务允许多个模块发布和订阅事件。它还允许在发布事件时发送消息。下图显示了模块通过 EventAggregator 的订阅和事件发布进行通信。

Publishing and subcribing to events

  • Menubar 发布 ToolChangedEvent,该事件由 CanvasStatusbar 订阅
  • Canvas 发布 StrokeChangedEvent,该事件由 Statusbar 订阅

示例代码

Menubar ViewModel 发布 ToolChangedEvent

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

Canvas ViewModel 订阅 ToolChangedEvent

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

类似地,Statusbar ViewModel 订阅 ToolChangedEvent

eventAggregator.GetEvent<ToolChangedEvent>().Subscribe(param =>
{
    this.SelectedTool = string.Format("{0} {1}", "Selected Tool = ", param);
});

结论

PRISM 通过 Unity Container 使构建独立模块变得更加容易。我建议阅读 PRISM 文档以获取 PRISM 所有关键概念的完整详细信息。希望您喜欢阅读本文。也许,从我关于使用 InkCanvas 示例的 WPF、MVVM、PRISM 的涂鸦中学到了一些东西。

参考

  1. Prism 4 - Developer's Guide to Microsoft Prism[^]

历史

  • 初始帖子包含 Visual Studio 2010 的代码

更新

  • 添加了 Visual Studio 2012 的源代码
  • 代码包含以下更改:
    • 为 View 注入添加了 IView 接口
    • 视图到区域的注册从 Bootstrapper 类完成
    • BaseModule 修改为仅包含用于依赖注入的 unity 容器实例
    • 从 BaseModule 中移除了 RegionManager 实例,并相应更新了 Module Initializer 类
© . All rights reserved.