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

Биндеры

Биндер — мост между свойством ViewModel и UI-элементом. Он получает данные из ViewModel и обновляет UI, а в режимах TwoWay/OneWayToSource — отправляет изменения обратно.

Содержание


Иерархия классов

Binder (абстрактный, не MonoBehaviour)
└── MonoBinder (MonoBehaviour, абстрактный)
└── ComponentMonoBinder<TComponent>
└── ComponentMonoBinder<TComponent, TProperty>
└── TargetBinder<TTarget, TProperty>
└── TargetBinder<TTarget, TProperty, TConverter>
└── Конкретные биндеры (TextBinder, ImageSpriteBinder, ...)

Интерфейсы биндеров

ИнтерфейсНазначение
IBinder<T>void SetValue(T value) — получение значения от ViewModel
IReverseBinder<T>event Action<T> ValueChanged — отправка изменений из View
IAnyBindervoid SetValue<T>(T value) — принимает любой тип
INumberBinderSetValue(int), SetValue(float), SetValue(long), SetValue(double)
IColorBinderSetValue(Color)
IVectorBinderSetValue(Vector3)
INumberReverseBinderОбратная привязка для числовых типов

IBinder<T> — основной интерфейс

public interface IBinder<in T> : IBinder
{
void SetValue(T value);
}

Вызывается при каждом изменении свойства ViewModel (в режимах OneWay/TwoWay).

IReverseBinder<T> — обратная привязка

public interface IReverseBinder<T> : IBinder
{
event Action<T>? ValueChanged;
}

UI-элемент вызывает ValueChanged?.Invoke(newValue) при изменении (например, ввод текста, перемещение слайдера).


Binder — базовый класс

Не наследует MonoBehaviour. Содержит базовую логику привязки:

public abstract class Binder
{
public BindMode Mode { get; } // Режим привязки (сериализуется)
public virtual bool CanBind => true; // Можно отключить привязку
public bool IsBound { get; } // Привязан ли сейчас

public void Bind(IBinderAdder binderAdder); // Привязка
public void Unbind(); // Отвязка

// Виртуальные хуки:
protected virtual void OnBinding() { }
protected virtual void OnBound() { }
protected virtual void OnUnbinding() { }
protected virtual void OnUnbound() { }
}

MonoBinder

MonoBehaviour-обёртка над Binder. Базовый класс для всех Inspector-биндеров:

public abstract class MonoBinder : MonoBehaviour
{
// Сериализованный режим привязки — выбирается в Inspector
[SerializeField] private BindMode _mode;
}

Все готовые биндеры из StarterKit наследуют MonoBinder.


ComponentMonoBinder

Добавляет автоматический GetComponent<T>():

// Один generic-параметр: автоматически находит компонент
public abstract class ComponentMonoBinder<TComponent> : MonoBinder
{
protected TComponent CachedComponent { get; } // Ленивый GetComponent
}

// Два generic-параметра: + свойство для привязки
public abstract class ComponentMonoBinder<TComponent, TProperty> : ...
{
// Переопределите для привязки конкретного свойства
protected abstract TProperty Property { get; set; }
}

TargetBinder

Базовый класс StarterKit с поддержкой конвертеров:

public abstract class TargetBinder<TTarget, TProperty> : MonoBinder
{
protected TTarget Target { get; } // Целевой компонент
protected abstract TProperty Property { get; set; }
}

// С конвертером:
public abstract class TargetBinder<TTarget, TProperty, TConverter> : TargetBinder<TTarget, TProperty>
where TConverter : IConverter<TProperty?, TProperty?>
{
// Конвертер назначается через Inspector ([SerializeReference])
[SerializeReference] private TConverter? _converter;

// ViewModel → View
protected override TProperty? GetConvertedValue(TProperty? value) => ...

// View → ViewModel: срабатывает, только если конвертер реализует ITwoWayConverter
protected override TProperty? GetConvertedBackValue(TProperty? value) => ...
}

Конвертер хранится в приватном поле — производный класс переопределяет не его, а GetConvertedValue / GetConvertedBackValue. Ограничение TProperty → TProperty намеренное: конвертер на биндере меняет значение, а не его тип. Кросс-типовые преобразования (float → string) делает сам биндер.

Специализированные базовые классы:

