Как стать автором
Обновить

Автоматическая калькуляция вычислимых свойств моделей представлений в .NET

Время на прочтение15 мин
Количество просмотров12K

Мотивация на примере моделей представлений для WPF UI


Начать обсуждение и познакомиться с обсуждаемой проблематикой предлагается на примере подхода к архитектуре пользовательских интерфейсов в WPF.

Как известно, одна из главных фич WPF — это мощная система байндингов, позволяющая достаточно легко отделить модель представления (далее модель) от самого представления (далее View) как такового. Обычно программист создает XAML для представления, привязывает свойства его элементов к модели в том же XAML посредством байндингов и, фактически, забывает о View. Это становится возможным поскольку большинство UI-логики может быть реализовано через воздействие на модель и автоматически прокинуто на UI посредством байндингов. При таком подходе модель играет роль состояния View, являясь его прокси для слоя, реализующего UI-логику. Например, меняя свойство модели, мы тем самым меняем соответствующее ей свойство View (или его элементов). Последнее происходит автоматически благодаря системе байндингов, которая отслеживает изменения как в модели, так и во View, синхронизируя состояния на обоих концах по мере надобности. Одним из способов, посредством которых модель может сообщить наблюдателю (коим в нашем случае является байндинг) о своем изменении, является бросание события PropertyChanged с именем изменившегося свойства в качестве параметра. Это событие принадлежит интерфейсу INotifyPropertyChanged, который, соответственно, должен быть реализован в модели.

Рассмотрим описанную идею на конкретном примере. Начнем с простой модели, которая представляет собой некий Заказ и содержит два свойства — Цена и Количество. Оба свойства будут изменяемыми, поэтому для каждого нужно реализовать нотификацию об изменении. Это делается следующим кодом:

