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

Catel - 第 0 部分(共 n 部分): 为什么选择 Catel?

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (32投票s)

2011年4月7日

CPOL

26分钟阅读

viewsIcon

128479

本文旨在解释在开发 WPF、Silverlight 和 Windows Phone 7 应用程序时,为什么您应该选择 Catel 作为(或其中一个)框架。

文章浏览器

目录

  1. 引言
  2. 通用功能
  3. 目标框架特定主题
  4. 使用 MVVM 编写多层应用程序

1. 引言

欢迎阅读关于 Catel 系列文章的第 0 部分。一个相当奇怪的部分,您不觉得吗?我们从许多人那里听到,Catel 的真正强大之处乍一看是无法察觉的。当人们开始使用 Catel 时,他们会非常高兴,但为什么他们应该选择 Catel 呢?

本文旨在解释在开发 WPF、Silverlight 和 Windows Phone 7 应用程序时,为什么您应该选择 Catel 作为(或其中一个)框架。

本文分为几个部分,一个通用部分,其余部分则针对 Catel 所针对的不同框架。建议先阅读通用部分,然后您可以跳到您感兴趣的目标框架。

本文并非深入解释所有功能。如果您正在寻找 Catel 的深入探讨,建议阅读所有其他专注于框架特定部分的文章。本文更多是 Catel 功能的总结。

2. 通用功能

2.1. 这是您的选择

我们非常重视您作为软件开发人员所需的自由。大多数框架要求开发人员学习其约定,或使用整个框架,否则就什么都不能用。当 Catel 的开发人员使用外部框架时,我们选择该框架有特定原因,不希望被它提供的所有其他出色功能所困扰(也许以后会,但现在不会)。

在 Catel 的开发过程中,我们努力保持这种对我们非常重要的自由度。因此,所有功能都是松耦合的。听起来很棒,但现在一切都被称为松耦合。Catel 包含许多不同的方面,例如日志记录、诊断、反射、MVVM、用户控件、窗口等。所有这些方面都是相互补充的,但 Catel 的伟大之处在于,**您**决定是只使用其中一个、部分还是所有方面。

例如:您有一个遗留应用程序,想使用 DataWindow 来编写简单的输入窗口,但您还没有准备好使用 MVVM。没问题,DataWindow 是 MVVM 的补充,但**不需要它**。因此,您可以自由地在需要时使用您所需的任何功能。

大多数框架都需要一个引导程序,它完全决定您的应用程序结构应该是什么样子。例如,您的视图必须有这个名称,您的控件必须有那个名称。同样,在 Catel 中,我们希望为您提供您对框架所期望的自由。

这种自由的优点在于 Catel 的不同方面可以与其他框架并行使用,因此作为开发人员,您可以为应用程序中的每个方面选择最佳框架。

2.2. 数据处理

首先重要的一点是,许多开发人员花费太多时间编写自定义可序列化对象。序列化是一个专业领域,我认识的开发人员中只有少数人真正掌握了对象的序列化(想想程序集版本更改、类更改(新属性或移除属性)等)。大多数开发人员认为他们通过创建像下面代码所示的 `BinaryFormatter` 对象来掌握序列化。

BinaryFormatter serializer = new BinaryFormatter();
var myObject = (MyObject)serializer.Deserialize(stream);

大多数开发人员不知道二进制序列化会在以下情况中断:

  1. 您更改了程序集的版本号;
  2. 您添加或移除了属性或字段;
  3. 您添加或移除了事件。

即使您知道,也需要大量的知识和勇气才能开始征服这一重负。像每个开发人员一样,我也遇到了这个问题,并且一直在编写向后兼容代码,直到我受够了并决定掌握序列化领域。结果就是 `DataObjectBase` 类,它可以作为所有需要在内存中保存并可能序列化到磁盘(或流、或 XML,或...)的数据对象的基类。

2.2.1. DataObjectBase 类

使用该类非常简单。只需声明一个派生自 `DataObjectBase` 的新类,您就可以开始了