КлассТип PropertyДоп. возможности
TargetBinder<T, bool>bool_converter — опциональный IConverter<bool, bool>
TargetBinder<T, string>string_converter — опциональный IConverter<string, string>
TargetFloatBinder<T>floatIFloatBinder — принимает int/long/double
TargetIntBinder<T>intIIntBinder
TargetBinder<T, Vector3> + IVector3BinderVector3принимает Vector2 (Z = 0) и скаляр (во все три компоненты)
TargetBinder<T, Vector2> + IVector2BinderVector2принимает Vector3 (отбрасывает Z) и скаляр (в обе компоненты)
TargetBinder<T, Color> + IColorBinderColorпринимает hex/HTML-строку цвета
TargetBinder<T, Quaternion> + IRotationBinderQuaternionчитает Vector2/Vector3 как углы Эйлера, скаляр — как одинаковый угол по трём осям

Создание кастомного биндера

Пример: биндер для Text.color

using TMPro;
using UnityEngine;
using Aspid.MVVM;
using Aspid.MVVM.StarterKit;

// Ограничиваем режимы: только OneWay и OneTime
[BindModeOverride(BindMode.OneWay, BindMode.OneTime)]
public sealed class TextColorBinder : TargetBinder<TMP_Text, Color>, IColorBinder
{
// Читаем и пишем цвет текста
protected override Color Property
{
get => Target.color;
set => Target.color = value;
}
}

Пример: биндер с обратной привязкой

using UnityEngine;
using Aspid.MVVM;

public sealed class CustomToggleBinder : MonoBinder, IBinder<bool>, IReverseBinder<bool>
{
[SerializeField] private GameObject _indicator;

// IBinder<bool> — получаем значение от ViewModel
public void SetValue(bool value)
{
_indicator.SetActive(value);
}

// IReverseBinder<bool> — отправляем изменения в ViewModel
public event Action<bool>? ValueChanged;

// Вызвать при клике пользователя
public void OnClick()
{
var newValue = !_indicator.activeSelf;
_indicator.SetActive(newValue);
ValueChanged?.Invoke(newValue);
}
}

Пример: генерик-биндер из кода

using Aspid.MVVM.StarterKit;

// Без MonoBehaviour — для привязки из кода
var binder = new DelegateOneWayBinder<string>(value =>
{
Debug.Log($"Значение изменилось: {value}");
});

[BindModeOverride]

Ограничивает режимы привязки, доступные в Inspector:

// Только OneWay и OneTime
[BindModeOverride(BindMode.OneWay, BindMode.OneTime)]
public class MyBinder : MonoBinder { }

// Все режимы
[BindModeOverride(IsAll = true)]
public class UniversalBinder : MonoBinder { }

Если биндер не поддерживает обратную привязку (нет IReverseBinder<T>), ограничьте TwoWay и OneWayToSource.


[UsedInModes]

Помечает сериализованное поле как используемое только в перечисленных режимах — в Inspector оно становится серым, когда биндер привязан в другом режиме, и получает подсказку Not used in the current Mode.:

public class MyBinder : MonoBinder, IBinder<string>, IReverseBinder<string>
{
[Tooltip("Возвращается, когда обратное преобразование не удалось.")]
[UsedInModes(BindMode.TwoWay, BindMode.OneWayToSource)]
[SerializeField] private string _convertBackFallback = string.Empty;
}

Поле может лежать и на самом биндере, и внутри любого сериализуемого объекта, который биндер держит — вложенного класса, конвертера, элемента массива. Режим берётся у ближайшего биндера над полем: если биндер вложен в другой биндер, решает вложенный. Вне биндера поле остаётся активным.

Атрибут ничего не меняет в рантайме — он только для Inspector, и в сборку без UNITY_EDITOR не попадает.


DebugLogBinder

Утилитарный биндер для отладки — логирует все получаемые значения:

// DebugLogBinder поддерживает все режимы и все типы данных
// Добавьте его в Inspector рядом с обычным биндером
// для мониторинга значений

Реализует IAnyBinder и IAnyReverseBinder, принимает любой тип данных.


См. также

  • View — объявление биндеров в View
  • ViewModel — свойства для привязки
  • StarterKit — все готовые биндеры
  • Конвертеры — преобразование значений