MVP模式中Command的终极解决方案。WPF/Silverlight中的必杀技——AttachedBehavior。


(一)前言

      最简单的Command,是由Prism为Silverlight提供的,执行ButtonBase的Click事件。

      我在另一篇文章《MVP的前生来世》中已经使用过这个Command了。温习一下,基于MVVM实现,分为两个步骤:

      1) 在ViewModel中声明这个Command,并定义该Command所对应的方法:

        public DelegateCommand<RoutedEventArgs> SaveCommand = new DelegateCommand<RoutedEventArgs>(OnSave);

        
void OnSave(RoutedEventArgs e)
        {
            
//do something
        }

      2) 在XAML中的Button中,使用Command语法,取代原先的Click事件(这里是Silverlight):

<Button Height="20" cmd:Click.Command="{Binding SaveCommand}" Content="Save"/>

      更多Prism中关于Command的详细内容,请参见我的另一篇文章《Prism深入研究之Command》。


(二)WPF提供的ICommand接口

      WPF中有一个ButtonBase基类,这个基类中含有一个Command属性,它是ICommand类型的,还包括一个CommandParameter属性和一个Click事件:

Command之必杀技——AttachedBehavior_控件        public abstract class ButtonBase : ContentControl, ICommandSource
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
public ICommand Command Command之必杀技——AttachedBehavior_click事件_04getset; }
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
public object CommandParameter Command之必杀技——AttachedBehavior_click事件_04getset; }
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
public event RoutedEventHandler Click;
Command之必杀技——AttachedBehavior_控件_14        }

Command之必杀技——AttachedBehavior_控件
Command之必杀技——AttachedBehavior_控件        
public interface ICommand
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            
event EventHandler CanExecuteChanged;
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
bool CanExecute(object parameter);
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
void Execute(object parameter);
Command之必杀技——AttachedBehavior_控件_14        }

      触发Click事件会直接执行Command属性中定义的Execute方法,所以像Button(如上所示)、RadioButton、ToggleButton、CheckBox、RepeatButton和GridViewColumnHeader,这些直接或间接从这个基类派生的控件,都可以使用Command属性,而不使用事件机制。

      在此,要订正一点,并不是说点击Button就不触发Click事件了,Click事件还是要触发,只是我们不再手动为Click事件添加方法了,而是把相应的逻辑添加到Command的Execute方法中。

原先的事件模型: 

<Button x:Name="button1" Click="button1_Click" />


        this.button1.Click += new RoutedEventHandler(this.button1_Click);

        
private void button1_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(
"After click");
        }

      现在的Command模型: 

<Button Command="{Binding}" />


            this.button.DataContext = ClickCommand;


        public DelegateCommand<object> ClickCommand { getprivate set; }

        ClickCommand 
= new DelegateCommand<object>(OnClick, arg => true);

        
void OnClick(object e)
        {
            MessageBox.Show(
"After click");
        }

      信手写了个Demo,以比较二上述两种编程方式的不同,代码下载:WpfApplication10.zip

(二)Silverlight下ButtonBase的Click实现

      在Silverlight中,虽然也有ICommand接口,但是并未在ButtonBase基类中提供ICommand类型的Command属性,所以不能使用上述机制来“化Event为Command”。

      直到有一天,一位微软MVP提出了AttachedBehavior的思想,才彻底解决了这个问题。

      AttachedBehavior定义:把由控件触发的事件和Presenter(或ViewModel)中的代码相关联。

      AttachedBehavior由两部分组成:一个attached 属性和一个behavior对象。attached 属性建立目标控件和behavior对象之间的关系;behavior对象则监视着目标控件,当目标控件的状态改变或事件触发时,就执行一些操作,我们可以在这里写一些自己的逻辑。

      微软P & P Team根据这一思想,在Prism中成功地将ButtonBase的Click事件转换为了Command。

      实现过程如下:

      1)创建CommandBehaviorBase<T>泛型类,可以将其看作Behavior基类: 

    public class CommandBehaviorBase<T> 
        