public class Order : INotifyPropertyChanged
    {
        private decimal _price;
        private int _quantity;

        public decimal Price
        {
            get { return _price; }
            set
            {
                if (value == _price) return;
                _price = value;
                OnPropertyChanged();
            }
        }

        public int Quantity
        {
            get { return _quantity; }
            set
            {
                if (value == _quantity) return;
                _quantity = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Теперь давайте представим, что у нас есть View, представляющее экземпляр заказа в виде двух TextBlock'ов, которые привязаны к Цене и Количеству:

<TextBlock Text="{Binding Price, StringFormat='Price: {0}'}" />
<TextBlock Text="{Binding Quantity, StringFormat='Quantity: {0}'}" />

Если наша UI-логика поменяет любое из свойств модели, соответствующий байндинг получит уведомление об изменении и изменит текст в привязанном TextBlock'е. Пока все предельно просто.

Но теперь добавим в модель свойство Стоимость, вычисляемое по очевидной формуле:

 public int Cost
        {
            get { return _price * _quantity; }
        }

А также соответствующий этому свойству TextBlock:

<TextBlock Text="{Binding Cost, StringFormat='Cost: {0}'}" />

Наверное, тот, кто только знакомится с WPF, вправе ожидать, что при изменении Цены или Количества TextBlock, представляющий Стоимость, изменит свой текст тоже. Естественно, этого не произойдет, поскольку не было кинуто событие PropertyChanged для свойства Cost. Таким образом, мы подошли к проблеме реализации уведомлений об обновлениях вычислимых свойств (свойств, значение которых зависит от значений других свойств).

Возможные решения


В случае с рассмотренным примером решение, очевидно, весьма простое. Нужно бросать PropertyChanged для Cost из сеттеров Price и Quantity или же изменять свойство Cost из этих сеттеров (вызывая тем самым рейз нужного события уже из Cost). Ниже представлен код обоих вариантов:

//Raise Cost PropertyChanged from both Price and Quantity setters.
 public class Order : INotifyPropertyChanged
    {
        private decimal _price;
        private int _quantity;

        public decimal Price
        {
            get { return _price; }
            set
            {
                if (value == _price) return;
                _price = value;
                OnPropertyChanged();
                OnPropertyChanged("Cost");
            }
        }

        public int Quantity
        {
            get { return _quantity; }
            set
            {
                if (value == _quantity) return;
                _quantity = value;
                OnPropertyChanged();
                OnPropertyChanged("Cost");
            }
        }
        public int Cost
        {
            get { return _price * _quantity; }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

//Update Cost from both Price and Quantity setters.
 public class Order : INotifyPropertyChanged
    {
        private decimal _price;
        private int _quantity;

        public decimal Price
        {
            get { return _price; }
            set
            {
                if (value == _price) return;
                _price = value;
                OnPropertyChanged();
                Cost = _price * _quantity;
            }
        }

        public int Quantity
        {
            get { return _quantity; }
            set
            {
                if (value == _quantity) return;
                _quantity = value;
                OnPropertyChanged();
                Cost = _price * _quantity;
            }
        }
        public int Cost
        {
            get { return _cost; }
            private set
            {
                if (value == _cost) return;
                _cost = value;
                OnPropertyChanged();            
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

На самом деле, оба решения не очень хороши сразу с нескольких сторон.

С архитектурной точки зрения мы поступаем нехорошо поскольку инвертируем логически верное направление зависимости Стоимости от Цены и Количества. Теперь не Стоимость «знает» о Цене и Количестве, а, наоборот, Цена и Количество начинают «знать» о Стоимости, становясь ответственными за ее изменение. Это в свою очередь нарушает SRP на микроуровне, так как изначально не зависящие ни от чего (и простые в реализации) свойства теперь вынуждены иметь знания о существовании других свойств и деталей их реализации для того, чтобы иметь возможность в нужные моменты правильно эти свойства обновлять.

С технической точки зрения не все хорошо, потому что могут существовать достаточно сложные связи между свойствами, поддерживать вручную которые достаточно трудоемко (и багоемко). Примерами таких связей являются:

  • зависимости от свойств, которые сами являются зависимыми;
  • зависимости от свойств вложенных объектов (цепочки свойств), как в случае DiscountSum = Order.Sum * Order.Discount.Percent / 100;
  • зависимости от свойств элементов коллекции (TotalQuantity = Orders.Sum(o => o.Quantity)).

Например, чтобы поддержать последнюю описанную зависимость, программист должен написать большое количество рутинного кода (ситуация еще более усложнится, если в цепочке зависимости присутствует не одна, а сразу несколько коллекций):

  1. Подписаться на изменения ObservableCollection (через интерфейс INotifyCollectionChanged), представляющую Orders.
  2. Подписаться на PropertyChanged каждого элемента коллекции (заказа), чтобы отслеживать изменение его свойства Quantity.
  3. Поддерживать соответствующие подписки в актуальном состоянии: отписываться от удаляемых из коллекции элементов и подписываться на добавляемые, отписываться при смене экземпляра самой коллекции от старой коллекции и подписываться на события новой.

Для упрощения подобной работы а также для возможности декларативного представления зависимостей и был создан Трекер Зависимостей (DependenciesTracker), речь о котором пойдет ниже.

Трекер зависимостей (DependenciesTracker)


.NET библиотека DependenciesTracking реализует автоматическое обновление вычислимых свойств и возможность задавать зависимости в декларативном стиле. Она достаточно легковесна как с точки зрения простоты использования, так и с точки зрения реализации: для ее работы не требуется ни создания каких-либо оберток над свойствами (типа ObservableProperty<T>, IndependentProperty<T> и т.п.), ни наследования модели от какого-либо базового класса, ни необходимости помечать свойства какими-либо атрибутами. Реализация никак существенно не использует рефлексию и не базируется на переписывании сборок после компиляции. Основным компонентом сборки является класс DependenciesTracker, использование которого далее будет подробно разобрано.

В целом, для того, чтобы трекинг зависимых свойств начал работать, нужно сделать 2 простых вещи:

  1. определить зависимости свойств (для класса в целом),
  2. начать отслеживать эти зависимости (для конкретного экземпляра, обычно в конструкторе).

Указанные пункты рассмотрены ниже на различных примерах.

Простые (одноуровневые) зависимости


Начнем с примера, который был описан в начале статьи. Перепишем класс Order так, чтобы зависимость Cost от Price и Quantity отслеживалась автоматически и влекла пересчет Cost при изменении Price или Quantity. В соответствии с пп.1-2 для этого нужно реализовать класс Order следующим образом:

public class Order : INotifyPropertyChanged
    {
        private decimal _price;
        private int _quantity;
        private decimal _cost;   

        public decimal Price
        {
            get { return _price; }
            set
            {
                if (value == _price) return;
                _price = value;
                OnPropertyChanged();
            }
        }

        public int Quantity
        {
            get { return _quantity; }
            set
            {
                if (value == _quantity) return;
                _quantity = value;
                OnPropertyChanged();
            }
        }

        public decimal Cost
        {
            get { return _cost; }
            private set
            {
                if (value == _cost) return;
                _cost = value;
                OnPropertyChanged();
            }
        }

        //Определяем статическую "карту зависимостей", которая будет хранить зависимости для класса
        private static readonly IDependenciesMap<Order> _dependenciesMap = new DependenciesMap<Order>();

        static Order()
        {            
           //Определяем и добавляем в карту зависимости
            _dependenciesMap.AddDependency(o => o.Cost, o => o.Price * o.Quantity, o => o.Price, o => o.Quantity)
        }

        private IDisposable _tracker;

        public Order()
        {
            //Начинаем отслеживать зависимости для текущего экземпляра модели
            _dependenciesMap.StartTracking(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }

Из примера видно, что мы используем IDependenciesMap для того, чтобы определить зависимости и начать их отслеживать. Разберем этот интерфейс подробнее.

public interface IDependenciesMap<T>
    {
        IDependenciesMap<T> AddDependency<U>(Expression<Func<T, U>> dependentProperty, Func<T, U> calculator, Expression<Func<T, object>> obligatoryDependencyPath, params Expression<Func<T, object>>[] dependencyPaths);

        IDependenciesMap<T> AddDependency<U>(Action<T, U> setter, Func<T, U> calculator, Expression<Func<T, object>> obligatoryDependencyPath, params Expression<Func<T, object>>[] dependencyPaths);

        IDisposable StartTracking(T trackedObject);
    }

В примере для добавления зависимости мы использовали первую версию перегруженного метода AddDependency. Он имеет следующие параметры:

  1. dependentProperty — выражение (Expression), описывающее зависимое свойство (o => o.Cost),
  2. calculator — метод, который вычисляет значение зависимого свойства на конкретном экземпляре модели (o => o.Price * o.Quantity),
  3. obligatoryDependencyPath и dependencyPaths — Expression'ы, которые описывают пути, от которых свойство зависисит (o => o.Price, o => o.Quantity).

Вторая версия AddDependency первым параметром принимает сеттер зависимого свойства ((o, val) => o.Cost = val), вместо Expression'а, который его описывает (и который в итоге компилируется в этот же сеттер). В остальном методы аналогичны.

На втором шаге мы добавили вызов StartTracking в конструктор. Это означает, что отслеживание изменений свойств в путях зависимости начнется сразу при создании объекта заказа. В методе StartTracking производятся примерно следующие действия:

  1. делаются необходимые подписки на изменения свойств в путях зависимостей,
  2. происходит начальный подсчет и установка значений вычислимых свойств.

Метод возвращает IDisposable, который может быть использован для остановки отслеживания изменений на любом этапе жизненного цикла модели.

Зависимости от цепочек свойств


Теперь усложним пример. Для этого перенесем Price и Quantity в отдельный объект OrderProperties:

 public class OrderProperties : INotifyPropertyChanged
    {
        private int _price;
        private int _quantity;      

        public int Price
        {
            get { return _price; }
            set
            {
                if (_price != value)
                {
                    _price = value;
                    OnPropertyChanged("Price");
                }
            }
        }

        public int Quantity
        {
            get { return _quantity; }
            set
            {
                if (_quantity != value)
                {
                    _quantity = value;
                    OnPropertyChanged("Quantity");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

И положим объект OrderProperties внутрь Order, сделав, соответственно, свойство Cost зависимым от OrderProperties.Price и OrderProperties.Quantity:

public class Order : INotifyPropertyChanged
    {
        private OrderProperties _properties;
        private int _cost;

        public OrderProperties Properties
        {
            get { return _properties; }
            set
            {
                if (_properties != value)
                {
                    _properties = value;
                    OnPropertyChanged("Properties");
                }
            }
        }

        public int Cost
        {
            get { return _cost; }
            private set
            {
                if (_cost != value)
                {
                    _cost = value;
                    OnPropertyChanged("Cost");
                }
            }
        }      

        private static readonly IDependenciesMap<Order> _map = new DependenciesMap<Order>();       

        static Order()
        {
            _map.AddDependency(o => o.Cost, o => o.Properties != null ? o.Properties.Price * o.Properties.Quantity : -1, o => o.Properties.Price, o => o.Properties.Quantity);
        }

        private IDisposable _tracker;

        public Order()
        {
            _tracker = _map.StartTracking(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Теперь Cost будет автоматически пересчитываться при изменении Price или Quantity свойства Properties у заказа или же при изменении самого экземпляра Properties. Как видим, определить зависимость от цепочки свойств оказалось не сложнее, чем простую одноуровневую зависимость.

Зависимость от свойств элементов коллекций


Представляется, что зависимость от свойств элементов коллекций является наиболее трудоемкой с точки зрения ручной поддержки. Шаги, которые нужно предпринять программисту для имплементации такой связи, были описаны на примере TotalQuantity = Orders.Sum(o => o.Quantity) выше в статье. Но стоит отметить, что это лишь случай зависимости от одной коллекции. Если в цепочке встретятся две и более коллекции, то реализация существенно усложнится. DependenciesTracker поддерживает этот тип зависимости и, также как и в предыдущих случаях, делает его определение декларативным:

public class Invoice : INotifyPropertyChanged
    {
        private readonly ObservableCollection<Order> _orders = new ObservableCollection<Order>();
        private decimal _totalCost;

        public ObservableCollection<Order> Orders
        {
            get { return _orders; }
            set
            {
                if (value == _orders) return;
                _orders = value;
                OnPropertyChanged();
            }
        }

        public decimal TotalCost
        {
            get { return _totalCost; }
            set
            {
                if (value == _totalCost) return;
                _totalCost = value;
                OnPropertyChanged();
            }
        }

        private static readonly IDependenciesMap<Invoice> _dependenciesMap = new DependenciesMap<Invoice>();

        static Invoice()
        {
            _dependenciesMap.AddDependency(i => i.TotalCost, i => i.Orders.Sum(o => o.Price * o.Quantity), 
                                           i => i.Orders.EachElement().Price, i => i.Orders.EachElement().Quantity);
        }

        public Invoice()
        {
            _dependenciesMap.StartTracking(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Как и ранее, мы определили вычислимое свойство, его калькулятор, пути зависимости и запустили трекинг в конструкторе экземпляра. Единственная новая конструкция, которая нам встретилась — это метод EachElement для перехода от коллекции к ее элементу в цепочке свойств. В нашем случае expression i => i.Orders.EachElement().Price означает, что TotalCost зависит от цены каждого заказа из коллекции Orders. EachElement предназначен только для конструирования путей зависимостей, которые являются expression'ами, поэтому вызов метода в рантайме не поддерживается:

 public static class CollectionExtensions
  {
    ... 
    public static T EachElement<T>(this ICollection<T> collection)
    {
      throw new NotSupportedException("Call of this method is not supported");
    }
  }

Зависимость от некоторого агрегата коллекции


Одним из случаев зависимости от элементов коллекции, который стоит рассмотреть отдельно, является зависимость от агрегата коллекции, вычисление которого не использует каких либо CLR-свойств элементов коллекции. Примеры:

  • HasNullValues = Orders.Any(o => o == null)
  • EvensCount = Ints.Count(i % 2 == 0)

В таких случаях пути необходимо заканчивать методом EachElement (а не просто коллекцией):

  • i => i.Orders.EachElement(), а не i => i.Orders,
  • i => i.Ints.EachElement(), а не i => i.Ints.

Статус проекта, ссылки, дальнейшие планы


Текущая стабильная версия: 1.0.1.
Поддерживаемая платформа: .NET 4.0 и выше.
Ссылки: страница проекта на github, вики проекта (на англ. яз.), NuGet пакет.

Весь описанный функционал покрыт юнит-тестами (степень покрытия — 88%).

В следующих версиях библиотеки планируется добавить поддержку трекинга при наследовании моделей (добавление, замену и переопределение зависимостей в производных классах), а также вынести «путь» из внутренностей трекера, сделав это понятие публичным. Последнее, в частности, позволит легко реализовывать триггеры, зависящие от сложных путей.

Альтернативные решения


DependenciesTracker — это, разумеется, не единственное существующее решение для трекинга изменений зависимых свойств.
Ниже рассмотрены некоторые из альтернатив.

  1. Простая (прототипная) реализация подхода, основанного на атрибутах:

    [DependentProperty("Price", "Quantity")]
    public decimal Cost
          {
              get { return _cost; }
              private set
              {
                  if (value == _cost) return;
                  _cost = value;
                  OnPropertyChanged();
              }
          }
    

    Решение:

    • базируется на рефлексии,
    • требует наследования модели от специального базового класса,
    • не поддерживает зависимости от цепочек свойств и коллекций.

  2. NotifyPropertyChangeWeaver (add-in к Fody):

    [DependsOn("Price", "Quantity")]
    public decimal Cost { get; set; }
    

    Решение:

    • основано на переписывании сборки, результирующий IL для примера выше будет содержать рейз события об изменении Cost из сеттеров Price и Quantity;
    • не поддерживает зависимости от цепочек свойств и коллекций.

  3. Один из стандартных аспектов PostSharp'a NotifyPropertyChanged:

    [NotifyPropertyChanged]
    public class CustomerViewModel
    {
       public CustomerModel Customer { get; set; }
       public string FullName { get { return string.Format("{0} {1} ({2})", 
                                             Customer.FirstName, Customer.LastName, Customer.Address.City); } }
    }
    
    [NotifyPropertyChanged]
    public class CustomerModel
    {
       public AddressModel Address { get; set; }
       public FirstName { get; set; }
       public LastName { get; set; }
    }
    

    Решение:

    • базируется на переписывании сборки;
    • поддерживает (распознает) зависимости от цепочек свойств;
    • не поддерживает зависимости от элементов коллекций;
    • не поддерживает (не распознает) зависимости от цепочек свойств, «завернутых» в методы, как например:

      [NotifyPropertyChanged]
      public class CustomerViewModel
      {
       public CustomerModel Customer { get; set; }
      
       public string FullName { get { return FormatFullName(); } }
      
       public string FormatFullName()
       {
         return string.Format("{0} {1} ({2})", 
                   Customer.FirstName, Customer.LastName, Customer.Address.City);
       }
      }
      

      Последний факт, возможно, не является большой проблемой, но, тем не менее, может приводить к трудноуловимым багам, особенно в динамике — при изменениях кода и рефакторингах.

  4. Аспект NotifyPropertyChanged из PostSharp Domain Toolkit:
    • по сравнению со стандартным аспектом из предыдущего примера это такой вариант «на стероидах», способный распознавать много кейсов зависимостей;
    • основан на переписывании сборки;
    • кроме зависимостей от свойств поддерживает также зависимости от полей и методов (того же класса);
    • поддерживает зависимости от цепочек свойств;
    • вызывает ошибки компиляции (что очень важно), если при вычислении зависимости встречает неподдерживаемую конструкцию (например, зависимость от метода другого класса);
    • не поддерживает зависимости от элементов коллекций.

  5. Observables в стиле Knockout (прототип).
    Решение:

    • основано на обертках над свойствами;
    • не поддерживает зависимостей от цепочек свойств и элементов коллекций;
    • имеет достаточно многословный синтаксис:

      class ViewModel
      {
         readonly ObservableValue<string> firstName = new ObservableValue<string>("Alan");
      
         public ObservableValue<string> FirstName
         {
             get
             {
                 return firstName;
             }
         }
      
        readonly ObservableValue<string> lastName = new ObservableValue<string>("Turing");
      
        public ObservableValue<string> LastName
        {
            get
            {
                return lastName;
            }
        }
      
        readonly ComputedValue<string> fullName;
      
        public ComputedValue<string> FullName
        {
            get
            {
                return fullName;
            }
        }
      
        public MainWindowViewModel()
        {
            fullName = new ComputedValue<string>(() => FirstName.Value + " " + ToUpper(LastName.Value));
        }
      
        string ToUpper(string s)
        {
            return s.ToUpper();
        }
      }
      


  6. Автоматический трекер зависимостей, основанный на анализе стека вызовов.

    Решение:

    • поддерживает простые зависимости;
    • поддерживает зависимости от цепочек свойств, но реализация оставляет желать лучшего (при изменении свойства внутреннего объекта бросается событие об изменении всего этого объекта целиком);
    • требует наследования модели от специального базового класса и оборачивание кода геттеров и сеттеров в специальные конструкции.

    public class Person : BindableObjectBase3
    {
       private string firstName;
       private string lastName;
    
       public Person(string firstName, string lastName)
       {
           this.FirstName = firstName;
           this.LastName = lastName;
       }
    
       public string FirstName
       {
           get
           {
               using (this.GetPropertyTracker(() => this.FirstName))
               {
                   return this.firstName;
               }
           }
           set
           {
                  this.SetValue(ref this.firstName, value, () => this.FirstName);
           }
       }
    
       public string LastName
       {
           get
           {
               using (this.GetPropertyTracker(() => this.LastName))
               {
                    return this.lastName;
               }
           }
           set
           {
               this.SetValue(ref this.lastName, value, () => this.LastName);
           }
      }
    
      public string FullName
      {
          get
          {
              using (this.GetPropertyTracker(() => this.FullName))
              {
                  return this.FirstName + " " + this.LastName;
              }
          }
      }
    }
    

  7. Решение от Wintellect, предлагающее декларативный fluent-синтаксис задания зависимостей:

    • требует наследования модели от специального базового класса;
    • не поддерживает зависимостей от цепочек свойств и коллекций;
    • поддерживает «триггеры» (возможность вызова делегата по изменению свойства модели).

    public class MyViewModel : ObservableObject {
        string _firstName;
        string _lastName;
        bool _showLastNameFirst;
     
        public string FirstName {
            get { return _firstName; }
            set { SetPropertyValue(ref _firstName, value); }
        }
     
        public string LastName {
            get { return _lastName; }
            set { SetPropertyValue(ref _lastName, value); }
        }
     
        public string FullName {
            get { return ShowLastNameFirst ? String.Format ("{0}, {1}", _lastName, _firstName) : String.Format ("{0} {1}", _firstName, _lastName); }
        }
     
        public bool ShowLastNameFirst {
            get { return _showLastNameFirst; }
            set { SetPropertyValue(ref _showLastNameFirst, value); }
        }
     
        public string Initials {
            get { return (String.IsNullOrEmpty(FirstName) ? "" : FirstName.Substring(0,1)) + (String.IsNullOrEmpty(LastName) ? "" : LastName.Substring(0,1)); }
        }
     
        public DelegateCommand SaveCommand { get; private set; }
     
        public MyViewModel() {
            SaveCommand = new DelegateCommand(() => {
                    // Save Data
                },
                    () => !(String.IsNullOrEmpty (FirstName) || String.IsNullOrEmpty (LastName)));
     
     
           WhenPropertyChanges(() => FirstName)
                .AlsoRaisePropertyChangedFor(() => FullName)
                .AlsoRaisePropertyChangedFor(() => Initials)
                .AlsoInvokeAction(SaveCommand.ChangeCanExecute);
            WhenPropertyChanges(() => LastName)
                .AlsoRaisePropertyChangedFor(() => FullName)
                .AlsoRaisePropertyChangedFor(() => Initials)
                .AlsoInvokeAction(SaveCommand.ChangeCanExecute);
            WhenPropertyChanges(() => ShowLastNameFirst )
                .AlsoRaisePropertyChangedFor(() => FullName);
        }
    }
    

Теги:
Хабы:
Всего голосов 12: ↑11 и ↓1+10
Комментарии24

Публикации

Истории

Работа

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань