Ещё один способ реализации binding-а вычислимых свойств в WPF

от автора

Допустим, есть проект на 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).

Код 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.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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *