Search code examples
c#wpfxamlmvvmbinding

WPF and MVVM: pass object using binding through nested UserControl and ViewModel


I have the object MyObject Object in the class MainClass.xaml. I want to pass this object to a ViewModel class of a nested user control called SubSubUserControl through a SubUserControl.

Here is the code in MainClass.xaml:

<UserControl x:Class="Test.SubUserControl"
             d:DataContext="{d:DesignInstance d:Type=local:SubUserControlViewModel}">
    <Grid>
        <local:SubSubUserControl Object="{Binding Object, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SubUserControl}}}"/>
    </Grid>
</UserControl>

In SubUserControl I have a classical DependencyProperty in order to pass Object to it.

public static readonly DependencyProperty ObjectProperty = DependencyProperty.Register(nameof(Object), typeof(Object), typeof(SubUserControl));

public Object Object
{
    get => (Object)GetValue(ObjectProperty);
    set => SetValue(ObjectProperty, value);
}

protected override void OnInitialized(EventArgs e)
{
    InitializeComponent();
    DataContext = new SubUserControlViewModel(Object);
    base.OnInitialized(e);
}

SubUserControl is only a "bridge", the object Object is needed in SubSubUserControl. I pass to it with a binding (changing Datacontex beacause the standard DataContext for SubUserControl is his ViewModel).

<local:SubSubUserControl Object="{Binding Object, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />

and SubSubUserControl has the classical DependencyProperty.

SubSubUserControl has a ViewModel as DataContext and I'd like to pass that object to it. I try with

public static readonly DependencyProperty ObjectProperty = DependencyProperty.Register(nameof(Object), typeof(Object), typeof(SubSubUserControl));

public Object Object
{
    get => (Object)GetValue(ObjectProperty);
    set => SetValue(ObjectProperty, value);
}

protected override void OnInitialized(EventArgs e)
{
    InitializeComponent();
    DataContext = new SubSubUserControlViewModel(Object);
    base.OnInitialized(e);
}

but Object is null. The same code in SubUserControl works.

Is there a way to pass an object using binding to a ViewModel class that belongs to a nested UserControl?

I saw a lot of similar questions but noone works with my specific case...

EDIT

The class Object is instanced code-behind in MainClass.xaml.cs. In order to pass it I set the DataContext of MainClass to Self.

public MyObject Object { get; set; } = new MyObject();

Here is how I set DataContext in MainClass.xaml

<Window x:Class="Test.MainWindow"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <local:SubUserControl Object="{Binding Object}"/>
    </Grid>
</Window>

Setting the DataContext to the entire Window avoid me to write something like ElementName or Relative Source.

P.s. Resharper do not warn me about any DataContext, each instance is seen correctly.


Solution

  • I don't know the method OnInitialized, but from the documentation :

    Raises the Initialized event.

    And the event Initialized :

    Whether you choose to handle Loaded or Initialized depends on your requirements. If you do not need to read element properties, intend to reset properties, and do not need any layout information, Initialized might be the better event to act upon. If you need all properties of the element to be available, and you will be setting properties that are likely to reset the layout, Loaded might be the better event to act upon.

    I think the binding work, but you try to read the value in OnInitialized that is called before the binding is resolved.

    As suggested in the documentation, maybe you can use the event Loaded like :

    public partial class SubSubUserControl : UserControl
    {
        public SubSubUserControl()
        {
            InitializeComponent();
            Loaded += SubSubControl_Loaded;
        }
    
        private void SubSubControl_Loaded(object sender, RoutedEventArgs e)
        {
            DataContext = new SubSubViewModel(TargetFoo);
        }
    }
    

    A alternative is to update the data context when the dependency property is modified :

    public partial class SubSubUserControl : UserControl
    {
        public static readonly DependencyProperty ObjectProperty = DependencyProperty.Register(
            "Object", typeof(MyObject),
            typeof(SubSubUserControl),
            new PropertyMetadata(OnObjectChanged)
        );
    
        private static void OnObjectChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if(sender is SubSubControl ssc)
            {
                ssc.DataContext = new SubSubViewModel((MyObject)e.NewValue);
            }
        }
    }
    

    EDIT from exchanges in comments.

    It looks like you have a binding problem too. I suspect that a other element of type UserControl encapsulate the SubSubUserControl, like :

    MainClass
      SubUserControl
         OtherUserControl
             SubSubUserControl
    

    In this case, the binding will use OtherUserControl as source (and not the expected SubUserControl).

    In this case, you can specify in the binding the source's type is SubUserControl to avoid a other element of type UserControl is selected :

    <local:SubSubUserControl Object="{Binding BridgeFoo, RelativeSource={RelativeSource AncestorType={x:Type local:SubUserControl}}}" />
    

    A alternative is to use Binding.ElementName instead of Binding.RelativeSource :

    <UserControl x:Class="ProjectNamespace.SubUserControl"
                 ...
                 xmlns:local="clr-namespace:ProjectNamespace"
                 Name="Sub">
        <local:SubSubUserControl Object="{Binding Object, ElementName=Sub}" />
    </UserControl>
    

    I prefer this, because I never remember the RelativeSource syntax.