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

表示模型 (MVVM) 最佳实践

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (13投票s)

2010年5月4日

CPOL

16分钟阅读

viewsIcon

72368

downloadIcon

961

展示一些可应用于演示模型/MVVM 模式的最佳实践。

目录

引言

大约两个月前,我发现了 Josh Smith 的 文章,其中介绍了一种名为 MVVM 的 WPF 设计模式。正如文章所述,MVVM 并非一个独立的设计模式,它只是 Martin Fowler 提出的 演示模型 模式的一个 WPF "改编版本"。MVVM 与演示模型的主要区别在于 ViewModels 依赖于 WPF 库,以便更好地利用 WPF 的原生同步机制并移除所有代码隐藏。这些演示模型非常适合 WPF/Silverlight 项目,但有些人(比如我)仍然不习惯在应用程序逻辑与特定 GUI 框架之间创建依赖关系。

在本篇文章的演示中,我将展示如何使用演示模型设计模式来设计 WPF 应用程序,其效率与使用 MVVM 相当,但又不创建对 WPF 或任何 GUI 框架的依赖。此外,还将涵盖其他一些主题。以下是摘要:

  • 使用演示模型 UI 设计模式创建 WPF 应用程序。
  • 在不改变主应用程序逻辑的情况下,在演示模型应用程序中使用多线程。这项技术称为面向切面编程。
  • 在演示模型中使用对话框,而无需创建对特定 GUI 框架的依赖。
  • 通过使用服务定位器设计模式来反转应用程序的控制。
  • 使用 TypeDescriptors 为类层次结构添加 "虚拟" 行为。
  • 展示 Castle Windsor (控制反转容器) 的一些基本用法。

必备组件

要最大化本文的价值,您应该熟悉演示模型设计模式。推荐阅读 原始论文,或 Josh Smith 的 文章。我不会展示任何 XAML,但建议对 WPF/Silverlight 有一定了解,以便理解 WPF 项目中的内容,尽管这并非必需(我第一次真正接触 WPF 是通过阅读 Josh Smith 的文章)。至少,您应该使用过某种提供高级数据绑定的技术,如 ASP.NET 或 Windows Forms (WPF 数据绑定是迄今为止最强大的)。为了打破组件之间的依赖,我将使用服务定位器模式,因此如果您之前使用过某种抽象服务/组件创建的模式,这将有所帮助。这是一篇关于该主题的精彩 阅读资料

演示概述

本次演示将是一个选项卡式 WPF 应用程序,与 Josh Smith 的教程中的类似(我甚至借用了一些 XAML :))。示例解决方案将由五个项目组成:

  • 演示模型项目 (PresentationModelBase)。为了更好的兼容性,它将是一个 .NET 2.0 类库项目。
  • 核心应用程序项目 (App.Core)。它将包含基本的应用程序逻辑/工作流 (演示模型) 和服务契约。这也将是一个 .NET 2.0 库。
  • 数据持久化项目 (App.Data)。它将是核心项目中定义的唯一数据访问接口的一个模拟实现。
  • 配置项目 (App.Configuration)。为了简单起见,此项目将配置并包含所有将 "注入" 到应用程序中的代码。
  • WPF 前端项目 (WPFUI)。除了 XAML,它还将包含调用 App.Configuration 中的配置类并启动应用程序的代码。

虽然本次演示远非复杂,但这里使用的设计可以应用于更真实的商业项目。

演示模型项目

此项目将包含演示模型模式的一些基类。几乎每个演示模型类都将有一个对应的接口,公开其公共 API。这是一个好习惯,因为如果您的组件通过接口通信,它们的依赖关系可以轻松替换,或者为单元测试进行模拟 (本文不涵盖这一点,有很多示例)。让我们从基础演示模型接口开始:

public interface IPresentationModel : INotifyPropertyChanged, IDisposable
{
    string DisplayName { get; set; }

    void InvokeMethod(string methodName, object[] parameters, params Type[] typeParameters);
}

DisplayName 属性不言自明。InvokeMethod 方法可用于调用 PresentationModel 类中的方法。目前,只需知道这是一个连接用户界面和应用程序逻辑的 "网关"。这是实现:

public abstract class PresentationModel : IPresentationModel
{
    static PresentationModel()
    {
        _serviceLocator = AppDomain.CurrentDomain.GetData("servicelocator") 
                          as IServiceProvider;
        if (_serviceLocator == null)
            _serviceLocator = CreateProvider();
    }

    private static IServiceProvider CreateProvider()
    {
        ServiceCreator creator = new ServiceCreator();
        return new ServiceContainer(creator);
    }

