Scribble: 使用 PRISM、MVVM 的 WPF InkCanvas 应用程序
Scribble 是一个简单的 WPF InkCanvas 示例应用程序,使用遵循 MVVM 模式的 PRISM 框架构建。
目录
引言
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 模式
模型
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 集成到主应用程序中。示例应用程序可以分为三部分,如下图所示
- 主应用程序,包含 Shell
- PRISM 和基础设施框架充当 Shell 和模块之间的桥梁。
- 模块,包含功能单元,即
MenuBar
、Canvas
和StatusBar
构建应用程序
重要提示
本文中的代码块均参考初始版本的 VS 2010。如果您使用 VS 2012 进行操作,请在需要时参考源代码。2012 年的实现有一些重要更改,这些更改将在文章末尾(请参见历史记录部分中的更新)进行介绍。
下图显示了需要构建的应用程序的关键部分。最外层是 Shell,即核心应用程序。Shell 包含定义应用程序布局的区域。区域充当 Views 的容器,这些 Views 可以通过 PRISM 加载到其中。每个 View 都连接到一个包含表示逻辑的 ViewModel。
Shell:主应用程序
Shell 是应用程序的起点。通常在 WPF 应用程序中,启动 URI 在 App.xaml 文件中指定,用于启动主窗口。在 PRISM 应用程序中,通过创建和初始化 Shell 来启动主窗口。这通过 Bootstrapper 实现。Bootstrapper 是负责初始化应用程序的类。Prism 库包含默认的 Bootstrapper 抽象基类。要创建 Shell,请执行以下步骤:
- 打开 Visual Studio 并创建一个新的 WPF 应用程序项目
- 添加 Prism 库的引用
- 将 MainWindow.xaml 重命名为 Shell.xaml
- 在 App.xaml 中,删除 startupUri
- 创建一个 Bootstrapper 类,并重写 UnityBootstrapper 的以下方法:
CreateShell
InitializeShell
- 在 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 库包含一个抽象基类、复合事件、枚举、接口。这些在其他模块中使用。在应用程序中拥有通用库可以提高可重用性。
类图
BaseModule
BaseModule
是一个实现 IModule
接口的抽象类,并声明了一个抽象方法 RegisterTypes
。该类的构造函数保留 UnityContainer
和 RegionManager
的引用,它们将由 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
的枚举列表。它包括所需的 InkCanvasEditingModes
、EraserShapes
、FileModes
。
/// <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
该库定义了两个特定于应用程序的通用事件,即 ToolChangedEvent
和 StrokeChangedEvent
。事件聚合器服务使用这些事件来实现模块之间的通信。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
,该命令通过数据绑定绑定到视图。
类图
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 的 MenuItemCommand
,CommandParameter
被赋值为相关的绘图模式。
<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:模块初始化
/// <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));
}
}
绘图属性
用于 Pen
和 Highlighter
的 Color
、Width
、Height
、StylusTip
等绘图属性存储在一个静态变量中。
/// <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 视图。当选择特定的绘图工具(Pen
、Highlighter
)时,会调用转换器来返回相应的绘图属性。
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
交互的编辑模式,通过 Pen
、Highlighter
和 Eraser
工具。但是,我们需要为橡皮擦模式编写一些额外的代码。橡皮擦工具提供了选择不同尺寸(小、中、大)橡皮擦形状的选项。InkCanvas
控件提供了 EraserShape
属性来实现这一点。但是,不能为 EraserShape
属性设置绑定。它不是依赖属性,也不能在 XAML 中使用。如果这样做,将会引发如下所示的异常。
在代码隐藏中编写逻辑不是一个好的做法。逻辑应该放在 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 模块用于在窗口底部显示笔触计数和选定的工具。
类图
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 笔触计数和选定工具的任何更改。笔触计数和选定工具的值通过订阅 ToolChangedEvent
和 StrokeChangedEvent
(EventAggregator
将在稍后讨论)来更新。
/// <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 的 SelectedTool
和 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:管理器
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 的订阅和事件发布进行通信。
Menubar
发布ToolChangedEvent
,该事件由Canvas
和Statusbar
订阅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 的涂鸦中学到了一些东西。
参考
历史
- 初始帖子包含 Visual Studio 2010 的代码
更新
- 添加了 Visual Studio 2012 的源代码
- 代码包含以下更改:
- 为 View 注入添加了 IView 接口
- 视图到区域的注册从 Bootstrapper 类完成
- BaseModule 修改为仅包含用于依赖注入的 unity 容器实例
- 从 BaseModule 中移除了 RegionManager 实例,并相应更新了 Module Initializer 类