/// <summary>
/// MyObject Data object class which fully supports serialization,
///          property changed notifications,
/// backwards compatibility and error checking.
/// </summary>
#if !SILVERLIGHT
[Serializable]
#endif
public class MyObject : DataObjectBase<MyObject>
{
    /// <summary>
    /// Initializes a new object from scratch.
    /// </summary>
    public MyObject() { }

#if !SILVERLIGHT 
/// <summary>
/// Initializes a new object based on <see cref="SerializationInfo"/>.
/// </summary>
/// <param name="info"><see cref="SerializationInfo"/>
//     that contains the information.</param>
/// <param name="context"><see cref="StreamingContext"/>.</param>
protected MyObject(SerializationInfo info, StreamingContext context)
    : base(info, context) { }
#endif
}

正如您在上面的代码中看到的,`MyObject` 类派生自 `DataObjectBase` 并提供了一个空构造函数,还有一个用于二进制反序列化的构造函数。上面的代码可能看起来很复杂,但它是使用 *dataobject* 代码片段创建的,您只需输入类的名称即可。

2.2.2. 定义属性

为类定义属性非常容易,并且与依赖属性的工作方式相同。这种定义属性的方式的优点是:

  • 以这种方式定义的属性会自动包含在序列化中;无需指定复杂的数据契约;
  • 您可以为属性指定默认值,当类构建或在反序列化期间未找到该属性时(如果该属性已添加到现有类中),将使用该默认值;
  • `PropertyData` 对象可用于检索属性值,因此编译器会检查错误;
  • 您可以直接订阅更改通知,并且所有属性都自动开箱即用支持 `INotifyPropertyChanged`。

下面是定义一个类型为 `string` 的新属性 `Name` 的代码

/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name
{
    get { return GetValue<string>(NameProperty); }
    set { SetValue(NameProperty, value); }
}

/// <summary>
/// Register the Name property so it is known in the class.
/// </summary>
public static readonly PropertyData NameProperty = 
       RegisterProperty("Name", typeof(string), string.Empty);

如果需要,已注册的属性可以从序列化中排除。在这种情况下,当对象反序列化时,将使用该属性的默认值。

2.2.3. 序列化

我已经提到过几次序列化了。让我们来看看(反)序列化您的对象是多么容易,无论您使用什么程序集版本。首先,不从 `DataObjectBase` 派生,而是从 `SavableDataObjectBase` 派生非常重要。

根据目标框架,有几种序列化模式可供选择:

  • 二进制;
  • XML;
  • 数据契约;
  • JSON。

下面的代码展示了如何保存一个对象(当然,它可以是一个复杂的嵌套对象图)

var myObject = new MyObject();
myObject.Save(@"C:\myobject.dob");

看起来太简单了,但这确实是您需要做的唯一事情。您可以在 `Save` 方法的几个可用重载中指定序列化模式。

加载和保存一样简单,如下面的代码所示

var myObject = MyObject.Load(@"C:\myobject.dob");

2.2.4. 开箱即用的功能

`DataObjectBase` 开箱即用提供了大量功能。我想提及的几点是:

所有使用 `RegisterProperty` 方法注册的属性都会自动处理更改通知。

使用可在可重写的 `ValidateFields` 和 `ValidateBusinessRules` 方法中使用的 `SetFieldError` 和 `SetBusinessError` 方法设置字段和业务错误非常容易。

数据对象可以自动创建内部备份,并在需要时使用 `IEditableObject` 接口进行恢复。

如前所述,使用 `SavableDataObjectBase`,您可以简单地将文件保存到流中(磁盘文件、内存流等)。

  • INotifyPropertyChanged
  • IDataErrorInfo
  • IEditableObject
  • 序列化

请记住,此类不适用于数据库通信,有更好的方法来处理此问题(ORM 映射器,例如 Entity Framework、NHibernate、LLBLGen Pro 等)。

此 API 位于 *Catel.Core* 程序集中,无需 WPF 即可使用(因此可在 ASP.NET、Windows Forms 等中使用)。

