0

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:

  • user input in the Window reach the App's C# code fine: I can act on check box changes etc.
  • the opposite direction does not work: whenever items are changed in the dataContext via code, the Window is not automatically updated, even though iNotifyProperty is implemented and executed.

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:

  • I try to ensure that the DataContext on the Window is set before calling InitializeComponent on the Window. Does not really make a difference unfortunately ...
  • I get a single clue in VS in the MainWindow.xaml file: the CheckBox Path as well as the ListBox Binding NameList are annotated with Cannot resolve symbol due to unknown DataContext However, when the App constructor terminates the Window is updated and when I click the CheckBox, the correct NotifyProperty event is triggered. This tells me that the runtime bindings actually should work ... apparently only one-way, not two-way though.

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 ?

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Woelund
  • 25
  • 5
  • 1
    I suggest you delete all that weird `MyDataContext` stuff in `App` and just create the viewmodel in the MainWindow constructor or something. Also, always use `ObservableCollection` for collections that will be bound to controls in WPF. Don't use List. It doesn't raise events when its contents change. – 15ee8f99-57ff-4f92-890c-b56153 Sep 23 '16 at 15:45
  • 2
    `ItemsSource="{Binding NameList, Mode=TwoWay}"` doesn't make sense. An ItemsControl never changes its `ItemsSource` property, so that setting `Mode=TwoWay` never has any effect. Besides that, `MyDataContext.OnPropertyChanged("NameList");` is silently ignored because the NameList instance doesn't change. So use an ObservableCollection. – Clemens Sep 23 '16 at 16:06
  • Gentlemen - thank you ! Indeed, my main error was to not use an ObservableCollection. I also removed the ´Mode=TwoWay´ on the NameList binding and - as you mention, Clemens - it indeed had no effect (and worked perfectly without the TwoWay mode being explicitly set). Thanks for pointing that out. Ed, I will give the viewmodel creation in MainWindow a try. My code for now was more a feasibility check for myself. That (and my limited WPF knowledge) led to the unusual structure. I will correct that. – Woelund Sep 26 '16 at 10:05

1 Answers1

0

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/

Brandon Hood
  • 327
  • 2
  • 12
  • Thanks, Brandon - works like a charm ! Thanks a lot for the effort - much appreciated. I now saw as well that moving the mouse wheel down indeed updated the list ... interesting. Of course, my main mistake was the non-use of ObservableCollection. I also had tried to create a new thread at the end of the App constructor to toggle the IsChecked flag, but ran into the same problem you mention (collection not modifiable from different thread). Toggling via the dispatcher (I am learning things every day :-) ) works nicely. My code is a feasibility check - will improve the structure as you suggest. – Woelund Sep 26 '16 at 09:55