UserControls and viewmodels in View-First-MVVM

12,709

An immediate solution is to use a RelativeSource and set it to look for the DataContext of a parent UserControl:

<UserControl>
    <UserControl.DataContext>
        <local:ParentViewModel />
    </UserControl.DataContext>
    <Grid>
        <local:ChildControl MyProperty="{Binding DataContext.PropertyInParentDataContext, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
    </Grid>
</UserControl>

You could also treat the child viewmodels as properties of the parent viewmodel, and propagate it from the parent. That way, the parent viewmodel is aware of the children so it can update their properties. The child viewmodels also may have a "Parent" property which holds a reference to the parent, injected by the parent itelf upon their creation, which may grant direct access to the parent.

public class ParentViewModel : INotifyPropertyChanged
{
    #region INotifyPropertyChanged values

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion

    private ChildViewModel childViewModel;
    public ChildViewModel ChildViewModel
    {
        get { return this.childViewModel; }
        set
        {
            if (this.childViewModel != value)
            {
                this.childViewModel = value;
        this.OnPropertyChanged("ChildViewModel");
            }
        }
    }       
}

<UserControl>
    <UserControl.DataContext>
        <local:ParentViewModel />
    </UserControl.DataContext>
    <Grid>
        <local:ChildControl DataContext="{Binding ChildViewModel}"
            MyProperty1="{Binding PropertyInTheChildControlledByParent}"                
            MyProperty2="{Binding Parent.PropertyWithDirectAccess}"/>
    </Grid>
</UserControl>

EDIT Another approach and more complex would be making the parent's DataContext available to the child UserControl using an attached property. I have not fully implemented it, but it would consist in an attached property to request the feature (something like "HasAccessToParentDT"), in which DependencyPropertyChanged event you would hook up the Load and Unload events of the ChildUserControl, access the Parent property (available if the control is loaded) and bind its DataContext to a second attached property, "ParentDataContext", which could then be used in xaml.

        <local:ChildControl BindingHelper.AccessParentDataContext="True"
            MyProperty="{Binding BindingHelper.ParentDataContext.TargetProperty}"   />
Share:
12,709
Neutrino
Author by

Neutrino

Updated on June 16, 2022

