0

There are several "WPF Databinding Not Updating" questions on here, and I've read through them all, including the top-ranked one: WPF DataBinding not updating?

However, I'm swapping out entire objects in my situation, and what's worse is that when I try to create a reproducible version of what I'm doing, it works in that version:

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp2"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Label Content="Contact Address" />
        <TextBox x:Name="txtContactAddress" Text="{Binding Path=Contact.Address, Mode=OneWay}" />
        <Label Content="Organization Address" />
        <TextBox x:Name="txtOrgAddress" Text="{Binding Path=Organization.Address, Mode=OneWay}" />
        <Button Content="Next Contact" Click="Button_Click" />
    </StackPanel>
</Window>

Code-Behind:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public RecordWrapper CurrentRecord;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            CurrentRecord = new RecordWrapper(new ContactData());
            this.DataContext = CurrentRecord;
        }
    }

    public class RecordWrapper : INotifyPropertyChanged
    {
        private ContactData _Contact;
        public ContactData Contact
        {
            get { return _Contact;  }
            set
            {
                _Contact = value;
                OnPropertyChanged("Contact");
                Organization = _Contact.Organization;
            }
        }
        private OrganizationData _Organization;
        public OrganizationData Organization
        {
            get { return _Organization; }
            set
            {
                _Organization = value;
                OnPropertyChanged("Organization");
            }
        }

        public RecordWrapper(ContactData contactData)
        {
            Contact = contactData;
        }

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string name)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        #endregion
    }

    public class ContactData
    {
        public string Address { get; set; }
        public OrganizationData Organization { get; set; }

        public ContactData()
        {
            Address = new Random().Next(1000, 9999) + " Contact Street";
            Organization = new OrganizationData();
        }
    }

    public class OrganizationData
    {
        public string Address { get; set; }
        public OrganizationData()
        {
            Address = new Random().Next(1000, 9999) + " Org Street";
        }
    }
}

So this particular sample works as expected - when I click on the "Next Contact" button, it generates a new RecordWrapper object that contains a new ContactData and new OrganizationData object, and then assigns the new object to DataContext and then I see the textboxes update properly.

Since I'm replacing the entire object in the DataContext, I can even ditch the property notifications and reduce the RecordWrapper down to just:

public class RecordWrapper
{
    public ContactData Contact { get; set; }
    public OrganizationData Organization { get; set; }

    public RecordWrapper(ContactData contactData)
    {
        Contact = contactData;
        Organization = Contact.Organization;
    }
}

...and it still works great.

My real code, however, reflects the first example.

In the real code, it seems like updating this.DataContext with a new object doesn't work. Nothing is updated, but there are no errors, either. Setting the path in XAML doesn't work, either. In fact, the only way I can get it to work is to use code-behind to set the bindings directly to the data object:

BindingOperations.SetBinding(txtContactAddress, TextBox.TextProperty, new Binding() { Source = this.RecordWrapper, Path = new PropertyPath("Contact.Address"), Mode = BindingMode.OneWay });

When I do this, I can see it working, but that's obviously missing the point of binding if I have to run code on each field each time to update the bindings.

I'm stumped because the real code is nearly identical to the example aside from having more fields, different field names, and differences that have nothing to do with data or binding. When I click on a button, this is the exact code:

this.Data = new DataBindings(CurrentContact.FetchFake());
this.DataContext = this.Data;

The FetchFake() method generates a new CurrentContact object with some fake data, and I have verified that after that first line runs, this.Data.CurrentContact.Login is populated and is a read-only property of the CurrentContact object (not a field).

...and one of the XAML textboxes looks like this:

<TextBox x:Name="txtAccountNumber" Grid.Column="1" Grid.Row="1"  Width="Auto" Text="{Binding Path=CurrentContact.Login, Mode=OneWay}"/>

The result is blank. I have also tried variations on order (e.g. setting DataContext to this.Data first, then setting the this.Data.CurrentContact property and letting the property-changed notification kick off, but still no luck. But if I run:

BindingOperations.SetBinding(tabAccount.txtAccountNumber, TextBox.TextProperty, new Binding() { Source = this.Data, Path = new PropertyPath("CurrentContact.Login"), Mode = BindingMode.OneWay });

...then I'll see the value show up.

Based on these symptoms, and the fact that my first example works but the real code doesn't, can anyone think of any place I should look for some culprits?

jhilgeman
  • 1,543
  • 10
  • 27
  • When executing the real code, are there any binding errors in the debug output? – Emond Jul 05 '18 at 16:15
  • No, there are none. – jhilgeman Jul 05 '18 at 16:43
  • Perhaps your Contact Data and OrganizationData classes should implement INotifyPropertyChanged interface too? – Jackdaw Jul 05 '18 at 17:29
  • @Jackdaw - they don't implement that interface in the initial example that works. – jhilgeman Jul 05 '18 at 18:07
  • This is bad (or not very MVVM) design. You should have a viewmodel that is being set to context in the xaml or the window constructor (or even DI it) and then have that viewmodel update some observablecollection property..say named CurrentContact using a bound command on your button instead of the button click event. Forcing a new data context reference once the window has loaded is terribly inefficient as all of the bindings need to be recreated under the hood as opposed to just being updated via normal binding action. That's why its "working" as is without having proper bound notifications – Paul Swetz Jul 05 '18 at 18:36
  • @Paul - the real code is like that (at least it was before I tried updating the DataContext each time - sorry that the above example was invalid). The button click is just for testing. – jhilgeman Jul 05 '18 at 19:42

1 Answers1

0

Ughhhhh. Figured it out. I had a REALLY old line of code in there that changed the DataContext of a container element that was between the Window and the Textbox, which interrupted the correct binding. I thought I had removed all of it, but hadn't. It DID throw an error, as Erno de Weerd had suggested, but the error was lost among some of the startup logging. We can close this one out.

jhilgeman
  • 1,543
  • 10
  • 27
  • Glad you figured it out. Two tips: 1. The debug output window supports Ctrl+F and 2. Just delete this question; chances of someone else finding this question when having the same problem are very slim. – Emond Jul 06 '18 at 04:25