用于 WPF 命令组合的多重绑定





5.00/5 (6投票s)
在单一用户界面操作后联合执行一组命令的技术。
下载 MultiCommandButton.zip - 52.5K
引言
本文提出了一种在单一用户界面操作(例如,按下按钮)后联合顺序执行一组命令的方法。该方法非常自然地基于 MultiBinding
——一种常用且有用的 WPF 技术。它的实现不需要开发任何新的模式或类。相反,它采用了 MultiBinding
的一些已知特性来实现目标。
对于 GUI 中出现的一些恼人的花哨效果,我深感抱歉——有时生活因这些古怪而显得更加光明。
背景
在与用户交互的过程中,常见的 WPF 控件允许调用一些命令来完成这种交互并执行一些系统工作。在大多数情况下,此类控件只能调用一个命令。有一个特殊的控件属性 Command
,它提供了命令的调用。
在我项目的开发过程中,我曾面临在单一 GUI 操作后调用多个不同命令的需求。例如,按下按钮应该同时激活项目的某些功能并在 GUI 中进行有价值的更改。当然,这些操作本质上非常不同,不应混合在一个模块中。这对于 MVVM 等技术尤其如此。
几年前,Josh Smith 实现了一个 CommandGroup
接口
——一种完美的联合命令合并方法,我一直在使用它。该方法不使用 MVVM 技术。
在 MVVM 技术的所有优点中,有一个要求是所有 View 的操作都必须在 ViewModel 的基础上进行。默认情况下,ViewModel 会评估用户控件的 DataContext
,因此在 Binding
的 XAML 定义中可以省略命令的源。这对于 CommandGroup
是不可能的——那里使用的 Member
属性需要显式的源引用。
除了 Command
属性,大多数控件还有一个 CommandParameter
属性。当命令需要额外信息进行执行时,此属性非常有用。CommandGroup
不允许此类命令参数。
我决定以另一种方式为控件提供对多个命令的支持——使用 MultiBinding
。MultiBinding
是一种非常常见的 WPF 方法,用于将多个项绑定到单个目标属性。这将允许同时使用默认的 DataContext
和参数。
实现概览
演示解决方案说明了命令合并的技术:一次按钮单击会触发多个命令的执行;下面将它们称为部分命令。
演示解决方案中的每个项目都实现了 MVVM 模式:MainVindow.xaml 及其后面的干净代码 MainVindow.cs 用作 View;ViewModel 类分布在 3 个文件中——ViewModel.cs、ViewModel.Commands.cs 和 ViewModel.Properties.cs。Model 不存在。
我决定实现 4 个部分命令。为了好玩,它们以象限命名:NorthActionCommand
、WestActionCommand
、SouthActionCommand
和 EastActionCommand
。这些命令遵循 ICommand 接口的命名规则,并作为熟悉的 RelayCommand
类的实例实现。任何命令的执行都会在 TextBlock
中生成相应的消息。
这 4 个部分命令的调用是通过绑定 Button
的 Command
属性来完成的。但是,实现的是 MultiBinding
而不是简单的 Binding
。这导致了一组四个部分命令的顺序执行。帮派 组
命令的执行带有适当的延迟,以演示执行顺序。这是通过 Task
表示的异步执行来实现的。
命令在 TextBlock
中显示的邮件由相应的属性生成。这些属性以常见的 INotifyPropertyChanged
方式绑定到 TextBlock
的 Run.Text
属性。
提供的解决方案中有 4 个项目:第一个是一个定义了烦人的 GUI 美学的 dll——它不值得考虑。其他三个项目演示了无参数 MultiCommand、带一个参数的 MultiCommand 和带多个参数的 MultiCommand。
在我的项目中,我始终努力摆脱“魔法”数字和字符串。这对于在引发 PropertyChanged
事件时遵循 INotifyPropertyChanged
接口的属性命名尤其重要。在这种情况下,常量字符串被替换为形式为 () => Property
的 lambda 表达式。这个机制是众所周知的。
View 的组织 - MainWindow.xaml
每个项目的 View 都基于 MainWindow
类。它使用 ViewModel 类来评估其 DataContext
属性。
View 定义了一个 Grid
,其中包含所有 View 功能——TextBlock
和 Button
。
下面是 TextBlock
的截断定义。查看 Run.Text
依赖属性。它们绑定到相应的 ViewModel 属性——报告属性——这些属性提供相应命令消息的显示。此绑定是以常见的 INotifyPropertyChanged
接口方式实现的。
来自文件 MainWindow.xaml 的摘录(MultiCommandButtonNoParams 项目)
<TextBlock ...>
<TextBlock.Inlines>
<Run Text="{Binding Path=NorthCommandManifest}" .../>
<LineBreak/>
<Run Text="{Binding Path=WestCommandManifest}" .../>
<LineBreak/>
<Run Text="{Binding Path=SouthCommandManifest}" .../>
<LineBreak/>
<Run Text="{Binding Path=EastCommandManifest}" .../>
</TextBlock.Inlines>
</TextBlock>
另一方面,Button
控件的组织方式与众不同。Button
的 Command
属性通过 MultiBinding
绑定到一组 4 个部分命令。除了 MultiBinding
的性质,这个 Binding
遵循常见的 Command Binding
原则。MultiBinding
的每个部分 Binding
将相应的部分命令附加到 Binding
的 Path
属性。
来自文件 MainWindow.xaml 的摘录(MultiCommandButtonNoParams 项目)
<Button Grid.Column="1" ... Focusable="False">
<Button.Command>
<!--
Multicommand construction that consists of a set of sequentially executed commands.
Each command sends a message about execution to the TextBlock defined above.
-->
<MultiBinding Converter="{StaticResource multiCommandConverter}" >
<Binding Path="NorthActionCommand"/>
<Binding Path="WestActionCommand"/>
<Binding Path="SouthActionCommand"/>
<Binding Path="EastActionCommand"/>
</MultiBinding>
</Button.Command>
</Button>
MultiCommand 引擎
如上所述,每个部分命令都实现为 RelayCommand
。
来自 RelayCommand.cs 的片段(MultiCommandButtonNoParams 项目)文件显示构造函数
public RelayCommand( Action<object> execute, Predicate<object> canExecute )
{
if ( execute == null )
{
_execute = ( p ) => { };
_canExecute = ( p ) => true;
}
_execute = execute;
_canExecute = canExecute;
}
构造函数看起来很简单,需要两个参数——第一个用于提供执行例程的委托,第二个用于批准命令执行的委托。在此演示解决方案中,不提供任何错误处理。
我们稍后将讨论部分命令的创建。
MultiCommand 的组织方式与部分命令相同。但它基于 MultiBinding
。像往常一样对于 MultiBinding
,细节决定成败——即 IMultivalueConverter
的实现。实现是在 MultiCommandConverter
类中完成的。MultiCommandConverter
类遵循 IMultiValueConverter
接口,并且必须实现两个强制方法——Convert
和 ConvertBack
。ConvertBack
方法对我们的目标不重要,它只是返回 null。
另一方面,Convert
方法执行 MultiCommandConverter
应该完成的所有工作——它创建并返回一个通用的 RelayCommand
,该命令调用 4 个部分命令。
Convert
方法——来自 MultiCommandConverter.cs 的摘录(MultiCommandButtonNoParams 项目) 文件
public object Convert( object[ ] value, Type targetType,
object parameter, CultureInfo culture )
{
_value.AddRange( value );
return new RelayCommand( GetCompoundExecute( ), GetCompoundCanExecute( ) );
}
该方法通过所有部分命令的 Binding
获取其第一个参数值。该值是部分命令的数组。首先,Convert
将此数组存储在一个私有变量中,以便在 Convert
执行期间可以使用它——而不仅仅是在 BAML 编译期间。
之后,Convert
构建一个新的 RelayCommand
,该命令应该代表 MultiCommand 本身。这个 RelayCommand
基于私有方法 GetCompoundExecute
和 GetCompoundCanExecute
,它们返回所需的委托。这两个私有方法(在下面的片段中)以相同的方式工作——它们返回 lambda 表达式,在调用时依次执行每个部分命令的 Execute
和 CanExecute
方法。
来自 MultiCommandConverter.cs 文件(MultiCommandButtonNoParams 项目)
private Action<object> GetCompoundExecute( )
{
return ( parameter ) =>
{
foreach ( RelayCommand command in _value )
{
if ( command != default( RelayCommand ) )
command.Execute( parameter );
}
};
}
private Predicate<object> GetCompoundCanExecute( )
{
return ( parameter ) =>
{
bool res = true;
foreach ( RelayCommand command in _value )
if ( command != default( RelayCommand ) )
res &= command.CanExecute( parameter );
return res;
}
}
MultiCommandConverter
生成的 RelayCommand
在 Button
单击后作为一个简单的 Button
命令被调用。然后它会展开以正确执行所有部分命令。
应特别注意带参数命令的执行。这将在稍后讨论。
部分命令的结构
每个部分命令的作用都非常简单——它宣告其执行的事实。但为了清晰起见,我希望在 GUI 中显示这些宣告,而不是立即显示,而是彼此之间带有适当的延迟。直接使用 Thread.Sleep
不起作用。由于 .Net 4.0 的限制,无法使用 Asynch 和 Await。我决定的最简单的事情是使用 Task
进行异步处理。假设在这种情况下,典型的执行操作如下所示。
来自 ViewModel.Commands.cs 的摘录(MultiCommandButtonNoParams 项目)文件
private void NorthAction( object parameter )
{
string name = MethodBase.GetCurrentMethod( ).Name;
Task.Factory.StartNew( ( ) => { Thread.Sleep( ir_delay1000 ); } ).
ContinueWith( t => { NorthCommandManifest = name + sr_executed; } );
}
在上面的片段中,Task
使用 StartNew
方法创建并启动。它执行所需的延迟。之后,Task
继续使用 ContinueWith
方法,该方法会评估适当的属性。此评估通过属性更改通知(如上所述)对 TextBlock
的相应 Run
进行更改。最后,TextBlock
成功显示消息。
每个部分命令使用自己的延迟——NorthAction 使用 1000 毫秒,WestAction 使用 2000 毫秒,SouthAction 使用 3000 毫秒,EastAction 使用 4000 毫秒。这对于后续的考虑很重要。
所有 CanExecute
方法都返回 true 以求简洁。
报告属性的结构
这些属性遵循 INotifyPropertChanged
的规则,即设置新值后,它们会引发 PropertyChanged
事件,从而更新 Binding
。下面是一个这些属性之一的片段——NorthCommandManifest
属性。
来自 ViewModel.Properties.cs 的摘录(MultiCommandButtonNoParams 项目)文件
public string NorthCommandManifest
{
get { return _northCommandManifest; }
set
{
if ( value != _northCommandManifest )
{
_northCommandManifest = value;
RaisePropertyChanged( ( ) => NorthCommandManifest );
}
}
}
其他声明属性的定义类似。
MultiCommandButtonNoParams 项目的执行
正如预期的那样,按下按钮会导致报告 4 条消息。每条消息都比前一条消息有一定延迟出现(到目前为止您应该相信我)。
MultiCommandButtonOneParam 项目的描述和执行
现在我们来考虑一个参数 MultiCommand 的实现。让我们在 GUI 中显示每个命令执行报告之间的延迟(以毫秒为单位)。
这是 MultiCommandButtonOneParam 项目的任务。
首先,让我们在 View 的 Button
中添加一个 CommandParameter
属性。
来自文件 MainWindow.xaml 的摘录(MultiCommandButtonOneParam 项目);更改为粗体。
<Button Grid.Column="1" ... Focusable="False">
<Button.Command>
<!--
Multicommand construction that consists of a set of sequentially executed commands.
The execution is done in the order of commands. Each command sends a message about
execution to the TextBlock defined above.
-->
<MultiBinding Converter="{StaticResource multiCommandConverter}" >
<Binding Path="NorthActionCommand"/>
<Binding Path="WestActionCommand"/>
<Binding Path="SouthActionCommand"/>
<Binding Path="EastActionCommand"/>
</MultiBinding>
</Button.Command>
<Button.CommandParameter>
<Binding Path=Delay/>
</Button.CommandParameter>
</Button>
正如您所猜测的,Delay
是一个属性,它强制每个命令延迟适当的毫秒数。Delay
在初始化期间获得初始值 0。
来自文件 ViewModel.Commands.cs 的摘录( MultiCommandButtonOneParam 项目);Delay
属性。
private int _delay = 0;
public int Delay
{
get { return _delay; }
set
{
_delay = value;
RaisePropertyChanged( ( ) => Delay );
}
}
在每个 Task
开始之前,Delay
会获取要在 Task
中使用的值。此 Delay
值用于以下 Task.Factory.StartNew
,以获得适当的延迟效果。
来自文件 ViewModel.Commands.xaml 的摘录(MultiCommandButtonOneParam 项目);WestActionCommand
的 _execute 委托值。
private void WestAction (object parameter )
{
Delay = ir_delay2000;
Stopwatch w = new Stopwatch( );
string name = MethodBase.GetCurrentMethod( ).Name;
Task.Factory.StartNew( ( ) => { w.Start( ); Thread.Sleep( (int )parameter ); } ).
ContinueWith( t=>
{
w.Stop( );
WestCommandManifest = string.Format( name + sr_wasted,
( int )w.ElapsedMilliseconds );
};
}
现在让我们运行 MultiCommandButtonOneParam 项目。
哇!延迟不起作用——命令只花一点时间处理自己的事情。看来 MultiCommand 获取其参数时,其初始值为 0(来自初始 Delay
),并将其用于所有部分 Command
。
这是正确的。Button
在 MultiCommand 执行之前将其参数绑定到 Delay
。那时它有一个初始值 0。之后,MultiCommandConverter
开始工作,它使用在其工作开始之前绑定的参数值。即使有属性更改通知魔法,新的更改也不会有任何效果。
为了实现不同部分命令的不同延迟,应该使用多个参数。
MutiCommandButtonMultiParam 项目的描述和执行
多参数命令绑定是一个众所周知的问题。可以在 此处 找到参考。这是 View 中——MainWindow.xaml 文件中的定义。
来自MainWindow.xaml 文件(MultiCommandButtonMultiParam 项目)的摘录
<!--
Multiparameter construction that consists of a set of bound parameters.
MultiValueConverter builds an IEnumerable aggregate from these parameters
and passes it to commands' MultiValueConverter
-->
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource multiParameterConverter}"/>
<Binding Path="NorthDelay"/>
<Binding Path="WestDelay"/>
<Binding Path="SouthDelay"/>
<Binding Path="EastDelay"/>
</MultiBinding>
</Button.CommandParameter>
绑定到参数的变量是只读属性,应在 MultiCommand 绑定之前绑定。
与往常一样,IMultiValueConverter
的实现是此 MultiBinding
的核心。
来自 MultiParameterConverter.cs 文件(MultiCommandButtonMultiParam 项目)的摘录——转换器的 Convert
方法
public object Convert( object[ ] value, Type targetType,
object parameter, CulturInfo cultue )
{
return new List<object>( value );
}
转换器仅返回参数的 List<object>
。此返回值将传递给 MultiCommandConverter
以便在 MultiCommand 的 _execute 中使用。我选择了一种顺序方式:下一个部分命令使用下一个部分参数。当然,这里可以实现更复杂的方式来供部分命令使用部分参数。
来自 MultiCommandConverter.cs 文件(MultiCommandMultiParam 项目)的摘录——GetCompoundExecute
和 GetCompoundCanExecute
方法
private Action<object> GetCompoundExecute( )
{
return ( parameter ) =>
{
var items = ( ( List<object> )_value ).Zip( ( List<object> )parameter, ( v, p ) =>
new { First = v, Second = p } );
foreach ( var item in items )
if ( item.First != default( RelayCommand ) )
( ( RelayCommand )item.First ).Execute( item.Second );
};
}
private Predicate<object> GetCompoundCanExecute( )
{
return ( parameter ) =>
{
bool res = true;
var items = ( ( List<object> )_value ).Zip( ( List<object> )parameter, ( v, p ) =>
new { First = v, Second = p } );
foreach ( var item in items )
if ( item.First != default( RelayCommand ) )
res &= ( ( RelayCommand )item.First ).CanExecute( item.Second );
return res;
}
}
现在最后剩下的就是显示——部分命令的 _execute 委托订阅的方法。这是一个——NortAction 方法。
来自 ViewModel.Commands.cs(MultiCommandMultiParam 项目)的摘录
private void NorthAction( object parameter )
{
StopWatch w = new StopWatch( );
string name = MethodBase.GetCurrentMethod( ).Name;
Task.Factory.StartNew( ( ) => { w.Start( ); Thread.Sleep( ( int )parameter ); } ).
ContinueWith( t =>
{
w.Stop( );
NortCommandManifest = string.Format( name+ sr_wasted,
( int )w.ElapsedMilliseconds );
}
);
}
让我们运行 MultiCommandMultiParam 项目。
现在看起来不错。
结论和可能的缺点
我从未检查过 MultiCommand 的时间损耗。MultiCommand 可能无法在时间关键型应用程序中使用。
部分命令并发执行实现的有趣方面超出了本文的范围。