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

动态视图模型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (9投票s)

2013年6月30日

CPOL

5分钟阅读

viewsIcon

64225

downloadIcon

1206

一种动态生成 ViewModel 对象的技术。

引言

模型-视图-视图模型 (Model-View-ViewModel) 模式是一种强大的工具,它利用了 WPF 的数据绑定功能。然而,编写 ViewModel 类可能非常繁琐且耗时。在本文中,我将介绍一种可以在运行时动态生成 ViewModel 层的方法。

本文现已成为系列文章的一部分。该系列的第二部分可在下方找到。

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

背景   

通常,在 WPF 中开发应用程序时,界面背后有两个数据层。最底层是模型 (Model),它可以是薄数据层或代理。在其上方是 ViewModel 层,它将模型属性暴露给 XAML,并提供从 XAML 到 C# 的双向绑定或推送绑定。WPF 中的用户界面状态几乎是不可确定的;它无法可靠地查询任何可能对界面逻辑有用的信息(如高度、宽度、展开、选择、焦点等)。这些信息可以存储在 ViewModel 层,以便始终有可靠的界面状态记录。此外,可以使用 RelayCommand 对象在 ViewModel 层托管撤销和其他服务。这正是我在本文中采用的方法。

优点和缺点

优点

 

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

缺点

 

 

  • 虽然该技术很好地表示了数据,但它不适用于方法。任何在 ViewModel 层所需的自定义函数都需要专门的派生类。
  • 如果您不想要 ViewModel 层提供的某些功能,则无法为特定模型关闭它们。

 

技巧

有三个主要类在起作用:ViewModelProperty、ViewModelCollection 和 DynamicViewModel。

ViewModelProperty 保存对 ModelContext、宿主 ViewModel 的 NotifyPropertyChanged 事件代码、PropertyInfo 对象以及 ViewModelManager 或 UndoManager 的引用。模型层所做的更改会更新 CachedValue 字段,而界面层所做的更改则被捕获并封装为 UndoManager 的命令。

ViewModelCollection 在监听模型层更改方面起着类似的作用。不同之处在于,它向界面暴露命令以允许进行操作。当调用 AddCommand 时,ViewModelCollection 使用 Activator 向模型列表添加项。可以通过以下代码访问提供给 Activator 的 Type:

m_modelType = modelCollection.GetType().GetGenericArguments()[0]; 

ViewModelProperty 和 ViewModelCollection 都使用弱事件模式来监听模型更改,从而防止 ViewModel 对象保留在内存中。该示例需要 .NET 4.5,但通过使用自定义的弱事件管理器,可以轻松地将其移植到 .NET 4.0。

这项技术的核心位于 DynamicViewModel 类中的 LoadProperties 方法中。

private void LoadProperties()
{
    m_properties = new Dictionary<string, object>();
    Type type = m_modelContext.GetType();
    var reflectedProperties = type.GetProperties();
    foreach (var reflectedProperty in reflectedProperties)
    {
        if (typeof(IObservableList).IsAssignableFrom(reflectedProperty.PropertyType))
        {
            m_properties.Add(reflectedProperty.Name, new ViewModelCollection(reflectedProperty.GetValue(m_modelContext) as IObservableList, this, m_manager));
        }
        else
        {
            m_properties.Add(reflectedProperty.Name, new ViewModelProperty(m_modelContext, reflectedProperty.Name, m_manager));
        }
    }
}  

此方法为原始属性创建 ViewModelProperty 实例,为集合属性创建 ViewModelCollection 实例。DynamicViewModel 继承自 DynamicObject。它重写了两个方法:TryGetMember 和 TrySetMember。

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    if (m_properties.ContainsKey(binder.Name))
    {
        object member = m_properties[binder.Name];
        if (member is ViewModelCollection)
        {
            result = member;
        }
        else
        {
            ViewModelProperty property = member as ViewModelProperty;
            result = property.CachedValue;
        }
        return true;
    }
    return base.TryGetMember(binder, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
    if (m_properties.ContainsKey(binder.Name))
    {
        object member = m_properties[binder.Name];
        if(member is ViewModelProperty)
        {
            ViewModelProperty property = member as ViewModelProperty;
            property.CachedValue = value;
            return true;
        }
    }
    return base.TrySetMember(binder, value);
} 