where T: Control

      这里T继承自Control基类,我们后面会看到这样设置的灵活性。

      类图如下所示:


      我们看到,CommandBehaviorBase<T>中有3个属性,看着眼熟,这不由使我们想起了WPF中ButtonBase的定义: 

Command之必杀技——AttachedBehavior_控件    public abstract class ButtonBase
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
public ICommand Command Command之必杀技——AttachedBehavior_click事件_04getset; }
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
public object CommandParameter Command之必杀技——AttachedBehavior_click事件_04getset; }
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
public IInputElement CommandTarget Command之必杀技——AttachedBehavior_click事件_04getset; }
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
//省略一些成员
Command之必杀技——AttachedBehavior_控件_14
    }

      就是说,我们把这3个属性从ButtonBase提升到了Control级别,从而可以让这个CommandBehaviorBase<T>泛型类适用于绝大多数控件(这里没有说全部哦,有一些特例我会在下文介绍)。

      TargetObject会在构造函数中初始化,这个属性就是目标控件了。

      CommandBehaviorBase<T>暴露了一个ExecuteCommand方法,可以在它的派生类中调用甚至重写该方法,以执行相应的Command: 

Command之必杀技——AttachedBehavior_控件        protected virtual void ExecuteCommand()
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            
if (this.Command != null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                
this.Command.Execute(this.CommandParameter);
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_控件_14        }

      CommandBehaviorBase<T>的UpdateEnabledState方法是比较有趣的,在设置Command和CommandParameter属性的时候,都会调用该方法,从而根据Command的CanExecute方法返回true/false,决定目标控件的IsEnabled属性。

      2)定义ButtonBaseClickCommandBehavior类,这时一个具体的Behavior,所以它派生自CommandBehaviorBase<ButtonBase>。类的定义及其关系图如下所示:


Command之必杀技——AttachedBehavior_控件    public class ButtonBaseClickCommandBehavior : CommandBehaviorBase<ButtonBase>
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
public ButtonBaseClickCommandBehavior(ButtonBase clickableObject) : base(clickableObject)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            clickableObject.Click 
+= OnClick;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08      
Command之必杀技——AttachedBehavior_ico_08        
private void OnClick(object sender, System.Windows.RoutedEventArgs e)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ExecuteCommand();
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      这个类很简单,在它的构造函数中传递ButtonBase目标控件,并把OnClick方法绑到这个目标控件的Click事件上。

      至此,我们创建了一个Behavior,它是为ButtonBase的Click事件量身打造的。

      3)最后我们来创建attached属性,并建立目标控件和behavior对象之间的关系。

      为此,需要为ButtonBase的Click事件单独创建一个Click类。这个Click类是用于在XAML绑定Command的: 

public static class Click

      我们要在类中注册一个依赖属性(Dependency Property,简称DP)ClickCommandBehavior,用来将之前创建的ButtonBaseClickCommandBehavior永久性保存在类中,而GetOrCreateBehavior方法则用来得到这个behavior:

