Проблемы WPF с перехватом событий CollectionChanged

У меня есть сетка данных, в которой мне нужно рассчитать общую сумму столбца «Цена» вложенной сетки данных, например:

Изображение

Я пытаюсь следовать этому примеру, чтобы моя наблюдаемая коллекция Items для каждого объекта Person получала уведомления об изменениях. Разница в том, что я реализую его внутри класса, а не модели представления.

public class Person : NotifyObject
    {
        private ObservableCollection<Item> _items;
        public ObservableCollection<Item> Items
        {
            get { return _items; }
            set { _items = value; OnPropertyChanged("Items"); }
        }
        private string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value; OnPropertyChanged("Name"); }
        }
        public double Total
        {
            get { return Items.Sum(i => i.Price); }
            set { OnPropertyChanged("Total"); }
        }

        public Person()
        {
            Console.WriteLine("0001 Constructor");
            this.Items = new ObservableCollection<Item>();
            this.Items.CollectionChanged += Items_CollectionChanged;
            this.Items.Add(new Item());
        }
        private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Console.WriteLine("0002 CollectionChanged");
            if (e.NewItems != null)
                foreach (Item item in e.NewItems)
                    item.PropertyChanged += Items_PropertyChanged;

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

        private void Items_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Console.WriteLine("0003 PropertyChanged");
            this.Total = Items.Sum(i => i.Price);
        }
    }

Код внутри конструктора не перехватывает события при инициализации нового элемента или изменении существующего. Поэтому событие Items_PropertyChanged никогда не срабатывает. Я могу только обновить весь список вручную. Что я здесь делаю неправильно?

Или, может быть, есть другой подход к подсчету суммы для списка покупок каждого человека?

Ниже приведен весь код, если кому-то тоже интересно посмотреть на него.

XAML

<Window x:Class="collection_changed_2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:collection_changed_2"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="Height" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <DataGrid x:Name="DataGrid1"
                  Grid.Row="0"
                  ItemsSource="{Binding DataCollection}"
                  SelectedItem="{Binding DataCollectionSelectedItem}"
                  AutoGenerateColumns="False" 
                  CanUserAddRows="false" >
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="2*"/>
                <DataGridTemplateColumn Header="Item/Price" Width="3*">
                    <DataGridTemplateColumn.CellTemplate >
                        <DataTemplate>
                            <DataGrid x:Name="DataGridItem" 
                                      ItemsSource="{Binding Items}"
                                      SelectedItem="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ItemsSelectedItem}"
                                      Background="Transparent"
                                      HeadersVisibility="None"
                                      AutoGenerateColumns="False"
                                      CanUserAddRows="false" >
                                <DataGrid.Columns>
                                    <DataGridTextColumn Binding="{Binding ItemName}" Width="*"/>
                                    <DataGridTextColumn Binding="{Binding Price}" Width="50"/>
                                    <DataGridTemplateColumn Header="Button" Width="Auto">
                                        <DataGridTemplateColumn.CellTemplate>
                                            <DataTemplate>
                                                <StackPanel>
                                                    <Button  Content="+"
                                                             Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.AddItem }"
                                                             Width="20" Height="20">
                                                    </Button>
                                                </StackPanel>
                                            </DataTemplate>
                                        </DataGridTemplateColumn.CellTemplate>
                                    </DataGridTemplateColumn>
                                </DataGrid.Columns>
                            </DataGrid>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTextColumn Header="Total" Binding="{Binding Total, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="Auto"/>
                <DataGridTemplateColumn Header="Buttons" Width="Auto">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel VerticalAlignment="Center">
                                <Button  Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.AddPerson}" Width="20" Height="20">+</Button>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Grid.Row="1" Margin="10">
            <Button  Width="150" Height="30"  Content="Refresh" Command="{Binding Refresh}" />
        </StackPanel>
    </Grid>
</Window>

C#

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;

