Допустим, есть проект на WPF и в нём ViewModel, в которой есть два свойства Price и Quantity, и вычислимое свойство TotalPrice=Price*Quantity
public 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) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
Если Price будет изменен в коде, то изменения цены автоматически отобразятся в View, потому что ViewModel сообщит View об изменении Price посредством вызовом события RaisePropertyChanged(«Price»). Вычисляемое TotalPrice же не изменится в View, потому что никто не вызывает RaisePropertyChanged(«TotalPrice»). Можно вызывать RaisePropertyChanged(«TotalPrice») в тех же местах, где вызывается RaisePropertyChanged(«Price») и RaisePropertyChanged(«Quantity»), но не хотелось бы размазывать по множеству мест информацию о том, что TotalPrice зависит от Price и Quantity, а хотелось бы хранить информацию об этом в одном месте. С этой целью люди пишут разнообразные менеджеры зависимостей, но давайте посмотрим какой минимальный код на самом деле нужен для этого.
Стандартный способ прокинуть логику туда, где ей не место с точки зрения дизайна, — это события. Подход в лоб заключается в создании двух событий OnPriceChanged и OnQuantityChanged. При срабатывании этих событий делать RaisePropertyChanged(«TotalPrice»). Сделаем подписку на эти события в конструкторе ViewModel. После этого информация о том, что TotalPrice зависит от Price и Quantity будет в одном месте — в конструкторе (ну, или в отдельном методе, если вы того пожелаете).
Немного упростим задачу: у нас уже есть событие PropertyChanged, срабатывающее при изменении Price, вот его и используем.
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"});
У этого кода есть несколько недостатков: во-первых, я бы не советовал зашивать имена свойств в строки, лучше доставать их из лямбд, а во-вторых, этот код не сработает, если вычисляемой свойство имеет более сложный вид, например: TotalCost = o.OrderProperties.Orders.Sum(o => o.Price * o.Quantity).
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; }
Подпишемся через события на изменения Price и Quantity каждого элемента коллекции. Но в коллекцию могут добавляться\удаляться элементы. При изменении коллекции нужно вызвать RaisePropertyChanged(«TotalPrice»). При добавлении элемента нужно подписаться на его изменении Price и Quantity. Ещё необходимо учесть, что в OrderProperties кто-то может присвоить новую коллекцию, или в ViewModel новый OrderProperties.
Получился вот такой код:
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.Action == NotifyCollectionChangedAction.Add) { foreach (var addedItem in args.NewItems) { RegisterElementPropertyDependencies(propertyName, addedItem, destinationPropertyNames, actionOnChanged); } } }; }
В данном случае, для OrderProperties.Orders.Sum(o => o.Price*o.Quantity) его нужно использовать вот так:
RegisterElementPropertyDependencies("Summa", this, new[] {"OrderProperties"}, () => RegisterElementPropertyDependencies("Summa", OrderProperties, new[] {"Orders"}, () => RegisterCollectionPropertyDependencies("Summa", OrderProperties.Orders, new[] { "Price", "Quantity" })));
Протестировал этот код в разных ситуациях: менял Quantity у элементов, создавал новые Orders и OrderProperties, сначала менял Orders а потом Quantity и т.п., код отработал корректно.
P.S. Кстати, рекомендую посмотреть в сторону Observables в стиле Knockout. Там вообще не нужно указывать от чего зависит свойство, нужно просто передать алгоритм его вычисления:
fullName = new ComputedValue(() => FirstName.Value + " " + ToUpper(LastName.Value));
Библиотека проанализирует дерево выражений, увидит в нём доступ к членам FirstName и LastName, и сама проконтролирует зависимости. Исчезает риск забыть переуказать зависимости после изменения алгоритма вычисления свойства. Правда, говорят, что библиотека немного не доработана, и не отслеживает вложенные коллекции, но, если у вас вагон свободного времени, то можно открыть исходники (доступны по предыдущей ссылке) и немного поработать напильником, или написать свой велосипед-анализатор дерева выражений.
ссылка на оригинал статьи http://habrahabr.ru/post/271105/
Добавить комментарий