Comments

  • Neutrino
    Neutrino almost 2 years

    I'm forced to use View First MVVM in a WPF application and I'm struggling to see how it can be made to work elegantly.

    The root of the problem lies with nested UserControls. In an MVVM architecture each UserControl needs to have its view model assigned to its DataContext, this keeps the binding expressions simple, and what's more this is also the way WPF will instantiate any view generated via a DataTemplate.

    But if a child UserControl has dependency properties which the parent needs to bind to its own viewmodel then the fact that the child UserControl has its DataContext set to its own viewmodel means that an 'implicit path' binding in the parent XAML file will resolve to the child's viewmodel instead of the parent's.

    To work around this every parent of every UserControl in the application will either need to use explicit named bindings for everything by default (which is verbose, ugly and errorprone), or it will have to know whether a specific control has its DataContext set to its own viewmodel or not and use the appropriate binding syntax, (which is equally errorprone, and a major violation of basic encapsulation).

    After days of research I haven't come across a single half decent solution to this issue. The closest thing to a solution I've come across is setting the UserControl's viewmodel to an inner element of the UserControl (the topmost Grid or whatever), which still leaves you facing a problem trying to bind properties of the UserControl itself to its own viewmodel! (ElementName binding won't work in this case because the binding would be declared before the named element with the viewmodel assigned to its DataContext).

    I suspect that the reason not many other people run into this it that they are either using viewmodel first MVVM which doesn't have this issue, or they are using view first MVVM in conjunction with a dependency injection implementation that amelliorates this issue.

    Does anyone have a clean solution for this please?

    UPDATE:

    Sample code as requested.

    <!-- MainWindow.xaml -->
    <Window x:Class="UiInteraction.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:UiInteraction"
            Title="MainWindow" Height="350" Width="525"
            x:Name="_this">
    
        <Window.DataContext>
            <local:MainWindowVm/>
        </Window.DataContext>
    
        <StackPanel>
            <local:UserControl6 Text="{Binding MainWindowVmString1}"/>  
        </StackPanel>
    
    </Window>
    
    namespace UiInteraction
    {
        // MainWindow viewmodel.
        class MainWindowVm
        {
            public string MainWindowVmString1
            {
                get { return "MainWindowVm.String1"; }
            }
        }
    }
    
    <!-- UserControl6.xaml -->
    <UserControl x:Class="UiInteraction.UserControl6" x:Name="_this"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                  xmlns:local="clr-namespace:UiInteraction">
    
        <UserControl.DataContext>
            <local:UserControl6Vm/>
        </UserControl.DataContext>
    
        <StackPanel>
            <!-- Is bound to this UserControl's own viewmodel. -->
            <TextBlock Text="{Binding UserControlVmString1}"/>
    
            <!-- Has its value set by the UserControl's parent via dependency property. -->
            <TextBlock Text="{Binding Text, ElementName=_this}"/>
        </StackPanel>
    
    </UserControl>
    
    namespace UiInteraction
    {
        using System.Windows;
        using System.Windows.Controls;
    
        // UserControl code behind declares DependencyProperty for parent to bind to.
        public partial class UserControl6 : UserControl
        {
            public UserControl6()
            {
                InitializeComponent();
            }
    
            public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
                "Text", typeof(string), typeof(UserControl6));
    
            public string Text
            {
                get { return (string)GetValue(TextProperty); }
                set { SetValue(TextProperty, value); }
            }
        }
    }
    
    namespace UiInteraction
    {
        // UserControl's viewmodel.
        class UserControl6Vm
        {
            public string UserControlVmString1
            {
                get { return "UserControl6Vm.String1"; }
            }
        }
    }
    

    This results in:

    System.Windows.Data Error: 40 : BindingExpression path error: 'MainWindowVmString1' property not found on 'object' ''UserControl6Vm' (HashCode=44204140)'. BindingExpression:Path=MainWindowVmString1; DataItem='UserControl6Vm' (HashCode=44204140); target element is 'UserControl6' (Name='_this'); target property is 'Text' (type 'String')

    because in MainWindow.xaml the declaration <local:UserControl6 Text="{Binding MainWindowVmString1}"/> is attempting to resolve MainWindowVmString1 on UserControl6Vm.

    In UserControl6.xaml commenting out the declaration of the DataContext and the first TextBlock the code will work, but the UserControl needs a DataContext. In MainWIndow1 using an ElementName instead of an implict path binding will also work, but in order to use the ElementName binding syntax you would either have to know that the UserControl assigns its viewmodel to its DataContext (encapsulation failure) or altenatively adopt a policy of using ElementName bindings everywhere. Neither of which is appealing.

  • Neutrino
    Neutrino over 11 years
    The first approach is an example of exactly what I'm trying to avoid. I want my bindings to look like MyProperty="{Binding PropertyInParentDataContext}" instead of MyProperty="{Binding DataContext.PropertyInParentDataContext, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}". This isn't just a stylistic consideration because errors in XAML are a nightmare to track down.
  • Neutrino
    Neutrino over 11 years
    The problem with the second approach is that if the parent wants to bind to any of the ChildControl's dependency properties in addition to its DataContext then that also requires a convoluted binding expression because if one of the assigned bindings is the ChildControl's DataContext then that affects the implicit binding source for all the other binding expressions.
  • Neutrino
    Neutrino over 8 years
    I don't like the idea of referencing a parent's viewmodel. While I agree in some specific situations this may be logical I wouldn't consider it a generic solution, especially when using view first MVVM where it is the view's dependency properties that primarily form the interface between the various view elements rather than a direct reference between their view models.
  • Neutrino
    Neutrino over 8 years
    I agree that using RelativeSource in the manner proposed in Arthur Nune's answer is a better approach. On the one hand it doesn't quite answer the question because it can't be considered syntactically elegant, but on the other hand it is a localised generic solution. But how is using RelativeSource better than x:Naming the parent UI element and using an ElementName binding though? E.g. Text="{Binding DataContext.Text, ElementName=_this}"
  • eMko
    eMko over 8 years
    Referencing parent viewmodel is surely not generic solution - I it here only as an example of a different approach (although you can also use a dependency injection in some cases, but it still doesn't solve the problem).
  • eMko
    eMko over 8 years
    The RelativeSource can be better because it doesn't tightly couple one element to other which IMHO is better, however YMMV.