Допустим, есть проект на WPF и в нём ViewModel, в которой есть два свойства Price и Quantity, и вычислимое свойство TotalPrice=Price*Quantity
Если 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, вот его и используем.
У этого кода есть несколько недостатков: во-первых, я бы не советовал зашивать имена свойств в строки, лучше доставать их из лямбд, а во-вторых, этот код не сработает, если вычисляемой свойство имеет более сложный вид, например: TotalCost = o.OrderProperties.Orders.Sum(o => o.Price * o.Quantity).
Подпишемся через события на изменения Price и Quantity каждого элемента коллекции. Но в коллекцию могут добавляться\удаляться элементы. При изменении коллекции нужно вызвать RaisePropertyChanged(«TotalPrice»). При добавлении элемента нужно подписаться на его изменении Price и Quantity. Ещё необходимо учесть, что в OrderProperties кто-то может присвоить новую коллекцию, или в ViewModel новый OrderProperties.
Получился вот такой код:
В данном случае, для OrderProperties.Orders.Sum(o => o.Price*o.Quantity) его нужно использовать вот так:
Протестировал этот код в разных ситуациях: менял Quantity у элементов, создавал новые Orders и OrderProperties, сначала менял Orders а потом Quantity и т.п., код отработал корректно.
P.S. Кстати, рекомендую посмотреть в сторону Observables в стиле Knockout. Там вообще не нужно указывать от чего зависит свойство, нужно просто передать алгоритм его вычисления:
fullName = new ComputedValue(() => FirstName.Value + " " + ToUpper(LastName.Value));
Библиотека проанализирует дерево выражений, увидит в нём доступ к членам FirstName и LastName, и сама проконтролирует зависимости. Исчезает риск забыть переуказать зависимости после изменения алгоритма вычисления свойства. Правда, говорят, что библиотека немного не доработана, и не отслеживает вложенные коллекции, но, если у вас вагон свободного времени, то можно открыть исходники (доступны по предыдущей ссылке) и немного поработать напильником, или написать свой велосипед-анализатор дерева выражений.
P.P.S. По поводу сборки мусора: если добавить в финализаторы элементов вывод сообщений, то можно обнаружить, что при закрытии окна все элементы собираются сборщиком мусора (несмотря на то, что ViewModel имеет ссылку на дочерний элемент, а дочерний элемент имеет ссылку на ViewModel в обработчике события). Это объясняется тем, что в WPF для устранения утечек памяти при DataBinding-е используются слабые события посредством PropertyChangedEventManager-а. Подробнее можно почитать по ссылкам: [1], [2], [3]
Код
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)
{
var propertyChanged = PropertyChanged;
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).
Код OrderProperties и ViewModel. Тут всё очевидно, можно не смотреть
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.NewItems != null)
{
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, и сама проконтролирует зависимости. Исчезает риск забыть переуказать зависимости после изменения алгоритма вычисления свойства. Правда, говорят, что библиотека немного не доработана, и не отслеживает вложенные коллекции, но, если у вас вагон свободного времени, то можно открыть исходники (доступны по предыдущей ссылке) и немного поработать напильником, или написать свой велосипед-анализатор дерева выражений.
P.P.S. По поводу сборки мусора: если добавить в финализаторы элементов вывод сообщений, то можно обнаружить, что при закрытии окна все элементы собираются сборщиком мусора (несмотря на то, что ViewModel имеет ссылку на дочерний элемент, а дочерний элемент имеет ссылку на ViewModel в обработчике события). Это объясняется тем, что в WPF для устранения утечек памяти при DataBinding-е используются слабые события посредством PropertyChangedEventManager-а. Подробнее можно почитать по ссылкам: [1], [2], [3]