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<ItemViewModel>();
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<ItemViewModel>();
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
.