Search code examples
c#wpfdata-binding

Why binding doesn't work for TestClass, but setting the same property does work?


So i probably don't understand binding very well, and i came up with this test project, where i can show my problem in a simple way. Where do i need to change the project to make the binding updates the view?

Here is my project in case you want to download it: https://1drv.ms/u/s!AqdZJMIRBGu7z1V8K1jXN9BQWFQ-?e=oxdlhL

And here is some code:

TestClass.cs:

 public class TestClass
 {
     public string Text { get; set; } = "Default";
 } 

TestClassView.xaml:

<UserControl x:Class="Test.TestClassView" ...>
   <Grid>
      <TextBlock Text="{Binding TestClass.Text}" />
   </Grid>
</UserControl>

TestClassView.xaml.cs:

public partial class TestClassView : UserControl
{
    public TestClass TestClass 
    {
        get { return (TestClass)GetValue(TestClassProperty); }
        set { SetValue(TestClassProperty, value); }
    }
    public static readonly DependencyProperty TestClassProperty = DependencyProperty.Register("TestClass", typeof(TestClass), typeof(TestClassView), new PropertyMetadata(new TestClass() { Text = "Default"}));

    public TestClassView()
    {
        InitializeComponent();

        DataContext = this;
    }

MainWindow.xaml:

<Window x:Class="Test.MainWindow" ...>
   <Grid>
      <WrapPanel Margin="10">
         <Button Content="Test" Click="Button_Click" />
         <local:TestClassView x:Name="TestClassView" TestClass="{Binding TestClass}" />
      </WrapPanel>
   </Grid>
</Window>

MainWindow.xaml.cs:

public partial class MainWindow : Window, INotifyPropertyChanged
{
    private TestClass testClass = new TestClass() { Text = "DefaultProperty" };
    public TestClass TestClass { get { return testClass; } set{ testClass = value; OnPropertyChanged(); } }
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
    }
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName]string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        TestClass = new TestClass() { Text = "Test1" };
        MessageBox.Show("Test 1 Done");
        await Task.Delay(1000);
        TestClassView.TestClass = new TestClass() { Text = "Test2" };
        MessageBox.Show("Test 2 Done");
    }
}

So when i click on the button, first i set the TestClass instance of the MainWindow, which is bound to the TestClassView's DependencyProperty call TestClass. I also call PropertyChanged, but the TestClassView property doesn't updates.

After a second, at the second part of the click event handler, actually sets the TestClassView TestClass property, and view ll be updated.

So how to changed the code, to make the binding work?


Solution

  • I would say TestClassView doesn't need TestClass property. Its use is the same as DataContext property.

    public partial class TestClassView : UserControl
    {
        public TestClassView()
        {
            InitializeComponent();
        }
    }
    
    <UserControl x:Class="Test.TestClassView" ...>
       <Grid>
          <TextBlock Text="{Binding Text}" />
       </Grid>
    </UserControl>
    

    and Window:

    <Window x:Class="Test.MainWindow" ...>
       <Grid>
          <WrapPanel Margin="10">
             <Button Content="Test" Click="Button_Click" />
             <local:TestClassView x:Name="TestClassView" DataContext="{Binding TestClass}" />
          </WrapPanel>
       </Grid>
    </Window>
    
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private TestClass testClass = new TestClass() { Text = "DefaultProperty" };
        public TestClass TestClass { get { return testClass; } set { testClass = value; OnPropertyChanged(); } }
    
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName]string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    
        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            TestClass = new TestClass() { Text = "Test1" };
            MessageBox.Show("Test 1 Done");
            await Task.Delay(1000);
            TestClass = new TestClass() { Text = "Test2" };
            MessageBox.Show("Test 2 Done");
        }
    }
    

    In the original example binding doesn't work, because TestClassView control breaks DataContext propagation by setting DataContext = this; in constructor. It is a bad practice, and leads to more issues than solves.

    Setting DataContext = this; in Window is acceptable for exercise, as it allows quick setup, but usually Views use separate ViewModel classes for DataContext.