-1

I have the following classes:

MainWindow.xaml

<Window.DataContext>
    <local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
    <local:MyControl Margin="8" MyProperty="{Binding MyPropertyParent}" />
</Grid>

MainWindowViewModel.cs

public class MainWindowViewModel : ObservableObject
{
    public string MyPropertyParent
    {
        get
        {
            return _myProperty;
        }

        set
        {
            _myProperty = value;
            this.OnPropertyChanged();
        }
    }

    private string _myProperty;

    public MainWindowViewModel()
    {
        MyPropertyParent = "IT WORKS!!!";
    }
}

MyControl.xaml

<UserControl.DataContext>
    <local:MyControlViewModel/>
</UserControl.DataContext>
<Border CornerRadius="4" Background="White" BorderThickness="1" BorderBrush="#FFB0AEAE">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Border Grid.Row="0" Background="#FFC6C6FB">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Margin="4" FontSize="16" VerticalAlignment="Center" HorizontalAlignment="Left"
                            Text="{Binding MyPropertyHack}" />
            </Grid>
        </Border>
    </Grid>
</Border>

MyControl.xaml.cs

public partial class MyControl : UserControl
{
    public string MyProperty
    {
        get { return (string)GetValue(MyPropertyProperty); }
        set { SetValue(MyPropertyProperty, value); }
    }

    public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register(
            nameof(MyProperty),
            typeof(string),
            typeof(MyControl),
            new PropertyMetadata(null, MyPropertyPropertyChanged));

    private static void MyPropertyPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        ((MyControl)o)._viewModel.MyPropertyHack = (string)e.NewValue;
    }

    public MyControlViewModel _viewModel;

    public MyControl()
    {
        InitializeComponent();

        if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
            return;

        _viewModel = App.Provider.GetService<MyControlViewModel>();
        this.DataContext = _viewModel;
    }
}

MyControlViewModel.cs

public class MyControlViewModel : ObservableObject
{
    private string _myPropertyHack;
    public string MyPropertyHack
    {
        get
        {
            return _myPropertyHack;
        }

        set
        {
            if (_myPropertyHack != value)
            {
                _myPropertyHack = value;
                this.OnPropertyChanged();
            }
        }
    }

    public MyControlViewModel()
    {

    }
}

I'm trying to get it so MainWindow.xaml can pass a property set in its view model through to the DependencyProperty of MyControl. However, when I run it the TextBlock's Text field is empty and so "IT WORKS!!!" is not being shown on the screen and my breakpoint in MyPropertyPropertyChanged is not being hit.

What am I doing wrong?

thatguy
  • 21,059
  • 6
  • 30
  • 40
TheLethalCoder
  • 6,668
  • 6
  • 34
  • 69
  • you have a spelling mistake in MyControl.xaml "{Binding MyProperty**e**Hack}" – T.Schwarz Jun 16 '21 at 08:58
  • @T.Schwarz cheers, not _the_ problem. I'd just changed the names when posting here. Fixed now. – TheLethalCoder Jun 16 '21 at 09:00
  • Are you binding to the correct viewmodel? You instantiate it both in xaml and in code-behind and i'm not exactly sure which one ends up being the final DataContext. You shouldn't need the xaml one. – T.Schwarz Jun 16 '21 at 09:13
  • @T.Schwarz I removed the XAML one. However, I noticed a XAML Binding Failure.: MyPropertyParent property not found on object of type MyControlViewModel. – TheLethalCoder Jun 16 '21 at 09:23
  • Some kind of incomprehensible solution. Can you explain why you do such a trick with binding: first, you pass the property value to a private ViewModel, and then bind to the property of this private ViewModel? Why not immediately bind the TextBlock to the DependencyProperty of the control itself? Also clarify after making a change to the `Text="{Binding MyPropertyHack}"` binding? problem still exists? – EldHasp Jun 16 '21 at 10:02

3 Answers3

2

The expression

MyProperty="{Binding MyPropertyParent}"

is supposed to create a Binding to the MyPropertyParent property of the object in the current DataContext.

