wpfxamlbindingtwo-way

WPF two way binding not working on CheckBox and List<string>


Even after reviewing a multitude of solution proposals I cannot get a simple two way binding in xaml to work. I have a Window, a dataContext and an App. Problem is that:

a) while the App constructor runs, the Window (initialized and .Show-ed in the same constructor) shows up, but is not updated at all, even though I toggle the checkbox value in my C# code a couple times;

b) when the App constructor finishes, the Window is updated exactly once; I have it set up so that if I click the checkbox in the Window, the event handler in App (bound to the DataContext property change notification) should increase the size of a list of strings, which is also displayed. The increase of the list happens correctly in the code, but is not reflected in the Window.

Summary:

What I would expect is that:

a) while the App constructor runs and toggles the CheckBox value, the Window should reflect the changes by setting / clearing the tick on the box;

b) after the App constructor finishes, whenever I toggle the CheckBox from FALSE to TRUE, the NameList is appended with a new string. I would expect the list in the Window to increase accordingly and automatically show the complete, appended NameList contents.

Observations:

MainWindow.xaml:

<Window x:Class="StatisticsEvaluation.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <StackPanel Orientation="Vertical">

        <CheckBox IsChecked="{Binding Path=IsChecked, Mode=TwoWay}" Content="CheckBox" />

        <ListBox ItemsSource="{Binding NameList, Mode=TwoWay}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBlock FontSize="18" FontFamily="Arial" Foreground="Black" Text="TextBlock" Visibility="Visible" />

    </StackPanel>
</Grid>

MainWindow.xaml.cs:

namespace StatisticsEvaluation
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {            
    }
}

}

The App and DataContext:

namespace StatisticsEvaluation
{
    public class DataContextClass : INotifyPropertyChanged
    {
        private bool isChecked;

        public bool IsChecked
        {
            get
            {
                return isChecked;
            }

            set
            {
                isChecked = value;
                OnPropertyChanged("IsChecked");
            }
        }

        private List<string> nameList;

        public List<string> NameList
        {
            get
            {
                return nameList;
            }

            set
            {
                nameList = value;
                OnPropertyChanged("NameList");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null) 
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }


    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>

    public partial class App : Application
    {
        private MainWindow MyWindow { get; set; }

        private DataContextClass MyDataContext{ get; set; }

        private void HandleDataContextPropertyChange(object sender, PropertyChangedEventArgs e)
        {
            // If the CheckBox was just toggled to TRUE, increase the NameList
            // with an additional name and call OnPropertyChanged on it ... 
            // hoping that this would trigger a Window UI update - but no luck !

            if ((e.PropertyName == "IsChecked") && MyDataContext.IsChecked)
            {
                var randomProvider = new Random();
                MyDataContext.NameList.Add(randomProvider.Next().ToString());
                MyDataContext.OnPropertyChanged("NameList");
            }
        }

        public App()
        {
            MyDataContext = new DataContextClass();
            MyDataContext.PropertyChanged += HandleDataContextPropertyChange;

            MyWindow = new MainWindow {DataContext = MyDataContext};
            MyWindow.InitializeComponent();
            MyWindow.Show();

            MyDataContext.NameList = new List<string>();
            MyDataContext.NameList.Add("FirstName");
            MyDataContext.NameList.Add("SecondName");
            MyDataContext.NameList.Add("ThirdName");

            MyDataContext.IsChecked = true;
            Thread.Sleep(3000);
            MyDataContext.IsChecked = false;
            Thread.Sleep(3000);
            MyDataContext.IsChecked = true;
        }       
    }
}

When I start the App the following Window appears, once the App constructor hits .Show:

enter image description here

Once the App contructor has finished, the Window is updated once, but never again afterwards, regardless how many strings are added to NameList:

enter image description here

Any ideas, why my two way binding only works in one direction ?


Solution

  • If a bound collection doesn't implement INotifyCollectionChanged (e.g. ObservableCollection<T>), you'll get inconsistent or nonexistent behavior when trying to update the view. I noticed that the list would indeed update when flicking my mouse's scroll wheel after toggling the check state to true. Also, as @Clemens said, your ItemsSource binding should be Mode=TwoWay because that's the only mode that makes sense.

    As an aside, you should be using an INotifyCollectionChanged-compliant collection anyway because you can run into a leak[1] if you don't clear the binding when you're done. This isn't an issue in your single-window application, but it's worth mentioning now.

    As for the IsChecked toggling between sleeps, my educated guess is that Thread.Sleep is happening on the UI thread (and thus tying it up), so you've got 6 seconds of dead time in which PropertyChanged is useless. I was able to solve this with the following (assuming the proper collection type is being used):

    private async void Toggle()
    {
        MyDataContext.IsChecked = true;
        await Task.Delay(3000);
        MyDataContext.IsChecked = false;
        await Task.Delay(3000);
        MyDataContext.IsChecked = true;
    }
    

    and a call to Toggle() at the end of the App constructor. This unfortunately caused the app to try to modify the collection from a different thread which doesn't work. You could then solve that with something ridiculous like:

            ...
            Toggle(Application.Current.Dispatcher);
        }
    
        private async void Toggle(System.Windows.Threading.Dispatcher d)
        {
            d.Invoke(() => { MyDataContext.IsChecked = true; });
            await Task.Delay(3000);
            d.Invoke(() => { MyDataContext.IsChecked = false; });
            await Task.Delay(3000);
            d.Invoke(() => { MyDataContext.IsChecked = true; });
        }
    

    but that's just enforcing your program's poor structure. EDIT: I forgot to mention that using async/await has the additional benefit of freeing up the UI thread; it no longer locks up your entire window between check states.

    I suggest you separate your code into the proper files and then separate the logic into the proper locations. Your HandleDataContextPropertyChange could take place inside the setter for IsChecked, minus the Notify call.

    [1] https://blog.jetbrains.com/dotnet/2014/09/04/fighting-common-wpf-memory-leaks-with-dotmemory/