如何使 ViewModel 从 View 中使用的组件调用方法 - WPF Prism
How to make ViewModel invoke method from component used in View - WPF Prism
在我的视图中,我使用了一个组件(自定义控件),它提供了一些功能。当我的 ViewModel 收到它订阅的事件时,我想调用其中之一。
我想尽可能干净地完成此操作,因为我可能会以这种方式使用更多功能。
我知道我可以创建一个像 "InvokeFunctionA" 这样的变量,绑定到这个变量并在我的视图中创建 OnChange 方法,该方法将调用相应的函数。但是仅仅调用一个函数就需要很多代码。还有一个额外的变量,这看起来也很不必要。
有更好的方法吗?比如,也许 View 可以将某种处理函数传递给 ViewModel 来完成这项工作?我做了很多研究,但还没有找到适合我的问题的东西。或许我遗漏了一些明显的东西?
[编辑]
Haukinger 解决方案目前有效(以这种方式完成: https://blog.machinezoo.com/expose-wpf-control-to-view-model-iii ),但我认为这不是最干净的解决方案(我没有提供对一些功能的访问,而是将整个控制权暴露给 ViewModel)。
在您的视图中公开一个依赖项属性,其类型是提供的接口,将其绑定到视图模型上的属性,然后在视图模型上调用接口上的方法属性 来自视图模型。
澄清一下,我并不是要公开组件本身,而是要公开仅包含一个方法的接口。视图必须有一个私有 class 来实现接口和到实际组件的路由,以及转换参数和结果,以便接口中不需要出现属于组件的类型。
但我同意 sa.he,因为首先应该避免这种情况。不过,这可能无法实现,具体取决于所使用的第三方组件。
在完美的 MVVM 世界中(因为您要求的是一个干净的解决方案),ViewModel 不会调用位于视图中的任何内容(既不直接也不间接调用)。我会这样处理问题:
如果 'component' 不是用户控件,请尝试将其移动到 ViewModel 并在视图中使用绑定或命令来操作您的 'component'.
如果 'component' 是一个用户控件,给 'component' 一个依赖项 属性 并通过与 ViewModel 的 属性 的绑定来填充它。在 'compontent' 中,您可以注册依赖项 属性 的值更改回调以开始您的工作。 <local:UserControlComponent MyDependencyProperty="{Binding PropertyInViewModel}" />
不得已:
您可以将 C# 事件添加到视图模型并在视图内的代码隐藏中处理它。
您也可以使用 IObservable 模式 (https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=netframework-4.8, https://github.com/dotnet/reactive)
而不是事件
为了完整起见,一个不可行的选项:Prism 有一个 EventAggregator,可用于松散通信。我不得不从一个相当大的应用程序中删除 EventAggregator 的使用,因为它不再可维护。
是的,从 VM 调用视图的方法与纯 MVVM 非常不一致,并且不会有 'clean' 解决方案。
但至少可以像样地完成一半。您需要在 VM 中创建一个特殊的附加 属性(或行为,但 属性 似乎是更好的选择)和一个 ICommand 属性,然后将 AP 绑定到属性 使用 OneWayToSource 绑定并在 VM 中使用命令调用。仍然会有很多代码,但是一旦完成,您只需要在 VM 中创建新属性。
下面是我写的一些代码,把它作为一个起点,你可以添加对命令参数和转换器的支持。
public class MethodDelegation : DependencyObject
{
public static readonly DependencyProperty CommandDelegatesProperty =
DependencyProperty.RegisterAttached("CommandDelegatesInternal", typeof(CommandDelegatesCollection), typeof(MethodDelegation), new PropertyMetadata(null));
private MethodDelegation() { }
public static CommandDelegatesCollection GetCommandDelegates(DependencyObject obj)
{
if (obj.GetValue(CommandDelegatesProperty) is null)
{
SetCommandDelegates(obj, new CommandDelegatesCollection(obj));
}
return (CommandDelegatesCollection)obj.GetValue(CommandDelegatesProperty);
}
public static void SetCommandDelegates(DependencyObject obj, CommandDelegatesCollection value)
{
obj.SetValue(CommandDelegatesProperty, value);
}
}
public class CommandDelegatesCollection : FreezableCollection<CommandDelegate>
{
public CommandDelegatesCollection()
{
}
public CommandDelegatesCollection(DependencyObject targetObject)
{
TargetObject = targetObject;
((INotifyCollectionChanged)this).CollectionChanged += UpdateDelegatesTargetObjects;
}
public DependencyObject TargetObject { get; }
protected override Freezable CreateInstanceCore()
{
return new CommandDelegatesCollection();
}
private void UpdateDelegatesTargetObjects(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (CommandDelegate commandDelegate in e?.NewItems ?? Array.Empty<CommandDelegate>())
{
commandDelegate.TargetObject = TargetObject;
}
}
}
public class CommandDelegate : Freezable
{
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register("MethodName", typeof(string), typeof(CommandDelegate), new PropertyMetadata(string.Empty, MethodName_Changed));
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandDelegate), new PropertyMetadata(null));
public static readonly DependencyProperty TargetObjectProperty =
DependencyProperty.Register("TargetObject", typeof(DependencyObject), typeof(CommandDelegate), new PropertyMetadata(null, TargetObject_Changed));
private MethodInfo _method;
public string MethodName
{
get { return (string)GetValue(MethodNameProperty); }
set { SetValue(MethodNameProperty, value); }
}
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public DependencyObject TargetObject
{
get { return (DependencyObject)GetValue(TargetObjectProperty); }
set { SetValue(TargetObjectProperty, value); }
}
protected override Freezable CreateInstanceCore()
{
return new CommandDelegate();
}
private static void MethodName_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var del = (CommandDelegate)d;
del.UpdateMethod();
del.UpdateCommand();
}
private static void TargetObject_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var del = (CommandDelegate)d;
del.UpdateMethod();
del.UpdateCommand();
}
private void UpdateMethod()
{
_method = TargetObject?.GetType()?.GetMethod(MethodName);
}
private void UpdateCommand()
{
Command = new RelayCommand(() => _method.Invoke(TargetObject, Array.Empty<object>()));
}
}
XAML用法如下:
<TextBox>
<l:MethodDelegation.CommandDelegates>
<l:CommandDelegate MethodName="Focus"
Command="{Binding TestCommand, Mode=OneWayToSource}" />
</l:MethodDelegation.CommandDelegates>
</TextBox>
向上冒泡您的事件。让您的 VM 发布一些自己的事件。你的 V 可以订阅它(如果它愿意的话)。
缺点是您需要代码隐藏,理想情况下 V 应尽可能 XAML-only。好处是您的 VM 仍然非常冷漠(即它不依赖于 V 使用的任何特定控件)。它说 "something has happened worthy of note",但它不假设 (a) 任何人都在特别倾听,或者 (b) 它留给听众(在你的例子中是 V)来决定究竟要采取什么行动(即如何更改 UI)。
这是一个长期存在的问题 - VM 如何导致 V 以某种方式更新,据我所知,这仍然是一个有待争论的问题。
上面的机制,我隐约记得Prism本身可能包含类似的东西。我相当确定它使用类似于 INotifyPropertyChanged 的东西(即一些接口或其他接口)而不是 "event",因为我们可能只是从 .net 的工作知识中理解它。您甚至可以使用此机制完全免除代码隐藏。首先使用 Prism 的缺点是体积大,但如果您已经在使用它...
由您来决定这有多干净。我认为一些代码隐藏比 VM 直接干预 UI 更可取。
在我的视图中,我使用了一个组件(自定义控件),它提供了一些功能。当我的 ViewModel 收到它订阅的事件时,我想调用其中之一。
我想尽可能干净地完成此操作,因为我可能会以这种方式使用更多功能。
我知道我可以创建一个像 "InvokeFunctionA" 这样的变量,绑定到这个变量并在我的视图中创建 OnChange 方法,该方法将调用相应的函数。但是仅仅调用一个函数就需要很多代码。还有一个额外的变量,这看起来也很不必要。
有更好的方法吗?比如,也许 View 可以将某种处理函数传递给 ViewModel 来完成这项工作?我做了很多研究,但还没有找到适合我的问题的东西。或许我遗漏了一些明显的东西?
[编辑] Haukinger 解决方案目前有效(以这种方式完成: https://blog.machinezoo.com/expose-wpf-control-to-view-model-iii ),但我认为这不是最干净的解决方案(我没有提供对一些功能的访问,而是将整个控制权暴露给 ViewModel)。
在您的视图中公开一个依赖项属性,其类型是提供的接口,将其绑定到视图模型上的属性,然后在视图模型上调用接口上的方法属性 来自视图模型。
澄清一下,我并不是要公开组件本身,而是要公开仅包含一个方法的接口。视图必须有一个私有 class 来实现接口和到实际组件的路由,以及转换参数和结果,以便接口中不需要出现属于组件的类型。
但我同意 sa.he,因为首先应该避免这种情况。不过,这可能无法实现,具体取决于所使用的第三方组件。
在完美的 MVVM 世界中(因为您要求的是一个干净的解决方案),ViewModel 不会调用位于视图中的任何内容(既不直接也不间接调用)。我会这样处理问题:
如果 'component' 不是用户控件,请尝试将其移动到 ViewModel 并在视图中使用绑定或命令来操作您的 'component'.
如果 'component' 是一个用户控件,给 'component' 一个依赖项 属性 并通过与 ViewModel 的 属性 的绑定来填充它。在 'compontent' 中,您可以注册依赖项 属性 的值更改回调以开始您的工作。
<local:UserControlComponent MyDependencyProperty="{Binding PropertyInViewModel}" />
不得已:
您可以将 C# 事件添加到视图模型并在视图内的代码隐藏中处理它。
您也可以使用 IObservable 模式 (https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=netframework-4.8, https://github.com/dotnet/reactive)
而不是事件
为了完整起见,一个不可行的选项:Prism 有一个 EventAggregator,可用于松散通信。我不得不从一个相当大的应用程序中删除 EventAggregator 的使用,因为它不再可维护。
是的,从 VM 调用视图的方法与纯 MVVM 非常不一致,并且不会有 'clean' 解决方案。 但至少可以像样地完成一半。您需要在 VM 中创建一个特殊的附加 属性(或行为,但 属性 似乎是更好的选择)和一个 ICommand 属性,然后将 AP 绑定到属性 使用 OneWayToSource 绑定并在 VM 中使用命令调用。仍然会有很多代码,但是一旦完成,您只需要在 VM 中创建新属性。
下面是我写的一些代码,把它作为一个起点,你可以添加对命令参数和转换器的支持。
public class MethodDelegation : DependencyObject
{
public static readonly DependencyProperty CommandDelegatesProperty =
DependencyProperty.RegisterAttached("CommandDelegatesInternal", typeof(CommandDelegatesCollection), typeof(MethodDelegation), new PropertyMetadata(null));
private MethodDelegation() { }
public static CommandDelegatesCollection GetCommandDelegates(DependencyObject obj)
{
if (obj.GetValue(CommandDelegatesProperty) is null)
{
SetCommandDelegates(obj, new CommandDelegatesCollection(obj));
}
return (CommandDelegatesCollection)obj.GetValue(CommandDelegatesProperty);
}
public static void SetCommandDelegates(DependencyObject obj, CommandDelegatesCollection value)
{
obj.SetValue(CommandDelegatesProperty, value);
}
}
public class CommandDelegatesCollection : FreezableCollection<CommandDelegate>
{
public CommandDelegatesCollection()
{
}
public CommandDelegatesCollection(DependencyObject targetObject)
{
TargetObject = targetObject;
((INotifyCollectionChanged)this).CollectionChanged += UpdateDelegatesTargetObjects;
}
public DependencyObject TargetObject { get; }
protected override Freezable CreateInstanceCore()
{
return new CommandDelegatesCollection();
}
private void UpdateDelegatesTargetObjects(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (CommandDelegate commandDelegate in e?.NewItems ?? Array.Empty<CommandDelegate>())
{
commandDelegate.TargetObject = TargetObject;
}
}
}
public class CommandDelegate : Freezable
{
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register("MethodName", typeof(string), typeof(CommandDelegate), new PropertyMetadata(string.Empty, MethodName_Changed));
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandDelegate), new PropertyMetadata(null));
public static readonly DependencyProperty TargetObjectProperty =
DependencyProperty.Register("TargetObject", typeof(DependencyObject), typeof(CommandDelegate), new PropertyMetadata(null, TargetObject_Changed));
private MethodInfo _method;
public string MethodName
{
get { return (string)GetValue(MethodNameProperty); }
set { SetValue(MethodNameProperty, value); }
}
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public DependencyObject TargetObject
{
get { return (DependencyObject)GetValue(TargetObjectProperty); }
set { SetValue(TargetObjectProperty, value); }
}
protected override Freezable CreateInstanceCore()
{
return new CommandDelegate();
}
private static void MethodName_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var del = (CommandDelegate)d;
del.UpdateMethod();
del.UpdateCommand();
}
private static void TargetObject_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var del = (CommandDelegate)d;
del.UpdateMethod();
del.UpdateCommand();
}
private void UpdateMethod()
{
_method = TargetObject?.GetType()?.GetMethod(MethodName);
}
private void UpdateCommand()
{
Command = new RelayCommand(() => _method.Invoke(TargetObject, Array.Empty<object>()));
}
}
XAML用法如下:
<TextBox>
<l:MethodDelegation.CommandDelegates>
<l:CommandDelegate MethodName="Focus"
Command="{Binding TestCommand, Mode=OneWayToSource}" />
</l:MethodDelegation.CommandDelegates>
</TextBox>
向上冒泡您的事件。让您的 VM 发布一些自己的事件。你的 V 可以订阅它(如果它愿意的话)。
缺点是您需要代码隐藏,理想情况下 V 应尽可能 XAML-only。好处是您的 VM 仍然非常冷漠(即它不依赖于 V 使用的任何特定控件)。它说 "something has happened worthy of note",但它不假设 (a) 任何人都在特别倾听,或者 (b) 它留给听众(在你的例子中是 V)来决定究竟要采取什么行动(即如何更改 UI)。
这是一个长期存在的问题 - VM 如何导致 V 以某种方式更新,据我所知,这仍然是一个有待争论的问题。
上面的机制,我隐约记得Prism本身可能包含类似的东西。我相当确定它使用类似于 INotifyPropertyChanged 的东西(即一些接口或其他接口)而不是 "event",因为我们可能只是从 .net 的工作知识中理解它。您甚至可以使用此机制完全免除代码隐藏。首先使用 Prism 的缺点是体积大,但如果您已经在使用它...
由您来决定这有多干净。我认为一些代码隐藏比 VM 直接干预 UI 更可取。