UserControls and View Modes in View-First-MVVM

I have to use View First MVVM in a WPF application, and I'm struggling to figure out how it can be made to work elegantly.

The root of the problem is the nested UserControls . In the MVVM architecture, each UserControl must have its own view model assigned to its DataContext , which simplifies binding expressions, as well as how WPF will instantiate any view generated using the DataTemplate .

But if the UserControl child has dependency properties that the parent must associate with its own view model, then the fact that the UserControl child has its own DataContext set to its own view model means that the "implicit path" binding in the parent XAML file child viewmodel instead of parent will be resolved.

To get around this, each parent of each UserControl in the application must either use explicit named bindings for everything by default (which is verbose, ugly and erroneous), or it will need to know if its control has its DataContext set to its own view model or no and uses the appropriate binding syntax (which is equally erroneous and serious violation of basic encapsulation).

After several days of research, I have not seen a single decent solution to this problem. The closest to the solution I came across sets the UserControl's view mode for the internal UserControl element (the topmost Grid or any other), which still leaves you with a problem trying to relate the UserControl properties to its own view model! ( ElementName binding will not work in this case, because the binding will be declared before the named element using the viewmodel assigned to its DataContext ).

I suspect that the reason why not many other people are faced with this is because they either use the first MVVM with a viewmodel that does not have this problem, or use the first MVVM for viewing in combination with the implementation of dependency injection, which improves this problem.

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 own viewmodel. --> <TextBlock Text="{Binding UserControlVmString1}"/> <!-- Has its value set by the UserControl 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 viewmodel. class UserControl6Vm { public string UserControlVmString1 { get { return "UserControl6Vm.String1"; } } } } 

This leads to:

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 'UserControl6' (Name = '_ this'); target property - "Text" (type 'String')

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

In UserControl6.xaml , commenting on 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 ElementName path binding will also work, but to use the ElementName binding ElementName you will need to know that UserControl sets its view model to its DataContext style (no encapsulation) or smooth the policy of using ElementName bindings everywhere. None of them are attractive.

+7
wpf mvvm binding xaml
source share
3 answers

The immediate solution is to use a RelativeSource and set it to find the DataContext 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 can also consider child viewmodels as properties of the parent view model and distribute it from the parent. Thus, the parent view model is aware of children so that it can update its properties. Child modifications can also have the "Parent" property, which contains a reference to the parent injected by the parent when it was created, which can provide direct access to the parent object.

 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 difficult would be to make the parent DataContext available to the child UserControl using the attached property. I did not fully implement it, but it had to be in an attached property to request a function (for example, "HasAccessToParentDT" ) in which the DependencyPropertyChanged event you would connect the Load and Unload to ChildUserControl , access the Parent property (available if the element control loaded) and bind its DataContext to the second attached property "ParentDataContext" , which can then be used in xaml.

  <local:ChildControl BindingHelper.AccessParentDataContext="True" MyProperty="{Binding BindingHelper.ParentDataContext.TargetProperty}" /> 
0
source share

The most obvious solution is to use a RelativeSource. The binding itself does not look very nice, but in fact it is very often seen. I would not have avoided it - this is exactly the scenario why it is there.

Another approach you can use is to reference the parent view model, if it is logical to have one. Similar to how I have a FlightPlan view that shows a list of navigation points and its graphic “map” side by side. The list of points is a separate view with a separate viewing model:

 public class PlanPointsPartViewModel : BindableBase { //[...] private FlightPlanViewModel _parentFlightPlan; public FlightPlanViewModel ParentFlightPlan { get { return _parentFlightPlan; } set { SetProperty(ref _parentFlightPlan, value); OnPropertyChanged(() => ParentFlightPlan); } } //[...] } 

Then the view can be attached to this property as follows:

 <ListView ItemsSource="{Binding Path=ParentFlightPlan.Waypoints}" AllowDrop="True" DragEnter="ListViewDragEnter" Drop="ListViewDrop" > [...] </ListView> 

However, the compilation of such viewing models quite often raises doubts.

0
source share

How about having ParentDataContextProperty at the second level of the UserControl ViewModel. Then create a dependency property for this usercontrol with the same name and let it set the value for the VM property in the xaml.cs file. Parentcontrol can then bind its DataContext to the dependency property of the child controls to provide the child VM with access to its (parent) data text. Childcontrol can communicate with the parent datacontext through its own ParentDataContextProperty VM property. (probably just PContext or something short should be called).

You can create a base class derived from UserControl that has this DependencyProperty setting, so you don't need to write it for every new control.

0
source share

All Articles