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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2014 年 7 月 20 日

CPOL

7分钟阅读

viewsIcon

14433

downloadIcon

258

一种动态生成 ViewModel 对象的技术,具有类似外键的引用。

引言

本文是关于在使用 WPF 的 Model-View-ViewModel 模式时如何动态生成 ViewModel 层部分的系列文章之一。第一篇文章链接如下

动态视图模型

背景

考虑以下问题:您需要一个列表对象在父级上,用户可以动态编辑。我们将这些对象称为选项。在父级的某个深度之下,有另一个子列表,其中包含一个代表选项的键。当编辑子对象时,您如何让用户在 ComboBox 中选择选项,以便在选项列表更新时,ComboBox 项目也能随之更新,而子对象只存储一个简单的原始键?

本文中我将描述的主要技术是在 WPF 的上下文中解决这个问题。我还将介绍 DynamicVM 库的一些其他改进和升级,包括一个 ProjectManager 类。

优点、缺点和更新

优点

  1. 通过消除编写 ViewModel 类所花费的时间来提高程序员的生产力。
  2. 当模型层发生变化时,ViewModel 层无需更新。
  3. 开箱即支持撤销、添加、删除、上移、下移和共享选择。
  4. 在域数据和接口数据之间提供了清晰的分离。
  5. 模型只需要实现 INotifyPropertyChanged。模型不需要派生自特定类,并且不假定包含任何接口数据。

缺点

  1. 虽然使用此技术可以很好地表示数据,但方法不行。ViewModel 层所需的任何自定义函数都需要一个专门的派生类。(下面第一点)
  2. 如果您不想要 ViewModel 层提供的某些功能,则无法为特定模型关闭它们。
  3. 该库不包含内置的数据验证系统。

更新

  1. 此新版本的 DynamicVM 有一个新类,它促进了 ViewModel 层中的自定义功能,即 ViewModelFactory。
  2. 支持在逻辑树上层级的子对象和选项之间的类似外键的引用,且编码量极少。
  3. 包含一个 ProjectManager,它负责 XML 对象的序列化和整个 ViewModel 层的构建。ProjectManager 类支持新建、保存等样板命令,并监听文件和设备更改(例如拔出闪存驱动器)。
  4. 如果指定,则支持整数属性上的唯一 ID 约束。
  5. 通过 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");
    }
}

该属性的构造函数需要五个参数:

  1. 父对象中包含要从中选择的对象的列表属性的名称。
  2. 父对象的类型。
  3. 列表中目标对象的“主键”属性的名称。
  4. 目标对象的类型。
  5. 系统生成的 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 对象直接插入逻辑树。

© . All rights reserved.