Статьи

Композиционный подход к модели представления ViewModel с наблюдаемым свойством

MVVM и XAML очень хорошо играют вместе. Основным строительным блоком для создания пользовательских интерфейсов в XAML является возможность привязки к моделям представления (VM в MVVM, мы говорим о половине шаблона здесь). Одно или двухстороннее связывание, это не имеет значения. Ваша логика может обновлять свойства, а пользовательские интерфейсы будут волшебным образом обновляться.

Привязываемый объект и его свойства

XAML ожидает, что модели представления удовлетворяют определенным критериям для достижения этой привязки. Классы, которые реализуются  INotifyPropertyChanged, считаются  связываемыми,  и любое изменение в базовом свойстве должно использовать уведомления, чтобы правильно уведомить XAML о том, что значение необходимо получить снова.

Рассмотрим эту маленькую модель:

public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
     
    // Cache across instances
    private static readonly PropertyChangedEventArgs NameChangedArgs
        = new PropertyChangedEventArgs(nameof(Name));
     
    private string _name;
     
    public string Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                _name = value;
                PropertyChanged?.Invoke(this, NameChangedArgs);
            }
        }
    }
}

Там довольно много шума, и мы только что реализовали одно свойство (правильно). Код выглядит повторяющимся, и во многих MVVM-фреймворках появились вспомогательные классы для снижения уровня шума. Одним из таких примеров является  MVVMLight , потрясающая библиотека для быстрой реализации MVVM. Наш класс сверху можно сократить на:

public class UserViewModel : ViewModelBase // implements INotifyPropertyChanged
{  
    private string _name;
     
    public string Name
    {
        get { return _name; }
        set { Set(ref _name, value); }
    }
}

У нас все еще остаются детали реализации, а именно наше частное поле, и мы потеряли некоторую небольшую производительность, но сокращение по свойствам и классам очевидно.

Можно уменьшить этот код еще больше, исключив необходимость в вспомогательном поле. Вы можете найти такую ​​попытку в YAWL.Common.Mvvm.ViewModelBase :

public class UserViewModel : YAWL.Common.MVVM.ViewModelBase
{  
    public string Name
    {
        get { return Get<string>(); }
        set { Set(value); }
    }
}

Закрытое поле автоматически создается для каждого свойства и помещается в словарь для последующего поиска. Дальнейшее сокращение кода требует небольших жертв в отделе производительности.

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

[ImplementPropertyChanged]
public class UserViewModel
{  
    public string Name { get; set; }
}

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

Производные свойства

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

public class CartViewModel : ViewModelBase
{
    public ObservableCollection<ItemViewModel> Items { get; }
    public double Price => Items.Sum(x => x.Price);
}

Совершенно очевидно, что  Price это не более чем сумма цен на предметы в  Items собственности. Так как у него нет установщика, он не будет обновляться, когда товар добавляется или удаляется из корзины. Мы все еще можем вручную запустить обновление, добавив обработчик событий в  Items коллекцию, который будет уведомлять о каждом изменении. Код может выглядеть примерно так:

public CartViewModel()
{
    Items = new ObservableCollection&ltItemViewModel>();
    Items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Price));
}

Существует несколько способов написания таких зависимостей, и есть библиотеки, которые помогают создавать такие цепочки уведомлений. Уведомление других свойств об изменениях быстро становится утомительным, а фрагменты уведомлений разбрасываются по всем моделям представления. Еще хуже то, что цепочка уведомлений между родителями и потомками становится громоздкой, поскольку все должно быть сделано вручную. И когда код развивается, эти отношения настолько взаимосвязаны, что увеличивают стоимость обслуживания.

Совершенно очевидно, что зависимые и производные свойства не могут быть созданы из других свойств. Вы не можете передать собственность кому-то другому и позволить им зависеть от изменений. Наличие единого уровня уведомлений для привязки XAML для просмотра моделей довольно ограниченно по мере усложнения приложений.

Разве не так легко, как  c := a + b лучше?

Есть способ сделать это.

ObservableProperty

Вдохновленные  ReactiveProperty  и  ReactiveUI,  мы можем создать свойство, которое способно генерировать события изменения и может объединяться с другими свойствами. Давайте посмотрим, как мы будем рефакторинг нашего кода сверху:

public class UserViewModel
{  
    public ObservableProperty<string> Name { get; } = new ObservableProperty<string>();
}

Немного многословно, базовый класс не требуется, и XAML необходимо изменить с  {Binding Name} на,  {Binding Name.Value} так как значение теперь переносится аналогично  Nullable<T> классу. Чтобы изменить значение, измените внутреннее  Value свойство.

Name.Value = "new name";

Итак, стоит ли увеличивать сложность при доступе к свойствам? Некоторые модели представлений могут не воспользоваться этим подходом, другие могут. Однако применение этого шаблона во всей кодовой базе дает согласованность.

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

public class CartViewModel : ViewModelBase
{
    public ObservableCollection<ItemViewModel> Items { get; }
    public ObservableProperty<double> Price { get; }
     
    public CartViewModel()
    {
        Items = new ObservableCollection&ltItemViewModel>();
        Price = Items.Reduce(enumerable => enumerable.Sum(x => x.Price));
    }
}

Reduce — это метод расширения, который присоединяется  ObservableCollection и отслеживает любые изменения. Дайте редуктору лямбда типа  Func<IEnumerable<T>, R> начальное значение. Когда исходная коллекция изменится, будет произведен пересчет, а целевое свойство обновится само.

Кодекс лаконичен, согласован и декларативно написан в одном месте. Сводя инициализацию производных свойств в одном месте, больше не нужно сканировать весь файл или искать ссылки, если они хотят понять, когда он изменяется. Это также предотвращает забыть обновить зависимое / производное свойство при изменении значения. Меньше работы для программиста означает лучший код. Этот код также реактивен, что важно, когда мы хотим отделить вещи.

Давайте посмотрим на другие операторы композиции.

FirstName = new ObservableProperty<string>();
LastName = new ObservableProperty<string>();
Initial = FirstName.Map(s => s?.Length > 0 ? s[0] : '')
FullName = FirstName.Combine(LastName, (first, last) => $"{first} {last}");
CanRegister = IsUsernameAvailable && InternetHelper.HasInternetConnection

Обратите внимание, что в последнем примере показано, как естественным и лаконичным образом комбинировать логические свойства.

Map создает новое зависимое свойство, которое с помощью функции преобразования гарантирует, что целевое свойство всегда является преобразованной версией исходного значения.
Combine позволяет создавать новые значения путем объединения нескольких значений с указанной функцией объединения.

Читатели, знакомые с Rx, заметят, что это те же самые фундаментальные блоки, которые используются в потоках. Подобные реализации можно найти в уже упоминавшейся библиотеке ReactiveUI. Этот стиль на самом деле вдохновлен функциональным программированием и пытается создавать сложные функции с помощью общих операций и простых строительных блоков.

В следующих статьях мы рассмотрим, как преобразовывать другие стандартные блоки в MVVM-подобных командах.

ObservableProperty можно найти более на GitHub:  YAWL.Composition.ObservableProperty.

Последний раз обновлялся  Тони  на ,