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

Туториал 4. Stats

Разбор сэмпла Path 4. Stats — команда с параметром, CanExecute, черновик, который попадает в модель только по Confirm.

Предполагается знание: Greeter.


Что строим

Strength [-] 3 [+]
Agility [-] 1 [+]
Intelligence [-] 2 [+]
Points available: 4
[ Confirm ] [ Reset ]

Кнопки +/- меняют черновик. Confirm отдаёт черновик модели, Reset возвращает значения модели. Обе кнопки активны только пока черновик отличается от модели.

Файлы: Samples~/04. Stats/.


Модель

public sealed class Hero
{
public event Action Changed;

public int PointsAvailable { get; private set; }

public int this[Skill skill] => _skills[skill];

public void Apply(IReadOnlyDictionary<Skill, int> skills, int pointsAvailable)
{
// validation, then:
PointsAvailable = pointsAvailable;
Changed?.Invoke();
}
}

Модель ничего не знает о MVVM. Правила (минимум очков, бюджет) живут в ней, а не во ViewModel.


ViewModel

Команда с параметром

[RelayCommand(CanExecute = nameof(CanAdd))]
private void Add(Skill skill)
{
Set(skill, Get(skill) + 1);
PointsAvailable--;
}

private bool CanAdd(Skill skill) =>
PointsAvailable > 0;

Генератор создаёт AddCommand : IRelayCommand<Skill>. Метод CanExecute принимает те же параметры, что и команда — или ни одного, если параметр для решения не нужен.

CanExecute от свойства

[OneWayBind] private bool _isDraft;

[RelayCommand(CanExecute = nameof(IsDraft))]
private void Confirm() { /* ... */ }

В CanExecute можно указать bool-свойство. Но команда не узнаёт об изменении сама — об этом нужно сообщить:

partial void OnIsDraftChanged(bool newValue)
{
ConfirmCommand.NotifyCanExecuteChanged();
ResetCommand.NotifyCanExecuteChanged();
}

ButtonCommandMonoBinder слушает это уведомление и переключает interactable кнопки.

Черновик и модель

public StatsViewModel(Hero hero)
{
_hero = hero;
_hero.Changed += Reset; // модель изменилась — черновик сбрасывается к ней
Reset();
}

public void Dispose() =>
_hero.Changed -= Reset;

Confirm вызывает _hero.Apply(...), модель поднимает Changed, тот вызывает Reset, и IsDraft становится false. ViewModel не дублирует логику «применить», она просит модель.


Биндер для параметра-enum

StarterKit поставляет ButtonCommandMonoBinder<T> и закрытые версии для int, float, string, bool, Object. Для собственного enum закрытие занимает одну строку:

[AddComponentMenu("Aspid/MVVM/Binders/Samples/Button Binder – Skill Command")]
public sealed class ButtonCommandSkillMonoBinder : ButtonCommandMonoBinder<Skill> { }

Значение Skill для каждой кнопки задаётся в Inspector в поле Param.


Два ViewModel над одной моделью

_editView.Initialize(new StatsViewModel(hero));
_committedView.Initialize(new StatsViewModel(hero));

Вторая панель без кнопок показывает только то, что уже в модели: её StatsViewModel подписан на Hero.Changed и обновляется после Confirm в первой.


Резюме

КонцепцияГде
IRelayCommand<T>Add(Skill), Remove(Skill)
CanExecute методом с параметромCanAdd, CanRemove
CanExecute свойством + NotifyCanExecuteChangedIsDraft, OnIsDraftChanged
Модель с событием и IDisposable во ViewModelHero.Changed, Dispose
Закрытие generic-биндераButtonCommandSkillMonoBinder

Следующий шаг

TodoItem → — дочерний ViewModel над моделью, затем TodoList.