这使得 ViewModelProperty 和 ViewModelCollection 对象对 XAML 可见。

使用代码

1) 定义您的模型类。对于集合,请使用 ObservableList。

示例

[XmlArray("Children")]
[XmlArrayItem("ChildModel")]
public ObservableList<ChildModel> Children
{
    get
    {
        return m_children;
    }
    set
    {
        m_children = value;
        NotifyPropertyChanged("Children");
    }
}  

2) 创建一个新的 DynamicViewModel 作为 DataContext。您也可以创建一个 ProjectManager 来托管 DynamicViewModel 的根实例(此处未涵盖)。对于根 VM,将以下内容传递给构造函数:

 

  • 根模型类的新实例。
  • null
  • ViewModelManager 的新实例。

 

示例:

DataContext = new DynamicViewModel(new ParentModel(), null, new ViewModelManager());  

3) 要访问撤销命令,请绑定到 UndoCommand 和 RedoCommand RelayCommand 对象。

示例

<KeyBinding Key="Z" Modifiers="Control" Command="{Binding Path=Manager.UndoManager.UndoCommand}"/>   

4) 要绑定到属性,请使用标准语法。

示例

Text="{Binding Path=Name, UpdateSourceTrigger=LostFocus, Mode=TwoWay}" 

5) 您也可以使用标准语法绑定到集合。

示例

ItemsSource="{Binding Path=Children}"  

6) 要将选择绑定到 SharedVisualState 实例,请向 ItemsControl 的 ItemContainerStyle 属性添加一个 Setter。

示例

<Setter Property="IsSelected" Value="{Binding Path=SharedVisualState.IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>    

7) 使用 ViewModelCollection 上的 RelayCommand 属性来扩展您的 UI,使其具有添加、删除、上移和下移功能。只要 IsSelected 绑定到界面项,这四个命令就会自动工作。

示例

<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.AddCommand}">Add Child</Button>
<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.RemoveCommand}">Remove Child</Button>
<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.MoveUpCommand}">Move Up</Button>
<Button VerticalAlignment="Center" Margin="10, 0, 0, 0" Command="{Binding Path=Children.MoveDownCommand}">Move Down</Button> 

兴趣点 

请注意 ObservableList 类和接口。

public interface IObservableList : ICollection, IList, INotifyCollectionChanged, INotifyPropertyChanged
{
    void Move(int oldIndex, int newIndex);
}
public class ObservableList<T> : ObservableCollection<T>, IObservableList
{
    public ObservableList()
        : base()
    {
    }
    public ObservableList(IEnumerable<T> collection)
        : base(collection)
    {
    }
    public ObservableList(IList<T> list)
        : base(list)
    {
    }
    void IObservableList.Move(int oldIndex, int newIndex)
    {
        Move(oldIndex, newIndex);
    }
} 

此代码扩展了 ObservableCollection,并为其添加了一个模板无关的 Move 方法,供 ChangeCollectionCommand 使用。这只是 ObservableCollection 如何被扩展或完全替换以向系统添加特定功能的示例之一。

另一件值得注意的是,多个 ViewModel 实例可以共享相同的视觉状态。请注意,即使每个窗口都有不同的 ViewModel 树,下面的屏幕截图中两个窗口中的同一项都是选中的。

这是通过使用位于 System.Runtime.CompilerServices 命名空间中的 ConditionalWeakTable 来实现的。ViewModelManager 将模型对象与 SharedVisualState 相关联,并且当模型被垃圾回收时,状态也会丢失。

历史

  • 初始文章。2013 年 6 月 29 日
  • 消除了对集合进行特殊属性装饰的需要,并将索引器添加到 ViewModelParentUtility。2013 年 6 月 30 日
  • 将技术更改为使用 DynamicObject。非常感谢 FatCatProgrammer 指出这一点。2013 年 7 月 1 日
  • 更改 SetPropertyCommand 以在传入数据上使用 WPF 类型转换器,以防止由于类型转换错误而导致的崩溃。2013 年 11 月 30 日
  • 添加了第二部分的链接。2014 年 7 月 25 日

第二部分预览

我添加了一个键/值对系统,以便模型对象可以相互关联。可以使用 ViewModelParentUtility 类来填充 ComboBox,并且选定的实例会更新模型中的键字段。

© . All rights reserved.