2.3. MVVM 基础

近几年,MVVM 已成为使用 WPF、Silverlight 和 Windows Phone 7 编写应用程序的首选模式。实际模式非常简单,但仍存在一些缺陷和许多 MVVM 用户遇到的问题,例如:

  • 如何在视图模型中显示模态对话框或消息框?
  • 如何在视图模型中运行进程?
  • 如何让用户在视图模型中选择文件?

在我看来,这就是优秀框架与劣质框架的区别。例如,直接在视图模型中调用 `MessageBox.Show` 的人,其模式使用方式是错误的。如果您是直接在视图模型中调用 `MessageBox` 的开发人员之一,请问自己:在单元测试期间谁来点击按钮?

在我们真正开始开发 Catel 之前,我们进行了大量的调查,以确保 MVVM 模式在线业务 (LoB) 应用程序中真正有用,并且不会遗漏画龙点睛之笔。通过这项调查和研究,我们创建了一个坚实的 MVVM 框架,解决了 MVVM 模式所有已知的问题。

2.3.1. ViewModelBase

与几乎所有其他 MVVM 框架一样,所有视图模型的基类都是 `ViewModelBase`。该基类派生自本文前面解释的 `DataObjectBase` 类,这带来了以下优点:

  • 类似于依赖属性的属性注册;
  • 自动更改通知;
  • 支持字段和业务错误。

由于该类派生自 `DataObjectBase`,您可以简单地添加字段和业务错误,这些错误会自动反映到 UI。编写视图模型从未如此简单!

2.3.2. 模型到视图模型的映射

在使用 MVVM 模式的过程中,我们注意到很多很多开发人员都有一个模型,并将模型的属性值映射到视图模型的所有属性。当 UI 关闭时,开发人员又将所有属性映射回模型。当使用 Catel 的视图模型时,所有这些冗余代码都是不必要的。

在 Catel 中,我们创建了属性,允许您将属性定义为模型。模型是视图模型的一部分向用户表示的属性。如果视图模型是多个模型的组合,则它可能具有多个模型。

定义模型非常简单,您只需用 `Model` 属性装饰您的属性即可

/// <summary>
/// Gets or sets the shop.
/// </summary>
[Model]
public IShop Shop
{
    get { return GetValue<IShop>(ShopProperty); }
    private set { SetValue(ShopProperty, value); }
}

/// <summary>
/// Register the Shop property so it is known in the class.
/// </summary>
public static readonly PropertyData ShopProperty = 
              RegisterProperty("Shop", typeof(IShop));

使用 `Model` 属性功能非常强大。基本上,这是视图模型中的扩展功能。如果模型支持 `IEditableObject`,则在视图模型的 `Initialize` 中会自动调用 `BeginEdit`。当视图模型被取消时,会调用 `CancelEdit`,从而撤消更改。

定义模型后,可以使用 `ModelToViewModel` 属性,如下面代码所示

/// <summary>
/// Gets or sets the name of the shop.
/// </summary>
[ViewModelToModel("Shop")]
public string Name
{
    get { return GetValue<string>(NameProperty); }
    set { SetValue(NameProperty, value); }
}

/// <summary>
/// Register the Name property so it is known in the class.
/// </summary>
public static readonly PropertyData NameProperty = RegisterProperty("Name", typeof(string));

上面代码示例中的 `ViewModelToModel` 属性会自动将视图模型的 `Name` 属性映射到 `Shop.Name` 属性。通过这种方式,您无需手动将值从模型映射到模型,反之亦然。另一个不错的效果是视图模型会自动验证使用 `Model` 属性定义的所有对象,并且所有映射的字段和业务错误都会自动映射到视图模型。

总而言之,`Model` 和 `ViewModelToModel` 属性确保不需要重复验证和手动映射。

2.3.3. 命令

Catel 完美支持命令。Catel 支持 *Command* 类,在其他框架中也称为 `RelayCommand` 或 `DelegateCommand`。在视图模型上定义命令非常简单,如下面的代码所示