Command之必杀技——AttachedBehavior_控件    public static class Click
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
private static readonly DependencyProperty ClickCommandBehaviorProperty = DependencyProperty.RegisterAttached(
Command之必杀技——AttachedBehavior_ico_08            
"ClickCommandBehavior",
Command之必杀技——AttachedBehavior_ico_08            
typeof(ButtonBaseClickCommandBehavior),
Command之必杀技——AttachedBehavior_ico_08            
typeof(Click),
Command之必杀技——AttachedBehavior_ico_08            
null);
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
private static ButtonBaseClickCommandBehavior GetOrCreateBehavior(ButtonBase buttonBase)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ButtonBaseClickCommandBehavior behavior 
= buttonBase.GetValue(ClickCommandBehaviorProperty) as ButtonBaseClickCommandBehavior;
Command之必杀技——AttachedBehavior_ico_08            
if (behavior == null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                behavior 
= new ButtonBaseClickCommandBehavior(buttonBase);
Command之必杀技——AttachedBehavior_ico_08                buttonBase.SetValue(ClickCommandBehaviorProperty, behavior);
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
return behavior;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      为了能写出下面这样的绑定语法,Click必须是静态的(static),其中cmd是对Click所在namespace的引用: 

<Button cmd:Click.Command="{Binding SaveCommand}" cmd:Click.CommandParameter="BaoBao" />

      我们要在Click类中添加两个属性Command和CommandParameter,而且为了实现持久性绑定,要把它们设计成DP: 

Command之必杀技——AttachedBehavior_控件        public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
Command之必杀技——AttachedBehavior_控件            
"Command",
Command之必杀技——AttachedBehavior_控件            
typeof(ICommand),
Command之必杀技——AttachedBehavior_控件            
typeof(Click),
Command之必杀技——AttachedBehavior_控件            
new PropertyMetadata(OnSetCommandCallback));
Command之必杀技——AttachedBehavior_控件
Command之必杀技——AttachedBehavior_控件        
public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached(
Command之必杀技——AttachedBehavior_控件            
"CommandParameter",
Command之必杀技——AttachedBehavior_控件            
typeof(object),
Command之必杀技——AttachedBehavior_控件            
typeof(Click),
Command之必杀技——AttachedBehavior_控件            
new PropertyMetadata(OnSetCommandParameterCallback));

      我们分别为这两个DP设计了回调方法: 

Command之必杀技——AttachedBehavior_控件        private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ButtonBase buttonBase 
= dependencyObject as ButtonBase;
Command之必杀技——AttachedBehavior_ico_08            
if (buttonBase != null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                ButtonBaseClickCommandBehavior behavior 
= GetOrCreateBehavior(buttonBase);
Command之必杀技——AttachedBehavior_ico_08                behavior.Command 
= e.NewValue as ICommand;
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_控件_14        }

Command之必杀技——AttachedBehavior_控件
Command之必杀技——AttachedBehavior_控件        
private static void OnSetCommandParameterCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ButtonBase buttonBase 
= dependencyObject as ButtonBase;
Command之必杀技——AttachedBehavior_ico_08            
if (buttonBase != null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                ButtonBaseClickCommandBehavior behavior 
= GetOrCreateBehavior(buttonBase);
Command之必杀技——AttachedBehavior_ico_08                behavior.CommandParameter 
= e.NewValue;
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_控件_14        }

      它们分别调用GetOrCreateBehavior方法,以获取永久性存储在类中的behavior实例,并将在XAML中设置的Command和CommandParameter保存在behavior实例中。

      至此,一个AttachedBehavior设计完毕。

      Prism提供的实现位于Composite.Presentation.Desktop这个项目中,如图所示:


(三)对Click的重构

      Prism为我们提供的ButtonBase的Click实现,是一个很不错的参考,为我们提供了AttachedBehavior的编程模型。

      但是,根据我的经验,CommandParameter一般是不会使用的,因为我们在实际编程中,是有机会在Presenter(或ViewModel)中把Command所需要的参数传递到要执行的OnExecute方法的。所以,我尝试着简化Click的编程模型,就是把和CommandParameter、TargetObject有关的逻辑全都删除:

      CommandBehaviorBase类修改如下: 

