动态视图模型






4.89/5 (9投票s)
一种动态生成 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,并且选定的实例会更新模型中的键字段。