// TODO: Move code below to constructor
Edit = new Command<object, object>(OnEditExecute, OnEditCanExecute);
// TODO: Move code above to constructor

/// <summary>
/// Gets the Edit command.
/// </summary>
public Command<object, object> Edit { get; private set; }

/// <summary>
/// Method to check whether the Edit command can be executed.
/// </summary>
/// <param name="parameter">The parameter of the command.</param>
private bool OnEditCanExecute(object parameter)
{
    return true;
}

/// <summary>
/// Method to invoke when the Edit command is executed.
/// </summary>
/// <param name="parameter">The parameter of the command.</param>
private void OnEditExecute(object parameter)
{
    // TODO: Handle command logic here
}

有些人不喜欢 `ICommand` 的实现。例如,Caliburn (Micro) 使用约定,不需要创建命令。这有一些缺点:

  • 它要求您确保控件的名称与方法相同;
  • 如果您不完全熟悉约定,则不清楚它实际上是一个命令;
  • 方法需要是公共的(否则,在单元测试期间您将如何调用命令?),这使得它们可以自由使用(这不是我们喜欢的东西);
  • 您将始终需要在 `Execute` 中再次调用 `CanExecute`,因为您无法保证 `Execute` 的来源实际上是约定映射;
  • 无法手动刷新绑定控件上的 `CanExecute` 状态。

2.3.4. 服务

服务是 Catel 解决您需要向用户显示消息、让用户运行进程等问题的方式。`ViewModelBase` 有一个 `GetService` 方法,可以通过控制反转 (IoC) 容器检索接口的实际实现。对于 WPF 和 Silverlight,Catel 使用 Unity。对于 Windows Phone 7,则使用自定义的 IoC 实现。

Catel 中服务的优点在于,默认情况下,实际实现已注册到 Unity 容器中。通过 IoC 容器,您可以注册测试实现(当然,Catel 也附带了这些实现),以便您可以决定例如消息框点击的结果。通过这种方式,您可以模拟用户点击“确定”,或者如果您愿意,也可以模拟“取消”。这样,您就可以对用户可以在视图模型上执行的所有可用可能性进行单元测试。

使用服务非常简单。如前所述,`GetService` 方法检索服务的实际实现。例如,要向用户显示消息,您可以使用以下代码

var messageService = GetService<IMessageService>();
messageService.ShowError("An error occurred");

所有服务都可以如此轻松地使用。Catel 支持多种服务。下表列出了每个目标框架可用的所有服务。

服务名称和描述 WPF Silverlight WP7

ILogger - 可以将消息写入日志

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\yes.png

ILocationService - 检索地理位置

Description: C:\Users\Geert\Desktop\no.png

Description: C:\Users\Geert\Desktop\no.png

Description: C:\Users\Geert\Desktop\yes.png

IMessageService - 显示消息框

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\yes.png

INavigationService - 导航服务

Description: C:\Users\Geert\Desktop\no.png

Description: C:\Users\Geert\Desktop\no.png

Description: C:\Users\Geert\Desktop\yes.png

IOpenFileService - 允许用户选择要打开的文件

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\no.png

Description: C:\Users\Geert\Desktop\no.png

IPleaseWaitService - 在 UI 线程上运行委托并显示忙碌指示器

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\no.png

IProcessService - 运行进程

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\no.png

Description: C:\Users\Geert\Desktop\no.png

ISaveFileService - 允许用户选择要保存的文件

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\no.png

Description: C:\Users\Geert\Desktop\no.png

IUIVisualizerService - 显示(模型)窗口

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\yes.png

Description: C:\Users\Geert\Desktop\no.png

当然,您也可以编写和注册自己的服务。

2.3.5. 与其他视图模型的通信

大多数框架要求您建立复杂的邮件系统(Messenger)或其他技术来与其他视图模型进行通信。这种方法的缺点是,一旦在模块 X 中编写了一个视图模型,而您对该视图模型感兴趣,模块 X 的开发人员必须负责通知其他视图模型。我们认为这不是原始视图模型的责任。

如果一个视图模型对另一个视图模型的更改感兴趣,那么关注该视图模型是感兴趣的视图模型的责任,而不是反过来。要收到其他视图模型更改的通知,您只需用 `InterestedIn` 属性装饰视图模型即可,如下面代码所示

[InterestedIn(typeof(FamilyViewModel))]
public class PersonViewModel : ViewModelBase

然后,在 `PersonViewModel`(对 `FamilyViewModel` 的更改感兴趣)中,您只需重写 `OnViewModelPropertyChanged` 方法

/// <summary>
/// Called when a property has changed for a view model type
/// that the current view model is interested in. This can
/// be accomplished by decorating the view model with the <see cref="InterestedInAttribute"/>.
/// </summary>
/// <param name="viewModel">The view model.</param>
/// <param name="propertyName">Name of the property.</param>
protected override void OnViewModelPropertyChanged(IViewModel viewModel, string propertyName)
{
    // You can now do something with the changed property
}

可以对多个视图模型感兴趣。由于视图模型被传递给 `OnViewModelPropertyChanged` 方法,因此检查视图模型的类型非常容易。

2.3.6. 嵌套用户控件问题

在 MVVM 中,存在一个复杂的架构问题,我们称之为“嵌套用户控件问题”。到目前为止,我们**没有找到任何其他框架**能够以清晰的方式解决此问题。问题在于嵌套用户控件通常应该拥有自己的视图模型,但是您如何管理这样的事情呢?我们看到了几种(糟糕的)解决方案,例如:

  • 在另一个视图模型上定义嵌套视图模型;
  • 在顶级视图模型上定义嵌套用户控件的属性,并假定嵌套用户控件内部的属性。

下面是问题的图形表示

常规 MVVM 框架

Catel MVVM 框架

如上图所示,Catel 解决问题的方法更加专业。以下是一些原因:

  • 职责分离(每个控件都有一个视图模型,只包含自己的信息,而不包含子控件的信息);
  • 用户控件旨在可重用。如果用户控件不能拥有自己的视图模型,那么如何实际使用带有 MVVM 的用户控件呢?

`UserControl` 能够根据用户控件的实际数据上下文构造一个视图模型。例如,当您定义一个嵌套用户控件时,您唯一需要做的就是确保用户控件的数据上下文具有一个可以注入到属于该用户控件的视图模型中的对象。

例如,假设我们有一个 `Person` 控件。此 Person 控件只能使用有效的 `IPerson` 实例(唯一可用的视图模型构造函数)进行构造。那么在 XAML 中定义它的方式如下

<Controls:PersonControl DataContext=”{Binding Person}” />

用户控件注意到数据上下文已更改,并尝试使用真实的数据上下文(在我们的例子中是 `IPerson` 接口的实例)构造 `PersonViewModel`。然后,它自动将 `IPerson` 对象转换为 `PersonViewModel`,并且该控件拥有自己的视图模型。

2.4. 用户界面

正如我们之前可能多次告诉您的那样,Catel 不仅仅是另一个 MVVM 框架。它还提供了许多开箱即用的 UI 元素。下面的控件是最受欢迎的。

2.4.1. InfoBarMessageControl

`InfoBarMessageControl` 能够使用 `IDataWarningInfo` 和 `IDataErrorInfo` 接口向用户显示警告和错误。该控件非常有助于以一致的方式向用户显示窗口或控件的当前状态。

2.4.2. DataWindow

在开发 WPF 软件时,我总是需要以下三种类型的窗口:

  • 数据窗口的“确定/取消”按钮;
  • 应用程序设置/选项的“确定/取消/应用”按钮;
  • 操作窗口的“关闭”按钮。

创建这些窗口只是无聊的,而且步骤总是相同的:

  1. 在窗口底部创建一个 `WrapPanel`
  2. 一遍又一遍地添加具有相同 `RoutedUICommand` 对象的按钮

