【问题标题】:WPF two way binding not working on CheckBox and List<string>WPF 两种方式绑定在 CheckBox 和 List<string> 上不起作用
【发布时间】:2016-09-23 15:34:24
【问题描述】:

即使在查看了大量解决方案建议之后,我也无法在 xaml 中使用简单的双向绑定。我有一个窗口、一个 dataContext 和一个应用程序。问题是:

a) 当 App 构造函数运行时,窗口(在同一个构造函数中初始化和.Show-ed)出现,但根本没有更新,即使我在 C# 代码中切换了复选框值几次;

b) 当 App 构造函数完成时,Window 只更新一次;我已经设置好了,如果我单击窗口中的复选框,App 中的事件处理程序(绑定到 DataContext 属性更改通知)应该会增加字符串列表的大小,该列表也会显示出来。列表的增加在代码中正确发生,但没有反映在 Window 中。

总结

  • Window 中的用户输入可以很好地到达应用程序的 C# 代码:我可以对复选框更改等进行操作。
  • 相反的方向不起作用:每当通过代码更改 dataContext 中的项目时,即使实现并执行了 iNotifyProperty,窗口也不会自动更新。

我所期望的是

a) 当 App 构造函数运行并切换 CheckBox 值时,窗口应通过设置/清除框上的勾号来反映更改;

b) 在 App 构造函数完成后,每当我将 CheckBox 从 FALSE 切换为 TRUE 时,NameList 都会附加一个新字符串。我希望 Window 中的列表会相应增加并自动显示完整的、附加的 NameList 内容。

观察

  • 我尝试确保在Window 上调用InitializeComponent 之前设置了Window 上的DataContext。不幸的是,并没有真正的不同...
  • 我在 VS 中的 MainWindow.xaml 文件中得到了一条线索:CheckBox Path 和 ListBox Binding NameList 被注释为 Cannot resolve symbol due to unknown DataContext 但是,当 App 构造函数终止时,Window 会更新并且当我单击 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()
    {            
    }
}

}

App 和 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,就会出现以下窗口:

一旦 App 构造器完成,Window 会更新一次,但以后不会再更新,无论有多少字符串添加到 NameList

任何想法,为什么我的双向绑定只能在一个方向上工作?

【问题讨论】:

  • 我建议你删除 App 中所有奇怪的 MyDataContext 东西,然后在 MainWindow 构造函数或其他东西中创建视图模型。此外,对于将绑定到 WPF 中的控件的集合,请始终使用 ObservableCollection&lt;T&gt;。不要使用列表。当其内容发生变化时,它不会引发事件。
  • ItemsSource="{Binding NameList, Mode=TwoWay}" 没有意义。 ItemsControl 永远不会更改其ItemsSource 属性,因此设置Mode=TwoWay 永远不会有任何效果。除此之外,MyDataContext.OnPropertyChanged("NameList"); 被静默忽略,因为 NameList 实例没有改变。所以使用 ObservableCollection。
  • 先生们-谢谢!事实上,我的主要错误是不使用 ObservableCollection。我还删除了 NameList 绑定上的“Mode=TwoWay”,并且 - 正如您提到的,Clemens - 它确实没有任何效果(并且在没有明确设置 TwoWay 模式的情况下完美地工作)。感谢您指出了这一点。 Ed,我将尝试在 MainWindow 中创建视图模型。我现在的代码更像是对我自己的可行性检查。那(以及我有限的 WPF 知识)导致了不寻常的结构。我会改正的。

标签: wpf xaml binding two-way


【解决方案1】:

如果绑定集合没有实现INotifyCollectionChanged(例如ObservableCollection&lt;T&gt;),则在尝试更新视图时会出现不一致或不存在的行为。我注意到在将检查状态切换为 true 后轻弹鼠标滚轮时,列表确实会更新。另外,正如@Clemens 所说,您的ItemsSource 绑定应该是Mode=TwoWay,因为这是唯一有意义的模式。

顺便说一句,您应该使用符合INotifyCollectionChanged 的集合,因为如果您在完成后不清除绑定,您可能会遇到泄漏[1]。这在您的单窗口应用程序中不是问题,但现在值得一提。

至于 IsChecked 在睡眠之间切换,我有根据的猜测是 Thread.Sleep 正在 UI 线程上发生(因此将其绑定),因此您有 6 秒的死时间,其中 PropertyChanged没用。我可以通过以下方法解决这个问题(假设使用了正确的集合类型):

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

并在App 构造函数的末尾调用Toggle()。不幸的是,这导致应用程序尝试从另一个不起作用的线程修改集合。然后你可以用一些荒谬的东西来解决那个

        ...
        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 有释放 UI 线程的额外好处;它不再锁定检查状态之间的整个窗口。

我建议您将代码分离到适当的文件中,然后将逻辑分离到适当的位置。您的 HandleDataContextPropertyChange 可以在 IsChecked 的设置器中发生,减去 Notify 调用。

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

【讨论】:

  • 谢谢,布兰登 - 就像一个魅力!非常感谢您的努力 - 非常感谢。我现在也看到向下移动鼠标滚轮确实更新了列表……很有趣。当然,我的主要错误是没有使用 ObservableCollection。我还尝试在 App 构造函数的末尾创建一个新线程来切换 IsChecked 标志,但遇到了您提到的相同问题(无法从不同线程修改集合)。通过调度程序切换(我每天都在学习东西:-))效果很好。我的代码是一个可行性检查 - 将按照您的建议改进结构。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-11-13
  • 1970-01-01
  • 2013-04-12
  • 2013-08-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多