namespace collection_changed_2
{
    public class Item : NotifyObject
    {
        private string _itemName;
        public string ItemName
        {
            get { return _itemName; }
            set { _itemName = value; OnPropertyChanged("ItemName"); }
        }
        private double _price;
        public double Price
        {
            get { return _price; }
            set { _price = value; OnPropertyChanged("Price"); }
        }
    }

    public class Person : NotifyObject
    {
        private ObservableCollection<Item> _items;
        public ObservableCollection<Item> Items
        {
            get { return _items; }
            set { _items = value; OnPropertyChanged("Items"); }
        }
        private string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value; OnPropertyChanged("Name"); }
        }
        public double Total
        {
            get { return Items.Sum(i => i.Price); }
            set { OnPropertyChanged("Total"); }
        }

        public Person()
        {
            Console.WriteLine("0001 Constructor");
            this.Items = new ObservableCollection<Item>();
            this.Items.CollectionChanged += Items_CollectionChanged;
            this.Items.Add(new Item());
        }
        private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Console.WriteLine("0002 CollectionChanged");
            if (e.NewItems != null)
                foreach (Item item in e.NewItems)
                    item.PropertyChanged += Items_PropertyChanged;

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

        private void Items_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Console.WriteLine("0003 PropertyChanged");
            this.Total = Items.Sum(i => i.Price);
        }
    }

    public abstract class NotifyObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    public class RelayCommand : ICommand
    {
        private Action<object> executeDelegate;
        readonly Predicate<object> canExecuteDelegate;

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new NullReferenceException("execute");
            executeDelegate = execute;
            canExecuteDelegate = canExecute;
        }

        public RelayCommand(Action<object> execute) : this(execute, null) { }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return canExecuteDelegate == null ? true : canExecuteDelegate(parameter);
        }

        public void Execute(object parameter)
        {
            executeDelegate.Invoke(parameter);
        }
    }

    public class ViewModel : NotifyObject
    {
        public ObservableCollection<Person> DataCollection { get; set; }

        public Person DataCollectionSelectedItem { get; set; }
        public Item ItemsSelectedItem { get; set; }

        public RelayCommand AddPerson { get; private set; }
        public RelayCommand AddItem { get; private set; }
        public RelayCommand Refresh { get; private set; }

        public ViewModel()
        {
            DataCollection = new ObservableCollection<Person>
            {
                new Person() {
                    Name = "Friedrich Nietzsche",
                    Items = new ObservableCollection<Item> {
                        new Item { ItemName = "Phone", Price = 220 },
                        new Item { ItemName = "Tablet", Price = 350 },
                    }
                },
                new Person() {
                    Name = "Jean Baudrillard",
                    Items = new ObservableCollection<Item> {
                        new Item { ItemName = "Teddy Bear Deluxe", Price = 2200 },
                        new Item { ItemName = "Pokemon", Price = 100 }
                    }
                 }
            };

            AddItem = new RelayCommand(AddItemCode, null);
            AddPerson = new RelayCommand(AddPersonCode, null);
            Refresh = new RelayCommand(RefreshCode, null);
        }

        public void AddItemCode(object parameter)
        {
            var collectionIndex = DataCollection.IndexOf(DataCollectionSelectedItem);
            var itemIndex = DataCollection[collectionIndex].Items.IndexOf(ItemsSelectedItem);
            Item newItem = new Item() { ItemName = "Item_Name", Price = 100 };
            DataCollection[collectionIndex].Items.Insert(itemIndex + 1, newItem);
        }
        public void AddPersonCode(object parameter)
        {
            var collectionIndex = DataCollection.IndexOf(DataCollectionSelectedItem);
            Person newList = new Person()
            {
                Name = "New_Name",
                Items = new ObservableCollection<Item>() { new Item() { ItemName = "Item_Name", Price = 100 } }
            };
            DataCollection.Insert(collectionIndex + 1, newList);
        }
        private void RefreshCode(object parameter)
        {
            CollectionViewSource.GetDefaultView(DataCollection).Refresh();
        }
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new ViewModel();
        }
    }
}