    #region Fields

    static IServiceProvider _serviceLocator;

    #endregion

    #region Methods

    protected T Get<T>>()
    {
        return (T)_serviceLocator.GetService(typeof(T));
    }

    public virtual void InvokeMethod(string methodName, 
           object[] parameters, params Type[] typeParameters)
    {
        MethodInfo currentMethod = this.GetType().GetMethod(methodName, 
          BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        if (currentMethod == null)
            throw new ArgumentException("Cannot find this method");

        Type returnType = currentMethod.ReturnType;
        if (returnType != typeof(void))
            throw new ArgumentException("Methods called " + 
                      "by this command must return void");

        if (currentMethod.IsGenericMethodDefinition)
            currentMethod = currentMethod.MakeGenericMethod(typeParameters);

        currentMethod.Invoke(this, parameters);
    }

    #endregion

    #region Properties

    private string _displayName;
    public virtual string DisplayName
    {
        get { return _displayName; }
        set { _displayName = value; }
    }

    #endregion

    #region INotifyPropertyChanged Members

    private PropertyChangedEventHandler _handler;
    public event PropertyChangedEventHandler PropertyChanged
    {
        add { _handler += value; }
        remove { _handler -= value; }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (_handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            _handler(this, e);
        }
    }

    #endregion

    #region IDisposable Members

    public void Dispose()
    {
        OnDispose();
    }

    protected virtual void OnDispose() { }

    #endregion
}

如您所见,将从演示模型框架的核心使用服务定位器模式,并带有帮助方法 'Get<T>()'。基本上,使用此模式意味着每当您需要一个用于某个任务的对象时,都必须向另一个对象 (服务定位器) 请求它,而不是直接实例化它们。如果您从未使用过控制反转,这可能听起来毫无用处,但正如我将在本文中展示的,使用服务定位器模式允许程序员做一些非常有趣的事情,例如在不直接修改代码的情况下向代码注入某些行为。对于此应用程序,我将使用 Castle Windsor,一个实现了 .NET 框架中服务定位器基本接口 IServiceProvider 的 IoC 容器。如静态构造函数所示,当初始化 PresentationModel 类型时,它会在 AppDomain 的 servicelocator 属性中查找服务定位器,但如果找不到,它将使用默认服务定位器,因此它不依赖于额外的代码即可工作。它使用的服务定位器是 .NET Framework 自带的 IServiceContainer (它也是一个 IServiceProvider) 的实现。此实现可以与另一个 IServiceProvider 挂钩,该提供程序将在其不包含所请求的服务时使用。因此,我创建了一个特殊的 IServiceProvider 实现,它具有以下属性:

  • 它包含一些预注册的组件。
  • 如果请求一个组件但未预注册,它将尝试使用反射实例化它 (如果它是一个具体类)。

ServiceCreator 已与 ServiceContainer 挂钩,因此此框架无需任何预配置即可工作。在此示例中,我将不使用此自定义服务定位器。我只是想展示如何构建一个使用服务定位器模式的框架,并且仍然使其在没有配置的情况下工作。我将介绍的第一类具体演示模型是 ICustomActionPresentationModel;在 Josh Smith 的 MVVM 示例中,与之等价的是 CommandViewModel。它表示某个 UI 元素,当用户与之交互时可以执行自定义操作。这是代码:

public interface ICustomActionPresentationModel : IPresentationModel
{
    IPresentationModel Owner { get; set; }

    string ActionName { get; set; }

    object[] Parameters { get; set; }

    Type[] TypeParameters { get; set; }

    void ExecuteAction();
}

public class CustomActionPresentationModel : 
       PresentationModel, ICustomActionPresentationModel
{
   //implementation
}

Owner 属性代表拥有此演示模型所表示的 UI 元素的视图。ActionName 属性是要在 Owner 中调用的方法。其余部分不言自明。此处不展示实现,因为没有什么特别之处。

正如我在引言中所说,我将在演示模型中使用对话框,但为此,必须将对话框概念引入框架:

public interface IDialogSystem
{
    QuestionResult AskQuestion(string question);

    QuestionResult AskQuestion(string question, string title);

    FileInfo ChooseFile(string title, string filter);

    FileInfo ChooseImage();

