Перейти к основному содержимому

DynamicViewModel

DynamicViewModel создаёт типизированную ViewModel во время выполнения, без отдельного класса и Source Generator. Он предназначен для тестов View, прототипов, отладочных экранов и интерфейсов, схема которых определяется конфигурацией.

Для обычных production-экранов используйте [ViewModel]: сгенерированный тип лучше выражает фиксированную схему, проверяет идентификаторы во время компиляции и поддерживает [RelayCommand].

Быстрый старт

Метод Add<T> возвращает типизированный handle свойства:

var viewModel = new DynamicViewModel();

IDynamicProperty<int> health = viewModel.Add("Health", 100);
IDynamicProperty<string> name = viewModel.Add("Name", "Hero", BindMode.TwoWay);
IDynamicProperty<Sprite> icon = viewModel.Add("Icon", heroSprite, BindMode.OneTime);

view.Initialize(viewModel);

health.Value = 75; // View получает 75
Debug.Log(name.Value); // актуальное значение, включая ввод из View

По умолчанию создаётся OneWay-свойство.

Инициализатор коллекции

DynamicViewModel поддерживает компактный синтаксис для случаев, когда handles не нужны:

var viewModel = new DynamicViewModel
{
{ "Title", "Settings" },
{ "Volume", 0.8f, BindMode.TwoWay },
{ "Icon", settingsIcon, BindMode.OneTime }
};

Количество свойств не ограничено. В отличие от старого Create<T1, ..., T8>, новые типы свойств не требуют дополнительных перегрузок.

Чтение и изменение

Через типизированный handle

var score = viewModel.Add("Score", 0);

score.ValueChanged += value => Debug.Log($"Score: {value}");
score.Value = 10;

Повторная установка равного значения ничего не отправляет и не вызывает ValueChanged.

Через ViewModel

IDynamicProperty<int> property = viewModel.Get<int>("Score");
property.Value = 20;

if (viewModel.TryGet<int>("Score", out var scoreProperty))
scoreProperty.Value = 30;

viewModel["Score"].UntypedValue = 40; // без generic-параметра, для config-driven кода

Get<T> бросает:

  • KeyNotFoundException, если ID отсутствует;
  • ArgumentException, если свойство существует, но имеет другой тип.

TryGet<T> возвращает false в обоих случаях.

Режимы

РежимПоведение
OneWayНачальное значение и последующие изменения передаются из ViewModel во View
TwoWayЗначение синхронизируется в обе стороны; Value всегда содержит актуальный ввод View
OneTimeКаждый новый binder получает текущее значение один раз; уже подключённые binder-ы не обновляются
OneWayToSourceПринимает изменения из View, не подписывая View на последующие изменения
NoneНе поддерживается и отклоняется конструктором

Режим свойства определяет возможности bindable member. Режим конкретного binder-а по-прежнему задаётся в самом binder-е и должен быть совместим с этими возможностями.

Нетипизированный доступ

Для конфигураций, где тип становится известен только во время выполнения, доступны IDynamicProperty, Properties и индексатор:

foreach (IDynamicProperty property in viewModel.Properties)
{
Debug.Log($"{property.Id}: {property.ValueType.Name} = {property.UntypedValue}");
}

viewModel["Title"].UntypedValue = "Profile";

Запись значения несовместимого типа бросает ArgumentException. Запись null устанавливает default(T).

Пользовательское свойство можно реализовать через IDynamicProperty и добавить тем же методом:

viewModel.Add(customProperty);

Проверка идентификаторов

Пустые ID и дубликаты отклоняются сразу. По умолчанию регистр учитывается (StringComparer.Ordinal), но comparer можно заменить:

var viewModel = new DynamicViewModel(
idComparer: StringComparer.OrdinalIgnoreCase);

Обычно отсутствующий ID binder-а означает, что привязка просто не создаётся. В тестах и при разработке экрана удобно включить строгий режим:

var viewModel = new DynamicViewModel(throwOnMissingMember: true);

Тогда запрос неизвестного ID бросает KeyNotFoundException с именем отсутствующего свойства.

Добавление свойств и жизненный цикл View

Свойства следует добавлять до view.Initialize(viewModel). Add после инициализации делает свойство доступным для будущих запросов, но уже инициализированная View не выполняет поиск binder-ов повторно автоматически.

Когда использовать

Подходящие сценарии:

  • изолированные тесты View;
  • быстрый прототип;
  • debug/admin UI;
  • свойства, составленные из JSON или конфигурации;
  • небольшие переиспользуемые View с runtime-набором полей.

Не используйте DynamicViewModel как замену обычной ViewModel с фиксированной схемой. Строковые ID переносят часть ошибок из compile time в runtime, а бизнес-логика, команды и зависимости лучше выражаются отдельным типом.

Сравнение со сгенерированной ViewModel

АспектSource GeneratorDynamicViewModel
Фиксированная схемаОптимальноИзбыточно
Runtime-схемаТребует нового типаПоддерживается
Проверка ID при компиляцииДаНет
Типизированное чтение значенийДаДа
Обновление и наблюдениеДаДа
[RelayCommand] и generated hooksДаНет
Разрешение binder-аСгенерированный кодОдин поиск в словаре

Словарный поиск выполняется при подключении binder-а, а не при каждом изменении значения.

См. также