0

I have a datagrid where I need to calculate the total of the Price column of a nested datagrid, like so:

Image

I'm trying to follow this example so that my Items observable collection for each Person object gets notified on changes. The difference is that I'm implementing it inside a class, not 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);
        }
    }

The code inside the constructor doesn't hook events when a new Item is initialized or an existing one has been edited. Therefore, the Items_PropertyChanged event never fires. I only can refresh the entire list manually. What am I doing wrong here?

Or maybe there's a different approach to calculate the total for each Person's purchase list?

Below is the entire code if anyone cares too look at it.

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();
        }
    }
}
Community
  • 1
  • 1
Disodium
  • 105
  • 1
  • 2
  • 7

3 Answers3

0

Do not use Eventhandlers between ViewModels - this is black magic and can bring u to memory leaks, because of the created references.

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

I know it is not beautiful but it works

Peter
  • 1,655
  • 22
  • 44
  • I'm trying to follow your logic. What do I have to pass as argument to an Item object when initializing it in ViewModel? var smtn = new Item (?) { ItemName = "Phone", Price = 220 }; – Disodium Mar 28 '17 at 22:29
0

I think there is a simple solution...

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

Generally the total will change when the list changes. You still need the other sum in case the price changes on an item...but this would be the less common situation.

AQuirky
  • 4,691
  • 2
  • 32
  • 51
  • My problem is that the Items_CollectionChanged event never fires when I add an Item to observablecollection. So this line of code doesn't execute. – Disodium Mar 28 '17 at 22:32
  • The Items_CollectionChanged event will fire. The Items_PropertyChanged event won't fire. The only reason the Items_CollectIonChanged event would not fire when you add a item to the collection is if there is another event handler which throws an exception. – AQuirky Mar 28 '17 at 22:39
0

I've eventually figured out what was wrong with my original code. I used this constructor:

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

The attached event was then effectively overwritten by this initializer:

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

That is why the event never fired. It wasn't there! The correct way is to use a parametric constructor:

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

And then initialize it like so:

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

And that was it. Works like a charm now. Below is the full code of the original example reworked:

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();
        }
    }
}
Disodium
  • 105
  • 1
  • 2
  • 7