`DataWindow` 类通过简单地指定窗口模式,使得创建这些基本窗口变得更加容易。通过使用此窗口,您可以专注于实际实现,而无需担心按钮本身的实现,这为您节省了时间!在下面的示例中,XAML 中手动编码的唯一内容是实际的输入控件(文本框)。

2.4.3. PleaseWaitWindow

`PleaseWaitWindow` 是在长时间操作期间显示的一个很好的窗口。还有一个 `PleaseWaitHelper` 类,可以更轻松地使用 `PleaseWaitWindow`。

2.5. IO

很多人不知道的是,Catel 提供了用于 IO 处理的扩展 API。现在您可能会问自己为什么。IO 框架进行了以下添加:

  • 支持长度超过 255 个字符的文件和目录;
  • 将多个路径或 URL 组合在一起,例如 `Path.Combine(“C:”, “Windows”, “Temp”);`。

该 API 旨在匹配 `System.IO` 样式,因此使用起来非常直观。

此 API 位于 *Catel.Core* 程序集中,无需 WPF 即可使用(因此可在 ASP.NET、Windows Forms 等中使用)。

2.6. 反射

Catel 内部使用了大量的反射,以确保所有承诺的功能都能实现。本章解释了这些反射实现的一些优点,它们并非旨在完全取代 .NET 反射类。Catel 的反射类更多是对默认类的补充。

程序集扩展主要是为了在 Catel 的所有不同目标框架之间创建一致的行为。例如,在 WPF 中,我们可以简单地使用当前的 `AppDomain` 获取已加载的程序集。然而,Silverlight 要求我们查询当前的 `Deployment` 对象。对于 Windows Phone 7,获取已加载的程序集甚至更难。通过在单独的类中实现差异,可以在目标框架之间共享所有共享类。

此 API 位于 *Catel.Core* 程序集中,无需 WPF 即可使用(因此可在 ASP.NET、Windows Forms 等中使用)。

2.7. 诊断与日志记录

Catel 使用 log4net 作为所有日志记录活动的基础。它确实提供了额外的功能和扩展方法,使记录异常和自定义消息变得更加容易。由于 Silverlight 和 Windows Phone 7 没有日志记录功能(客户端),因此 `ILog` 实现只是一个虚拟实现。

此 API 位于 *Catel.Core* 程序集中,无需 WPF 即可使用(因此可在 ASP.NET、Windows Forms 等中使用)。

2.8. 质量

每个框架都声称提供完美的质量。我们希望做得更好。因此,编写了超过 450 个单元测试来证明 Catel 提供了质量和一致性。单元测试在 WPF 和 Silverlight 之间共享,以确保两个框架的预期行为相同。通过这种方式,我们努力确保 Silverlight 的功能没有损失,我们在这方面大部分都成功了(有些事情在 Silverlight 中就是不可能实现的)。

2.9. 示例与文档

示例和文档对我们非常重要。因此,每个框架实现都有一个或多个示例应用程序,以展示如何使用每个目标框架的各个方面。

Catel 的文档也非常完善。除了大量的文章和博客文章外,它还包括包含所有类定义及其含义的参考文档。

除了示例和文档,Catel 还为每个目标框架提供了项目和项目模板。这样,您就不必进行大量繁琐的工作,让工具(Visual Studio)为您完成。除了模板之外,Catel 还提供了代码片段,如果您希望能够使用 Catel 快速编写软件,这些片段非常重要。

最后但同样重要的是,您喜欢 Catel 对我们来说非常重要。因此,我们在 CodePlex 项目网站的讨论区提供了非常好的支持。如果您有敏感信息问题,也可以亲自给项目成员发送电子邮件。

3. 目标框架特定主题

您现在可能已经知道,Catel 支持 Silverlight 实现。Silverlight 是一个非常流行的软件开发平台,但它缺少 WPF 中已经提供的许多功能。因此,在开发 Catel 时,我们尝试平衡 Catel 中所有可用类和控件的功能。

