【问题标题】:Wiring up CollectionChanged and PropertyChanged (Or : Why do some WPF Bindings not refresh?)连接 CollectionChanged 和 PropertyChanged(或者:为什么某些 WPF 绑定不刷新?)
【发布时间】:2011-05-22 02:13:01
【问题描述】:

WPF DataBindings 曾经让我很开心。我刚才偶然发现的一件事是,在某些时候它们只是没有按预期刷新。请看下面(相当简单的)代码:

<Window x:Class="CVFix.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="300">
  <Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition Height="40"></RowDefinition>
    </Grid.RowDefinitions>
    <ListBox Grid.Row="0" Grid.Column="0" 
                  ItemsSource="{Binding Path=Persons}"
                  SelectedItem="{Binding Path=SelectedPerson}"
                  x:Name="lbPersons"></ListBox>
    <TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=SelectedPerson.Name, UpdateSourceTrigger=PropertyChanged}"/>
  </Grid>
</Window>

XAML 背后的代码:

using System.Windows;
namespace CVFix
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    public ViewModel Model { get; set; }

    public MainWindow()
    {
        InitializeComponent();
        this.Model = new ViewModel();
        this.DataContext = this.Model;
    }
  }
}

最后是 ViewModel 类:

using System.Collections.ObjectModel;
using System.ComponentModel;

namespace CVFix
{
  public class ViewModel : INotifyPropertyChanged
  {
    private PersonViewModel selectedPerson;

    public PersonViewModel SelectedPerson
    {
        get { return this.selectedPerson; }
        set
        {
            this.selectedPerson = value;

            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs("SelectedPerson"));
        }
    }

    public ObservableCollection<PersonViewModel> Persons { get; set; }

    public ViewModel()
    {
        this.Persons = new ObservableCollection<PersonViewModel>();
        this.Persons.Add(new PersonViewModel() { Name = "Adam" });
        this.Persons.Add(new PersonViewModel() { Name = "Bobby" });
        this.Persons.Add(new PersonViewModel() { Name = "Charles" });
    }

    public event PropertyChangedEventHandler PropertyChanged;
  }
}

public class PersonViewModel : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            this.name = value;
            if(this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs("Name"));
        }
    }

    public override string ToString()
    {
        return this.Name;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

我想要发生的事情:当我从 ListBox 中选择一个条目并在 TextBox 中修改其名称时,列表会更新以显示新值。

会发生什么:什么都没有。这是正确的行为,如果我是任何法官。 我确保 SelectedItem 的 PropertyChanged 被触发,但这(当然)不会导致 CollectionChanged 被触发。

为了解决这个问题,我创建了一个具有公共 OnCollectionChanged 方法的 ObservableCollection 派生类,请参见此处:

public class PersonList : ObservableCollection<PersonViewModel>
{
    public void OnCollectionChanged()
    {
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ));
    }
}

我从 ViewModel 的构造函数中访问它,如下所述:

    public ViewModel()
    {
        PersonViewModel vm1 = new PersonViewModel()
        {
            Name = "Adam"
        };
        PersonViewModel vm2 = new PersonViewModel()
        {
            Name = "Bobby"
        };
        PersonViewModel vm3 = new PersonViewModel()
        {
            Name = "Charles"
        };
        vm1.PropertyChanged += this.PersonChanged;

        this.Persons = new PersonList();


        this.Persons.Add(vm1);
        this.Persons.Add(vm2);
        this.Persons.Add(vm3);
    }

    void PersonChanged(object sender, PropertyChangedEventArgs e)
    {
        this.Persons.OnCollectionChanged();
    }

它有效,但它不是一个干净的解决方案。我的下一个想法是创建 ObservableCollection 的派生类,它会在 CollectionChanged-handler 中自动进行连接。

public class SynchronizedObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                {
                    foreach (INotifyPropertyChanged item in e.NewItems)
                    {
                        item.PropertyChanged += this.ItemChanged;
                    }
                    break;
                }

            case NotifyCollectionChangedAction.Remove:
                {
                    foreach (INotifyPropertyChanged item in e.OldItems)
                    {
                        item.PropertyChanged -= this.ItemChanged;
                    }
                    break;
                }
        }
        base.OnCollectionChanged(e);
    }

    void ItemChanged(object sender, PropertyChangedEventArgs e)
    {
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

问题是:有没有更好的方法来做到这一点?这真的有必要吗?

非常感谢您的任何意见!

【问题讨论】:

    标签: wpf data-binding observablecollection inotifypropertychanged


    【解决方案1】:

    不,根本没有必要。您的样本失败的原因很微妙,但很简单。

    如果您没有为 WPF 提供数据项的模板(例如列表中的 Person 对象),它将默认使用 ToString() 方法来显示。那是一个成员,而不是一个属性,因此当值更改时您不会收到任何事件通知。

    如果您将DisplayMemberPath="Name" 添加到您的列表框,它会生成一个模板,该模板会正确绑定到您的人的Name - 然后会按照您的预期自动更新。

    【讨论】:

    • 非常感谢,您的答案是最具描述性的答案(其他答案也一样)。
    【解决方案2】:

    我相信这与您对 PersonViewModel 的 ToString() 覆盖有关。如果您删除它,并在 ListBox 上使用 DataTemplate,那么您应该会得到预期的行为:

    <Window x:Class="CVFix.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition Height="40"></RowDefinition>
        </Grid.RowDefinitions>
        <ListBox Grid.Row="0" Grid.Column="0" 
                  ItemsSource="{Binding Path=Persons}"
                  SelectedItem="{Binding Path=SelectedPerson}"
                  x:Name="lbPersons">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>            
        </ListBox>
        <TextBox Grid.Row="1" Grid.Column="0" Text="{Binding Path=SelectedPerson.Name, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
    

    【讨论】:

      【解决方案3】:

      DisplayMemberPath="Name" 添加到ListBox。问题是您依靠ToString() 来显示人名而不是任何财产。这就是为什么提高PropertyChanged 没有任何区别。从现在开始,不要使用方法来评估 Bindings 中的任何值。

      【讨论】:

      • 非常感谢您的回答!
      猜你喜欢
      • 2011-07-29
      • 2021-08-16
      • 1970-01-01
      • 1970-01-01
      • 2011-03-23
      • 2013-08-13
      • 1970-01-01
      • 2011-12-23
      • 1970-01-01
      相关资源
      最近更新 更多