C# WPF – 利用“Attached Property” 把 RoutedEvent 接上 ICommand


本文說明怎樣把 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 內填多個組合。

我在這群里,歡迎加入交流:
開發板玩家群 578649319開發板玩家群 578649319
硬件創客 (10105555)硬件創客 (10105555)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM