表示模型 (MVVM) 最佳实践






4.81/5 (13投票s)
展示一些可应用于演示模型/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 允许您以多种可能的 LifeStyle
s 注册服务/组件接口和实现。如您所见,我只使用了两种 LifeStyle
s:Singleton
和 Transient
。如果您注册一个 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 会完成大部分工作。这是不直接实例化类的巨大优势之一:当我们请求一个组件时,它不会返回我们的实现,而是给我们一个具有自定义行为的动态子类!在解释如何实现这一点之前,让我们分析一下演示模型和视图之间的基本交互:
- 演示模型被创建并与视图关联。
- 视图向演示模型的通知注册处理程序。
- 用户在视图中执行某些操作。
- 视图会将这些更改传播到演示模型。
- 演示模型执行某些操作来改变其状态。
- 已调用注册的通知处理程序。
我们必须拦截这个工作流程两次:
- 我们必须将步骤 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 包 的一部分。