动态WPF MVVM视图模型事件、命令和属性生成





5.00/5 (7投票s)
支持属性、查找值、命令、事件和模型链接的视图模型自动生成
引言
尽管模型-视图-视图模型 (MVVM) 模式为基于 XAML 的应用程序 (WPF、Silverlight 和 Windows RT) 提供了许多优势 (关注点分离和易于测试)。但在实现功能方面存在一些令人沮丧的地方,尤其是在完成简单任务时必须生成的代码量。
大量时间花在编写视图模型上,这些视图模型仅仅是属性的镜像,并且“包装”底层的模型对象,实现 INotifyPropertyChanged
(INTP) 接口,并创建委托来处理视图生成的命令操作和事件。
以下示例说明了一个简单的“包装”模型对象。相应的 ViewModel
接收一个 Model
对象并暴露其属性。
public partial class CarType
{
public Int32 CarTypeID {get;set;}
public string CarTypeName {get;set;}
}
public class CarTypeViewModel : ViewModelBase
{
CarType wrapped;
public CarTypeViewModel (CarType carType)
{
wrapped = carType;
}
public CarTypeViewModel (CarType carType)
{
wrapped = carType;
}
public string CarTypeName {get
{ return wrapped.CarTypeName; }
set {if (value!=wrapped.CarTypeName)
{
wrapped.CarTypeName =value;
RaisePropertyChange("CarTypeName");
}
}
}
}
}
因此,理想情况下,我想简化许多重复性的任务,并研究了各种选项,通过自动生成相应的委托、属性和事件。
最终结果,除其他外,允许命令、事件和 INotifyPropertyChanged
事件的自动绑定,同时仍然保持强大的关注点分离,并且不需要与视图进行直接链接。
以下示例说明了一个将自动映射属性、命令和事件的视图模型。
//note no property change events fired or separate variable to store value
public string FirstName {get;set;}
public string LastName{ get; set; }
// FullName shows combined first and last name properties.
//The LinkToProperty attributes ensures related property is fired when changes are made
[LinkToProperty("FirstName")][LinkToProperty("LastName")]
public string FullName
{
get
{
return String.Format("{0} {1}", FirstName, LastName);
}
}
// Demonstrates wiring view events - this wires to datagrid AddingNewItem event
[LinkToEvent("AddingNewItemEvent")]
private void DataGrid_AddingNewItem(AddingNewItemEventArgs e)
{
}
//links to View's RollBackChangesCommand command
[LinkToCommand("RollBackChangesCommand")]
private void RollBackChanges()
{
this.RollBack();
}
/// Wire up all property change events through attribute.
[PropertyChangedEvent]
void person_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName != "Log" && sender is ICustomObjectProxyObjects)
{
SetPropertyValue("Log", e.PropertyName + " changed to:" +
((ICustomObjectProxyObjects)sender).GetPropertyValue(e.PropertyName).ToString() + "\r\n" + Log);
}
}
一种解决方案可以自动将视图的方法和事件与相应的视图模型进行匹配——但这会使视图和视图模型紧密耦合。我还研究了动态 ExpandoObject
,它看起来很有前景,但最终也无法很好地工作。
解决方案以 ICustomType
接口的形式出现,该接口在 Silverlight 5 和 WPF 4.5 中引入。
public interface ICustomTypeProvider
{
Type GetCustomType();
}
它看起来 deceptively simple - 确实如此。如果视图的 DataContext
实现了该接口,WPF 4.5 和 Silverlight 5 都将使用此自定义类型。
为了实现,您需要替换底层的属性 Type
类的实现。当视图发出请求时,自定义类型会“拦截”该请求并进行相应处理。这取决于您如何实现。这里的强大之处在于您可以完全控制返回给视图的内容。但您还必须处理底层类中公开的任何“常规”属性。
Alexandra Rusina 的文章 使用 ICustomTypeProvider 绑定到动态属性 描述了这一过程,并构成了此解决方案的基础。
由此产生的 ProxyTypeHelper
解决方案旨在帮助简化以下任务:
- 从底层模型自动生成视图模型属性
- 自动创建命令和事件委托
- 自动映射查找值/属性
- 按字段/属性级别的脏标记
- 存储原始值/回滚更改
它在保持关注点分离的同时完成这些工作——底层模型、视图或视图模型之间没有添加依赖关系。包含的示例代码提供了这些任务的示例,并演示了动态视图模型。
映射视图到视图模型属性和动态属性
ProxyTypeHelper
库映射模型到视图模型的一种方式是通过 AssociatedModel
属性。AssociatedModel
属性标识要自动映射的类。请注意,您应用属性的类必须继承自 ProxyHelper
。以下代码将之前的 CarType
模型类与 CarTypeViewModel
关联起来。
using ProxyHelper;
using Model;
namespace WPFEventInter.ViewModel
{
[AssociatedModel(typeof(CarType))]
public class CarTypeViewModel : ViewModelValidated<cartypeviewmodel>{}
}
为了让 ProxyTypeHelper
识别使用 AssociatedModel
属性的视图模型,您必须在实际使用之前调用 TypeStuff
类静态 InitializeProxyTypes
方法。您只需要调用一次此方法,它将查找所有带有该属性的类。App.xaml.cs 是放置它的一个好地方。
TypeStuff.InitializeProxyTypes();
如果模型类属性实现 ICollection
(例如 List<>
),它们将在视图模型中自动创建为 ObservableCollections
,并根据需要创建或删除相应的模型对象。
您可以使用静态 AddProxyObject
方法手动将模型关联到视图模型——因此一个视图可以拥有多个关联模型。
PersonViewModel.AddProxyObject(typeof(Person));
或者,您可以使用静态 AddProperty
方法将单个属性添加到类中,该方法需要属性名称和类型。
PersonViewModel.AddProperty("Name", typeof(string));
您需要在视图创建类的实例之前使用 AddProperty
或 AddProxyObject
。在对象创建后添加的任何属性都不会被视图看到。
继承 ProxyTypeHelper
的类中创建的任何属性,在更改时都会自动触发 INotifyPropertyChanged
事件。
using ProxyHelper;
namespace WPFEventInter.ViewModel
{
public class ProductViewModel : ProxyTypeHelper<productviewmodel>
{
public string Description { get; set; }
public double Price { get; set; }
}
}
请注意,您无需手动触发任何属性通知,因为它们是自动生成的。
要创建属性更改事件,只需将 PropertyChangedEvent
属性添加到相应的方法。事件会自动分配给对象——任何子对象也会自动分配。
[PropertyChangedEvent]
void person_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName != "Log" && sender is ICustomObjectProxyObjects)
{
SetPropertyValue("Log", e.PropertyName + " changed to:" +
((ICustomObjectProxyObjects)sender).GetPropertyValue(e.PropertyName).ToString()
+ "\r\n" + Log);
}
}
关联视图模型中的属性不能直接从 CLR 中看到——但可以从视图/窗体中看到。使用 GetPropertyValue/SetPropertyValue
来获取和设置单个属性。
GetPropertyValue(propertyName);
SetPropertyValue(propertyName,propertyValue);
使用 SetValue
来设置相应对象的所有属性。
SetValue(Type proxyType, object value);
其中 proxyType
是底层代理对象的类型,value 是您要分配的值。
persons.SetValue(typeof(Person), person);
如果您有需要与属性更改而触发的属性,请添加 LinkToProperty
属性。在以下示例中,如果 FirstName
或 SurName
属性发生更改,FullName
属性更改事件将触发。
[LinkToProperty("FirstName")] [LinkToProperty("SurName")]
public string FullName
{
get
{
return String.Format("{0} {1}", GetPropertyValue<string>("FirstName"), GetPropertyValue<string>("SurName"));
}
}
如果您不希望触发属性更改事件,请添加 DoNotRaiseProperyChangedEvents
属性。
命令和事件委托的自动创建
将视图的命令操作链接到视图模型。虽然技术上不难,但如果视图、菜单或按钮栏特别复杂,代码量可能会增加。每个操作都需要一个相应的委托和一个公共属性将其暴露给视图。
要将方法分配给事件命令,请将 LinkToCommand
属性添加到方法。
[LinkToCommand("RollBackChangesCommand")]
private void btnRollBackChanges()
{
}
传递给 LinkToCommand
属性的参数必须与 XAML 控件中绑定的 Command 匹配,如下面的按钮控件所示。
<Button Content="Rollback changes"
Command="{Binding RollBackChangesCommand}" Width="140" ></Button>
LinkToCommand
创建一个链接到该方法的委托和一个将委托暴露给视图的 public
属性。
事件可以以类似的方式链接,但需要在视图端做更多工作。请注意,可以说将事件传递给视图模型会使视图和视图模型,或者至少底层控件,紧密耦合。从实际角度来看,通常没有其他解决方案可以将事件放入视图模型,特别是当视图模型被广泛重用时。
您需要为视图中希望触发事件的每个控件分配 EventToCommandBehavior
。这不是标准的 XAML 功能。某些框架和控件库(如 MVVM Light)包含此行为。
项目中包含了一个实现——您可能需要更改此代码以适用于其他框架。在以下代码片段中,DataGrid
的 AddingNewItem
事件已绑定到 AddingNewItemEvent
视图模型命令。
<i:Interaction.Behaviors>
<rmwpfi:EventToCommandBehavior Command="{Binding AddingNewItemEvent}" Event="AddingNewItem" PassArguments="True" />
</i:Interaction.Behaviors>
在您的视图模型中,您将添加该方法。请注意,事件参数必须适合正在触发的事件。确定这一点最简单的方法是在视图的代码隐藏中创建事件并将其复制到视图模型。以下代码片段将为前面的示例创建委托。
[LinkToEvent("AddingNewItemEvent")]
private void DataGrid_AddingNewItem(AddingNewItemEventArgs e)
{
}
查找值的自动映射
查找值也可以通过将值列表与类型关联来自动生成。
TypeStuff.SetLookupData("BrandsLookup", typeof(Brand), carsContext.Brands.ToList());
TypeStuff.SetLookupData("ColoursLookup", typeof(Colour), carsContext.Colours.ToList());
从上面的示例中,任何基于多对一实体模型对象的视图模型都将自动创建属性。因此,在包含的示例中,Cars ViewModel
将自动创建 ColoursLookup
属性。
跟踪脏属性和原始值
确定对象属性是否已更改的能力是视图模型的一个常见需求。最简单的方法是在属性更改事件中设置一个布尔标志,该标志在任何属性更改时都会被设置。
ProxyTypeHelper
通过 PropertyValues
属性跟踪属性。它存储属性的显式值以及属性信息,例如 IsDirty
属性和原始属性值。这可以用于与更改后的值进行比较或回滚任何更改。
ProxyTypeHelper
包含一个 IsDirty
属性,该属性检查属性是否具有任何设置的 IsDirty
标志。还有一个 RollBack
方法可以将对象回滚到原始值。
请注意,目前 IsDirty
不会检查任何子对象的脏值,也不会回滚任何对象值。
结束语
这仍然是一个进行中的工作。还有一些事情需要解决,例如克隆/回滚对象属性。最新的源代码可以在 https://github.com/steinborge/ProxyTypeHelper. 找到。
附带的示例演示了该库以及通用视图模型的概念。有几个简单的数据维护屏幕暴露了一些 SQLLite 表,还有一个屏幕演示了一些基本操作。
当从开发环境中运行时,您可能会遇到未实现异常,这不会影响库的运行。将您的设置更改为忽略此异常。
该库是为 WPF 开发的,但只需少量修改即可将其部分用于 Silverlight 或 Win RT,但由于涉及大量的反射,可能无法正常工作。
它与 Prism 配合使用,但尚未尝试过与其他 MVVM 框架(如 MVVM Light 或 Caliburn)一起使用。最近添加了 ProxyLinker
类以支持框架集成。我还在研究集成 Reactive extensions 的能力。
这些功能是相互独立的,因此您不需要实现“包装”的代理属性/对象即可使用命令链接或脏标志检查。