DelegateWatchCommand - 一个没有 CommandManager 的 DelegateCommand






4.89/5 (11投票s)
一个小型库,通过用户输入或任何异步更改引起的属性更改,可以简单可靠地触发 CanExecuteChange 事件。它可与 WPF 和 Silverlight 配合使用。
目录
- 背景部分阐述了问题。
- 然后我们看一下演示应用程序。
- 然后描述了“手动”命令通知。
- 接下来是一个通用但相当冗长、“幼稚”的通知实现。
- 最后,介绍了
DelegateWatchCommand
,它使代码更简洁。如果您对一般性讨论和众所周知的技术兴趣不大,请直接跳到“DelegateWatchCommand”部分。 - 最后,讨论了一些扩展(库也支持)和其他用法。
背景
WPF 和 Silverlight 的 DelegateCommand
的几种变体是众所周知的,我想所有遵循 MVVM 模式的人都在使用它们。ICommand
接口只有三个成员:Execute
、CanExecute
和 CanExecuteChanged
。使用 DelegateCommand
,可以轻松地将 Execute
和 CanExecute
定义为委托。如果 CanExecute
的值取决于某些会更改的属性,那么每次它们更改时,都必须触发 CanExecuteChanged
,以便重新评估 CanExecute
。这种更改通知的实现并不困难,但直接的方法首先是乏味的,其次是容易出错的。
Josh Smith 在 http://joshsmithonwpf.wordpress.com/2009/07/11/one-way-to-avoid-messy-propertychanged-event-handling/ 中建议利用 CommandManager
。我假设我们中的许多人都使用他的 RelayCommand
。但是这个解决方案仍然有一些缺点。第一个缺点是 Silverlight 中根本没有 CommandManager
,没有人会提示再次检查 CanExecute
。第二个缺点是 CommandManager
只知道用户输入。如果由于异步事件(例如,由于通过 WCF 传递的数据)而更改了重要属性,CommandManager
就会休眠,无法唤醒您的命令。
Prism 避免使用 CommandManager (http://compositewpf.codeplex.com/Thread/View.aspx?ThreadId=226136),Karl Shifflett 的观点应予以考虑。
本文提出了另一种在通知 DelegateCommand
时避免代码混乱的方法。
基本假设是所有涉及通知的对象都实现 INotifyPropertyChanged
接口(也支持 INotifyCollectionChanged
)。
演示应用程序
演示应用程序非常简单,仅演示了与其它实现相比该库的用法。下面是快照:
UI 相当不引人注目,类似于 WinForms。下图显示了 UI 背后的类:
这些对象是我们的沙盒,我们可以在其中使用不同的命令。有一个名为 MasterVM
的根对象,它有一个名为 Model
且类型为 Model
的属性。Model
对象有一个名为 SubModel
且类型为 SubModel
的属性。当演示程序启动时,会创建一个 MasterVM
实例,并将其分配给窗口的 DataContext
。
游戏的目标是编写一个命令,该命令递增 MasterVM
的 Result
属性。仅当满足某个条件(下文指定)时才允许递增结果。命令必须在其 CanExecute
中遵守此条件。该条件取决于 Model
和 SubModel
对象的属性。影响条件的两个 string
和两个 int
属性绑定到 TextBox
,并且可以由用户更改。布尔属性 ToggledProp
也很重要,但不依赖于用户输入 - 它每秒由计时器切换(该属性绑定到带有适当标签的 CheckBox
,但 CheckBox
被禁用)。
此外,游戏规则规定 SubModel
对象并非永久存在。用户可以通过窗口顶部的按钮创建或删除它。条件所依赖的两个属性位于 SubModel
中。因此,我们将要编写的命令必须处理动态创建和置空对象的属性。
首先,我们将前往由蓝色边框包围的沙盒区域(在演示应用程序窗口和对象图中)。在窗口底部绿色边框内,较大的孩子正在玩集合。我们将在本文接近尾声时到达那里。
这是定义命令“可执行性”的条件:
Model.IntProp
必须大于SubModel.AnotherInt
并且Model.StringProp
必须长于SubModel.AnotherString
并且ToggledProp
必须为 true。
绑定到不同按钮的不同类型命令
所有绑定到蓝色边框内按钮的命令都具有相同的 Execute 委托。
_ => this.Result++
CanAlwaysCommand
是无条件的。它的 CanExecute
委托未设置,这意味着它始终为 true。按钮“Unconditional”绑定到此命令,并且始终启用。此命令违反了游戏规则,并且永远不会赢。显然,此命令不需要属性值更改的通知。
所有其他命令都将其 CanExecute
委托给相同的 CanIncrement()
方法。如下所示:
private bool CanIncrement( object parameter ) {
return this.Model.SubModel != null
&& this.Model.IntProp > this.Model.SubModel.AnotherInt
&& this.Model.StringProp.Length > this.Model.SubModel.AnotherString.Length
&& this.Model.ToggledProp;
}
Model
属性在构造函数中分配且永不改变,因此无需检查 this.Model
是否为空。
RelayCommand
“Relay”按钮指的是 Josh Smith 在 http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/ 中描述的 RelayCommand
,它已成为经典的实现。它挂钩了 CommandManager
的 RequerySuggested
事件,并提示按钮在用户点击某个地方(包括更改 CanIncrement
方法所需的重要属性)时重新检查 CanExecute
。但是 ToggledProp
属性在没有用户交互的情况下发生变化,“Relay”按钮无法响应其变化。它的 IsEnabled
属性在您手动更改某些内容时计算。根据此时 ToggledProp
的值,按钮的 IsEnabled
设置为 true 或 false,并且在您再次点击某些内容之前不会改变。
经典 DelegateCommand
ManualCommand
是一个简单的 DelegateCommand
,它不挂钩 RequerySuggested
(就像 Prism http://compositewpf.codeplex.com/releases/view/55580 中那样)。每当重要属性发生变化时,必须显式调用其方法 RaiseCanExecuteChanged()
。这需要多行代码(对于这些代码的简单透明的“任务”来说太多了),而且代码容易出错,因为它依赖于定义为字符串的属性名称。如果属性被重命名,则必须检查所有字符串出现。如果您的属性名称在许多类中大量使用(例如,“Name”或“Parent”),则必须仔细区分必须更改和不必更改的字符串。
所以,我不建议以这种方式编程;我们只是要看看“手动”应该做什么,然后,我们将尝试以更好的方式做同样的事情。
由于 CanIncrement
取决于 Model
的属性,因此我们必须在 MasterVM
中(以绝对标准的方式;您每天都编写此代码,我在此处列出它只是为了提醒您它有多无聊)挂钩 this.Model
的 PropertyChanged
事件。显然,直接从 Model
属性的设置器通知命令不是一个好主意 - Model
绝不能以任何方式依赖于 ViewModel
,它绝不能包含任何知道 ViewModel
的代码。即使我们忘记 MVVM,将通知代码分散到几乎所有对象也与良好设计无关。
// ------------- Model --------------- //
private Model _model;
public Model Model {
get { return _model; }
private set {
if( _model != value ) {
if( _model != null )
_model.PropertyChanged -=
new PropertyChangedEventHandler( _model_PropertyChanged
_model = value;
if( _model != null )
_model.PropertyChanged +=
new PropertyChangedEventHandler( _model_PropertyChanged );
this.AddSubmodelCommand.RaiseCanExecuteChanged();
}
}
}
请注意,每当 Model
更改时都会调用 RaiseCanExecuteChanged()
,尽管 CanIncrement()
并不明确依赖于 this.Model
。我们需要这样做,因为如果分配了新的模型实例,它将有自己的属性值,并且必须重新评估 CanIncrement()
。这是事件处理程序:
void _model_PropertyChanged( object sender, PropertyChangedEventArgs e ) {
if( e.PropertyName == "IntProp" || e.PropertyName == "StringProp"
|| e.PropertyName == "ToggledProp" )
this.ManualCommand.RaiseCanExecuteChanged();
else if( e.PropertyName == "SubModel" )
this.SubmodelCopy = this.Model.SubModel;
}
每次重要属性发生变化时,命令都会收到通知。
如您所见,SubModel
属性的处理方式不同。我们必须挂钩其 PropertyChanged
并以与 Model
的事件相同的方式处理它。我们可以直接在 SubmodelCopy
获取其值的位置执行此操作:
void _model_PropertyChanged( object sender, PropertyChangedEventArgs e ) {
. . .
// this is WRONG !!!
else if( e.PropertyName == "SubModel" )
if( this.Model.Submodel != null )
this.Model.Submodel.PropertyChanged +=
new PropertyChangedEventHandler( _submodel_PropertyChanged );
此代码有一个问题:我们可以挂钩事件,但不能取消挂钩。当代码通知 this.Model.SubModel
更改时,旧值已消失。因此,我们必须在 ViewModel
中拥有自己的 SubModel
副本。这就是创建额外属性 SubmodelCopy
的原因。该属性与 Model
属性完全相同。它附加/分离一个事件处理程序,该处理程序观察当前 SubModel
实例的重要属性并通知命令。此属性可以是私有的 - 除了通知代码之外,没有人对此感兴趣。
有了这段代码,ManualCommand
就能正常工作。如果您启动演示并单击“创建子模型”按钮,绑定到该命令的“手动”按钮将每秒启用/禁用,并尊重 CanIncrement
中的所有其他值(演示程序中所有属性的初始值都满足 CanIncrement
条件)。
当我们查看代码时,我们可以看到要通知命令
- 我们需要每个涉及的子对象的副本(更准确地说:其属性影响
CanExecute
条件的每个对象), - 我们必须挂钩/取消挂钩
PropertyChanged
,并且 - 我们必须知道属性的名称。
所以,让我们尝试编写一个以通用方式实现此功能的程序。
NaiveCommand (通用通知的尝试)
为了观察形如 x.P1.P2. ... Pm
的表达式的结果,我们将编写一个观察一个对象的单个属性的类(并将其命名为 PropertyWatch
,因为“observer”这个词已被大量使用),并将所需数量的实例链接成一个链(PropertyWatchChain
类)。链中的每个“监视器”(最后一个除外)都为其后继提供要观察的对象。链中的最后一个对象在其观察的属性更改时触发事件。附加的事件处理程序调用我们要通知的命令的 RaiseCanExecuteChanged()
。这是我们本节的计划。
PropertyWatch
类的基本属性和字段是:
INotifyPropertyChanged Source { get; set; }
- 要观察的对象;string PropertyName{ get; }
- 要观察的属性名称;Func<INotifyPropertyChanged, INotifyPropertyChanged> _getter;
- 一个委托,用于提取观察到的属性的值(构造函数中设置的私有字段);IPropertyWatch Next { get; set; }
- 链中的下一个元素。
链中的最后一个元素类型为 PropertyWatchTail
。它不需要 _getter
和 Next
成员,但它需要一个指向其所属的 PropertyWatchChain
的指针(称为 Parent
)。通过此指针,它可以触发事件。PropertyWatch
和 PropertyWatchTail
都实现了 IPropertyWatch
接口。Next
属性被类型化为该接口,并接受这两个类中的任何一个。
例如,在演示程序中,当 this.Model.SubModel.AnotherInt
更改时,必须通知命令。因此,对象应该构建以下结构:
这些类很简单,几乎没有什么需要注释的。不过,请注意所有被观察对象都必须实现 INotifyPropertyChanged
。如果一个 PropertyWatch
对象的新值被分配给其 Source
属性,它必须调用 _getter
来获取被观察属性的值,并将该值推送给其后继。这些赋值在图中用竖线表示。如果 Source
属性被设置为 null(这样就没有什么可以观察的,并且不能调用 getter),对象仍然必须将 null 推送给其后继。如果 PropertyWatchTail
实例得到一个 null 作为要观察的对象,它必须触发 PropertyWatchChain
的 WatchedPropertyChanged
事件。
到目前为止一切顺利。但是如果你看一下创建“链”的程序,你会发现它有多糟糕(代码片段中的 Cons()
函数将一个元素添加到链的开头):
// These 5 variables need not be declared as class members,
// the objects assigned to them will survive (would not be eaten by GC)
// even if they were not because they hang on the RaiseCanExecuteChanged event
// of the command.
// These variables can be though helpful for debugging
// and to see what really happens.
private PropertyWatchChain w2;
private PropertyWatchChain w3;
private PropertyWatchChain w4;
private PropertyWatchChain w5;
private PropertyWatchChain w6;
private void BuildNaiveCommand() {
// CanExecute() depends on the values of six properties:
// this.Model.SubModel
// this.Model.IntProp
// this.Model.SubModel.AnotherInt
// this.Model.StringProp
// this.Model.SubModel.AnotherString
// this.Model.ToggledProp
// We need only five PropertyWatchChain instances because the first path
// is a sub-path of the other ones.
w2 = new PropertyWatchChain( new PropertyWatchTail( "IntProp" ) )
.Cons( new PropertyWatch( "Model", x => ((MasterVM)x).Model ) );
w2.Head.Source = this;
w2.WatchedPropertyChanged
+= new EventHandler<EventArgs>( w_WatchedPropertyChanged );
w3 = new PropertyWatchChain( new PropertyWatchTail( "AnotherInt" ) )
.Cons( new PropertyWatch( "SubModel", x => ((Model)x).SubModel ) )
.Cons( new PropertyWatch( "Model", x => ((MasterVM)x).Model ) );
w3.Head.Source = this;
w3.WatchedPropertyChanged +=
new EventHandler<EventArgs>( w_WatchedPropertyChanged );
// watch the string, not its length (this is enough, since strings are immutable
// and the length cant change when the string doesnt):
w4 = new PropertyWatchChain( new PropertyWatchTail( "StringProp" ) )
.Cons( new PropertyWatch( "Model", x => ((MasterVM)x).Model ) );
w4.Head.Source = this;
w4.WatchedPropertyChanged
+= new EventHandler<EventArgs>( w_WatchedPropertyChanged );
// again, watch a string:
w5 = new PropertyWatchChain( new PropertyWatchTail( "AnotherString" ) )
.Cons( new PropertyWatch( "SubModel", x => ((Model)x).SubModel ) )
.Cons( new PropertyWatch( "Model", x => ((MasterVM)x).Model ) );
w5.Head.Source = this;
w5.WatchedPropertyChanged
+= new EventHandler<EventArgs>( w_WatchedPropertyChanged );
w6 = new PropertyWatchChain( new PropertyWatchTail( "ToggledProp" ) )
.Cons( new PropertyWatch( "Model", x => ((MasterVM)x).Model ) );
w6.Head.Source = this;
w6.WatchedPropertyChanged
+= new EventHandler<EventArgs>( w_WatchedPropertyChanged );
// This program is ugly!
// Please, never write such programs and better even don't read them.
}
void w_WatchedPropertyChanged( object sender, EventArgs e ) {
this.NaiveCommand.RaiseCanExecuteChanged();
}
关于 NaiveCommand
这一节的要点是:
- 实现命令的通用通知并不困难;
- 需要监视的内容定义集中在一个地方,而不是分散在整个程序中;
- 这些定义不透明,无法一眼看出哪些对象的哪些属性被观察;
- 这些定义容易出错,因为属性名称未经编译器检查。
我们可以使用以下构造函数来修复最后一个提到的缺陷:
public PropertyWatch(
Expression<Func<INotifyPropertyChanged, INotifyPropertyChanged>> getterExpr )
{
_propName = PropertySelectorUtils.GetPropertyName( getterExpr );
if( getterExpr == null )
throw new ArgumentNullException( "getterExpr" );
_getter = getterExpr.Compile();
}
但我们最好朝这个方向迈出两步,并使用一个 lambda 表达式定义整个观察者链。
但首先,进行一些讨论。PropertyWatch
类将分配给其 Source
属性的所有观察对象都视为 INotifyPropertyChanged
。由于这种简化,所有作为 getterExpr
参数传递的 getter 必须首先强制转换其参数。因此,每个链中第一个观察者的 getter 是 x => ((MasterVM)x).Model
。可以编写一个泛型类型版本的 PropertyWatch
,它有两个类型参数:T1
是被观察对象的类型,T2
是被观察属性的类型,因此也是后续观察对象的类型。对于链中的最后一个元素,我们需要一个只有一个类型参数的泛型类型 PropertyWatch
。PropertyWatch<T1,T2>
继承自 PropertyWatch<T1>
。PropertyWatch<T1,T2>
中 Next
的类型将是 PropertyWatch<T2>
,这样任何 PropertyWatch<T2,T3>
作为中间元素或 PropertyWatch<T2>
作为最后一个链元素都可以作为后继类型。我不相信所有这些复杂性是值得的。我猜,避免每次用户交互进行一对类型转换并不是一个合理的目标。但这是可能的,如果介意的话。
DelegateWatchCommand
这是一个简单的 DelegateCommand
,通过一个 PropertyWatchChain
列表进行扩展,这些链是在构造函数中从 lambda 表达式构建的。嗯,我没有什么可以补充的了。让我们看看在演示程序中如何定义命令实例。
private DelegateWatchCommand<MasterVM> _watchCommand;
public DelegateWatchCommand<MasterVM> WatchCommand {
get {
if( _watchCommand == null )
_watchCommand = new DelegateWatchCommand<MasterVM>(
_ => this.Result++, // Execute
this.CanIncrement, // CanExecute
this, // the root object of watch chains
// properties to watch
x => x.Model.SubModel.AnotherString,
x => x.Model.SubModel.AnotherInt
x => x.Model.IntProp,
x => x.Model.StringProp,
x => x.Model.ToggledProp
);
return _watchCommand;
}
}
实际文本与此片段略有不同,因为它(指演示代码)使用/说明了下一节中描述的扩展。请注意,命令类的类型参数不是某些 DelegateCommand
实现中命令参数的类型(稍后会详细介绍类型参数)。
对我来说,这个命令看起来好多了。一眼就能看出它的 CanExecute
依赖于哪些属性。lambda 表达式由编译器检查。没有混乱的代码,并且命令仍然会收到所有重要更改的通知,包括由异步事件引起的更改。
DelegateWatchCommand
类继承自 DelegateCommand
,它以众所周知的方式处理 Execute
和 CanExecute
委托,并限制类型参数。
public class DelegateWatchCommand<T>: DelegateCommand
where T: class, INotifyPropertyChanged {
. . .
让我们看看最有趣的构造函数及其辅助方法。
public DelegateWatchCommand( Action<object> execute, Func<object, bool> canExecute,
T source, params Expression<Func<T, object>>[] exprList )
: base( execute, canExecute )
{
_chains = exprList.Select( e => this.MakePropertyWatchChain( e ) ).ToList();
this.Source = source;
}
辅助方法从一个 lambda 表达式创建一个链并挂钩其事件。
private PropertyWatchChain MakePropertyWatchChain( Expression<Func<T, object>> expr ) {
var ret = PropertyWatchChain.FromLambda( expr );
ret.WatchedPropertyChanged
+= new EventHandler<EventArgs>( ret_WatchedPropertyChanged );
return ret;
}
现在谈谈构造函数中的类型参数和“source
”参数。构造函数假定所有 PropertyWatch
链都以相同的对象实例开始,并且此实例应作为第三个参数传递给构造函数,或者稍后分配给命令的 Source
属性。这种限制(只有一个根实例)似乎是可行的,原因如下。这些链必须指向影响命令 CanExecute()
的所有重要且可更改的属性。如果 CanExecute()
设法访问这些值,那么我们也可以这样做,同样从相同的点开始——从 CanExecute()
所在的实例开始。因此,类型参数是实现 CanExecute()
的类型,而参数“source
”在大多数情况下是 CanExecute()
的“this
”。
或者,在演示程序中,我们可以使用 this.Model
作为起点。如果您查看 lambda 表达式,您会发现它们都以 this.Model
开头。因此,我们可以使用 Model
作为类型参数,并将 this.Model
作为第三个参数传递。该值在开始时可能为 null(观察者将以休眠形式构建——除非分配了非 null 的东西,否则它们永远不会触发事件)。我们必须注意并在每次将新值分配给 this.Model
时将 this.Model
分配给命令的 Source
属性。实际上,这意味着我们“手动”通知命令所有链中第一个对象的更改,并让它自动观察其余部分,因此又回到了混乱的实现。如前所述,在演示程序中,Model
在构造函数中创建并且永不更改。因此在这种特殊情况下,以 this.Model
开头的链可能是合理的。
如果您跟踪 DelegateWatchCommand
构造函数中 lambda 表达式的命运,您会发现调用静态方法 PropertyWatchChain.FromLambda()
的地方,是 PropertyWatchChain
类的一种构造函数。不使用“真实”构造函数的原因是 PropertyWatchChain
类不需要类型参数,而 FromLambda
需要。该方法使用 Visitor
类来处理表达式。Visitor
类(毫不奇怪)遵循 Visitor 模式。它必须处理一种相当受限制的表达式。它们仅由属性访问器组成,并以参数结尾。可能会发生类型转换,但它们会被简单地跳过(我们已同意将所有类型都视为 INotifyPropertyChanged
)。对于第一个属性访问器,会创建 PropertyWatchTail
类的一个实例(lambda 表达式树从右到左读取)。当 Visitor
中的递归返回时,每个进一步的属性都会生成一个 PropertyWatch
实例,该实例被添加到结果链的开头。
扩展一。链连接成树
通常,几个属性监视链具有共同的开头。这允许节省输入并引入一些结构,如果我们连接(一些)链并将它们表示为树。请注意,用于构建 PropertyWatch
链的 lambda 表达式永远不会(整体上)执行。因此,我们可以插入额外的“伪函数”,它们不表示任何有意义的计算,而只向 Visitor
提供辅助信息。
因此,我们定义了一个扩展方法:
public static object MultiWatch<T>( this T source,
params Expression<Func<T, object>>[] branches )
where T: class, INotifyPropertyChanged
{
throw new InvalidOperationException( "This method is not intended to be called" );
}
我们可以这样使用该方法:
_watchCommand = new DelegateWatchCommand<MasterVM>(
_ => this.Result++,
this.CanIncrement,
this,
x => x.Model.SubModel.MultiWatch(
y => y.AnotherString,
y => y.AnotherInt
),
x => x.Model.IntProp,
x => x.Model.StringProp,
x => x.Model.ToggledProp
);
它值得吗?这取决于。如果公共部分比演示中的长,此功能可能会很有用。
其背后的实现如下。观察 x.Model.SubModel
的根链以新类 PropertyWatchMulti
的实例而不是 PropertyWatchTail
结束。该类包含“连续链”,并有两个目的:
- 它将观察到的对象推送到所有子链,并且
- 它监听它们的
WatchedPropertyChanged
,将信号传输到它自己的PropertyWatchChain
实例。
当然,Visitor
类必须改变。如果它找到 MethodCallExpression
且该方法是 MultiWatch
,它将生成 PropertyWatchMulti
类的一个实例,并递归处理每个“continuation”表达式。
扩展二。集合
到目前为止,我们只处理了实现 INotifyPropertyChanged
的类。还有另一个常用的通知接口——IObservableCollection
。支持此接口也将非常有用。例如,一个集合包含 Point
,并且如果存在 X 值为负的点,或者所有点的 X 都大于 Y,或者所有集合成员都“有效”(无论在特定程序中意味着什么),则命令必须是可执行的。
所以我们需要一个新类 WatchCollection
。它观察成员资格变化,并可以观察每个集合元素中的一个或多个属性路径。这个类有点类似于 PropertyWatchMulti
,因为它有多个后继(每个集合元素一个),并且可能每个元素有多个链。以下是该类如何工作的想法。当一个类的实例被构造时,会创建一个“模式”链。然后为每个新的集合成员克隆它。Source
属性不会通过克隆复制,因为每个副本都有自己的源(要观察的新集合元素)。如果每个元素中必须观察多个链,则模式以一个几乎是虚拟的链开始,该链仅包含一个 PropertyWatchMulti
。
在 lambda 表达式中,我们将使用另一个永远不会被调用的扩展方法来表示观察集合元素:
public static object CollectionWatch<T>( this ObservableCollection<T> source,
params Expression<Func<T, object>>[] branches )
{
throw new InvalidOperationException( "This method is not intended to be called" );
}
第一个参数是一个 lambda 表达式,它从根对象开始,并指向应该被观察的集合。接下来的参数是 lambda 表达式,它们从集合元素开始,并以应该被观察的属性结束。
当然,Visitor
会再次更改。此更改需要 Reflection,因为要生成的 WatchCollection
实例的类型参数不是静态已知的。
现在我们回到演示程序。窗口底部的控件说明了当集合元素属性满足某个条件时必须启用的命令的情况。让我们看看绿色边框中的内容。MasterVM
类有一个类型为 ObservableCollection<DemoCollectionElement>
的属性 DemoCollection
。DemoCollectionElement
类只有一个整数属性 X
,并且仍然必须实现 INotifyPropertyChanged
。集合成员显示在左侧的 ListBox
中。上面两个按钮允许:
- 添加 X 值在 0 到 10 之间随机的项,以及
- 删除所有元素。
“添加项”按钮最多允许添加四个项。游戏规则就是这样。
然后规则规定我们必须编写一个命令,当集合中所有 X
的平均值大于 9 时启用。演示中的命令属性名为 CollectionDependingCommand
,并绑定到标有“Depends on Average”的按钮。该命令必须观察集合元素内部的变化。命令的操作是递增窗口顶部的“结果”字段,以产生单击按钮的一些可见效果。命令的构造函数和 CanExecute
列在下面,并说明了 CollectionWatch
的用法:
public class MasterVM: NotifyPropertyChangedBase {
. . .
// --------------- CollectionDependingCommand --------------- //
private DelegateWatchCommand<MasterVM> _collectionDependingCmd;
public ICommand CollectionDependingCommand {
get {
if( _collectionDependingCmd == null )
_collectionDependingCmd = new DelegateWatchCommand<MasterVM>(
_ => this.Result++,
this.IsAverageOK,
this,
x => x.DemoCollection.CollectionWatch( y => y.X ) // watch X in elements
);
return _collectionDependingCmd;
}
}
private bool IsAverageOK( object dummy ) {
if( this.DemoCollection.Count == 0 ) // can't compute Average if there are no items
return false;
return this.DemoCollection.Average( x => x.X ) > 9;
}
条件(平均值 > 9)在随机数小于 10 的情况下很难满足。但是,如果您在列表中选择一个项目并在 TextBox
“Selected Item”中编辑其值,则按钮可以启用。这表明如果任何元素的 X
属性发生变化,条件会重新评估。如果您只插入一个元素并将其值更改为 10,则按钮将启用。再添加一个元素几乎总是导致平均值小于 9,并且按钮将禁用。这表明该命令知道集合成员资格的变化。好吧,就是这样。
其他用途
演示程序说明了属性链观察器的另一种可能用法。PropertyWatchChain
类可以被认为是穷人绑定的四分之一。它是一半,因为它是一次性的,它是四分之一,因为它观察变化,但不分配任何东西。
PropertyWatchTrigger
类允许在属性发生更改时触发操作。如果此操作分配更改后的值,您就拥有了一个简单的单向绑定。当然,该操作可以指定任何计算,不一定是赋值。在演示程序中,绿色边框右侧的“平均值”字段必须在 DemoCollection
的成员更改其 X
(或添加或删除成员)时进行更新。以下几行定义了执行此任务的触发器:
_triggerAverage = new PropertyWatchTrigger(
PropertyWatchChain.FromLambda<MasterVM>(
x => x.DemoCollection.CollectionWatch( y => y.X ) ), // watch X in all elements
() => this.Average = this.DemoCollection.Count == 0
? 0.0
: this.DemoCollection.Average( e => e.X ), // compute Average, if smth changes
this );
第一个参数定义监视,第二个参数定义操作。第三个参数是观察到的属性链的根。根对象可以为空,稍后分配。
整合 ICommand 参数类型
库中命令的实现完全忽略了 ICommand
参数,直接将其传递给委托。如您所见,DelegateWatchCommand
继承自 DelegateCommand
,只关心通知。应该很容易以相同的方式实现一个额外参数化 ICommand
参数类型的版本,该版本继承自 Prism 风格的 DelegateCommand
。
实际操作
演示程序和库是用 VS 2010 Express 编写的。
我想你可以直接使用编译好的 DLL。库和所有类都非常小,所以最好将源代码包含在您的库中(您的所有基本类都在那里),并避免多一个 DLL,多一个引用等。
关注点
用于通知命令属性值更改的类是根据 lambda 表达式动态生成的。
在某些特殊情况下生成这些类所需的附加信息由“伪函数”表示,这些伪函数包含在 lambda 中但从不被调用。
历史
- 初始版本:2011年3月7日。