Notify / bind parental property to calculate the amount for children's property

I have two classes, one for ViewModel and one for Product . The Product class has the Line Total property, and the ViewModel class has the Total Amount property. The Product class is associated with the DataGrid, and the user inserts a quantity, which subsequently automatically updates the Total String .

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 can TotalAmount calculate the sum of all products [Total Rows] ? How can Child Products notify the ViewModel parent to update TotalAmount ?

Sort of:

 foreach(var product in Products) { TotalAmount += product.LineTotal; } 
+4
source share
4 answers

To achieve this, one could recalculate the total amount each time the total amount has been edited by the user, and each time the product is added or removed from the ObservableCollection .

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

ObservableCollection has a CollectionChanged event that occurs when an item is added or removed from it, so the ViewModel can also handle this event and recount. (This part is not needed if the products can be modified and not added / deleted by the user, etc.).

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

Code for

 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> 
+7
source

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

However, since you have a ChildModels collection, a handler that ChildModels PropertyChanged event must be 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"); } } 
+1
source

I think that the value of TotalAmount in the user interface is updated only when TotalAmount is installed, so the NotifyPropertyChanged event is fired. For this to work, you must listen to the PropertyChangedEvent from all products, and when the changes to the collection or LineTotal of the product change, you must set TotalAmount to some value other than _TotalAmount.

But this code is really hard to understand: it is not clear why you store the value that is calculated each time it is read (TotalAmount) in the variable (_TotalAmount). And since _TotalAmount is not set to zero, this is not a valid value.

0
source

I believe the answer of bernd_rausch is going in the right direction. The main question is: why do you want to store TotalAmount in your ViewModel? The only reason may be that you have so many products that it affects performance. But even in this scenario, you have to be careful about maintaining value.

The safest way is to write a TotalAmount property that calculates TotalAmount on the fly. Then connect the modified 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 result of the calculation, I use this method anyway, and caching with the internal lazy value that gets reset in one of the change handlers.

0
source

Source: https://habr.com/ru/post/1437396/


All Articles