    FileInfo ChooseImage(string title);
}

public enum QuestionResult
{
    Yes,
    No,
    Ok,
    Cancel
}

目前没有实现,因为对话框系统取决于正在使用的 UI 引擎。请注意 ChooseFile 方法。正如您稍后将看到的,我在演示模型中使用它,其过滤器格式与 WPF 的 OpenFileDialog 使用的相同。我这样做是为了简单化,但要使其更健壮,应该添加一个翻译机制,就像 QuestionResult 枚举一样,它会将消息框结果翻译成演示模型。

我们还需要一个可以关闭的演示模型,所以这是:

public interface IClosableViewPresentationModel : IPresentationModel
{
    void Close();

    event EventHandler RequestClose;
}

public abstract class ClosableViewPresentationModel : 
       PresentationModel, IClosableViewPresentationModel
{
    public ClosableViewPresentationModel()
    {
        _dialogServicesProvider = Get<IDialogSystem>();
    }

    #region Fields

    IDialogSystem _dialogServicesProvider;

    #endregion

    #region Methods

    public void Close()
    {
        OnRequestClose();
    }

    #endregion

    #region Events

    private EventHandler _handler;
    public event EventHandler RequestClose
    {
        add { _handler += value; }
        remove { _handler -= value; }
    }

    protected virtual void OnRequestClose()
    {
        if (_handler != null)
            _handler(this, EventArgs.Empty);
        Dispose();
    }

    #endregion

    protected QuestionResult AskQuestion(string question)
    {
        return _dialogServicesProvider.AskQuestion(question);
    }

    protected QuestionResult AskQuestion(string question, string title)
    {
        return _dialogServicesProvider.AskQuestion(question, title);
    }

    protected FileInfo ChooseFile(string title)
    {
        return _dialogServicesProvider.ChooseFile(title, null);
    }

    protected FileInfo ChooseImage()
    {
        return _dialogServicesProvider.ChooseImage();
    }

    protected FileInfo ChooseImage(string title)
    {
        return _dialogServicesProvider.ChooseImage(title);
    }
}

我添加了用于对话框服务的 protected 方法,并将对话框系统设为 private,因此这将是应用程序核心中演示模型的基类。演示模型框架项目就到此为止。

核心应用程序项目

此项目将包含主应用程序工作流、模型以及它将使用的服务的定义。在展示代码之前,必须定义需求。它将是一个简单的应用程序,用于管理商店中产品的信息。用户必须能够存储产品的名称、价格和照片。用户还必须能够编辑、删除和查看已存储的产品。相当有创意,不是吗?那么,让我们从模型中唯一的类开始:

public class Product
{       
    private string _name;
    public virtual string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    private decimal _price;
    public virtual decimal Price
    {
        get { return _price; }
        set { _price = value; }
    }

    private byte[] _photography;
    public virtual byte[] Photography
    {
        get { return _photography; }
        set { _photography = value; }
    }
}

由于数据存储是必需的,我们需要一个数据访问接口。我可以添加一个专门处理产品 CRUD 操作的接口,但也许需求以后会增长,所以我将使用一个通用的定义。这将是应用程序定义的唯一数据服务:

public interface IDao<T>
{
    void SaveOrUpdate(T item);

    void Remove(T item);

    T GetByKey(int key);

    IList<T> SelectAll();

    event ListChangedEventHandler Changed;
}

好的,现在我们来看看应用程序需要什么样的视图:

  • 一个应用程序启动时显示的主视图。它的基本功能是导航到执行特定任务的其他视图。
  • 一个显示产品详细信息的视图。此视图可用于插入新产品或编辑现有产品。
  • 一个显示应用程序存储的所有产品列表的视图。此视图可用于选择产品进行编辑或删除。

如果应用程序的模型增长,它最终将需要更多视图,其功能与最后两个视图的行为相同,所以现在是创建一个更通用的定义来处理最后两个视图的好时机:

  • 一个显示模型实体实例详细信息的视图。此视图可用于插入实体的新实例或编辑现有实例。
  • 一个显示应用程序存储的特定实体所有实例列表的视图。此视图可用于选择实体实例进行编辑或删除。

由于视图的行为来自演示模型,我应该以以下方式开始它们的定义:

public interface ISelectableViewPresentationModel : IClosableViewPresentationModel
{
    bool IsSelected { get; set; }
}

public interface IMainViewPresentationModel : IClosableViewPresentationModel
{
    ICollection<ICustomActionPresentationModel> NavigationItems { get; }

    ICollection<ISelectableViewPresentationModel> Workspaces { get; }

    void CreateNew<T>() where T : new();

    void EditExisting<T>(T item) where T : new();

