在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); } }