Проблемы WPF с перехватом событий CollectionChanged
У меня есть сетка данных, где мне нужно вычислить итоговое значение столбца Price вложенной сетки данных, например:
Я пытаюсь следовать этому примеру, чтобы моя наблюдаемая коллекция Предметов для каждого объекта Person получала уведомления об изменениях. Разница в том, что я реализую это внутри класса, а не View Model.
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);
}
}
Код внутри конструктора не перехватывает события, когда инициализируется новый Item или редактируется существующий. Поэтому событие 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();
}
}
}
3 ответа
В конце концов я понял, что не так с моим исходным кодом. Я использовал этот конструктор:
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();
}
}
}
Не используйте Eventhandlers между 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();
}
}
}
Я знаю, что это не красиво, но это работает
Я думаю, что есть простое решение...
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);
}
Как правило, общая сумма будет меняться при изменении списка. Вам по-прежнему нужна другая сумма на случай, если цена товара изменится... но это будет менее распространенная ситуация.