Suppose there is a project on WPF and in it a
ViewModel , in which there are two properties Price and Quantity, and the computable property TotalPrice = Price * Quantity
Codepublic class Order : BaseViewModel { private double _price; private double _quantity; public double Price { get { return _price; } set { if (_price == value) return; _price = value; RaisePropertyChanged("Price"); } } public double Quantity { get { return _quantity; } set { if (_quantity == value) return; _quantity = value; RaisePropertyChanged("Quantity"); } } public double TotalPrice {get { return Price*Quantity; }} } public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { var propertyChanged = PropertyChanged; if (propertyChanged != null) propertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
')
If Price is changed in code, price changes will automatically appear in View, because the ViewModel will notify View of the Price change by calling the RaisePropertyChanged ("Price") event. The calculated TotalPrice will not change in View, because no one calls RaisePropertyChanged ("TotalPrice"). You can call RaisePropertyChanged (“TotalPrice”) in the same places where RaisePropertyChanged (“Price”) and RaisePropertyChanged (“Quantity”) are called, but I
don’t want to smear across many places the information that TotalPrice depends on Price and Quantity, but I would like to store information about this in one place . To this end,
people write various dependency managers, but let's see
what minimum code is really needed for this .
The standard way to throw logic where it is not in terms of design is events. The approach to the forehead is to create two events OnPriceChanged and OnQuantityChanged. When these events fire, do RaisePropertyChanged ("TotalPrice"). Subscribe to these events in the ViewModel constructor. After that, the information that TotalPrice depends on Price and Quantity will be in one place - in the constructor (well, or in a separate method, if you so wish).
Let's simplify the task a bit: we already have the PropertyChanged event, which is triggered when the Price changes, so we use it.
public void RegisterPropertiesDependencies(string propertyName, List<string> dependenciesProperties) { foreach (var dependencyProperty in dependenciesProperties) { this.PropertyChanged += (sender, args) => { if (args.PropertyName == dependencyProperty) RaisePropertyChanged(propertyName); }; } }
RegisterPropertiesDependencies("TotalPrice", new List<string> { "Price", "Quantity"});
This code has several drawbacks: firstly, I would not advise stitching property names in strings, it is better to get them out of lambda, and secondly, this code will not work if the calculated property has a more complex form, for example: TotalCost = o .OrderProperties.Orders.Sum (o => o.Price * o.Quantity).
OrderProperties code and ViewModel. It's all obvious, you can not watch public class OrderProperties : BaseViewModel { private ObservableCollection<Order> _orders = new ObservableCollection<Order>(); public ObservableCollection<Order> Orders { get { return _orders; } set { if (_orders == value) return; _orders = value; RaisePropertyChanged("Orders"); } } } public class TestViewModel : BaseViewModel { public double Summa {get { return OrderProperties.Orders.Sum(o => o.Price*o.Quantity); }} public OrderProperties OrderProperties { get { return _orderProperties; } set { if (_orderProperties == value) return; _orderProperties = value; RaisePropertyChanged("OrderProperties"); } } private OrderProperties _orderProperties; }
Subscribe through events for changes in the Price and Quantity of each item in the collection. But elements can be added \ to the collection. When changing the collection, you need to call RaisePropertyChanged ("TotalPrice"). When you add an item, you need to subscribe to change its Price and Quantity. It is also necessary to take into account that someone can assign a new collection in OrderProperties, or a new OrderProperties in ViewModel.
This is the result of this code:
public void RegisterElementPropertyDependencies(string propertyName, object element, ICollection<string> destinationPropertyNames, Action actionOnChanged = null) { if (element == null) return; if (actionOnChanged != null) actionOnChanged(); if (element is INotifyPropertyChanged == false) throw new Exception(string.Format(" {0}, .. INotifyPropertyChanged", element.GetType())); ((INotifyPropertyChanged)element).PropertyChanged += (o, eventArgs) => { if (destinationPropertyNames.Contains(eventArgs.PropertyName)) { RaisePropertyChanged(propertyName); if (actionOnChanged != null) actionOnChanged(); } }; } public void RegisterCollectionPropertyDependencies<T>(string propertyName, ObservableCollection<T> collection, ICollection<string> destinationPropertyNames, Action actionOnChanged = null) { if (collection == null) return; if (actionOnChanged != null) actionOnChanged(); foreach (var element in collection) { RegisterElementPropertyDependencies(propertyName, element, destinationPropertyNames); } collection.CollectionChanged += (sender, args) => { RaisePropertyChanged(propertyName); if (args.NewItems != null) { foreach (var addedItem in args.NewItems) { RegisterElementPropertyDependencies(propertyName, addedItem, destinationPropertyNames, actionOnChanged); } } }; }
In this case, for OrderProperties.Orders.Sum (o => o.Price * o.Quantity), you need to use it like this:
RegisterElementPropertyDependencies("Summa", this, new[] {"OrderProperties"}, () => RegisterElementPropertyDependencies("Summa", OrderProperties, new[] {"Orders"}, () => RegisterCollectionPropertyDependencies("Summa", OrderProperties.Orders, new[] { "Price", "Quantity" })));
I tested this code in different situations: I changed the Quantity of elements, created new Orders and OrderProperties, first I changed Orders and then Quantity, etc., the code worked correctly.
PS By the way, I recommend to look towards the
Observables in the style of Knockout . There you don’t need to specify what the property depends on, you just need to pass an algorithm for its calculation:
fullName = new ComputedValue (() => FirstName.Value + "" + ToUpper (LastName.Value));
The library will analyze the expression tree, see access to FirstName and LastName members in it, and check dependencies on its own. There is no risk of forgetting to re-order dependencies after changing the property calculation algorithm. True, it is said that the library is a bit unimproved, and does not keep track of nested collections, but if you have a car of free time, you can open the source code (available at the previous link) and work a bit with a file, or write your own expression tree analyzer bike.
PPS Regarding garbage collection: if you add message output to element finalizers, you may find that when you close a window, all elements are collected by the garbage collector (despite the fact that the ViewModel has a link to the child element, and the child element has a link to the ViewModel in the event handler ). This is because WPF uses
weak events through a
PropertyChangedEventManager to fix memory leaks during a DataBinding. More information can be found on the links:
[1] ,
[2] ,
[3]