person Disodium    schedule 28.03.2017    source источник


Ответы (3)


Не используйте обработчики событий между ViewModels - это черная магия и может привести к утечкам памяти из-за созданных ссылок.

public interface IUpdateSum
{
    void UpdateSum();
}


public class Person : IUpdateSum
{

    /* ... */

    public void UpdateSum()
    {
        this.Total = Items.Sum(i => i.Price);
    }


    /* ... */
}


public class Item
{
    private IUpdateSum SumUpdate;

    private double price;

    public Item(IUpdateSum sumUpdate)
    {
        SumUpdate = sumUpdate;
    }

    public double Price
    {
        get
        {
            return price;
        }
        set
        {
            RaisePropertyChanged("Price");
            SumUpdate.UpdateSum();
        }
    }
}

Я знаю, что это не красиво, но это работает

person Peter    schedule 28.03.2017
comment
Я пытаюсь следовать вашей логике. Что мне нужно передать в качестве аргумента объекту Item при его инициализации в ViewModel? var smtn = новый товар (?) { ItemName = Phone, Price = 220 }; - person Disodium; 29.03.2017

Думаю есть простое решение...

 private void Items_CollectionChanged(object sender,NotifyCollectionChangedEventArgs e)
    {
        Console.WriteLine("0002 CollectionChanged");
        if (e.NewItems != null)
            foreach (Item item in e.NewItems)
                item.PropertyChanged += Items_PropertyChanged;

        if (e.OldItems != null)
            foreach (Item item in e.OldItems)
                item.PropertyChanged -= Items_PropertyChanged;
        this.Total = Items.Sum(i => i.Price);
    }

Как правило, общая сумма будет меняться при изменении списка. Вам по-прежнему нужна другая сумма на случай, если цена на товар изменится... но это менее распространенная ситуация.

person AQuirky    schedule 28.03.2017
comment
Моя проблема в том, что событие Items_CollectionChanged никогда не срабатывает, когда я добавляю элемент в наблюдаемую коллекцию. Так что эта строка кода не выполняется. - person Disodium; 29.03.2017
comment
Событие Items_CollectionChanged сработает. Событие Items_PropertyChanged не сработает. Единственная причина, по которой событие Items_CollectIonChanged не срабатывает при добавлении элемента в коллекцию, заключается в том, что существует другой обработчик событий, выдающий исключение. - person AQuirky; 29.03.2017

В конце концов я понял, что не так с моим исходным кодом. Я использовал этот конструктор:

public Person()
        {
            this.Items = new ObservableCollection<Item>();
            this.Items.CollectionChanged += Items_CollectionChanged;
            this.Items.Add(new Item());
        }

Затем прикрепленное событие было эффективно перезаписано этим инициализатором:

Person newList = new Person()
            {
                Name = "New_Name",
                Items = new ObservableCollection<Item>() { new Item() { ItemName = "Item_Name", Price = 100 } }
            };

Вот почему событие никогда не срабатывало. Его там не было! Правильный способ - использовать параметрический конструктор:

public Person(string initName, ObservableCollection<Item> initItems)
        {
            this.Name = initName;
            this.Items = new ObservableCollection<Item>();
            this.Items.CollectionChanged += Items_CollectionChanged;
            foreach (Item item in initItems)
                this.Items.Add(item);
        }

И затем инициализируйте его так:

Person newList = new Person("New_Name", new ObservableCollection<Item>() { new Item() { ItemName = "Item_Name", Price = 100 } });

И это было все. Теперь работает как шарм. Ниже приведен полный код переработанного исходного примера:

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;

namespace collection_changed_4
{
    public class Item : NotifyObject
    {
        private string _itemName;
        public string ItemName
        {
            get { return _itemName; }
            set { _itemName = value; OnPropertyChanged("ItemName"); }
        }
        private double _price;
        public double Price
        {
            get { return _price; }
            set { _price = value; OnPropertyChanged("Price"); }
        }
    }

