在WPF的推薦的MVVM模式下,控件的依賴屬性可以容易地使用綁定的方式,由ViewModel觀察和控制,但是一旦需要調用控件的方法,比如ViewModel希望一個窗體能夠關閉,隱藏,似乎情況就變得沒那么簡單了,可以說,WPF的綁定本身並未提供這種機制,往往需要開發者單獨地去做一些設計上的折衷,即犧牲一些前后台解耦的代碼結構原則,還是需要直接調用前台控件,導致ViewModel的可測試性下降。
本博客提供了一種能夠直接將前台控件的方法通過綁定的方式直接將方法的委托傳入到ViewModel的方法,而無需后台直接調用前台的控件,本方法需要使用Microsoft.Xaml.Behaviors庫,也就是更早的System.Windows.Interactivity拓展庫的官方開源增強版,知名度較高的Prism庫(WPF部分)也是依賴了它。
首先是針對前台控件的無參方法的方法,需要定義一個Behavior:
/// <summary> /// 調用方法行為,通過指定方法名稱,將無參的方法作為委托傳入到后台; /// </summary> public class InvokeMethodBehavior:Behavior<DependencyObject> { /// <summary> /// 調用方法委托; /// </summary> public Action InvokeMethodAction { get { return (Action)GetValue(InvokeMethodActionProperty); } set { SetValue(InvokeMethodActionProperty, value); } } public static readonly DependencyProperty InvokeMethodActionProperty = DependencyProperty.Register(nameof(InvokeMethodAction), typeof(Action), typeof(InvokeMethodBehavior), new FrameworkPropertyMetadata(default(Action)) { BindsTwoWayByDefault = true }); /// <summary> /// 方法名稱; /// </summary> public string MethodName { get { return (string)GetValue(MethodNameProperty); } set { SetValue(MethodNameProperty, value); } } public static readonly DependencyProperty MethodNameProperty = DependencyProperty.Register(nameof(MethodName), typeof(string), typeof(InvokeMethodBehavior), new PropertyMetadata(null)); protected override void OnAttached() { InvokeMethodAction = InvokeMethod; base.OnAttached(); } protected override void OnDetaching() { InvokeMethodAction = null; base.OnDetaching(); } private void InvokeMethod() { if (string.IsNullOrEmpty(MethodName)) { Trace.WriteLine($"The {nameof(MethodName)} can not be null or empty."); } if(AssociatedObject == null) { return; } try { AssociatedObject.GetType().GetMethod(MethodName).Invoke(AssociatedObject, null); } catch (Exception ex) { Trace.WriteLine($"Error occured while invoking method({MethodName}):{ex.Message}"); } } }
以上方法通過控件加載時庫自動執行OnAttached方法,將InvokeMethod的方法當作委托傳入到綁定源,InvokeMethod內部則是通過方法名稱(MethodName),反射到控件的方法最后執行,使用時的Xaml代碼如下,此處所對應的控件方法是窗體的關閉方法:
<Window x:Class="Hao.Octopus.Views.ExampleWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:prism="http://prismlibrary.com/" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" xmlns:utils="http://schemas.hao.wpfutils" xmlns:tb="http://www.hardcodet.net/taskbar" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:behaviors="http://schemas.hao.wpfutils" mc:Ignorable="d" WindowStartupLocation="CenterScreen"> <i:Interaction.Behaviors> <behaviors:InvokeMethodBehavior MethodName="Close" InvokeMethodAction="{Binding CloseAction,Mode=OneWayToSource}"/> </i:Interaction.Behaviors> <Button Command="{Binding CloseCommand}">關閉</Button> </Window>
請注意,此處使用的綁定的模式一定得是OneWayToSource,否則委托將無法傳入綁定源,后台的代碼定義如下:
public class ExampleWindowViewModel
{
public Action CloseAction { get; set; }
private DelegateCommand _closeCommand;
public DelegateCommand CloseCommand => _closeCommand ??
(_closeCommand = new DelegateCommand(Close));
private void Close()
{
CloseAction?.Invoke();
}
}
當窗體被加載時,CloseAction 屬性才能被正確賦值,一切就緒后,點擊關閉按鈕,ViewModel會通過CloseAction調用到窗體的關閉方法。
以上方法對應的情況是所需調用的控件方法是無參的,如果方法是有參數的,那么情況就變得多樣了,因為參數的類型和個數不確定。但是還是可以定義一個泛型的Behavior去統一這些情況的,代碼如下:
/// <summary> /// 調用對象的方法行為,使用此行為,將前台元素的方法通過委托傳入依賴屬性,此行為適合調用的方法具備參數時的情況,因為使用了泛型,無法在XAML中直接使用,請在后台代碼中使用; /// </summary> /// <typeparam name="TObject"></typeparam> /// <typeparam name="TDelegate"></typeparam> public class InvokeMethodBehavior<TObject,TDelegate>: Behavior<TObject> where TDelegate : Delegate where TObject : DependencyObject { /// <summary> /// 方法委托; /// </summary> public TDelegate MethodDelegate { get { return (TDelegate)GetValue(MethodDelegateProperty); } set { SetValue(MethodDelegateProperty, value); } } public static readonly DependencyProperty MethodDelegateProperty = DependencyProperty.Register(nameof(MethodDelegate), typeof(TDelegate), typeof(InvokeMethodBehavior<TObject, TDelegate>), new FrameworkPropertyMetadata(default(TDelegate)) { BindsTwoWayByDefault = true }); private Func<TObject, TDelegate> _getMethodDelegateFunc; /// <summary> /// 獲取或設定獲得 方法委托 的委托; /// </summary> public Func<TObject, TDelegate> GetMethodDelegateFunc { get => _getMethodDelegateFunc; set { _getMethodDelegateFunc = value; RefreshMethodDelegate(); } } protected override void OnAttached() { RefreshMethodDelegate(); base.OnAttached(); } protected override void OnDetaching() { RefreshMethodDelegate(); base.OnDetaching(); } /// <summary> /// 刷新<see cref="MethodDelegate"/>屬性; /// </summary> private void RefreshMethodDelegate() { if(AssociatedObject == null || GetMethodDelegateFunc == null) { MethodDelegate = null; } try { MethodDelegate = GetMethodDelegateFunc(AssociatedObject); } catch (Exception ex) { Trace.WriteLine($"Error occured while refreshing method delegate:{ex.Message}"); } } }
由於WPF的XAML中無法直接識別到泛型,所以需要在后台代碼使用以上的Behavior,這里就演示一下將一個ListBox控件的public void ScrollIntoView(object item)綁定到綁定源。
XAML代碼平平無奇:
<ListBox Grid.Row="4" ItemsSource="{Binding ListBoxEntries}" x:Name="loggerListBox" > <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Text}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
在XAML.cs中需要使用上述定義的泛型Behavior:
public ExampleWindow() { InitializeComponent(); InitializeLoggerListBox(); } private void InitializeLoggerListBox() { //綁定滾動到指定項委托; var loggerListBoxBehaviors = Interaction.GetBehaviors(loggerListBox); var invokeScrollBehavior = new InvokeMethodBehavior<ListBox, Action<object>> { GetMethodDelegateFunc = listBox => listBox.ScrollIntoView }; BindingOperations.SetBinding(invokeScrollBehavior,InvokeMethodBehavior<ListBox,Action<object>>.MethodDelegateProperty,new Binding { Path = new PropertyPath("ScrollIntoLoggerBoxItemAction"),Mode = BindingMode.OneWayToSource }); loggerListBoxBehaviors.Add(invokeScrollBehavior); }
后台的ViewModel關鍵代碼如下:
public class ExampleWindowViewModel { /// <summary> /// 將控制台滾動到指定項的委托; /// </summary> public Action<object> ScrollIntoLoggerBoxItemAction {get;set;} }
在控件加載完畢后,屬性ScrollIntoLoggerBoxItemAction 將會被自動賦值。
類ListBoxEntry並不是此處代碼關心的重點,代碼如下:
public class ListBoxEntry:BindableBase { private string _text; public string Text { get => _text; set => SetProperty(ref _text, value); } }
