简体   繁体   中英

How to make ViewModel invoke method from component used in View - WPF Prism

In my View I'm using a component (custom control), which provides some functions. I want to invoke one of them when my ViewModel receives an event it is subscribed to.

I want to do this as cleanly as possible, since there might be more functions I would be using this way.


I know I can create a variable like "InvokeFunctionA", bind to this variable and create OnChange method in my View which will invoke the corresponding function. But it's quite a lot of code required just to invoke a single function. And an extra variable, which seems quite unnesessary, too.

Is there a better way to do this? Like, maybe a View can pass some kind of a handler function to ViewModel which will do the work? I've made quite a lot of research but haven't yet found anything that suits my problem. Or maybe I'm missing something obvious?


[ edit ] Haukinger solution works for now (done this way: https://blog.machinezoo.com/expose-wpf-control-to-view-model-iii ), but I don't think it's the cleanest solution (Instead of providing access to a few functions, I'm exposing whole control to the ViewModel).

In a perfect MVVM-world (as you are asking for a clean solution), the ViewModel does not call anything that is located in the view (neither directly nor indirectly). I'd approach the problem like this:

  1. If 'component' is NOT a usercontrol, try moving it to the ViewModel and use bindings or commands in the view to operate your 'component'.

  2. If 'component' is a usercontrol, give 'component' a dependency property and fill it via a binding with your property of the ViewModel. Inside of 'compontent' you can register value change callback of your dependency property to start your work. <local:UserControlComponent MyDependencyProperty="{Binding PropertyInViewModel}" />

As a last resort:

  1. You could add a C# event to the viewmodel and handle it in your code-behind inside the view.

  2. Instead of an event, you could alternatively use IObservable pattern ( https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=netframework-4.8 , https://github.com/dotnet/reactive )

For completeness sake a no-go option: Prism has an EventAggregator that can be used for loose communication. I've had to remove the usage of EventAggregator from a rather big App, because it was not maintainable any more.

Expose a dependency property in your view whose type is the provided interface, bind it to a property on your view model, then call the method on the interface on the view model property from the view model.

To clarify, I don't mean to expose the component itself, rather an interface that contains exactly one method. The view has to have a private class that implements the interface and routes to the actual component, as well as converting arguments and results so that types belonging to the components need not be present in the interface.

But I'm with sa.he in that this whole situation should be avoided in the first place. It may not be possible, depending on the third party components used, though.

Yes, invoking view's methods from VM is very much against pure MVVM and there's not going to be a 'clean' solution. But it can be done at least half decently. You would need to create a special attached property (or behavior, but property seems to be a better choice in this scenario) and an ICommand property in VM, then bind the AP to the property with OneWayToSource binding and use command invocation in VM. It would still be a lot of code, but once it's done, you would only need to create new properties in the VM.

Below is some code that I wrote, consider it as a starting point, you can add support for command parameters and converters.

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

The XAML usage is as follows:

<TextBox>
    <l:MethodDelegation.CommandDelegates>
        <l:CommandDelegate MethodName="Focus" 
                           Command="{Binding TestCommand, Mode=OneWayToSource}" />
    </l:MethodDelegation.CommandDelegates>
</TextBox> 

Bubble your event upwards. Have your VM publish some event of its own. Your V can subscribe to it (if it wishes).

The downside is that you'll need codebehind, where ideally a V should be XAML-only as far as possible. The upside is that your VM remains quite aloof (ie it's not dependent on any specific controls used by the V). It says "something has happened worthy of note", but it doesn't assume either that (a) anyone is particularly listening, or (b) it leaves it to the listener (in your case, the V) to decide exactly what to action to take (ie how to change the UI).

It's a perennial problem - how does a VM cause a V to update somehow, and as far as I can tell it is still something to be debated.

The mechanism above, I've got a vague recollection that Prism itself might include something similar. I'm fairly sure it uses something akin to INotifyPropertyChanged (ie some interface or other) rather than an "event" as we might understand it just from a working knowledge of .net. You might even be able to use this mechanism to dispense with codebehind altogether. The downside of using Prism in the first place is its bulk, but if you're already using it anyway...

It's for you to decide how clean this is. I decided that a bit of codebehind was preferable to the VM meddling directly with the UI.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM