Двусторонняя привязка WPF не работает с CheckBox и List‹string›

Даже после просмотра множества предложений решений я не могу заставить работать простую двухстороннюю привязку в xaml. У меня есть окно, dataContext и приложение. Проблема в том, что:

а) во время работы конструктора приложения окно (инициализированное и .Show-ed в том же конструкторе) появляется, но вообще не обновляется, хотя я несколько раз переключаю значение флажка в своем коде C#;

б) когда конструктор приложения завершает работу, окно обновляется ровно один раз; Я настроил его так, что если я щелкну флажок в окне, обработчик событий в приложении (привязанный к уведомлению об изменении свойства DataContext) должен увеличить размер списка строк, который также отображается. Увеличение списка происходит корректно в коде, но не отражается в окне.

Обзор:

  • пользовательский ввод в окне достигает кода С# приложения в порядке: я могу действовать при изменении флажка и т. д.
  • обратное направление не работает: всякий раз, когда элементы в dataContext изменяются с помощью кода, окно не обновляется автоматически, даже если iNotifyProperty реализовано и выполняется.

Я ожидаю, что:

а) в то время как конструктор приложения запускается и переключает значение CheckBox, окно должно отражать изменения, устанавливая/снимая галочку в поле;

б) после завершения конструктора приложения, всякий раз, когда я переключаю CheckBox с FALSE на TRUE, к NameList добавляется новая строка. Я ожидаю, что список в окне соответственно увеличится и автоматически покажет полное добавленное содержимое NameList.

Наблюдения:

  • Я пытаюсь убедиться, что DataContext в окне установлен перед вызовом InitializeComponent в окне. разницы особо нет к сожалению...
  • Я получаю единственную подсказку в VS в файле MainWindow.xaml: путь CheckBox, а также привязка ListBox NameList аннотируются с помощью Cannot resolve symbol due to unknown DataContext Однако, когда конструктор приложения завершает работу, окно обновляется, и когда я нажимаю CheckBox, запускается правильное событие NotifyProperty. Это говорит мне о том, что привязки времени выполнения на самом деле должны работать ... по-видимому, только в одну сторону, а не в две стороны.

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()
    {            
    }
}

}

Приложение и 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;
        }       
    }
}

Когда я запускаю приложение, появляется следующее окно, как только конструктор приложения достигает .Show:

введите здесь описание изображения

Как только конструктор приложения завершает работу, окно обновляется один раз, но никогда больше после этого, независимо от того, сколько строк добавлено к NameList:

введите здесь описание изображения

Любые идеи, почему моя двусторонняя привязка работает только в одном направлении?


person Woelund    schedule 23.09.2016    source источник
comment
Я предлагаю вам удалить все эти странные MyDataContext вещи в App и просто создать модель представления в конструкторе MainWindow или что-то в этом роде. Кроме того, всегда используйте ObservableCollection<T> для коллекций, которые будут привязаны к элементам управления в WPF. Не используйте Список. Он не вызывает событий при изменении его содержимого.   -  person 15ee8f99-57ff-4f92-890c-b56153    schedule 23.09.2016
comment
ItemsSource="{Binding NameList, Mode=TwoWay}" не имеет смысла. ItemsControl никогда не изменяет свое свойство ItemsSource, поэтому параметр Mode=TwoWay никогда не имеет никакого эффекта. Кроме того, MyDataContext.OnPropertyChanged("NameList"); молча игнорируется, потому что экземпляр NameList не изменяется. Поэтому используйте ObservableCollection.   -  person Clemens    schedule 23.09.2016
comment
Господа - спасибо! Действительно, моя главная ошибка заключалась в том, что я не использовал ObservableCollection. Я также удалил «Mode=TwoWay» в привязке NameList, и, как вы упомянули, Клеменс, это действительно не имело никакого эффекта (и отлично работало без явного задания режима TwoWay). Спасибо что подметил это. Эд, я попробую создать модель представления в MainWindow. Мой код на данный момент был скорее проверкой осуществимости для себя. Это (и мои ограниченные знания WPF) привели к необычной структуре. Я исправлю это.   -  person Woelund    schedule 26.09.2016


Ответы (1)


Если связанная коллекция не реализует INotifyCollectionChanged (например, ObservableCollection<T>), вы получите противоречивое или несуществующее поведение при попытке обновить представление. Я заметил, что список действительно будет обновляться при щелчке колеса прокрутки моей мыши после переключения состояния проверки на true. Кроме того, как сказал @Clemens, ваша привязка ItemsSource должна быть Mode=TwoWay, потому что это единственный режим, который имеет смысл.

Кроме того, вы все равно должны использовать INotifyCollectionChanged-совместимую коллекцию, потому что вы можете столкнуться с утечкой[1], если не очистите привязку, когда закончите. Это не проблема в вашем однооконном приложении, но об этом стоит упомянуть сейчас.

Что касается IsChecked переключения между режимами сна, то, по моему обоснованному мнению, Thread.Sleep происходит в потоке пользовательского интерфейса (и, таким образом, связывает его), поэтому у вас есть 6 секунд простоя, в течение которых PropertyChanged бесполезен. Мне удалось решить эту проблему следующим образом (при условии, что используется правильный тип коллекции):

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

и вызов Toggle() в конце конструктора App. К сожалению, это привело к тому, что приложение попыталось изменить коллекцию из другого потока, который не работает. Затем вы можете решить это с помощью чего-то нелепого, например:

        ...
        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; });
    }

но это просто усиливает плохую структуру вашей программы. РЕДАКТИРОВАТЬ: я забыл упомянуть, что использование async/await имеет дополнительное преимущество, заключающееся в освобождении потока пользовательского интерфейса; он больше не блокирует все окно между состояниями проверки.

Я предлагаю вам разделить код на соответствующие файлы, а затем разделить логику на соответствующие места. Ваш HandleDataContextPropertyChange может иметь место внутри сеттера для IsChecked, за вычетом вызова Notify.

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

person Brandon Hood    schedule 25.09.2016
comment
Спасибо, Брэндон - работает как шарм! Большое спасибо за усилия - очень признателен. Теперь я также увидел, что перемещение колесика мыши вниз действительно обновило список ... интересно. Конечно, моей главной ошибкой было неиспользование ObservableCollection. Я также пытался создать новый поток в конце конструктора приложения, чтобы переключить флаг IsChecked, но столкнулся с той же проблемой, о которой вы упоминаете (коллекция не может быть изменена из другого потока). Переключение через диспетчер (я учусь чему-то каждый день :-)) работает прекрасно. Мой код является проверкой осуществимости - улучшит структуру, как вы предлагаете. - person Woelund; 26.09.2016