Command之必杀技——AttachedBehavior_控件    public class CommandBehaviorBase<T> 
Command之必杀技——AttachedBehavior_控件        
where T: Control
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
private ICommand command;
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
public ICommand Command
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
get Command之必杀技——AttachedBehavior_click事件_04return command; }
Command之必杀技——AttachedBehavior_ico_08            
set
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                
this.command = value;
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
protected virtual void ExecuteCommand()
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            
if (this.Command != null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                
this.Command.Execute(null);
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      Click类修改如下: 

Command之必杀技——AttachedBehavior_控件    public static class Click
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
private static readonly DependencyProperty ClickCommandBehaviorProperty = DependencyProperty.RegisterAttached(
Command之必杀技——AttachedBehavior_ico_08            
"ClickCommandBehavior",
Command之必杀技——AttachedBehavior_ico_08            
typeof(ButtonBaseClickCommandBehavior),
Command之必杀技——AttachedBehavior_ico_08            
typeof(Click),
Command之必杀技——AttachedBehavior_ico_08            
null);
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
Command之必杀技——AttachedBehavior_ico_08            
"Command",
Command之必杀技——AttachedBehavior_ico_08            
typeof(ICommand),
Command之必杀技——AttachedBehavior_ico_08            
typeof(Click),
Command之必杀技——AttachedBehavior_ico_08            
new PropertyMetadata(OnSetCommandCallback));
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
public static void SetCommand(ButtonBase buttonBase, ICommand command)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            buttonBase.SetValue(CommandProperty, command);
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
public static ICommand GetCommand(ButtonBase buttonBase)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            
return buttonBase.GetValue(CommandProperty) as ICommand;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ButtonBase buttonBase 
= dependencyObject as ButtonBase;
Command之必杀技——AttachedBehavior_ico_08            
if (buttonBase != null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                ButtonBaseClickCommandBehavior behavior 
= GetOrCreateBehavior(buttonBase);
Command之必杀技——AttachedBehavior_ico_08                behavior.Command 
= e.NewValue as ICommand;
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
private static ButtonBaseClickCommandBehavior GetOrCreateBehavior(ButtonBase buttonBase)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ButtonBaseClickCommandBehavior behavior 
= buttonBase.GetValue(ClickCommandBehaviorProperty) as ButtonBaseClickCommandBehavior;
Command之必杀技——AttachedBehavior_ico_08            
if (behavior == null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                behavior 
= new ButtonBaseClickCommandBehavior(buttonBase);
Command之必杀技——AttachedBehavior_ico_08                buttonBase.SetValue(ClickCommandBehaviorProperty, behavior);
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
return behavior;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      ButtonBaseClickCommandBehavior类修改如下:

Command之必杀技——AttachedBehavior_控件    public class ButtonBaseClickCommandBehavior : CommandBehaviorBase<ButtonBase>
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
public ButtonBaseClickCommandBehavior(ButtonBase clickableObject)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            clickableObject.Click 
+= OnClick;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08      
Command之必杀技——AttachedBehavior_ico_08        
private void OnClick(object sender, System.Windows.RoutedEventArgs e)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ExecuteCommand();
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      精简之后的逻辑,是不是很清晰了呢?所以,我们在编写自己的Command时,功能够用就好,别搞得太复杂。

      我在此基础上写了一个示例,运行良好。示例下载:SLPrismApplicationForClickCommand.zip

(四)定义自己的Command

      说了这么多,无非是为编写我们自己的Command做准备,不光是Silverlight要用到AttachedBehavior,就连WPF也照用不误,因为WPF只在ButtonBase中实现了Command,而并没有在其他控件中建立类似的机制。

      授人以渔,不如授人以渔。看到有朋友在搜集各种AttachedBehavior以备不时之需,其实没有必要,完全可以随心所欲进行定制。下面将介绍5种自定义AttachedBehavior,都是我在项目中所遇到的,基本涵盖了所有类型的控件事件。

(五)ButtonBase的MouseOver事件

      其实很简单,只要把上面Click那个AttachedBehavior中相应的Click事件修改为MouseOver事件就可以了,然后当你把鼠标移动到Button,就会执行相应的命令。

      第一次重构后的代码下载,为了代码可读性更强,我还把带有Click的名字都改为了MouseOver:SLAttachedBehavior_1.zip

      但是,仔细观察新的代码,我们发现,它只适用于ButtonBase。而MouseOver事件是在UIELement这个类中定义的,类的关系如图所示:

Command之必杀技——AttachedBehavior_click事件_249

      所以,我们可以把MouseOver的AttachedBehavior的范围做的更广泛一些——适用于所有UIELement。

      修改CommandBehaviorBase<T>,让T的约束范围限制为DependencyObject: 

    public class CommandBehaviorBase<T>
        
where T : DependencyObject

      修改UIElementMouseMoveCommandBehavior,使其派生于CommandBehaviorBase<UIElement>,并修改其构造函数,即将ButtonBase修改为UIElement:

Command之必杀技——AttachedBehavior_控件    public class UIElementMouseMoveCommandBehavior : CommandBehaviorBase<UIElement>
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
public UIElementMouseMoveCommandBehavior(UIElement obj)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            obj.MouseMove 
+= OnMouseMove;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
private void OnMouseMove(object sender, System.Windows.RoutedEventArgs e)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ExecuteCommand();
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      修改MouseMove类,将ButtonBase修改为UIElement。

      示例代码下载如下:SLAttachedBehavior_2.zip

      一切工作良好。至此,我们得出一个结论:对一个事件,先找到它是由哪个类中提供的,然后将CommandBehaviorBase<T>中的T相应替换为这个类,其它地方照猫画虎即可。

(六)TextBox的TextChanged事件

      有了前面的指导思想,这个例子实现起来就简单了。

TextBox的TextChanged事件是定义在TextBox中生的,是TextBox所独有的,没有通用性,所以要将CommandBehaviorBase<T>中的T替换为TextBox: 

Command之必杀技——AttachedBehavior_控件    public class TextBoxTextChangedCommandBehavior : CommandBehaviorBase<TextBox>
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
public TextBoxTextChangedCommandBehavior(TextBox obj)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            obj.TextChanged 
+= OnTextChanged;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
private void OnTextChanged(object sender, TextChangedEventArgs e)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            ExecuteCommand();
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      TextBox是从Control派生的,所以将CommandBehaviorBase<T>的定义修改为: 

    public class CommandBehaviorBase<T>
        
where T : Control

      接下来定义静态类TextChanged,只要把上面那个例子中MouseMove类的MouseMove全都替换为TextChanged,把UIElement全都替换为TextBox即可。

      示例代码下载:SLAttachedBehavior_3.zip

      这个AttachedBehavior在《Command探究》一文中 Dirty Save示例中会使用到。

(七)TextBlock的MouseLeftButtonUp事件

      TextBlock控件没有Click事件,但是可以用MouseLeftButtonUp事件来模拟。

      如果是单独一个TextBlock的MouseLeftButtonUp事件,按照上面的编码模式去套,是很容易的。

      沿着TextBlock的继承关系一直向上找,直到在UIElement基类中发现MouseLeftButtonUp事件。

      于是创建适用于所有UIElement的MouseLeftButtonUpCommandBehavior, 

    public class CommandBehaviorBase<T> { }

    
public class MouseLeftButtonUpCommandBehavior : CommandBehaviorBase<UIElement>
    {
        
public MouseLeftButtonUpCommandBehavior(UIElement clickableObject)
        {
            clickableObject.MouseLeftButtonUp 
+= OnMouseLeftButtonUp;
        }

        
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            ExecuteCommand();
        }
    }

      然后仿照上面例子中的Click类,创建一个MouseLeftButtonUp类,实现很简单,把Click类中的Click全都替换为MouseLeftButtonUp,把ButtonBase全都替换为UIElement即可: 

Command之必杀技——AttachedBehavior_控件    public static class MouseLeftButtonUp
Command之必杀技——AttachedBehavior_控件_02Command之必杀技——AttachedBehavior_控件_03    
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08        
private static readonly DependencyProperty MouseLeftButtonUpCommandBehaviorProperty = DependencyProperty.RegisterAttached(
Command之必杀技——AttachedBehavior_ico_08        
"MouseLeftButtonUpCommandBehavior",
Command之必杀技——AttachedBehavior_ico_08        
typeof(MouseLeftButtonUpCommandBehavior),
Command之必杀技——AttachedBehavior_ico_08        
typeof(MouseLeftButtonUp),
Command之必杀技——AttachedBehavior_ico_08        
null);
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
Command之必杀技——AttachedBehavior_ico_08        
"Command",
Command之必杀技——AttachedBehavior_ico_08        
typeof(ICommand),
Command之必杀技——AttachedBehavior_ico_08        
typeof(MouseLeftButtonUp),
Command之必杀技——AttachedBehavior_ico_08        
new PropertyMetadata(OnSetCommandCallback));
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
public static void SetCommand(UIElement element, ICommand command)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            element.SetValue(CommandProperty, command);
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
public static ICommand GetCommand(UIElement element)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            
return element.GetValue(CommandProperty) as ICommand;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
private static void OnSetCommandCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            UIElement element 
= dependencyObject as UIElement;
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
if (element != null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                MouseLeftButtonUpCommandBehavior behavior 
= GetOrCreateBehavior(element);
Command之必杀技——AttachedBehavior_ico_08                behavior.Command 
= e.NewValue as ICommand;
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08        
private static MouseLeftButtonUpCommandBehavior GetOrCreateBehavior(UIElement element)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06        
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08            MouseLeftButtonUpCommandBehavior behavior 
= element.GetValue(MouseLeftButtonUpCommandBehaviorProperty) as MouseLeftButtonUpCommandBehavior;
Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
if (behavior == null)
Command之必杀技——AttachedBehavior_控件_05Command之必杀技——AttachedBehavior_click事件_06            
Command之必杀技——AttachedBehavior_click事件_04{
Command之必杀技——AttachedBehavior_ico_08                behavior 
= new MouseLeftButtonUpCommandBehavior(element);
Command之必杀技——AttachedBehavior_ico_08                element.SetValue(MouseLeftButtonUpCommandBehaviorProperty, behavior);
Command之必杀技——AttachedBehavior_click事件_53            }

Command之必杀技——AttachedBehavior_ico_08
Command之必杀技——AttachedBehavior_ico_08            
return behavior;
Command之必杀技——AttachedBehavior_click事件_53        }

