连接 CollectionChanged 和 PropertyChanged (或者:为什么某些 WPF 绑定不刷新?)

发布于 2024-10-06 18:02:09 字数 5423 浏览 9 评论 0原文

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

我想要发生的情况: 当我从列表框中选择一个条目并在文本框中修改其名称时,列表将更新以显示新值。

发生了什么:什么也没有。如果我有任何判断的话,这就是正确的行为。 我确保 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 处理程序中自动进行连接。

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 DataBindings used to make me happy. One thing I've stumbled over just now is that at some point they just don't refresh as intented. Please take a look at the following (fairly simple) code:

<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>

The Code behind for the 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;
    }
  }
}

Finally, here's the ViewModel classes:

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

What I'd want to happen: As I select an entry from the ListBox and modify its Name in the TextBox, the list gets updated to display the new value.

What happens : nothing. And that is the correct behaviour, If I'm any judge.
I made sure that the SelectedItem's PropertyChanged is fired, but that does (of course) not cause CollectionChanged to be fired.

To fix this, I created an ObservableCollection-derived class that has a public OnCollectionChanged method, see here :

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

I access this from the ViewModel's constructor, as described below:

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

It works, but it's not a clean solution. My next idea would be creating a derivate of ObservableCollection that does the wiring up automatically in a 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));
    }
}

The question is : is there a better way to do this? Is this really necessary?

Thanks a lot in advance for any input!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

深海少女心 2024-10-13 18:02:09

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

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

如果您将 DisplayMemberPath="Name" 添加到列表框,它将生成一个正确绑定到您的Name 的模板 - 然后该模板将根据您的情况自动更新d 期望。

No, it's not necessary at all. The reason your sample is failing is subtle, but quite simple.

If you don't provide WPF with a template for a data item (such as the Person objects in your list), it'll default to using the ToString() method to display. That's a member, not a property, and so you get no event notification when the value changes.

If you add DisplayMemberPath="Name" to your listbox, it'll generate a template that binds properly to the Name of your person - which will then update automatically as you'd expect.

养猫人 2024-10-13 18:02:09

我相信这与您对 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>

I believe this is to do with your ToString() override on PersonViewModel. If you remove this, and use a DataTemplate on the ListBox instead, then you should get your expected behaviour:

<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>

谜泪 2024-10-13 18:02:09

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

add DisplayMemberPath="Name" to ListBox. The problem is you are relying on ToString() to display person's name and not any property. That is why raising PropertyChanged does not make any difference. From now on, don't use a method to evaluate any value in Bindings.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文