Search code examples
c#wpfxamlbindinginotifypropertychanged

WPF property declared in C# code instead of XAML


For some particular reason I have declared a binding in C# code instead of declaring it in XAML as usual. The problem I am facing is that when I change only one property of the object, the INotifyPropertyChanged works properly and propagates the changes as expected, but when I change the entire object (the reference), it does not work as expected.

You can see it in this sample code, where I set one initial value of "111", which works well, then I change only one property value to "222" and also works as expected, but when I set a new object, then it does not work.

What should I change in the code so the thrid assignment also works and the Text property is set to "333" via binding?

    public class MainWindowViewModel : INotifyPropertyChanged
{
    private MySubClass _selectedItem;
    public MySubClass SelectedItem
    {
        get => _selectedItem;
        set
        {
            _selectedItem = value;
            OnPropertyChanged(nameof(SelectedItem));
        }
    }
    private MySubClass _selectedItemBefore;
    public MySubClass SelectedItemBefore
    {
        get => _selectedItemBefore;
        set
        {
            _selectedItemBefore = value;
            OnPropertyChanged(nameof(SelectedItemBefore));
        }
    }
    private MySubClass _selectedItemAfter;
    public MySubClass SelectedItemAfter
    {
        get => _selectedItemAfter;
        set
        {
            _selectedItemAfter = value;
            OnPropertyChanged(nameof(SelectedItemAfter));
        }
    }


    TextBox textBox = new TextBox();
    public MainWindowViewModel()
    {
        SelectedItemBefore = new MySubClass
        {
            FirstProperty = "111"
        };
        SelectedItemAfter = new MySubClass
        {
            FirstProperty = "333"
        };
        SelectedItem = SelectedItemBefore;
        var firstPropertyBinding = new Binding("FirstProperty") { Source = SelectedItem, Mode = BindingMode.TwoWay };
        BindingOperations.SetBinding(textBox, TextBox.TextProperty, firstPropertyBinding);
        Console.WriteLine($"Text here is: {textBox.Text}");
        // It's "111", so OK!!!

        SelectedItem.FirstProperty = "222";
        Console.WriteLine($"Text here is: {textBox.Text}");
        // It's "222", so OK!!!

        SelectedItem = SelectedItemAfter;
        Console.WriteLine($"Text here is: {textBox.Text}");
        // ¡¡¡FAIL!!!: expected "333" but it is "222" yet
        // (here SelectedItem.FirstProperty == "333")
    }
    

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        var changed = PropertyChanged;
        if (changed == null)
            return;

        changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion        
}

public class MySubClass : INotifyPropertyChanged
{
    private string _firstProperty;
    public string FirstProperty
    {
        get => _firstProperty;
        set
        {
            _firstProperty = value;
            OnPropertyChanged(nameof(FirstProperty));
        }
    }
    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        var changed = PropertyChanged;
        if (changed == null)
            return;

        changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion   
}

UPDATE: I know that a TextBox should never go in the ViewModel, nor anything related to the View, please, don't dwell on that detail. This code is just for learning purposes and to understand how Bindings work, but for now, I don't fully understand it. I would appreciate explanations on how to modify the code so that it does display "333".


Solution

  • First, let’s take a look at the data sources of Binding:

    • Source: Direct static binding source reference, which cannot be modified after binding is applied.

      • you can set this in XAML by {Reference name} to set a named element reference in this XAML scope.
    • RelativeSource: According to the element to which the binding is applied and the mode of this RelativeSource, obtain other object as binding source based on the relative position of the binding applied element.

      • If RelativeSource target changing will change the reference of this binding source.
    • ElementName: Set the name of an use x:Name named element defined in current XAML and in same visual tree.

      • It works similar to Source and also static, but simpler.
    • None of the above are set: Binding source is the object(Control) own DataContext property, DataContext can be inherited in visual tree or document flow.

      • If DataContext changing will change the reference of this binding source.

    Setted Binding.Source property will set a static reference of object as a binding source on this Binding.
    But looks you want Binding to the MySubClass.FirstProperty from MainWindowViewModel.SelectedItem, but you set the SelectedItemBefore static reference on this binding.
    So that you cannot do anyting with SelectedItemAfter.

    The right way is that you should binding to MySubClass.FirstProperty of this MainWindowViewModel.SelectedItem:

    var firstPropertyBinding = new Binding("SelectedItem.FirstProperty") { Source = this };
    // OR
    // textBox.DataContext = this;
    // var firstPropertyBinding = new Binding("SelectedItem.FirstProperty");
    BindingOperations.SetBinding(textBox, TextBox.TextProperty, firstPropertyBinding);
    

    If SelectedItem changed, it will notify this binding source is changed and binding engine will update this binging target value. .FirstProperty means find FirstProperty in the SelectedItem referecd object.

    And if in the Window or UserControl context, it would be better to use DataContext.
    For example in window:

    //set window DataContext as MainWindowViewModel
    this.DataContext = new MainWindowViewModel();
    //set textbox DataContext bind to MainWindowViewModel.SelectedItem
    //by inherited DataContext from parent control
    BindingOperations.SetBinding(textBox, TextBox.DataContextProperty,
        new Binding("SelectedItem"));
    //based on TextBox.DataContext
    //this binding source is refereced by MainWindowViewModel.SelectedItem
    //and it's dynamic
    BindingOperations.SetBinding(textBox, TextBox.TextProperty,
        new Binding("FirstProperty"));