Command之必杀技——AttachedBehavior_控件_14    }

      示例代码下载:WpfApplication3.zip

      这个例子只是让大家熟悉如何自定义AttachedBehavior,相信大家已经掌握了,下面我们研究一些高级货。

(八)GridView中TextBlock的MouseLeftButtonUp事件

      单个TextBlock的MouseLeftButtonUp事件是很容易实现的。但是,在GridView的模板列中的TextBlock的MouseLeftButtonUp事件,就比较难以实现了。

      如果我们还像过去一样编码,在模板列的TextBlock中绑定这个Command: 

        <data:DataGridTemplateColumn Header="UserName" >
            
<data:DataGridTemplateColumn.CellTemplate>
                
<DataTemplate>
                    
<TextBlock Text="{Binding Path=UserName}" cmd:MouseLeftButtonUp.Command="{Binding MouseLeftButtonUpCommand}" />
                
</DataTemplate>
            
</data:DataGridTemplateColumn.CellTemplate>
        
</data:DataGridTemplateColumn>

      但是,点击DataGrid中每一行的UserName列,却发现没有任何反应,就是说,Command失效了。

      错误代码下载:WpfApplication7_error.zip

      怎么办呢?

      有一个馊主意,既然TextBlock是在DataGrid中的,而且DataGrid也具有MouseLeftButtonUp事件,所以不妨在DataGrid添加这个Command,如下所示: 

        <data:DataGrid AutoGenerateColumns="False" Margin="12,12,66,0" Name="dataGrid1"
            ItemsSource