我们试图以同样的方式实现 Windows Phone 7,但在为 Windows Phone 7 开发框架时,需要考虑更多因素,例如性能以及完全不同的 UI 和状态管理方法。

3.1. Silverlight 中命令的自动刷新

在 Silverlight 中,命令不会自动重新评估,因为没有 `CommandManager` 会在每个路由事件上调用 `CanExecute`。如果用户通过在空白字段中添加值等方式解决窗口中的错误,则“确定”或“保存”命令的 `CanExecute` 状态不会更新(并且仍然禁用)。其他框架要求开发人员手动刷新命令。

Catel 提供了一种简洁的方式来提供与 WPF 相同的行为。由于 `ViewModelBase` 属性 `InvalidateCommandsOnPropertyChanged`(对于 Silverlight 和 Windows Phone 7 默认启用),视图模型上的所有 `ICommand` 实现都会自动为您重新评估。这样,用户在设置值后会立即看到“确定”或“保存”按钮变为启用状态。

3.2. 在 Silverlight 和 Windows Phone 7 中将数据序列化到独立存储

在为 Silverlight 或 Windows Phone 7 开发软件时,您可以访问独立存储。`SavableDataObjectBase` 开箱即用支持将复杂图形对象保存和加载到独立存储。要保存一个对象(包括所有子对象),您可以使用以下代码

using (var isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())
{
    using (var isolatedStorageFileStream = 
           isolatedStorageFile.OpenFile("UserData.dob", 
           FileMode.Create, FileAccess.ReadWrite))
    {
        myObject.Save(isolatedStorageFileStream);
    }
}

要再次加载数据,您可以使用以下代码

using (var isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())
{
    if (isolatedStorageFile.FileExists("UserData.dob"))
    {
        using (var isolatedStorageFileStream = 
               isolatedStorageFile.OpenFile("UserData.dob", 
               FileMode.Open, FileAccess.Read))
        {
            return MyObject.Load(isolatedStorageFileStream);
        }
    }
}

3.3. Windows Phone 7 上的导航

Windows Phone 7 上的导航非常重要。导航的工作方式与网页相同,因此带有请求变量。在 Catel 中,使用 `INavigationService` 导航到另一个视图(模型)非常容易。

要直接导航到另一个视图,只需在视图模型中使用此代码

var navigationService = GetService<INavigationService>();
navigationService.Navigate("/UI/Pages/MyPage.xaml");

当然,您也可以轻松传递参数

var parameters = new Dictionary<string, object>();
parameters.Add("MyObjectID", 1);

var navigationService = GetService<INavigationService>();
navigationService.Navigate("/UI/Pages/MyPage.xaml", parameters);

导航服务会自动将其转换为以下 URL

/UI/Pages/MyPage.xaml?MyObjectID=1

3.4. 在 Windows Phone 7 上检索和模拟地理位置

Windows Phone 7 设备有一个很棒的功能:它可以通过 GPS 检索当前地理位置。但是,我们如何在 MVVM 中正确实现这一点呢?在 Catel 中,有一个单独的服务可用于获取地理位置。用法非常简单:

var locationService = GetService<ILocationService>();
locationService.LocationChanged += OnCurrentLocationChanged;
locationService.Start();

当您不再需要该服务时,停止它非常重要(考虑用户的电池寿命)

var locationService = GetService<ILocationService>();
locationService.LocationChanged -= OnCurrentLocationChanged;
locationService.Stop();

在 `OnCurrentLocationChanged` 事件中,您可以简单地从 `EventArgs` 查询位置

private void OnCurrentLocationChanged(object sender, LocationChangedEventArgs e)
{
    if (e.Location != null)
    {
        MapCenter = new GeoCoordinate(e.Location.Latitude, 
                        e.Location.Longitude, e.Location.Altitude);
    }
}

4. 使用 MVVM 编写多层应用程序

我们收到许多用户关于如何使用 MVVM 实现 n 层应用程序的问题。大多数人认为 MVVM 取代了业务层,但情况并非如此。本章解释了几种使用 MVVM 编写业务线 (LoB) 应用程序的可能性,您可以根据需要将其复杂化或简化。

