Search code examples
c#wpfxamlmvvmdata-binding

Store reference to WPF element in view model object with MVVM pattern


I use the MVVM pattern to provide data to a user control in a window. I would like to implement a method on the view model object which forwards any calls to itself to the corresponding user control, that it supplies with data. For this I would need to have a reference to the control in the view model object.

I could do this by declaring a property on the user control that returns a reference to the control itself, and then bind to the view model object, but it's quite ugly:

<MyUserControl ReferenceToMyself="{Binding CorrespondingUserControl, Mode=OneWayToSource}"/>

And then in the view model object:

public void MethodThatForwardsCalls() => CorrespondingUserControl.ThisIsCalled();

Is there a better way to bind a control itself to a view model object?


Solution

  • Have you instead considered using an event-like interaction between the ViewModel and the View to avoid having to keep references of the View in the ViewModel?

    In the ViewModel, I would create a property of type Action and call it something like ThisIsCalledEvent;

    #region ThisIsCalledEvent
            private Action m_ThisIsCalledEvent;
            public Action ThisIsCalledEvent
            {
                get
                {
                    return m_ThisIsCalledEvent;
                }
                set
                {
                    if (m_ThisIsCalledEvent != value)
                    {
                        m_ThisIsCalledEvent = value;
                        OnPropertyChanged(nameof(ThisIsCalledEvent));
                    }
                }
            }
    
    #endregion
    

    Then, in MyUserControl, add an event callback to the DataContextChanged event;

    this.DataContextChanged += MyUserControl_DataContextChanged; in the CS constructor, or DataContextChanged="MyUserControl_DataContextChanged" in the XAML constructor.

    Now, in the event callback, you can set the Action for the ThisIsCalledEvent property of the ViewModel;

    private void MyUserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        MyUserControlViewModel vm = DataContext as MyUserControlViewModel; //Get the ViewModel from the View's DataContext
    
        if(vm == null)
            return;
    
        vm.ThisIsCalledEvent = delegate () { ThisIsCalled(); }; //When the action is invoked, call the desired function
    }
    

    Now, back to the ViewModel, to invoke you simply change your method's body to;

    public void MethodThatForwardsCalls()
    {
        this.ThisIsCalledEvent?.Invoke(); //Invoke the Action, if it exists, that has been set by the View
    }
    

    This way, you never keep an instance of the View in the ViewModel, and each time a new view is instantiated from the ViewModel, it sets the Action callback.

    EDIT As suggested by Jürgen Röhr in the comments, in the DataContextChanged event callback, you can use the args.NewValue to bind to the new DataContext. Furthermore, if you register the delegate to the Action property in addition instead of setting it, you can use args.OldValue to de-register it from the previous DataContext.