本文說明怎樣把 DoubleClick 連接至 ICommand。方法很多。推薦使用 Attach Property 方式,因為它能把任何 RoutedEvent 接上任何 ICommand。
之前寫過一篇博文關於 MVVM 中雙擊事件觸發 ICommand 的辦法,我說要么你自己寫 Attached Property,要么下載別人寫好的,比如支持 Collections 的 CommandBehaviors。我認為這兩個辦法是比較好的。有網友說我沒有解釋清楚,因為我覺得 Attached Property 有點離題,跟 MVVM 關系不太大。反正有得用就行了。
下面以 ListView 為例。
1. InputBindings
先不說 Attached Property,看看有什么辦法可以把雙擊綁定到 ICommand。最簡單的辦法是 InputBindings。
XAML:
<ListView.InputBindings><MouseBinding Gesture="LeftDoubleClick" Command=""/></ListView.InputBindings>
支持 KeyBinding (鍵盤),和 MouseBinding (鼠標)。能做到,如果只需要管鍵盤或鼠標,這是比較簡單。
2. 隱形 Button (不建議)
我見過第二個辦法,隱形 Button, (Visibility=”Collapsed”),ICommand 綁定進去,ListView MouseDoubleClick 在視圖建立句柄,由它再觸發 Button 的 Command.Execute(object)。
XAML:
<Button Name="button1" Visibility="Collapsed" Command=""/><ListView MouseDoubleClick="ListView_MouseDoubleClick"/>
Code:
privatevoid ListView_MouseDoubleClick(object sender, MouseButtonEventArgs e) { button1.Command.Execute(null); }
這比較傻,不建議。
3. Attached Property
MSDN 有介紹怎樣為控件添加新的屬性,這里不詳細說了。關鍵是靜態方法 Set,和靜態 DependencyProperty。(MSDN 說 GET SET 都要,但其實寫 XAML 時只用到 SET,后續啟動后,你需要拿回屬性值才需要 GET)。
先看一下,Attached Property 是怎樣寫的,熱熱身:
CODE:
publicstaticclass MyProperty { publicstaticreadonly DependencyProperty ParameterProperty = DependencyProperty.RegisterAttached( "Parameter", typeof(Object), typeof(MyProperty), new FrameworkPropertyMetadata(null) ); publicstatic Object GetParameter(UIElement obj) { return obj.GetValue(ParameterProperty); } publicstaticvoid SetParameter(UIElement obj, Object value) { obj.SetValue(ParameterProperty, value); } }
get、set 參數 UIElement 類型是為了確保所有控件能用它。這 Parameter 沒有配置CallBack,這個MyProperty不對值變化做什么動作,也不設置默認值,所以 RegisterAttached 時候 FrameworkPropertyMetadata是 null。
命名規范必須跟從,MSDN 有說明。當你希望在 XAML 這屬性叫做 Parameter 的時候(RegisterAttached 的第一個參數),它的get、set 方法必須命名為 GetParameter 和 SetParameter。編譯后 XAML 可用。
XAML:
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:y="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525"><Grid><ListView y:MyProperty.Parameter="ABC"/></Grid></Window>
新手記得加上正確的 XML namespace,xmlns:y="clr-namespace:WpfApplication1" 是因為我把MyProperty類放在這 WpfApplication1 項目的最外層。
知道了怎么寫 Attached Property 之后,入正題,加入 ICommand。為靈活性,做法是讓程序員配置要綁的 RoutedEvent ,和對應要觸發的 ICommand 同時作為 DependencyProperty,讓程序員自己配置哪個Event 接哪個 ICommand。(注:handler 那 Dictionary 的做法,和 Detach Attach 是參考某大神的)。為縮短代碼,只寫 ICommand 和 Event,沒寫 ICommand 的命令參數。
(以下代碼網上其實很多,也有很多版本,大同小異)
CODE:
using System.Collections.Generic; using System.Windows; using System.Windows.Input; namespace WpfApplication1 { publicstaticclass CommandBehavior { // UI,Handler Listprivatestatic Dictionary<UIElement, RoutedEventHandler> handlers =new Dictionary<UIElement, RoutedEventHandler>(); #region Command Propertypublicstaticreadonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached( "Command", typeof(ICommand), typeof(CommandBehavior), new FrameworkPropertyMetadata() { DefaultValue =null, PropertyChangedCallback =new PropertyChangedCallback(OnCommandPropertyChanged) } ); publicstatic ICommand GetCommand(UIElement obj) { return (ICommand)obj.GetValue(CommandProperty); } publicstaticvoid SetCommand(UIElement obj, ICommand value) { obj.SetValue(CommandProperty, value); } #endregion#region Event Propertypublicstaticreadonly DependencyProperty EventProperty = DependencyProperty.RegisterAttached( "Event", typeof(RoutedEvent), typeof(CommandBehavior), new FrameworkPropertyMetadata() { DefaultValue =null, PropertyChangedCallback =new PropertyChangedCallback(OnEventPropertyChanged) } ); publicstatic RoutedEvent GetEvent(DependencyObject obj) { return (RoutedEvent)obj.GetValue(EventProperty); } publicstaticvoid SetEvent(DependencyObject obj, RoutedEvent value) { obj.SetValue(EventProperty, value); } #endregion#region CallBacksprivatestaticvoid OnCommandPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { UIElement element = obj as UIElement; ICommand oldCommand = args.OldValue as ICommand; ICommand newCommand = args.NewValue as ICommand; RoutedEvent routedEvent = element.GetValue(EventProperty) as RoutedEvent; Detach(element, routedEvent, oldCommand); Attach(element, routedEvent, newCommand); } privatestaticvoid OnEventPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { UIElement element = obj as UIElement; RoutedEvent oldEvent = args.OldValue as RoutedEvent; RoutedEvent newEvent = args.NewValue as RoutedEvent; ICommand command = element.GetValue(CommandProperty) as ICommand; Detach(element, oldEvent, command); Attach(element, newEvent, command); } #endregionprivatestaticvoid Attach(UIElement element, RoutedEvent Event, ICommand command) { if (Event !=null&& element !=null&& command !=null) { RoutedEventHandler InvokeCommandHandler =new RoutedEventHandler(delegate { command.Execute(null); }); handlers.Add(element, InvokeCommandHandler); element.AddHandler(Event, InvokeCommandHandler); } } privatestaticvoid Detach(UIElement element, RoutedEvent Event, ICommand command) { if (Event !=null&& element !=null&& command !=null) { RoutedEventHandler handler = handlers[element]; if (handler !=null) { element.RemoveHandler(Event, handler); handlers.Remove(element); } } } } }
跟之前那個 Parameter 例子很像,只是同一個靜態類,做了兩個屬性,一個叫做 Event,一個叫做 Command。另外,多了一個 Dictionary,還有,這次 Event 和 Command 的變化,都注冊了 PropertyChangedCallback 的句柄。最下面的 Attach Detach 的 private 幫助方法,只是重構時從PropertyChangedCallBack 的句柄抽出來而已。
控件、事件、命令,三者是一起的組合,某 UIElement 的某 RoutedEvent 觸發到某 ICommand 的 Execute。但RoutedEvent 觸發的是 RoutedEventHandler 句柄,不是 ICommand。所以這個靜態類所做最重要的事,見 private static void Attach(),就是創建新的 RoutedEventHandler,讓它執行委托運行 command 的 Execute,然后把准備好 RoutedEventHandler 之后粘上 UIElement,即 AddHandler(RoutedEvent,RoutedEventHandler)。把這搭配,UIElement 和已做好ICommand委托的 RoutedEventHandler,放在 Dictionary,是為了 Detach 時候找回。
要做 Detach 是因為,DependencyProperty 的值是能變化的(上例中是 Event和Command這兩個,都能在運行時變),不一定是寫死在 XAML,比如 {Binding Path=XXX} 這情況。萬一 Command 變了,或者 RoutedEvent 變了,上述做好了的搭配就失效,是需要 RemoveHandler 然后重新組合。所以,PropertyChangedCallBack 所做的,都是先 Detach 舊值(args.OldValue),然后再 Attach 粘上新值(args.NewValue)。不管 Event 變還是 Command 變,都需要如此。
這靜態類的解釋到此為止。不復雜。用法如下:
XAML:
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:y="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525"><Grid><ListView y:CommandBehavior.Command="{Binding Path=TestCommand}" y:CommandBehavior.Event="ListView.MouseDoubleClick"></ListView></Grid></Window>
因為一開始設置了Command 和 Event 的默認值為 null (RegisterAttached 時候的 FrameworkPropertyMetadata 內,DefaultValue),所以 XAML 運行寫入值時,值變化觸發 CallBack,完成了我們需要的連接。
最后,改一下 CommandBehavior,讓它能接受參數,傳過去 ICommand。因為 ICommand 的命令參數類型是 object,所以寫的 CommandParameter 類型也是 object。
完整版本 CODE:
using System.Collections.Generic; using System.Windows; using System.Windows.Input; namespace WpfApplication1 { publicstaticclass CommandBehavior { // UI,Handler Listprivatestatic Dictionary<UIElement, RoutedEventHandler> handlers =new Dictionary<UIElement, RoutedEventHandler>(); #region Command Propertypublicstaticreadonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached( "Command", typeof(ICommand), typeof(CommandBehavior), new FrameworkPropertyMetadata() { DefaultValue =null, PropertyChangedCallback =new PropertyChangedCallback(OnCommandPropertyChanged) } ); publicstatic ICommand GetCommand(UIElement obj) { return (ICommand)obj.GetValue(CommandProperty); } publicstaticvoid SetCommand(UIElement obj, ICommand value) { obj.SetValue(CommandProperty, value); } #endregion#region Event Propertypublicstaticreadonly DependencyProperty EventProperty = DependencyProperty.RegisterAttached( "Event", typeof(RoutedEvent), typeof(CommandBehavior), new FrameworkPropertyMetadata() { DefaultValue =null, PropertyChangedCallback =new PropertyChangedCallback(OnEventPropertyChanged) } ); publicstatic RoutedEvent GetEvent(DependencyObject obj) { return (RoutedEvent)obj.GetValue(EventProperty); } publicstaticvoid SetEvent(DependencyObject obj, RoutedEvent value) { obj.SetValue(EventProperty, value); } #endregion#region CommandParameter Propertypublicstaticreadonly DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached( "CommandParameter", typeof(object), typeof(CommandBehavior), new FrameworkPropertyMetadata(null) ); publicstaticobject GetCommandParameter(UIElement obj) { return obj.GetValue(CommandParameterProperty); } publicstaticvoid SetCommandParameter(UIElement obj, object value) { obj.SetValue(CommandParameterProperty, value); } #endregion#region CallBacksprivatestaticvoid OnCommandPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { UIElement element = obj as UIElement; ICommand oldCommand = args.OldValue as ICommand; ICommand newCommand = args.NewValue as ICommand; RoutedEvent routedEvent = element.GetValue(EventProperty) as RoutedEvent; object commandParameter = element.GetValue(CommandParameterProperty); Detach(element, routedEvent, oldCommand); Attach(element, routedEvent, newCommand, commandParameter); } privatestaticvoid OnEventPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { UIElement element = obj as UIElement; RoutedEvent oldEvent = args.OldValue as RoutedEvent; RoutedEvent newEvent = args.NewValue as RoutedEvent; ICommand command = element.GetValue(CommandProperty) as ICommand; object commandParameter = element.GetValue(CommandParameterProperty); Detach(element, oldEvent, command); Attach(element, newEvent, command, commandParameter); } #endregionprivatestaticvoid Attach(UIElement element, RoutedEvent Event, ICommand command, object commandParameter) { if (Event !=null&& element !=null&& command !=null) { RoutedEventHandler InvokeCommandHandler =new RoutedEventHandler(delegate { command.Execute(commandParameter); }); handlers.Add(element, InvokeCommandHandler); element.AddHandler(Event, InvokeCommandHandler); } } privatestaticvoid Detach(UIElement element, RoutedEvent Event, ICommand command) { if (Event !=null&& element !=null&& command !=null) { RoutedEventHandler handler = handlers[element]; if (handler !=null) { element.RemoveHandler(Event, handler); handlers.Remove(element); } } } } }
完整版本的 CommandBehavior 在 XAML 用法:
XAML:
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:y="clr-namespace:WpfApplication1" Title="MainWindow" Height="350" Width="525"><Grid><ListView y:CommandBehavior.Command="{Binding Path=TestCommand}" y:CommandBehavior.Event="ListView.MouseDoubleClick" y:CommandBehavior.CommandParameter="TestParameter"/></Grid></Window>
Attach Property 方法介紹到此為止。點擊這里下載最終版本的代碼。
這類簡單,用來解釋工作原理比較合適。但我之前博文沒用這個類,因為以上代碼,有一個明顯缺陷。源於 Dictionary<UIElement, RoutedEventHandler> 這樣的簡單搭配,UIElement 作為 Key。而且 CommandBehavior 這靜態類,沒有集合暴露給 XAML。這意味着,一個控件,只能設置一次。比如,當一個控件你有兩個 RoutedEvent 希望綁定到兩個ICommand,這代碼不支持。
為了解決這問題,網上已經有很多人寫好了一個叫做 CommandBehaviorCollection 的類(懶到搜索都不想搜的,點擊這里),很多不同的版本,功能其實都一樣,讓你在 XAML 內一個控件能同時配置多個 Event 和 Command 的組合。這個類就是我在之前博文上用到的那個。我不打算解釋里面內容,其工作基本原理,與上述代碼一摸一樣,只是它暴露了集合讓你在 XAML 內填多個組合。