While you seem to assume that this is the DataContext object set in the MainWindow, it is actually the DataContext object set in the UserControl's constructor.

You should have noticed a data binding error message in the Output Window in Visual Studio, which tells you that a property named MyPropertyParent could not be found on an object of type MyControlViewModel.

In general, a control should never explicity set its own DataContext, since DataContext-based Bindings like the above won't work as expected.

The view model object assigned to the UserControl's DataContext is unknown to the rest of your application. The application will not be able to communicate with this view model.

Clemens
  • 123,504
  • 12
  • 155
  • 268
2

You set the DataContext of your MyControl explicitly, which means the following binding of MyProperty will bind to the property MyPropertyParent on an instance of MyControlViewModel, which does not have this property. You can also see this as a binding error in the debug window.

<local:MyControl Margin="8" MyProperty="{Binding MyPropertyParent}" />

If you refernce the parent data context (an instance of MainWindowViewModel) through an ElementName or RelativeSource binding or using Parent, it will work, e.g. here I assume a parent as Window. Replace it with a parent in your control, to check it yourself.

<local:MyControl Margin="8" MyProperty="{Binding DataContext.MyPropertyParent, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />

Another note, you set the data context twice, one time in XAML and then in the constructor of the control, which means one will definitely be overridden.

<UserControl.DataContext>
    <local:MyControlViewModel/>
</UserControl.DataContext>
public MyControl()
{
    InitializeComponent();

    if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
        return;

    _viewModel = App.Provider.GetService<MyControlViewModel>();
    this.DataContext = _viewModel;
}

In general, you should not set the DataContext manually on a UserControl, but inherit it as usual. As you can see this just introduces errors and breaks data binding. Apart from that, you already define a dependency property for the binding, so it is just redundant.

thatguy
  • 21,059
  • 6
  • 30
  • 40
  • I've removed the xaml data context setting. That was something I did when setting up the test app and isn't there in the main app (can't remember why I did that). I added the relative source code like `` but that's giving a xaml warning: Member not found in data context Window. Perhaps I'm misunderstanding what you mean here? – TheLethalCoder Jun 16 '21 at 09:32
  • @TheLethalCoder You should not be using a RelativeSource Binding in the MainWindow, when you have already set its DataContext. It is a hack. Instead, your control should not have a private view model at all. – Clemens Jun 16 '21 at 09:38
  • @TheLethalCoder Sorry, I missed the `DataContext` property that you need to specify here, I have updated the code. Furthermore, I agree with @Clemens that this is just a hack to make your current solution work, but as stated in the last paragraph, what you should really do is not set a view model explicitly at all, but use the dependency property only. – thatguy Jun 16 '21 at 09:40
  • @thatguy I am aware it is a hack but I'm adding to existing code and would be too much work to redo it all (usual problems). Thanks though, this has gotten it working in the test app! Now to port the changes into the main one. – TheLethalCoder Jun 16 '21 at 09:47
0

It is not clear to me why you need a private VM, but I will assume that you simply did not show some part of the task.

DataContext is a property for use by bindings as the default source. This property has no other function anymore.
And this property is not the only place where the ViewModel can be set. For a private ViewModel, it, in principle, is not suitable, as already noted in another answer.
Since it sets the source for the bindings of the properties of the control at the place of its application, which is quite unexpected for any programmer.

One of the acceptable options: set an instance in the DataContext of the main element in UserControl.

<Border x:Name="PART_MainBorder"
        CornerRadius="4" Background="White"
        BorderThickness="1" BorderBrush="#FFB0AEAE">
    <d:Border.DataContext>
        <local:MyControlViewModel/>
    </d:Border.DataContext>
    public MyControl()
    {
        InitializeComponent();

        if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
            return;

        _viewModel = App.Provider.GetService<MyControlViewModel>();
        PART_MainBorder.DataContext = _viewModel;
    }
Clemens
  • 123,504
  • 12
  • 155
  • 268
EldHasp
  • 6,079
  • 2
  • 9
  • 24