    void ViewAll<T>() where T : new();
}

public interface IEntityViewPresentationModel<T> : ISelectableViewPresentationModel
    where T : new()
{
    T Entity { get; set; }

    void SaveOrUpdate();

    void OpenEditView();
}

public interface IEntityCollectionViewPresentationModel<T> : ISelectableViewPresentationModel 
        where T : new()
{
    ICollection<IEntityViewPresentationModel<T>> EntityCollection { get; }

    IEntityViewPresentationModel<T> Selected { get; set; }

    void CreateNew();

    void RemoveSelected();

    bool CanRemove { get; }
}

IEntityViewPresentationModel<T> 将显示关于实体的信息,可以是专用视图中的详细信息,或者只是 IEntityCollectionViewPresentationModel<T> 中包含的数据网格中的摘要。仔细想想,不同实体的 IEntityViewPresentationModel<T> 之间的唯一区别在于表示屏幕上字段的属性。这类代码会非常重复,但幸运的是,.NET Framework 通过 Type Descriptor 元数据检查机制为我们提供了一种通用的方法。与读取已编译的类型信息的 Reflection 不同,Type Description 类使用 Reflection 来获取其初始数据,然后允许在运行时修改这些数据。当然,类型并没有真正被修改,但 Type Description 允许修改其初始数据,以便查询类型信息的其他对象将看到更改。幸运的是,.NET UI 框架 (不确定 Mono 中的 gtk#;据我所闻,它只有 gtk+的绑定) 使用此机制来检查类型,所以我们可以利用它来获得一些乐趣 :)

修改类型使用 Type Description 的基类是继承 CustomTypeDescriptor 并重写其查询方法。要使类型使用特定的 TypeDescriptor,您必须使用以下代码向类添加 TypeDescriptorProvider

TypeDescriptor.AddProvider(someTypeDescriptor, typeof(someOtherType));

我不会讨论 TypeDescriptor 的所有功能,只讨论我将要做的。继承自 EntityViewPresentationModel<T> 的演示模型类将具有不同的属性 (这些属性的唯一功能是封装实体的属性),这些属性基于实体属性和您想在视图中显示的信息。例如,我们将有一个用于编辑产品的 ProductEditViewPresentationModel。此视图将包含姓名和价格字段,以及一个显示产品照片 (并能够更改它) 的图片框。而不是显式实现这些属性,我们将使用这个:

[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
class EncapsulatesPropertyAttribute : Attribute
{
    public EncapsulatesPropertyAttribute(string propertyName) 
    {
        _propertyName = propertyName;
    }

    private string _propertyName;
    public string PropertyName
    {
        get { return _propertyName; }
        set { _propertyName = value; }
    }
}

此属性指定应封装在 EntityViewPresentationModel 中的单个属性。所以这是演示模型的定义:

[EncapsulatesProperty("Name")]
[EncapsulatesProperty("Price")]
[EncapsulatesProperty("Photography")]
public class ProductEditViewPresentationModel : 
       EntityViewPresentationModel<Product>
{
}
public class ProductsViewPresentationModel 
        : EntityCollectionViewPresentationModel<Product>
{
}

自定义类型描述符要做的就是读取 EncapsulatesPropertyAttribute 并创建封装属性中指定的属性。这些属性还必须在需要时引发演示模型上的 PropertyChanged 事件。出于文章篇幅的考虑,此处不展示实现细节。通过这项技术,我可以轻松地创建同一实体或其他模型中引入的新实体的其他视图。

在转到下一个项目之前,让我展示一下 MainViewPresentationModel,以便您能更好地理解到目前为止发生了什么:

public class MainViewPresentationModel : ClosableViewPresentationModel, 
                                         IMainViewPresentationModel
{
    public MainViewPresentationModel()
    {
        _navigationItems = Get<ICollection<ICustomActionPresentationModel>>();
        _workspaces = Get<ICollection<ISelectableViewPresentationModel>>();

        CreateNavigationItems();
    }

    private void CreateNavigationItems()
    {
        ICustomActionPresentationModel openNewProductView = 
                   Get<ICustomActionPresentationModel>();
        openNewProductView.Owner = this;
        openNewProductView.ActionName = "CreateNew";
        openNewProductView.TypeParameters = new Type[] { typeof(Product) };
        openNewProductView.DisplayName = "Create new product";

        ICustomActionPresentationModel openProductsView = 
                   Get<ICustomActionPresentationModel>();
        openProductsView.Owner = this;
        openProductsView.ActionName = "ViewAll";
        openProductsView.TypeParameters = new Type[] { typeof(Product) };
        openProductsView.DisplayName = "View all products";

        _navigationItems.Add(openNewProductView);
        _navigationItems.Add(openProductsView);
    }

    private ICollection<ICustomActionPresentationModel> _navigationItems;
    public ICollection<ICustomActionPresentationModel> NavigationItems
    {
        get { return _navigationItems; }
    }

    private ICollection<ISelectableViewPresentationModel> _workspaces;
    public ICollection<ISelectableViewPresentationModel> Workspaces
    {
        get { return _workspaces; }
    }

    public void CreateNew<T>()
        where T : new()
    {
        IEntityViewPresentationModel<T> workspace = 
                   Get<IEntityViewPresentationModel<T>>();
        workspace.Entity = new T();
        AddWorkspace(workspace);
    }

    public void EditExisting<T>(T item)
        where T : new()
    {
        IEntityViewPresentationModel<T> workspace = 
                   Get<IEntityViewPresentationModel<T>>();
        workspace.Entity = item;
        AddWorkspace(workspace);
    }

    public void ViewAll<T>()
        where T : new()
    {
        IEntityCollectionViewPresentationModel<T> workspace = 
                   Get<IEntityCollectionViewPresentationModel<T>>();
        AddWorkspace(workspace);
    }

    void AddWorkspace(ISelectableViewPresentationModel workspace)
    {
        if (!_workspaces.Contains(workspace))
        {
            workspace.RequestClose +=
                   (sender, e) => OnWorkspaceRequestClose(sender);
            Workspaces.Add(workspace);
        }
        SetActiveWorkspace(workspace);
    }

    void OnWorkspaceRequestClose(object sender)
    {
        ISelectableViewPresentationModel workspace = 
               sender as SelectableViewPresentationModel;
        Workspaces.Remove(workspace);
    }

    protected virtual void SetActiveWorkspace(
              ISelectableViewPresentationModel workspace)
    {
        workspace.IsSelected = true;
    }
}

如您所见,我直接实例化的唯一对象是实体。所有依赖项都从服务定位器获取。甚至使用的集合也不是直接指定的。

我不会解释 Data 项目,因为它只有一个类,DummyDao<T>,它是 IDao<T> 的一个模拟实现,将所有数据存储在内存中。

整合所有内容 - 配置项目

此项目将定义与应用程序主逻辑无关的代码,并将连接所有组件。我要展示的第一件事是如何 WPF 视图与演示模型通信。与 MVVM 类似,这将通过命令来实现,更具体地说,只有一个命令:

class MethodCallCommand : ICommand
{
    public MethodCallCommand(PresentationModel target)
    {
        _target = target;
    }

    PresentationModel _target;

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _target.InvokeMethod(parameter.ToString(), null);
    }

    protected virtual void Execute(MethodInfo mi)
    {
        mi.Invoke(_target, null);
    }

    #endregion
}

您现在可能已经猜到了,我们将使用 TypeDescriptor API 将此类型的属性 "注入" 到演示模型基类中。这将允许我们像这样调用任何演示模型上的方法:

<Button     
    Content="Save"
    Command="{Binding Call}"
    CommandParameter="SaveOrUpdate"
    />

相当简单,对吧?而且没有代码隐藏。在演示模型中使用命令的另一种方法是声明一个签名类似于 ICommand 接口的接口。然后,演示模型基类可以获得对 MethodCallCommand 的引用,而无需创建对 WPF 的依赖 (或者,您可以使用这两种技术之一将许多命令添加到具体演示模型中,每个方法一个,就像在 MVVM 中通常做的那样)。

我们需要做的下一件事是实现 IDialogSystem 接口:

class WpfDialogSystem : IDialogSystem
{
    #region IDialogServicesProvider Members

    public QuestionResult AskQuestion(string question)
    {
        return AskQuestion(question, "Question");
    }

    public QuestionResult AskQuestion(string question, string title)
    {
        var result = System.Windows.MessageBox.Show(question, title, 
            System.Windows.MessageBoxButton.YesNo, 
            System.Windows.MessageBoxImage.Question);
        switch (result)
        {
            case System.Windows.MessageBoxResult.Yes:
                return QuestionResult.Yes;
            default:
                return QuestionResult.No;
        }
    }

    public System.IO.FileInfo ChooseFile(string title, string filter)
    {
        OpenFileDialog dialog = new OpenFileDialog();
        dialog.Title = title;
        dialog.Filter = filter;
        dialog.ShowDialog();
        if (dialog.FileName != null && dialog.FileName.Trim() != string.Empty)
            return new System.IO.FileInfo(dialog.FileName);
        return null;
    }

    public System.IO.FileInfo ChooseImage()
    {
        return ChooseImage("");
    }

    public System.IO.FileInfo ChooseImage(string title)
    {
        return ChooseFile(title, "Image Files (*.bmp, *.jpg, *.jpeg, " + 
                          "*.png)|*.bmp;*.jpg;*.jpeg;*.png");
    }

    #endregion
}

现在,让我们看看这一切是如何整合在一起的:

public class ConfigurationManager
{
    internal static WindsorContainer _windsor = new WindsorContainer();

    public static void Configure()
    {
        WpfConfiguration();
    }

    static void WpfConfiguration()
    {
        GenericConfiguration();

        _windsor.Register
            (
            Component.For<IDialogSystem>().ImplementedBy<
              WpfDialogSystem>().LifeStyle.Is(LifestyleType.Singleton),
            Component.For(typeof(ObservableCollection<>), typeof(
              ICollection<>)).LifeStyle.Is(LifestyleType.Transient)
            );
        
        //Inserting the command properties on the presentation models.
        System.ComponentModel.TypeDescriptor.AddProvider(
            new PresentationModelTypeDescriptionProvider(
                System.ComponentModel.TypeDescriptor.GetProvider(
                typeof(PresentationModel))),
                typeof(PresentationModel));
    }

    static void GenericConfiguration()
    {
        AppDomain.CurrentDomain.SetData("servicelocator", _windsor);
        var propertyDIContributor = 
          _windsor.Kernel.ComponentModelBuilder.Contributors.OfType<
          PropertiesDependenciesModelInspector>().Single() ;
        _windsor.Kernel.
            ComponentModelBuilder.
            RemoveContributor(propertyDIContributor);
        _windsor.Register
            (
            Component.For<MainViewPresentationModel, 
              IMainViewPresentationModel>().LifeStyle.Is(LifestyleType.Singleton),
            Component.For<ProductsViewPresentationModel, 
              IEntityCollectionViewPresentationModel<
                Product>>().LifeStyle.Is(LifestyleType.Singleton),
            Component.For(typeof(DummyDao<>), 
                typeof(IDao<>)).LifeStyle.Is(LifestyleType.Singleton),
            Component.For(typeof(List<>), 
                typeof(IList<>)).LifeStyle.Is(LifestyleType.Transient),
            Component.For<CustomActionPresentationModel, 
                ICustomActionPresentationModel>().LifeStyle.Is(LifestyleType.Transient),
            Component.For<ProductEditViewPresentationModel, 
              IEntityViewPresentationModel<Product>>().LifeStyle.Is(LifestyleType.Transient)
            );
    }
}

Castle Windsor 允许您以多种可能的 LifeStyles 注册服务/组件接口和实现。如您所见,我只使用了两种 LifeStyles:SingletonTransient。如果您注册一个 Singleton,容器只会创建一个实例,因此每次请求该服务时,都会返回相同的实例。相反,Transient 意味着每次请求服务时都会创建一个新实例。虽然我将 Windsor 用于服务定位器模式,但它还将自动执行构造函数和 setter 依赖注入,这就是为什么我使用了此代码:

var propertyDIContributor = 
  _windsor.Kernel.ComponentModelBuilder.Contributors.OfType<
    PropertiesDependenciesModelInspector>().Single() ;
_windsor.Kernel.ComponentModelBuilder.RemoveContributor(propertyDIContributor);

我已删除的此贡献者负责注入属性依赖项。由于我只使用无参数构造函数,因此不必担心自动构造函数注入。最后要说明的是:

AppDomain.CurrentDomain.SetData("servicelocator", _windsor);

由于配置将是第一步,演示模型将通过 Get<T> 方法获得它们所需的一切。需要注意的是,我唯一引用 Castle 库的地方是配置项目,因为 PresentationModel 类正在与 IServiceProvider 通信,所以如果我将其替换为另一个提供正确服务的实现,应用程序不会察觉。

我不会展示 WPF 项目中的 XAML,所以这里是它包含的唯一代码:

protected override void OnStartup(StartupEventArgs e)
{
    ConfigurationManager.Configure();
    IServiceProvider prov = (IServiceProvider)
      AppDomain.CurrentDomain.GetData("servicelocator");
    base.OnStartup(e);
    MainWindow mainWindow = new MainWindow();
    IMainViewPresentationModel mainViewPresentationModel = 
      (IMainViewPresentationModel)prov.GetService(typeof(IMainViewPresentationModel));
    mainViewPresentationModel.RequestClose += (a, b) => mainWindow.Close();

    mainWindow.DataContext = mainViewPresentationModel;
    mainWindow.Show();
}

多线程处理

尽管应用程序现在运行正常,但仍缺少一件事:运行长时间操作时的 UI 响应性。此演示没有长时间运行的示例,因此我在加载产品列表时添加了这个:

Thread.Sleep(10000);

这十秒钟会让用户认为应用程序已停止响应,而类似的延迟在数据量很大的真实应用程序中很常见。为了解决这个问题,我将让应用程序代码在 UI 线程之外的单独线程中运行,而无需更改主逻辑。这项技术称为面向切面编程,对于处理任何非功能性需求 (如日志记录和数据库事务管理) 都非常有用。

我们通过动态代理拦截演示模型 (运行时生成的子类) 的方法调用来为代码添加这种多线程方面。虽然这听起来很复杂,但 Castle Windsor 会完成大部分工作。这是不直接实例化类的巨大优势之一:当我们请求一个组件时,它不会返回我们的实现,而是给我们一个具有自定义行为的动态子类!在解释如何实现这一点之前,让我们分析一下演示模型和视图之间的基本交互:

  1. 演示模型被创建并与视图关联。
  2. 视图向演示模型的通知注册处理程序。
  3. 用户在视图中执行某些操作。
  4. 视图会将这些更改传播到演示模型。
  5. 演示模型执行某些操作来改变其状态。
  6. 已调用注册的通知处理程序。

我们必须拦截这个工作流程两次:

  • 我们必须将步骤 5 重定向到一个单独的后台线程,以便如果需要一些时间来完成,UI 不会显得无响应。
  • 当后台线程即将调用通知处理程序时,我们必须将其重定向到 UI 线程。这是一个要求,因为通常 UI 控件只能由创建它们的线程修改。

我们可以用这个拦截器完成第一步:

class BackgroundWorkInterceptor : IInterceptor
{
    #region IInterceptor Members

    public void Intercept(IInvocation invocation)
    {
        if (!CheckIfShouldIntercept(invocation))
        { invocation.Proceed(); return; }

        PresentationModel pm = (PresentationModel)invocation.InvocationTarget;

        ThreadPool.QueueUserWorkItem(
            o =>
            {
                invocation.Proceed();
            });
    }

    #endregion
    
    bool CheckIfShouldIntercept(IInvocation invocation)
    {
        if (invocation.Method.Name == "InvokeMethod")
            return true;
        return false;
    }
}

正如我在解释 IPresentationModel 接口时所说,我们将使用 InvokeMethod 方法作为 UI 和演示模型之间的网关。这种后台线程重定向本可以在命令类中完成,因为 WPF 就是这样调用演示模型中的方法的,但我不能在其他 GUI 框架中使用此技术。InvokeMethod 为我们提供了一个更具可重用性的网关,因此此拦截器可以与非 WPF UI 一起使用。下一个拦截器将接收一个方法并将其发送到调度程序,因此它非常特定于 WPF:

class DispatchInterceptor : IInterceptor
{
    Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;
    #region IInterceptor Members

    public void Intercept(IInvocation invocation)
    {
        if (!CheckIfShouldIntercept(invocation))
        { invocation.Proceed(); return; }

        if (Thread.CurrentThread == _dispatcher.Thread)
            invocation.Proceed();
        else
        {
            ThreadStart ts = () => invocation.Proceed();
            _dispatcher.Invoke(ts, null);
        }
    }

    #endregion

    bool CheckIfShouldIntercept(IInvocation invocation) 
    {
        if (invocation.Method.Name == "OnPropertyChanged")
            return true;
        if (invocation.Method.Name == "OnCollectionChanged")
            return true;
        return false;
    }      
}

这次我检查调用是否不是已经来自 UI 线程,然后再进行调度。使用后台拦截器,我不需要这样做,因为只有 UI 线程会调用 InvokeMethood。请注意,拦截器会拦截 OnCollectionChanged。这是因为我们将此拦截器添加到 ObservableCollection,原因不难猜:当 ItemsControl (或类似控件) 绑定到 ObservableCollection 时,UI 将处理程序连接到 CollectionChanged 事件,因此这些处理程序也必须在 UI 线程中调用。

另一个要求是向演示模型基类添加一个布尔属性 (使用类型描述符)。此属性将称为 'IsWorking',并在演示模型执行后台工作时返回 true。这是修改后的后台拦截器:

class BackgroundWorkInterceptor : IInterceptor
{
    #region IInterceptor Members

    public void Intercept(IInvocation invocation)
    {
        if (!CheckIfShouldIntercept(invocation))
        { invocation.Proceed(); return; }

        PresentationModel pm = (PresentationModel)invocation.InvocationTarget;

        ThreadPool.QueueUserWorkItem(
            o =>
            {
                SetIsWorkingProperty(pm, true);
                invocation.Proceed();
                SetIsWorkingProperty(pm, false);
            });
    }

    #endregion

    void SetIsWorkingProperty(PresentationModel pm, bool value)
    {
        var property =
            TypeDescriptor.GetProperties(typeof(PresentationModel)).Find("IsWorking", false);
        property.SetValue(pm, value);
    }

    bool CheckIfShouldIntercept(IInvocation invocation)
    {
        if (invocation.Method.Name == "InvokeMethod")
            return true;
        return false;
    }
}

剩下要做的就是配置这些组件,并为用户控件添加一些特殊效果,当它们各自的演示模型正在工作时。这是更新的配置类:

public class ConfigurationManager
{
    internal static WindsorContainer _windsor = new WindsorContainer();

    public static void Configure()
    {
        WpfConfiguration();
    }

    static void WpfConfiguration()
    {
        _windsor.Kernel.ComponentModelCreated += ComponentModelCreated;
        GenericConfiguration();

        _windsor.Register
            (
            Component.For<DispatchInterceptor>(),
            Component.For<IDialogSystem>().ImplementedBy<
                      WpfDialogSystem>().LifeStyle.Is(LifestyleType.Singleton),
            Component.For(typeof(ObservableCollection<>), 
                      typeof(ICollection<>)).LifeStyle.Is(LifestyleType.Transient)
            );
        
        //Inserting the command properties on the presentation models.
        System.ComponentModel.TypeDescriptor.AddProvider(
            new PresentationModelTypeDescriptionProvider(
                System.ComponentModel.TypeDescriptor.GetProvider(
                typeof(PresentationModel))),
                typeof(PresentationModel));
    }

    static void GenericConfiguration()
    {
        AppDomain.CurrentDomain.SetData("servicelocator", _windsor);
        var propertyDIContributor = 
          _windsor.Kernel.ComponentModelBuilder.Contributors.OfType<
          PropertiesDependenciesModelInspector>().Single() ;
        _windsor.Kernel.
            ComponentModelBuilder.
            RemoveContributor(propertyDIContributor);
        _windsor.Kernel.ComponentModelCreated += ComponentModelCreated;
        _windsor.Register
            (
            Component.For<BackgroundWorkInterceptor>(),
            Component.For<MainViewPresentationModel, 
              IMainViewPresentationModel>().LifeStyle.Is(LifestyleType.Singleton),
            Component.For<ProductsViewPresentationModel, 
              IEntityCollectionViewPresentationModel<
                Product>>().LifeStyle.Is(LifestyleType.Singleton),
            Component.For(typeof(DummyDao<>), 
                typeof(IDao<>)).LifeStyle.Is(LifestyleType.Singleton),
            Component.For(typeof(List<>), 
                typeof(IList<>)).LifeStyle.Is(LifestyleType.Transient),
            Component.For<CustomActionPresentationModel, 
              ICustomActionPresentationModel>().LifeStyle.Is(LifestyleType.Transient),
            Component.For<ProductEditViewPresentationModel, 
              IEntityViewPresentationModel<
                Product>>().LifeStyle.Is(LifestyleType.Transient)
            );
    }

    static void ComponentModelCreated(ComponentModel model)
    {
        if (typeof(PresentationModel).IsAssignableFrom(model.Implementation))
        {
            model.Interceptors.Add(
               InterceptorReference.ForType<BackgroundWorkInterceptor>());
        }
        if (typeof(PresentationModel).IsAssignableFrom(model.Implementation) ||
            typeof(ObservableCollection<>) == model.Implementation)
        {
            model.Interceptors.Add(InterceptorReference.ForType<DispatchInterceptor>());
        }
    }
}

就是这样!我们已经让应用程序在不干扰其主逻辑的情况下使用多线程。希望您喜欢这篇文章;欢迎留下评论和建议。

关注点

  • 请注意,每个演示模型都有自己的后台线程,因此当一个视图正在处理时,您仍然可以与其他视图进行交互。
  • 请记住,这个示例并没有经过很好的测试。我没有处理输入验证或方法参数,所以很容易导致崩溃。本文的主要目的是展示一种使用演示模型 UI 设计模式来处理商业应用程序的有效方法。
  • 我使用了 Attached Command Behavior 来处理 ListView 的双击事件。
  • Castle Windsor/Microkernel 是一个出色的 IoC 容器,我只展示了它功能的 10%。它是可扩展的,并且与其他技术 (如 NHibernate) 集成得非常好。
  • 我在示例中使用的主题是 WPF Themes 包 的一部分。
© . All rights reserved.