Nito.MVVM (WPF) 库:命令





5.00/5 (12投票s)
介绍开源 Nito.MVVM (WPF) 库中的 ViewModel 命令类,并提供其使用指南。
引言
Nito.MVVM (WPF) 库包含多个命令类,在编写非依赖项对象的 ViewModel 类时非常有用。Nito.MVVM 没有为所有场景提供一个通用的 ICommand
实现,而是提供了许多命令类,每个类都满足一系列用例。特定用例的需求决定了应使用哪个命令类。
Nito.MVVM 命令类用于 ViewModel 命令,而非 GUI 命令。WPF 附带了许多常用的 GUI 命令,例如 Copy
、Paste
和 BrowseForward
。总的来说,GUI 命令操作的是 View 对象或状态,而不是 Model 对象。GUI 命令使用 RoutedCommand
实现,这是一种路由通过 View 元素树的命令对象。
ViewModel 命令操作 Model 对象,并通过 ViewModel 暴露给 View 进行绑定。它们封装了特定于域的逻辑,例如 SaveClient
或 GenerateInvoice
。Nito.MVVM 库中的命令类有助于编写需要暴露命令的 ViewModel 类。可以使用 RoutedCommand
实现 ViewModel 命令,但这效率低下且难以扩展。
命令类型和 CanExecuteChanged 原因
ViewModel 命令可以沿两个轴分组:命令类型和CanExecuteChanged 原因。共有三种命令类型:
- 委托命令 - 一个独立的普通命令,不依赖于其他命令。从概念上讲,它只有代码,尽管这些代码可以调用其他命令。
- 复合命令 - 一个完全由其他(不相关的)命令组成、不带额外代码的命令。从概念上讲,它有一个有序的子命令列表。
- 多命令 - 一个由子对象列表中的相同命令组成的命令。从概念上讲,它有一个无序的子集,并带有一个命令选择器字符串。例如,
SaveAllCommand
可以定义为每个子 ViewModel 的SaveCommand
。
大多数 ViewModel 命令是调用 Model 对象上的方法的委托命令。复合命令不太常见。多命令在多选场景中很有用。复合命令和多命令都可以在其子命令列表中包含任何类型的命令。
命令必须通知侦听者其 CanExecute
返回值是否可能已更改。此事件可能由不同的情况引起;有五种CanExecuteChanged 源:
- 静态 - 一个始终可以运行的命令。
ICommand.CanExecute
始终返回 true,并且ICommand.CanExecuteChanged
永远不会引发。此源在实践中很少见,通常是某种形式的New
命令。 - 自动 - 一个命令的可运行性由 View 状态决定。自动命令将其
ICommand.CanExecuteChanged
事件与CommandManager.RequerySuggested
绑定,后者在重要的用户输入事件时触发。因此,它们似乎“自动”引发此事件,故名“自动”。如果 Model 不会生成非用户发起的更改,则自动命令也可以用于依赖于 Model 状态的命令;但这很难扩展,尽管比使用RoutedCommand
要好。 ChildCanExecuteChanged
- 一个命令的可运行性完全取决于其子命令的ICommand.CanExecute
结果。这是复合命令和多命令的正常源。- 多订阅 - 一个命令的可运行性可能因其他对象的事件而改变,例如
INotifyPropertyChanged.PropertyChanged
和/或INotifyCollectionChanged.CollectionChanged
。如果 Model 对象实现INotifyPropertyChanged
,则此命令源对于由 Model 状态决定的可运行性命令很有用。 - 手动 - 一个具有定义其可运行性的特殊要求的命令。
每种命令类型可能具有不同的CanExecuteChanged 原因。
- 委托命令可以是静态的、自动的、多订阅的或手动的。通常,委托命令是多订阅的,但为了简单起见(以牺牲效率为代价)也可以是自动的。
- 复合命令通常是 ChildCanExecuteChanged。但是,如果其任何子命令是自动的,则复合命令可能出于优化目的而自动。
- 多命令通常是 ChildCanExecuteChanged。与复合命令一样,如果其任何子命令是自动的,则多命令可能出于优化目的而自动。
Nito.MVVM 中的命令类
Nito.MVVM 提供了几个命令类。带类型参数的类使用该参数作为传递给命令的参数类型。每种类型都旨在涵盖一个常见的用例:
StaticCommand
/StaticCommand<T>
- 静态委托命令。AutomaticCommand
/AutomaticCommand<T>
- 自动委托命令。ManualCommand
/ManualCommand<T>
- 手动委托命令或多订阅委托命令。CompositeCommand
- ChildCanExecute 复合命令。AutomaticCompositeCommand
- 自动复合命令或自动多命令。MultiCommand
- ChildCanExecute 多命令。
API 描述
委托命令有两个委托属性:CanExecute
和 Execute
。
//// Delegate properties for commands without parameters
/// <summary>
/// Gets or sets the delegate invoked to execute this command.
/// Setting this does not raise <see cref="CanExecuteChanged"/>.
/// </summary>
public Action Execute { get; set; }
/// <summary>
/// Gets or sets the delegate invoked to determine if this command may execute.
/// Setting this does not raise <see cref="CanExecuteChanged"/>.
/// </summary>
public Func<bool> CanExecute { get; set; }
//// Delegate properties for commands with parameters
/// <summary>
/// Gets or sets the delegate invoked to execute this command.
/// Setting this does not raise <see cref="CanExecuteChanged"/>.
/// </summary>
public Action<T> Execute { get; set; }
/// <summary>
/// Gets or sets the delegate invoked to determine if this command may execute.
/// Setting this does not raise <see cref="CanExecuteChanged"/>.
/// </summary>
public Func<T, bool> CanExecute { get; set; }
复合命令有一个子列表。AutomaticCompositeCommand
类将其公开为一个可枚举属性:public IEnumerable<ICommand> Subcommands
,它可以设置为 LINQ 查询的结果。每次调用 CanExecute
或 Execute
时,都会枚举 Subcommands
,重新评估查询。相比之下,CompositeCommand
类需要订阅其子命令的 CanExecuteChanged
事件,因此它充当命令的集合,实现 IList<ICommand>
。
MultiCommand
类也有一个类似子列表的结构。该类有两个值得注意的属性:
/// <summary>
/// Gets or sets the source collection of this multiproperty.
/// This class is more efficient if the source collection implements
/// <see cref="IList"/> and <see cref="INotifyCollectionChanged"/>.
/// </summary>
public IEnumerable SourceCollection { get; set; } // (not the actual implementation)
/// <summary>
/// Gets or sets the property path applied to the elements in the source collection.
/// </summary>
public string Path { get; set; } // (not the actual implementation)
MultiCommand
将监视 SourceCollection
(如果它实现了 INotifyCollectionChanged
);这通常设置为 ObservableCollection<T>
或 CollectionView
。Path
属性是一个简单的属性路径,例如“ChildVM.SaveCommand”。该路径会针对源集合的每个元素进行评估,从而生成一个子命令集合。此外,MultiCommand
将监视属性路径上的每个对象(如果它实现了 INotifyPropertyChanged
)并相应地更新自身。
ManualCommand
和 ManualCommand<T>
都支持手动和多订阅通知。手动通知使用 NotifyCanExecuteChanged
方法,并且这些类还公开了一个只读委托属性 CanExecuteChangedSubscription
,可用于订阅其他命令的 ICommand.CanExecuteChanged
事件。(有关此必要性的说明,请参阅下面的“关于 ICommand.CanExecuteChanged 的警告”。)请注意,对多订阅命令的此支持不使用弱事件,因此 Nito 命令对象的生存期与提供状态更改通知的所有对象的生存期相关联。
与其他解决方案的比较
已经开发了几种现有的解决方案:
- Josh Smith 的
RelayCommand
- 最受欢迎的选择之一,来自他 2009 年 2 月的里程碑式 MSDN 文章。这是一个自动委托命令。Nito.MVVM.AutomaticCommand<object>
与RelayCommand
相比只有细微的 API 差异。 - Ocean 的
RelayCommand
和RelayCommand<T>
- 来自 Karl Shifflett 的博客。这是一个自动委托命令,几乎与 Josh Smith 的RelayCommand
完全相同。 - Marlon 的
SimpleCommand
,来自他的博客文章。这也是一个自动委托命令,其 API 与Nito.MVVM.AutomaticCommand<object>
更加相似。 - Prism 的
DelegateCommand<T>
,来自 2009 年 2 月的版本。这是一个手动委托命令,因此它与Nito.MVVM.ManualCommand<T>
相似。但是,它存在一个错误,因为它没有将CanExecuteChanged
实现为弱事件。不过,遇到此错误的情况很少,它只是一个生存期扩展错误。 - Prism 的
CompositeCommand
,来自同一来源。它与Nito.MVVM.CompositeCommand
相似(尽管 Prism 的CompositeCommand
也旨在用作多命令)。但是,Prism 的CompositeCommand
不仅将CanExecuteChanged
实现为强事件,而且还假设其子命令将其实现为强事件。这个错误更严重:除非所有子命令都是 Prism 委托命令,否则可能导致事件丢失。 - MVVM Toolkit 的
DelegateCommand
和DelegateCommand<T>
,来自 2009 年 5 月的版本。这些是出色的类,满足了静态委托、自动委托和手动委托命令的需求。与 Prism 的命令不同,这些类具有正确的CanExecuteChanged
实现。 - Caliburn 的命令,来自更改集 26106(2009 年 7 月)。Caliburn 将其命令构建在其 Action 系统之上;当它们绑定到元素时,它们才成为 WPF 命令。因此,它们可以被视为一种可以通过 XAML 定义的自动委托命令。Caliburn 也有复合命令的概念。
关于 ICommand.CanExecuteChanged 的警告
对于那些尝试创建自己的 ICommand
派生类型的人来说,有一点警告:ICommand.CanExecuteChanged
**必须**实现为弱事件。Nathan Nesbit 在他的博客上对这个问题的描述是我找到的最好的,尽管他从编写自定义控件的角度来解决这个问题,而不是自定义命令。
可重用组件
Nito.MVVM 库中的所有源代码都符合 FxCop 和 StyleCop 标准,下载中包含一套单元测试。有许多类型用于支持命令类;其中一些更有趣的类型包括:
SimplePropertyPath
- 提供了一个非常简单的准绑定。与System.Windows.Data.Binding
不同,SimplePropertyPath
不需要依赖项属性。它仅将现有的INotifyPropertyChanged
实现“向前”传播到其他类,并将写入“向后”传播到原始属性。请查看单元测试以获取一些示例。ProjectedCollection
- 接受一个源集合和一个简单的属性路径,并沿该路径“投影”源集合的每个元素。MultiCommand
使用ProjectedCollection
将子 ViewModel 对象序列视为ICommand
对象集合。任何集合或属性更改都会被检测和传播。WeakCollection<T>
- 实现一个弱引用集合,该集合在迭代时会自我清理。CanExecuteChangedCore
使用它来正确实现ICommand.CanExecuteChanged
。- 许多类协同工作,为
INotifyPropertyChanged
提供“编译时反射”,从而无需使用属性名的文字字符串。值得注意的类包括NotifyPropertyChangedBase<T>
和PropertyChangedEventHandlerExtensions
。代码中还提供了对解决此问题的其他解决方案的进一步引用。
官方位置
本文附带的代码是 Nito.MVVM 库的预发布版本。官方发布通过 Nito.MVVM CodePlex 主页进行。
谢谢!
欢迎投票和评论!
文章历史
- 2009-07-21 - 初始修订。
- 2009-07-24 - 从“与其他解决方案的比较”部分删除了 Onyx 的
RelayCommand
,因为它实际上不是 Onyx 框架的一部分,并且将在未来版本中删除。