="{Binding StudentList, Mode=TwoWay}" Height="140" VerticalAlignment="Top"
            cmd:MouseLeftButtonUp.Command
="{Binding MouseLeftButtonUpCommand}"
            
>

      貌似运行良好哦。

      示例代码下载:WpfApplication7_error2.zip

      但是,大家想过没有,这是治标不治本的办法,如果DataGrid中有多个TextBlock,那该如何分辨是哪个TextBlock触发了MouseLeftButtonUp事件?

      此外,在DataGrid中,不光是TextBlock的MouseLeftButtonUp事件失效,我们将其换成Button的Click事件,会发现也不行。 

        <data:DataGridTemplateColumn Header="Score">
            
<data:DataGridTemplateColumn.CellTemplate>
                
<DataTemplate>
                    
<Button Content="{Binding Path=Score}" Command="{Binding Path=MouseLeftButtonUpCommand" />
                
</DataTemplate>
            
</data:DataGridTemplateColumn.CellTemplate>
        
</data:DataGridTemplateColumn>

      这次就不能把Button的Click事件放到DataGrid上了,因为DataGrid可是没有这个Click的哦?

      为什么会这样呢?

      在于我们的绑定语法写的不对。

      1)先来看一下WPF。像DataView、ListView这样的数据集合控件,对于其中的TextBlock、Button这样的控件,它们的Command绑定语法应该写成这样: 

        <data:DataGridTemplateColumn Header="UserName" >
            