    public class Person : NotifyObject
    {
        private ObservableCollection<Item> _items;
        public ObservableCollection<Item> Items
        {
            get { return _items; }
            set { _items = value; OnPropertyChanged("Items"); }
        }
        private string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value; OnPropertyChanged("Name"); }
        }
        public double Total
        {
            get { return Items.Sum(i => i.Price); }
            set { OnPropertyChanged("Total"); }
        }

        public Person(string initName, ObservableCollection<Item> initItems)
        {
            Console.WriteLine("0001 Constructor");
            this.Name = initName;
            this.Items = new ObservableCollection<Item>();
            this.Items.CollectionChanged += Items_CollectionChanged;
            foreach (Item item in initItems)
                this.Items.Add(item);
        }
        private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Console.WriteLine("0002 CollectionChanged");
            if (e.NewItems != null)
                foreach (Item item in e.NewItems)
                    item.PropertyChanged += Items_PropertyChanged;

            if (e.OldItems != null)
                foreach (Item item in e.OldItems)
                    item.PropertyChanged -= Items_PropertyChanged;
            OnPropertyChanged("Total");
        }

        private void Items_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Console.WriteLine("0003 PropertyChanged");
            OnPropertyChanged("Total");
        }
    }

    public abstract class NotifyObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    public class RelayCommand : ICommand
    {
        private Action<object> executeDelegate;
        readonly Predicate<object> canExecuteDelegate;

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new NullReferenceException("execute");
            executeDelegate = execute;
            canExecuteDelegate = canExecute;
        }

        public RelayCommand(Action<object> execute) : this(execute, null) { }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return canExecuteDelegate == null ? true : canExecuteDelegate(parameter);
        }

        public void Execute(object parameter)
        {
            executeDelegate.Invoke(parameter);
        }
    }

    public class ViewModel : NotifyObject
    {
        public ObservableCollection<Person> DataCollection { get; set; }

        public Person DataCollectionSelectedItem { get; set; }
        public Item ItemsSelectedItem { get; set; }

        public RelayCommand AddPerson { get; private set; }
        public RelayCommand AddItem { get; private set; }
        public RelayCommand Refresh { get; private set; }

        public ViewModel()
        {
            DataCollection = new ObservableCollection<Person>
            {
                new Person("Friedrich Nietzsche", new ObservableCollection<Item> {
                        new Item { ItemName = "Phone", Price = 220 },
                        new Item { ItemName = "Tablet", Price = 350 },
                    } ),
                new Person("Jean Baudrillard", new ObservableCollection<Item> {
                        new Item { ItemName = "Teddy Bear Deluxe", Price = 2200 },
                        new Item { ItemName = "Pokemon", Price = 100 }
                    }) 
            };

            AddItem = new RelayCommand(AddItemCode, null);
            AddPerson = new RelayCommand(AddPersonCode, null);
            Refresh = new RelayCommand(RefreshCode, null);
        }

        public void AddItemCode(object parameter)
        {
            var collectionIndex = DataCollection.IndexOf(DataCollectionSelectedItem);
            var itemIndex = DataCollection[collectionIndex].Items.IndexOf(ItemsSelectedItem);
            Item newItem = new Item() { ItemName = "Item_Name", Price = 100 };
            DataCollection[collectionIndex].Items.Insert(itemIndex + 1, newItem);
        }
        public void AddPersonCode(object parameter)
        {
            var collectionIndex = DataCollection.IndexOf(DataCollectionSelectedItem);
            Person newList = new Person("New_Name", new ObservableCollection<Item>() { new Item() { ItemName = "Item_Name", Price = 100 } });
            DataCollection.Insert(collectionIndex + 1, newList);
        }
        private void RefreshCode(object parameter)
        {
            CollectionViewSource.GetDefaultView(DataCollection).Refresh();
        }
    }

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new ViewModel();
        }
    }
}
person Disodium    schedule 30.03.2017