动态视图模型(ViewModel) 第二部分:键/引用





5.00/5 (4投票s)
一种动态生成 ViewModel 对象的技术,具有类似外键的引用。
引言
本文是关于在使用 WPF 的 Model-View-ViewModel 模式时如何动态生成 ViewModel 层部分的系列文章之一。第一篇文章链接如下
背景
考虑以下问题:您需要一个列表对象在父级上,用户可以动态编辑。我们将这些对象称为选项。在父级的某个深度之下,有另一个子列表,其中包含一个代表选项的键。当编辑子对象时,您如何让用户在 ComboBox 中选择选项,以便在选项列表更新时,ComboBox 项目也能随之更新,而子对象只存储一个简单的原始键?
本文中我将描述的主要技术是在 WPF 的上下文中解决这个问题。我还将介绍 DynamicVM 库的一些其他改进和升级,包括一个 ProjectManager 类。
优点、缺点和更新
优点
- 通过消除编写 ViewModel 类所花费的时间来提高程序员的生产力。
- 当模型层发生变化时,ViewModel 层无需更新。
- 开箱即支持撤销、添加、删除、上移、下移和共享选择。
- 在域数据和接口数据之间提供了清晰的分离。
- 模型只需要实现 INotifyPropertyChanged。模型不需要派生自特定类,并且不假定包含任何接口数据。
缺点
- 虽然使用此技术可以很好地表示数据,但方法不行。ViewModel 层所需的任何自定义函数都需要一个专门的派生类。(下面第一点)
- 如果您不想要 ViewModel 层提供的某些功能,则无法为特定模型关闭它们。
- 该库不包含内置的数据验证系统。
更新
- 此新版本的 DynamicVM 有一个新类,它促进了 ViewModel 层中的自定义功能,即 ViewModelFactory。
- 支持在逻辑树上层级的子对象和选项之间的类似外键的引用,且编码量极少。
- 包含一个 ProjectManager,它负责 XML 对象的序列化和整个 ViewModel 层的构建。ProjectManager 类支持新建、保存等样板命令,并监听文件和设备更改(例如拔出闪存驱动器)。
- 如果指定,则支持整数属性上的唯一 ID 约束。
- 通过 ViewModelDirectChild 类支持直接子对象,该类表现得与 ViewModelProperty 完全一样,只是它可以托管实现 INotifyPropertyChanged 的对象。
技术
已添加一个新属性 KeyRefAttribute,用于装饰子对象上的“外键”属性。
[KeyRef("Options", typeof(ParentModel), "Xuid", typeof(OptionModel), "OptionRef")]
public int OptionKey
{
get
{
return m_optionKey;
}
set
{
m_optionKey = value;
NotifyPropertyChanged("OptionKey");
}
}
该属性的构造函数需要五个参数:
- 父对象中包含要从中选择的对象的列表属性的名称。
- 父对象的类型。
- 列表中目标对象的“主键”属性的名称。
- 目标对象的类型。
- 系统生成的 ViewModelProperty 项目的名称。此名称对 Xaml 可见,用于在 DataTemplate 中绑定。
这些属性在 ProjectManager 中的 ViewModelManager 构建过程中由 KeyRefInitializationStage 类识别和加载。在 DynamicViewModel 的 LoadProperties 方法中,KeyRefUtility 对象中找到的任何属性都会导致生成额外的 ViewModelProperty 对象。该额外对象类型为 ViewModelPropertyRef,包含一个新属性 Source,它向 Xaml 公开父列表。
public ViewModelCollection Source
{
get
{
DynamicViewModel parent = m_parentUtility[m_keyRefAttribute.ParentType.FullName];
return parent.GetMemberByName(m_keyRefAttribute.ParentRefListName) as ViewModelCollection;
}
}
Source 的 get 访问器使用 ViewModelParentUtility 索引器向上遍历树并找到目标列表宿主。然后它获取包装目标列表的 ViewModelCollection。这允许 ComboBox 将 SelectedItem 属性绑定到我们上面示例中的“OptionRef”。
如果您还记得第一篇文章,ViewModelProperty 类中的 CachedValue 属性由 DynamicViewModel 在 TryGetMember 方法中用于非 INotifyPropertyChanged 类型。此属性在新类中被重载。
public override object CachedValue
{
get
{
return base.CachedValue;
}
set
{
if (NotEqual(value))
{
object key = value as DynamicViewModel != null ? m_refXuidInfo.GetValue((value as DynamicViewModel).ModelContext) : null;
object oldKey = m_cachedValue as DynamicViewModel != null ? m_refXuidInfo.GetValue((m_cachedValue as DynamicViewModel).ModelContext) : null;
m_manager.UndoManager.Do(new SetPropertyCommand(m_modelContext, key, oldKey, m_info.Name));
}
}
}
因此,ViewModelPropertyRef 可以被视为 ViewModelProperty 的一个特例,它存储对 DynamicViewModel 的引用,而不是对原始类型的引用。
现在考虑以下问题:假设您在根模型级别有两个属性,一个选项列表和一个子列表。如果先加载选项,那么 individual DynamicViewModel 目标将对 PropertyChanged 和 CollectionChanged 监听器可见。但是,如果先加载子对象,那么将适当的监听器连接起来的逻辑将变得更加复杂。为了解决这个问题,我们只需忽略系统提供的默认反射数据,而是替换我们自己的排序属性列表。
我们用来创建这个新属性列表的类叫做 ViewModelKeyRefUtility。它有一个对属性进行排序的方法,以便它们以正确的顺序加载,从而大大简化了子对象和选项对象之间连接监听器的过程。
DynamicViewModel 然后通过调用 ViewModelKeyRefUtility 上的一个方法来请求反射数据,而不是调用 Type.GetProperties()。
var reflectedProperties = m_manager.KeyRefUtility.GetPropertiesSortedByDependencies(m_modelContext.GetType());
UidManager 类有两个方法,LogUidFrom 和 SetUidOn,它们由 ViewModelFactory 上的 CreateModel 方法调用。ViewModelFactory 本身是一个简单的字典,用于将 Model 类型与 ViewModel 类型关联起来,允许您指定自定义功能和命令。这两类,以及 ProjectManager,相对简单,不会详细介绍。
使用代码
可以将 ProjectManager 添加为窗口的 DataContext,以如下方式访问相关的命令和初始化功能:
public MainWindow()
{
InitializeComponent();
var factory = new ViewModelFactory()
{
{ typeof(OptionModel), typeof(OptionViewModel) }
};
ProjectManager<ParentModel> projectManager = new ProjectManager<ParentModel>(Dispatcher, factory);
projectManager.New();
DataContext = projectManager;
}
为了让管理器监听窗口消息,请在您的窗口中也包含以下代码:
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
ProjectManager<ParentModel> currentContext = DataContext as ProjectManager<ParentModel>;
HwndSource source = PresentationSource.FromVisual(this) as HwndSource;
source.AddHook(currentContext.WndProc);
}
为了连接键/引用对,只需使用上一节中提到的 KeyRefAttribute。
[KeyRef("Options", typeof(ParentModel), "Xuid", typeof(OptionModel), "OptionRef")]
然后编写 Xaml 以分别绑定到 OptionRef 和 OptionRef.Source 以获得 SelectedItem 和 ItemsSource。
<ComboBox ItemsSource="{Binding Path=OptionRef.Source, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" SelectedItem="{Binding Path=OptionRef.CachedValue, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
要指定整数字段是唯一的且自动生成的,请用 UniqueConstraint 属性装饰该属性并传入初始 ID。
[UniqueConstraint(0)]
public int Xuid
{
get
{
return m_xuid;
}
set
{
m_xuid = value;
NotifyPropertyChanged("Xuid");
}
}
要在派生自 DynamicViewModel 的类中添加功能,只需添加返回标准 ICommand 对象的属性。在此示例中,用户可以单击一个按钮来删除对某个选项的所有引用。
public ICommand ClearReferencesCommand
{
get
{
return new RelayCommand(ClearReferences, CanClearReferences);
}
}
private bool CanClearReferences(object arg)
{
var parent = FindParent.GetRoot().ModelContext as ParentModel;
var optionModel = ModelContext as OptionModel;
foreach(var child in parent.Children)
{
if(child.OptionKey == optionModel.Xuid)
{
return true;
}
}
return false;
}
private void ClearReferences(object obj)
{
ComplexCommand complexCommand = new ComplexCommand();
var parent = FindParent.GetRoot().ModelContext as ParentModel;
var optionModel = ModelContext as OptionModel;
foreach (var child in parent.Children)
{
if (child.OptionKey == optionModel.Xuid)
{
complexCommand.Add(new SetPropertyCommand(child, -1, child.OptionKey, "OptionKey"));
}
}
Manager.UndoManager.Do(complexCommand);
}
将派生类与模型类型关联,如上面 MainWindow 构造函数示例所示,然后该命令将对 Xaml 可见。
<Button Height="16" Width="16" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{Binding Path=ClearReferencesCommand}"/>
关注点
此库可用于非常快速地创建非常简单的工具。我曾用它为 AAA 游戏工作室开发应用程序,在某些情况下开发时间不到一天。您只需要定义您的数据,DynamicViewModel 类就会注入大量的样板功能。
该库还可以用作常见 WPF 任务的参考,例如共享 ViewModel 对象之间的视觉状态、监听设备更改以及使用 DynamicVM.IO 中的 SerializationBase 类安全地加载 XML 文件而不丢失数据。此版本还包含拖放系统的初步工作。
在测试该库时,我发现虽然外键对于其他列表中的对象有效,但同一列表中的对象处理起来有点困难。我正在使用一个名为 InstanceDependencyManager 的类来创建一个隐藏的模型对象列表,该列表可以排序然后按正确的顺序加载以防止缺少依赖项。但是,这些子对象还需要访问父列表中的对象,因此 DynamicViewModel 对象需要在初始化期间被创建并插入到列表中,即使顺序不正确。如果此时其他系统恰好监听 CollectionChanged 事件,这就会造成问题。
为了解决这个问题,我将 ObservableList 修改为如下重写 OnCollectionChanged:
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!m_suppressChanges)
{
base.OnCollectionChanged(e);
}
}
这允许我在将项目按错误顺序插入列表时抑制 CollectionChanged 事件。我还必须修改 DynamicViewModel 中的 LoadProperties 方法,以便在创建 ViewModelCollection 对象后进行初始化,这也旨在防止当用户尝试使用 KeyRefAttribute 引用同一类上的键时发生的特定崩溃。
历史
初次文章。2014/7/19
修复了几个崩溃和功能问题。KeyRef 现在可以用于引用同一类上的键。2014/7/27
未来工作
我计划添加一个异步操作库,允许 UI 在任务完成时绑定到输出日志。因此,我已努力使该库中的一些类成为线程安全的。我还计划添加更多拖放功能。
如果您使用了该库,您可能已经注意到代码期望 ViewModel 树中的所有对象都是 DynamicVM 中定义的类型。将来,我想将功能分离到接口中,以便用户可以将自己的自定义、非 DynamicViewModel 对象直接插入逻辑树。