根据最终用户的复杂性和需求,您可能会决定为您的软件使用多层架构。我们不相信单一标准,而是相信一种务实的方法,即每个项目的层数都重新确定,以确保不会产生不必要的开销,同时也不会遗漏任何层。

本章中的每个 N 层描述都以图形方式展示了 MVVM 应如何在架构中使用。很久以前,N 层模型是为了实现职责分离而开发的。在这些日子里,数据访问层 (DAL) 本身就是一个复杂的东西。如今,有大量的 ORM 映射器为您生成 DAL。其中一些 ORM 映射器,例如 LLBLGen Pro,允许您在实体上添加自定义验证,并将 DAL 与一些(或在简单应用程序中,所有)业务功能结合起来。

4.1. 两层应用程序

两层应用程序只应该用于非常非常简单的应用程序,这些应用程序不需要任何存储或业务规则。下面是使用 MVVM 的两层应用程序架构的图形表示

在编写两层应用程序时,您只拥有一个业务层(BL)或数据访问层(DAL)。如果您的应用程序非常简单,几乎没有业务规则,那么这种架构是一个选择。在这种情况下,您可以简单地移除业务层,并在模型(DAL)或视图模型中处理业务规则。

4.2. 三层应用程序

随着应用程序变得越来越复杂,用户开始要求更多的功能和可能的业务规则。在编写大型应用程序时,建议编写三层应用程序,将业务规则与 DAL 分离。下面是 MVVM 如何融入三层架构的图形表示

如您所见,在使用三层应用程序时,DAL 完全不参与 MVVM。这是因为业务层提供用作模型的数据传输对象 (DTO),并将这些 DTO 对象转换为 DAL 可以处理的实体。我们看到很多用户从 UI 层引用 DAL,但这是错误的。下面是三层架构的良好和不良情况的图形表示

上图中箭头表示“使用”。

良好情况

在良好情况下,您会看到 UI 层使用 BL,而 BL 使用 DAL。顶层总是可以使用较低的“下坡”层。较低层绝不能使用上一层(如“非常错误”情况所示)。

由于完全没有引用 DAL,因此需要创建数据传输对象 (DTO) 来表示 BL 中来自 DAL 的数据。DTO 对象在 BL 中定义,因此 BL 负责将 DAL 中的实体转换为可从 UI 层访问的 DTO 对象。

错误情况

错误的场景展示了一种我们经常看到的“解决方案”。使用这种方法的开发人员没有意识到多层架构,而是直接在 UI 层引用 DAL。这样,他们就不必创建自定义 DTO 对象。

如果您不想编写自定义 DTO 对象,唯一的选择是编写一个可供所有层使用的跨领域关注库。在跨领域关注中,您可以为 DAL 的所有实体编写接口,并在 BL 和 UI 层中使用这些接口。

如今,大多数人使用 ORM 映射器。ORM 映射器可用于生成 DAL 的源代码,这样您就不必自己编写该层。一个建议是自定义您选择的 ORM 映射器的模板(如果映射器支持),以便它也为您生成 DTO 对象。

非常糟糕的情况

“非常糟糕”的情况展示了三层架构的三个层,其中用户引用了更高级别的层。这是非常糟糕的,它破坏了 n 层应用程序架构背后的职责分离 (SoC) 理念。如果您认为这是正确的,请回答以下问题:您的 BL 在 WPF 应用程序中引用了 UI,您将如何为 Windows Phone 7 或 ASP.NET 应用程序使用相同的 BL?

4.3. MVVM 与 Silverlight 和 RIA 服务结合使用

许多开发人员发现很难找到一种将 MVVM 与 Silverlight 和 RIA 服务结合使用的好方法。从架构的角度来看,RIA 服务所做的不过是以数据传输对象(DTO)的形式提供数据。因此,带有 RIA 服务的 Silverlight 应用程序必须以与上述三层架构相同的方式使用,将 DTO 用作模型。

© . All rights reserved.