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

Туториал 2. Greeter

Второй шаг обучающего маршрута. Пользователь вводит имя — приветствие обновляется в реальном времени.

Изучаем: [TwoWayBind], реактивность через On*Changed

Предполагается знание: Counter[OneWayBind], [RelayCommand]


Что строим

[ Введите имя... ] → Привет, Владислав!
InputField Text

Текст обновляется при каждом нажатии клавиши — без кнопки.


Отличие от Counter

В Counter данные шли только в одну сторону: ViewModel → UI.

Здесь добавляется обратный поток: UI → ViewModel. Когда пользователь печатает в InputField, значение записывается обратно в ViewModel, которая реагирует и обновляет приветствие.

ViewModel ──────────────────────► UI
(OneWay: Greeting)

ViewModel ◄──────────────────────► UI
(TwoWay: Name)

Шаг 1: ViewModel

using Aspid.MVVM;

[ViewModel]
public sealed partial class GreeterViewModel
{
// TwoWay — UI может читать и записывать это поле.
// Когда InputField изменится, Name обновится автоматически.
[TwoWayBind] private string _name = "";

// OneWay — только чтение для UI. Мы сами управляем значением.
[OneWayBind] private string _greeting = "Введите имя";

// Partial-метод, который Source Generator вызывает при каждом изменении Name.
// Имя формируется по правилу: On + {PropertyName} + Changed.
partial void OnNameChanged(string newValue) =>
SetGreeting(string.IsNullOrEmpty(newValue)
? "Введите имя"
: $"Привет, {newValue}!");
}

Как работает partial void On*Changed

Source Generator генерирует пустой partial void OnNameChanged(string newValue) в своей части класса. Ваша реализация вызывается внутри сгенерированного сеттера Name каждый раз при изменении значения.

InputField изменился
→ TwoWayBinder записывает значение в Name
→ сгенерированный SetName вызывает OnNameChanged
→ OnNameChanged вызывает SetGreeting
→ TextBinder обновляет Text

Вся цепочка — без рефлексии, прямые вызовы.

Разница между TwoWay и OneWay

[OneWayBind][TwoWayBind]
ViewModel → UIДаДа
UI → ViewModelНетДа
ГенерируетSet*, property, eventТо же + внутренний обратный канал
Типичное применениеТекст, счётчики, состоянияInputField, Toggle, Slider

Шаг 2: View

using UnityEngine;
using Aspid.MVVM;

[View]
public sealed partial class GreeterView : MonoView
{
// InputFieldMonoBinder — читает ввод из InputField и записывает в Name (TwoWay).
[SerializeField] private MonoBinder _name;

// TextMonoBinder — отображает Greeting (OneWay).
[SerializeField] private MonoBinder _greeting;
}

Никаких кнопок — всё реактивно.


Шаг 3: Bootstrap

using UnityEngine;
using Aspid.MVVM;

public sealed class Bootstrap : MonoBehaviour
{
[SerializeField] private GreeterView _greeterView;

private void Awake()
{
_greeterView.Initialize(new GreeterViewModel());
}

private void OnDestroy()
{
_greeterView.DeinitializeView()?.DisposeViewModel();
}
}

Шаг 4: Настройка сцены

Иерархия объектов

Greeter Scene
├── Bootstrap (Bootstrap.cs)
└── Greeter UI
├── GreeterView (GreeterView.cs)
├── Name Input (InputField + InputFieldMonoBinder)
└── Greeting Text (TextMonoBinder)

В Inspector

  1. Name Input → добавьте InputField (UI Toolkit или uGUI) и компонент InputFieldMonoBinder
  2. Greeting Text → добавьте TextMeshPro и компонент TextMonoBinder
  3. GreeterView → перетащите InputFieldMonoBinder в поле Name, TextMonoBinder в поле Greeting
  4. Bootstrap → назначьте ссылку на GreeterView

Шаг 5: раскрасить приветствие конвертером

ViewModel отдаёт "Hi, Vlad" — обычную строку. Покрасить имя это задача View, и во ViewModel её тащить не надо: для этого есть конвертеры.

У TextMonoBinder есть поле Converter. Нажмите на него и выберите в выпадающем списке Aspid → Composition → Sequence — цепочку, — а внутрь неё положите два звена:

  1. Aspid → String → Rich Text Color — заворачивает текст в тег <color=…>; задайте цвет.
  2. Aspid → String → String Format с форматом {0}! — добавляет восклицательный знак.

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

Ни строки кода. Раньше этот сэмпл возил с собой собственный PaintNameConverter — теперь RichTextColorConverter поставляется с пакетом, и писать свой не нужно. Полный каталог из 211 конвертеров — в Конвертерах.

Текст, который ввёл игрок, — отдельный разговор. TMP исполняет разметку в любой строке: ник <size=400%> растянет ярлык на экране каждого, кто его увидит. Если поле Name доступно другим игрокам, первым звеном цепочки должен идти RichTextSanitizeConverter или RichTextNoParseConverter.


Как это сделано в сэмпле пакета

Сэмпл 02. Greeter решает ту же задачу без Bootstrap и без On*Changed:

[ViewModel]
public sealed partial class GreeterMonoViewModel : MonoViewModel
{
[BindAlso(nameof(Greeting))]
[TwoWayBind]
[SerializeField] private string _name;

private string Greeting =>
string.IsNullOrEmpty(Name) ? string.Empty : $"Hi, {Name}!";

[RelayCommand]
private void Clear() => Name = string.Empty;
}
  • MonoViewModel живёт на GameObject, а ViewInitializer в сцене соединяет его с MonoView — кода инициализации нет.
  • [BindAlso(nameof(Greeting))] заново отправляет вычисляемое свойство Greeting при каждом изменении Name; само свойство никакого атрибута не требует.

Оба подхода равноправны: On*Changed удобен, когда реакция — это действие, [BindAlso] — когда реакция — это производное значение.


Расширение: кнопка сброса

Можно совместить реактивность с командой:

[ViewModel]
public sealed partial class GreeterViewModel
{
[TwoWayBind] private string _name = "";
[OneWayBind] private string _greeting = "Введите имя";

partial void OnNameChanged(string newValue) =>
SetGreeting(string.IsNullOrEmpty(newValue)
? "Введите имя"
: $"Привет, {newValue}!");

[RelayCommand]
private void Clear() => SetName("");
}
[View]
public sealed partial class GreeterView : MonoView
{
[SerializeField] private MonoBinder _name;
[SerializeField] private MonoBinder _greeting;
[SerializeField] private MonoBinder[] _clear;
}

SetName("") очистит InputField — потому что Name — это TwoWay, и изменение в ViewModel отразится в UI.


Резюме

КонцепцияЧто сделали
[TwoWayBind]_name синхронизируется с InputField в обе стороны
partial void On*ChangedРеакция на изменение без явной подписки
РеактивностьНет кнопки — UI обновляется при каждом вводе
КонвертерыЦвет и восклицательный знак добавлены во View, ViewModel про них не знает

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

Bind Modes → — все четыре режима биндинга на одном экране и свой конвертер.