当ObservableCollection中的项目更新时,如何更新ItemsControl?

15

问题:

  • 在视图中声明了一个ItemsControl(或从ItemsControl派生的控件)。
  • ItemsControl.ItemsSource属性绑定到ViewModel中的ObservableCollection
  • 当添加/删除ObservableCollection中的项时,您的视图会按预期更新。
  • 但是,当您更改ObservableCollection中项目的属性时,视图不会更新。

背景信息:

看起来这是许多WPF开发人员遇到的常见问题。已经有人问过几次:

当项更改时通知ObservableCollection

ObservableCollection在其中更改项时没有注意到(即使使用INotifyPropertyChanged)

ObservableCollection和Item PropertyChanged

我的实现:

我尝试实现Notify ObservableCollection when Item changes中的被接受的解决方案。基本思想是在MainWindowViewModel中为ObservableCollection的每个项连接一个PropertyChanged处理程序。当更改项的属性时,事件处理程序将被调用并且以某种方式更新视图。

我无法让实现工作。这是我的实施方式。

ViewModels:

class ViewModelBase : INotifyPropertyChanged 
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName = "")
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

项目视图模型:

class EmployeeViewModel : ViewModelBase
{
    private int _age;
    private string _name;

    public int Age 
    {
        get { return _age; }
        set
        {
            _age = value;
            RaisePropertyChanged("Age");
        }
    }

    public string Name  
    {
        get { return _name; }
        set
        {
            _name = value;
            RaisePropertyChanged("Name");
        }
    }

    public override string ToString()
    {
        return string.Format("{0} is {1} years old", Name, Age);
    }
}
主窗口视图模型:
class MainWindowViewModel : ViewModelBase
{
    private ObservableCollection<EmployeeViewModel> _collection;

    public MainWindowViewModel()
    {
        _collection = new ObservableCollection<EmployeeViewModel>();
        _collection.CollectionChanged += MyItemsSource_CollectionChanged;

        AddEmployeeCommand = new DelegateCommand(() => AddEmployee());
        IncrementEmployeeAgeCommand = new DelegateCommand(() => IncrementEmployeeAge());
    }

    public ObservableCollection<EmployeeViewModel> Employees 
    {
        get { return _collection; }
    }

    public ICommand AddEmployeeCommand { get; set; }
    public ICommand IncrementEmployeeAgeCommand { get; set; }

    public void AddEmployee()
    {
        _collection.Add(new EmployeeViewModel()
            {
                Age = 1,
                Name = "Random Joe",
            });
    }

    public void IncrementEmployeeAge()
    {
        foreach (var item in _collection)
        {
            item.Age++;
        }
    }

    private void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
            foreach (EmployeeViewModel item in e.NewItems)
                item.PropertyChanged += ItemPropertyChanged;

        if (e.OldItems != null)
            foreach (EmployeeViewModel item in e.OldItems)
                item.PropertyChanged -= ItemPropertyChanged;
    }

    private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        RaisePropertyChanged("Employees");
    }
}

查看:

<Window x:Class="WpfApplication2.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"
    xmlns:d="clr-namespace:Iress.IosPlus.DynamicOE.Controls"
    Title="MainWindow" Height="350" Width="350">

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="0.3*"></ColumnDefinition>
        <ColumnDefinition Width="0.7*"></ColumnDefinition>
    </Grid.ColumnDefinitions>

    <StackPanel Grid.Column="0">
        <Button Command="{Binding AddEmployeeCommand}">Add Employee</Button>
        <Button Command="{Binding IncrementEmployeeAgeCommand}">Increment Employee Age</Button>
    </StackPanel>

    <Grid Grid.Column="1">
        <Grid.RowDefinitions>
            <RowDefinition Height="0.1*"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Text="{Binding Path=Employees[0]}"></TextBlock>
        <ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Employees}" BorderBrush="Red" BorderThickness="1"></ItemsControl>
    </Grid>
</Grid>

我的结果:

为了验证我的实现,我创建了一个视图。 TextBlock.Text 绑定到集合的第一个项目。 ItemsControl 绑定到集合本身。

  • 按下“添加员工”按钮会在集合中添加一个 EmployeeViewModel 对象,TextBlockItemsControl 都将按预期更新。
  • 再次按下“添加员工”,ItemsControl 将更新为另一个条目。太好了!
  • 按下“增加员工年龄”按钮,每个项目的 Age 属性都会增加 1。 PropertyChanged 事件被触发。调用 ItemPropertyChanged 事件处理程序。 Textblock 将按预期更新。然而,ItemsControl 没有更新。

根据在Notify ObservableCollection when Item changes中的答案,我认为当 Employee.Age 更改时,ItemsControl 也应该更新。

enter image description here


你到底想做什么?可观察集合是观察集合本身,而不是子项的属性。如果子项的属性发生变化,进行集合更改事件有什么好处? - michael
1
@michael,我希望在集合中的某个项目更新时,ItemsControl能够刷新。 - Frank Liu
然而,这样做有什么意义呢?集合中的所有项都已经被考虑到了。让UI获取属性不会改变任何东西...引用仍然是相同的。 - michael
1个回答

12

我使用Snoop来调试XAML,找到了答案。

问题是你试图绑定到ToString()方法,但这不会引发PropertyChanged事件。如果你查看XAML绑定,你会注意到ObservableCollection实际上正在改变。

Snoop Showing Correct Binding

现在看看每个项控件及其文本在“Text”属性中的绑定。没有任何绑定,只是文本。

Snoop showing no data binding to elements in the items control

要解决此问题,只需添加一个ItemsControl ItemTemplate,其中包含要显示的元素的DataTemplate。

<ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Employees, UpdateSourceTrigger=PropertyChanged}" BorderBrush="Red" BorderThickness="1" >
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding StringFormat=" {0} is {1} years old">
                        <Binding Path="Name"/>
                        <Binding Path="Age"/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

现在我们已经可以进行绑定了。RaisePropertyChanged被调用。

正确显示绿色的绑定

哒哒!

显示解决方案


谢谢回复。我为Employees属性(ObservableCollection)添加了一个setter,并在setter中调用了RaisePropertyChanged。但是当按下增加员工年龄按钮时,它并没有触发ItemsControl更新。 - Frank Liu
2
@FrankLiu 声明一个ItemTemplate是在ItemsControl或ListBox等派生控件中可视化项的标准方式。您可能想阅读MSDN上的数据模板概述文章。 - Clemens
好眼力,我甚至没有注意到 OP 依赖 .ToString() 来显示项目!你是正确的,正确的方法是为显示项目创建一个 Template,并将 UI 元素绑定到属性,以便它们对更改通知做出反应。@FrankLiu 如果您想避免为 ItemsControl 定义 ItemTemplate,您也可以使用隐式 DataTemplate。这是一种告诉 WPF,每当它在 Visual Tree 中遇到 EmployeeViewModel 类型的项时,它应该使用指定的模板来呈现它,而不是默认的 .ToString()。 - Rachel
2
@FrankLiu 我认为你混淆了两者的使用场景。DataTemplate 的作用仅仅是展示 XAML 要展示的内容,它并不会确保当属性更新时也会更新 XAML。它并不关心这些。至于 INotifyPropertyChanged 的使用,你并不一定需要它。只有在试图确保当其他人修改该属性时它会在 UI 中更改时才需要它。请参考这里 - AzzamAziz
@AzzamAziz,我认为你误解了Rachel的解决方案的意图。它只是在集合中更新项目时向ViewModel提供通知。该通知不是用于刷新ItemsControl的UI。再次感谢您的回答。还有其他方法可以解决问题,例如使用DisplayMemberPath来更新视图。或者子类化ObservableCollection以在Item PropertyChanged时生成CollectionChanged事件。无论如何,感谢大家的帮助。 - Frank Liu
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接