Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Notify/Bind a Parent Property to compute the sum for a Children property

Tags:

c#

wpf

I have two Classes, one for ViewModel and one for Product. The Product class has a property called Line Total, and the ViewModel Class has a property called Total Amount. The Product class is bound to a DataGrid and the user inserts the quantity which subsequently and automatically updates the Line Total.

Here is the ViewModel class:

public class ViewModel : INotifyPropertyChanged
{

    public ObservableCollection<Product> products { get; set; }// the children

    private decimal _TotalAmount; 
    public decimal TotalAmount // <=== has to hold sum of [products.LineTotal]
    {
        get
        {
            return totalAmount;
        }
        set
        {
            if (value != _TotalAmount)
            {
                _TotalAmount = value;
                onPropertyChanged(this, "TotalAmount");
            }
        }
    }

Here is the Product class which is a child:

public class Product : INotifyPropertyChanged
    {
        private decimal _LineTotal;
        public decimal LineTotal
        {
            get
            {
                return _LineTotal;
            }
            set
            {
                if (value != _LineTotal)
                {
                    _LineTotal = value;
                    onPropertyChanged(this, "LineTotal");
                }

            }

        }
}

My question is: How the TotalAmount can compute the sum of all Products [Line Total] ? How the child Products can notify the parent ViewModel to update the TotalAmount?

Something like:

foreach(var product in Products)
{
     TotalAmount += product.LineTotal;
}
like image 454
Anwar Avatar asked Oct 02 '12 14:10

Anwar


3 Answers

A way to achieve this, would be to recalculate the total amount every time a line total has been edited by the user and every time a product is added or removed from the ObservableCollection.

Since Product implements INotifyPropertyChanged and raises the PropertyChanged event when a new line total is set, the ViewModel can handle that event and recalculate the total amount.

ObservableCollection has a CollectionChanged event that is raised when an item is added or removed from it, so the ViewModel can also handle that event and recalculate. (This part is not really necessary if products can only be changed and not added/removed by the user etc.).

You can try out this small program to see how it could be done:

Code-behind

public partial class MainWindow : Window
{
    ViewModel vm = new ViewModel();

    public MainWindow()
    {
        InitializeComponent();

        vm.Products = new ObservableCollection<Product>
        {
            new Product { Name = "Product1", LineTotal = 10 },
            new Product { Name = "Product2", LineTotal = 20 },
            new Product { Name = "Product3", LineTotal = 15 }
        };

        this.DataContext = vm;
    }

    private void AddItem(object sender, RoutedEventArgs e)
    {
        vm.Products.Add(new Product { Name = "Added product", LineTotal = 50 });
    }

    private void RemoveItem(object sender, RoutedEventArgs e)
    {
        vm.Products.RemoveAt(0);
    }
}

public class ViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Product> _products;
    public ObservableCollection<Product> Products
    {
        get { return _products; }
        set
        {
            _products = value;

            // We need to know when the ObservableCollection has changed.
            // On added products: hook up eventhandlers to their PropertyChanged events.
            // On removed products: recalculate the total.
            _products.CollectionChanged += (sender, e) =>
            {
                if (e.NewItems != null)
                    AttachProductChangedEventHandler(e.NewItems.Cast<Product>());
                else if (e.OldItems != null)
                    CalculateTotalAmount();
            };

            AttachProductChangedEventHandler(_products);
        }
    }

    private void AttachProductChangedEventHandler(IEnumerable<Product> products)
    {
        // Attach eventhandler for each products PropertyChanged event.
        // When the LineTotal property has changed, recalculate the total.
        foreach (var p in products)
        {
            p.PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == "LineTotal")
                    CalculateTotalAmount();
            };
        }

        CalculateTotalAmount();
    }

    public void CalculateTotalAmount()
    {
        // Set TotalAmount property to the sum of all line totals.
        TotalAmount = Products.Sum(p => p.LineTotal);
    }

    private decimal _TotalAmount;
    public decimal TotalAmount
    {
        get { return _TotalAmount; }
        set
        {
            if (value != _TotalAmount)
            {
                _TotalAmount = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("TotalAmount"));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class Product : INotifyPropertyChanged
{
    public string Name { get; set; }

    private decimal _LineTotal;
    public decimal LineTotal
    {
        get { return _LineTotal; }
        set
        {
            if (value != _LineTotal)
            {
                _LineTotal = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("LineTotal"));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

XAML:

<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <DataGrid ItemsSource="{Binding Products}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" />
                <DataGridTextColumn Binding="{Binding LineTotal}" />
            </DataGrid.Columns>
        </DataGrid>

        <Button Click="AddItem">Add item</Button>
        <Button Click="RemoveItem">Remove item</Button>

        <TextBlock>
            <Run>Total amount:</Run>
            <Run Text="{Binding TotalAmount}" />
        </TextBlock>
    </StackPanel>
</Window>
like image 134
Peter Hansen Avatar answered Nov 14 '22 23:11

Peter Hansen


If the ParentViewModel cares about when a property on the ChildModel gets updated, it should subscribe to its PropertyChanged event.

However since you have a Collection of ChildModels, the handler that hooks up the PropertyChanged event should get added/removed in the CollectionChanged event.

// Hook up CollectionChanged event in Constructor
public MyViewModel()
{
    Products = new ObservableCollection<Product>();
    MyItemsSource.CollectionChanged += Products_CollectionChanged;
}

// Add/Remove PropertyChanged event to Product item when the collection changes
void Products_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
    if (e.NewItems != null)
        foreach(Product item in e.NewItems)
            item.PropertyChanged += Product_PropertyChanged;

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

// When LineTotal property of Product changes, re-calculate Total
void Product_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "LineTotal")
    {
        TotalAmount = products.Sum(p => p.LineTotal);

        // Or if calculation is in the get method of the TotalAmount property
        //onPropertyChanged(this, "TotalAmount");
    }
}
like image 21
Rachel Avatar answered Nov 15 '22 01:11

Rachel


I believe bernd_rausch answer goes in the right direction. The basic question is why do you want to store the TotalAmount in your ViewModel? The only reason could be that you have so many products that it impacts performance. But even in this scenario you have to be carefull with keeping the value consistent.

The safest way would be to write a TotalAmount property that calculates the TotalAmount on the fly. Then chain the Changed events.

public class ViewModel : INotifyPropertyChanged
{
  ViewModel()
  {
    Products = new ObservableCollection<Product>();
    Products.CollectionChanged += OnProductsChanged;
  }
  public ObservableCollection<Product> Products { get; private set; }// the children
  public decimal TotalAmount { get { return Products.Select(p => p.LineTotal).Sum(); } }

  private void OnProductChanged(object sender, PropertyChangedEventArgs eventArgs)
  {
     if("LineTotal" != eventArgs.PropertyName)
           return;
     onPropertyChanged(this, "TotalAmount");
  }

  private void OnProductsChanged(object sender, NotifyCollectionChangeEventArgs eventArgs)
  {
     // This ignores a collection Reset...
     // Process old items first, for move cases...
     if (eventArgs.OldItems != null)
       foreach(Product item in eventArgs.OldItems)
         item.PropertyChanged -= OnProductChanged;

     if (eventArgs.NewItems != null)
       foreach(Product item in eventArgs.NewItems)
        item.PropertyChanged += OnProductChanged;


     onPropertyChanged(this, "TotalAmount");
  }

}

I ignored the reset case. But i think this should give you the right direction. If you want to cache the calculation result i would still use this method an do the caching by an internal lazy value that gets reset in one of the change handlers.

like image 29
sanosdole Avatar answered Nov 15 '22 01:11

sanosdole