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;
}
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>
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");
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With