<data:DataGridTemplateColumn.CellTemplate>
                
<DataTemplate>
                    
<TextBlock Text="{Binding Path=UserName}" local:MouseLeftButtonUp.Command="{Binding Path=DataContext.MouseLeftButtonUpCommand, RelativeSource={RelativeSource AncestorType={x:Type data:DataGrid}}}" />
                
</DataTemplate>
            
</data:DataGridTemplateColumn.CellTemplate>
        
</data:DataGridTemplateColumn>

      就是说,在WPF中,使用到了RelativeResource,将DataGrid中的TextBlock控件的数据源指到外面去(跳出三界外,不在五行中),此时路径要指向DataGrid的DataContext的MouseLeftButtonUpCommand。

      对于Button,也是如法炮制: 

        <data:DataGridTemplateColumn Header="Score">
            
<data:DataGridTemplateColumn.CellTemplate>
                
<DataTemplate>
                    
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                        
<Button Content="Buy" Command="{Binding Path=DataContext.BuyCommand, RelativeSource={RelativeSource AncestorType={x:Type data:DataGrid}}}" Width="30" Height="20" Cursor="Hand" />
                        
<Button Content="Sell" Command="{Binding Path=DataContext.SellCommand, RelativeSource={RelativeSource AncestorType={x:Type data:DataGrid}}}" Width="30" Height="20" Cursor="Hand" />
                    
</StackPanel>
                
</DataTemplate>
            
</data:DataGridTemplateColumn.CellTemplate>
        
</data:DataGridTemplateColumn>

      示例代码下载:WpfApplication8.zip

      2)再来看一下Silverlight。

      由于Silverlight不支持RelativeResource语法,所以我们要另想别的办法。

      还记得XAML中有一种Resource语法么,我们可以把Command存在Resource中,相当于建立了一个全局变量,让DataGrid中的TextBlock和Button直接指向这个Resource(有点儿直辖市的味道哦)。

      能够存储Command的Resource,要额外花些心思来构思,为此要创建一个名为ObservableCommand的类,以后存储Command就靠它了:

public class ObservableCommand : ObservableObject<ICommand> { }

      在Xaml中,我们建立3个资源:

        <UserControl.Resources>
            
<local:ObservableCommand x:Key="BuyCommand" />
            
<local:ObservableCommand x:Key="SellCommand" />
            
<local:ObservableCommand x:Key="MouseLeftButtonUpCommand" />
        
</UserControl.Resources>

      并在后台的代码文件中,在ViewModel的set属性中,手动把它们和ViewModel的Command绑定在一起: 

