Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UserControls and viewmodels in View-First-MVVM

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.

like image 273
Neutrino Avatar asked Jan 21 '13 15:01

Neutrino


1 Answers

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}"   />
like image 64
Arthur Nunes Avatar answered Oct 10 '22 00:10

Arthur Nunes