如何使 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 不会调用位于视图中的任何内容(既不直接也不间接调用)。我会这样处理问题:

  1. 如果 'component' 不是用户控件,请尝试将其移动到 ViewModel 并在视图中使用绑定或命令来操作您的 'component'.

  2. 如果 'component' 是一个用户控件,给 'component' 一个依赖项 属性 并通过与 ViewModel 的 属性 的绑定来填充它。在 'compontent' 中,您可以注册依赖项 属性 的值更改回调以开始您的工作。 <local:UserControlComponent MyDependencyProperty="{Binding PropertyInViewModel}" />

不得已:

  1. 您可以将 C# 事件添加到视图模型并在视图内的代码隐藏中处理它。

  2. 您也可以使用 IObservable 模式 (https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=netframework-4.8, https://github.com/dotnet/reactive)

  3. 而不是事件

为了完整起见,一个不可行的选项: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 更可取。