Command之必杀技——AttachedBehavior_控件    public partial class ScoreListView : UserControl
Command之必杀技——AttachedBehavior_控件    {
Command之必杀技——AttachedBehavior_控件        public ScoreListView()
Command之必杀技——AttachedBehavior_控件        {
Command之必杀技——AttachedBehavior_控件            InitializeComponent();
Command之必杀技——AttachedBehavior_控件
Command之必杀技——AttachedBehavior_控件            this.ViewModel = new ScoreListViewModel();
Command之必杀技——AttachedBehavior_控件            this.ViewModel.View = this;
Command之必杀技——AttachedBehavior_控件        }
Command之必杀技——AttachedBehavior_控件
Command之必杀技——AttachedBehavior_控件        public ScoreListViewModel ViewModel
Command之必杀技——AttachedBehavior_控件        {
Command之必杀技——AttachedBehavior_控件            get
Command之必杀技——AttachedBehavior_控件            {
Command之必杀技——AttachedBehavior_控件                return this.DataContext as ScoreListViewModel;
Command之必杀技——AttachedBehavior_控件            }
Command之必杀技——AttachedBehavior_控件
Command之必杀技——AttachedBehavior_控件            set
Command之必杀技——AttachedBehavior_控件            {
Command之必杀技——AttachedBehavior_控件                this.DataContext = value;
Command之必杀技——AttachedBehavior_控件
Command之必杀技——AttachedBehavior_控件                ((ObservableCommand)this.Resources["BuyCommand"]).Value = value.BuyCommand;
Command之必杀技——AttachedBehavior_控件                ((ObservableCommand)this.Resources["SellCommand"]).Value = value.SellCommand;
Command之必杀技——AttachedBehavior_控件                ((ObservableCommand)this.Resources["MouseLeftButtonUpCommand"]).Value = value.MouseLeftButtonUpCommand;
Command之必杀技——AttachedBehavior_控件            }
Command之必杀技——AttachedBehavior_控件        }
Command之必杀技——AttachedBehavior_控件    }

      这样,我们就可以在DataGrid的TextBlock和Button中把它们绑定到资源上了: 

        <data:DataGrid AutoGenerateColumns="False" Margin="12" Name="dataGrid1" ItemsSource="{Binding StudentList}">
            
<data:DataGrid.Columns>
                
<data:DataGridTemplateColumn Header="UserName" >
                    
<data:DataGridTemplateColumn.CellTemplate>
                        
<DataTemplate>
                            
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                                
<TextBlock Text="{Binding Path=UserName}" local:MouseLeftButtonUp.Command="{Binding Path=Value, Source={StaticResource MouseLeftButtonUpCommand}}" />
                            
</StackPanel>
                        
</DataTemplate>
                    
</data:DataGridTemplateColumn.CellTemplate>
                
</data:DataGridTemplateColumn>

                
<data:DataGridTemplateColumn Header="Score">
                    
<data:DataGridTemplateColumn.CellTemplate>
                        
<DataTemplate>
                            
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                                
<Button Commands:Click.Command="{Binding Path=Value, Source={StaticResource BuyCommand}}" Width="30" Height="20" Cursor="Hand" Content="Buy" />
                                
<Button Commands:Click.Command="{Binding Path=Value, Source={StaticResource SellCommand}}" Width="30" Height="20" Cursor="Hand" Content="Sell" />
                            
</StackPanel>
                        
</DataTemplate>
                    
</data:DataGridTemplateColumn.CellTemplate>
                
</data:DataGridTemplateColumn>
            
</data:DataGrid.Columns>
        
</data:DataGrid>

      注意绑定语法:

<Button Commands:Click.Command="{Binding Path=Value, Source={StaticResource BuyCommand}}" />

      这里绑定的是BuyCommand资源的Value值。

      示例代码下载:SilverlightApplication12.zip

(九)Popup Window的弹出和关闭事件

      这个例子是由Prism的StockTrader RI提供的,堪称绝世经典之作。

      关于这个功能的介绍,请参见《Prism研究RI分析 之六 PopupView》。别写到RI分析的时候没得写了。

(十)结语

      AttachedBehavior是好东西啊!虽然有点神秘,但却不是那么费解。希望大家仔细阅读此文,多研究我提供的源码。我尽量把示例做的简单,而没有使用太多的Prism框架。