Skip to main content

Binders

A binder is the bridge between a ViewModel property and a UI element. It receives data from the ViewModel and updates the UI, and in TwoWay/OneWayToSource modes sends changes back.

Contents


Class hierarchy

Binder (abstract, not a MonoBehaviour)
└── MonoBinder (MonoBehaviour, abstract)
└── ComponentMonoBinder<TComponent>
└── ComponentMonoBinder<TComponent, TProperty>
└── TargetBinder<TTarget, TProperty>
└── TargetBinder<TTarget, TProperty, TConverter>
└── Concrete binders (TextBinder, ImageSpriteBinder, ...)

Binder interfaces

InterfacePurpose
IBinder<T>void SetValue(T value): receives a value from the ViewModel
IReverseBinder<T>event Action<T> ValueChanged: sends changes from the View
IAnyBindervoid SetValue<T>(T value): accepts any type
INumberBinderSetValue(int), SetValue(float), SetValue(long), SetValue(double)
IColorBinderSetValue(Color)
IVectorBinderSetValue(Vector3)
INumberReverseBinderReverse binding for numeric types

IBinder<T>: the main interface

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

Called on every change of the ViewModel property (OneWay/TwoWay modes).

IReverseBinder<T>: reverse binding

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

The UI element raises ValueChanged?.Invoke(newValue) on change (typed text, a moved slider).


Binder: the base class

Does not inherit MonoBehaviour. Holds the core binding logic:

public abstract class Binder
{
public BindMode Mode { get; } // Binding mode (serialized)
public virtual bool CanBind => true; // Lets a binder opt out of binding
public bool IsBound { get; } // Whether it is bound right now

public void Bind(IBinderAdder binderAdder); // Bind
public void Unbind(); // Unbind

// Virtual hooks:
protected virtual void OnBinding() { }
protected virtual void OnBound() { }
protected virtual void OnUnbinding() { }
protected virtual void OnUnbound() { }
}

MonoBinder

A MonoBehaviour wrapper over Binder. The base of every Inspector binder:

public abstract class MonoBinder : MonoBehaviour
{
// Serialized binding mode, chosen in the Inspector
[SerializeField] private BindMode _mode;
}

Every ready-made StarterKit binder inherits MonoBinder.


ComponentMonoBinder

Adds an automatic GetComponent<T>():

// One generic parameter: finds the component itself
public abstract class ComponentMonoBinder<TComponent> : MonoBinder
{
protected TComponent CachedComponent { get; } // Lazy GetComponent
}

// Two generic parameters: plus the property to bind
public abstract class ComponentMonoBinder<TComponent, TProperty> : ...
{
// Override to bind a concrete property
protected abstract TProperty Property { get; set; }
}

TargetBinder

The StarterKit base class with converter support:

public abstract class TargetBinder<TTarget, TProperty> : MonoBinder
{
protected TTarget Target { get; } // Target component
protected abstract TProperty Property { get; set; }
}

// With a converter:
public abstract class TargetBinder<TTarget, TProperty, TConverter> : TargetBinder<TTarget, TProperty>
where TConverter : IConverter<TProperty?, TProperty?>
{
// The converter is assigned in the Inspector ([SerializeReference])
[SerializeReference] private TConverter? _converter;

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

// View → ViewModel: runs only when the converter implements ITwoWayConverter
protected override TProperty? GetConvertedBackValue(TProperty? value) => ...
}

The converter lives in a private field; a subclass overrides GetConvertedValue / GetConvertedBackValue, not the field. The TProperty → TProperty constraint is deliberate: a converter on a binder changes the value, not its type. Cross-type conversions (float → string) are done by the binder itself.

Specialized base classes:

ClassProperty typeExtras
TargetBinder<T, bool>bool_converter: optional IConverter<bool, bool>
TargetBinder<T, string>string_converter: optional IConverter<string, string>
TargetFloatBinder<T>floatIFloatBinder: accepts int/long/double
TargetIntBinder<T>intIIntBinder
TargetBinder<T, Vector3> + IVector3BinderVector3accepts Vector2 (Z = 0) and a scalar (all three components)
TargetBinder<T, Vector2> + IVector2BinderVector2accepts Vector3 (drops Z) and a scalar (both components)
TargetBinder<T, Color> + IColorBinderColoraccepts a hex/HTML color string
TargetBinder<T, Quaternion> + IRotationBinderQuaternionreads Vector2/Vector3 as Euler angles, a scalar as the same angle on all three axes

Writing a custom binder

Example: a binder for Text.color

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

// Restrict the modes: OneWay and OneTime only
[BindModeOverride(BindMode.OneWay, BindMode.OneTime)]
public sealed class TextColorBinder : TargetBinder<TMP_Text, Color>, IColorBinder
{
// Read and write the text color
protected override Color Property
{
get => Target.color;
set => Target.color = value;
}
}

Example: a binder with reverse binding

using UnityEngine;
using Aspid.MVVM;

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

// IBinder<bool>: receive the value from the ViewModel
public void SetValue(bool value)
{
_indicator.SetActive(value);
}

// IReverseBinder<bool>: send changes to the ViewModel
public event Action<bool>? ValueChanged;

// Call on user click
public void OnClick()
{
var newValue = !_indicator.activeSelf;
_indicator.SetActive(newValue);
ValueChanged?.Invoke(newValue);
}
}

Example: a generic binder from code

using Aspid.MVVM.StarterKit;

// No MonoBehaviour: binding from code
var binder = new DelegateOneWayBinder<string>(value =>
{
Debug.Log($"Value changed: {value}");
});

[BindModeOverride]

Restricts the binding modes offered in the Inspector:

// OneWay and OneTime only
[BindModeOverride(BindMode.OneWay, BindMode.OneTime)]
public class MyBinder : MonoBinder { }

// Every mode
[BindModeOverride(IsAll = true)]
public class UniversalBinder : MonoBinder { }

If the binder has no reverse binding (no IReverseBinder<T>), exclude TwoWay and OneWayToSource.


[UsedInModes]

Marks a serialized field as used only in the listed modes. In the Inspector it is greyed out when the binder is bound in another mode and gets the tooltip Not used in the current Mode.:

public class MyBinder : MonoBinder, IBinder<string>, IReverseBinder<string>
{
[Tooltip("Returned when the reverse conversion fails.")]
[UsedInModes(BindMode.TwoWay, BindMode.OneWayToSource)]
[SerializeField] private string _convertBackFallback = string.Empty;
}

The field may sit on the binder itself or inside any serializable object the binder holds: a nested class, a converter, an array element. The mode is taken from the nearest binder above the field; when a binder is nested in another binder, the inner one decides. Outside a binder the field stays active.

The attribute changes nothing at runtime. It is Inspector-only and is stripped from builds without UNITY_EDITOR.


DebugLogBinder

A utility binder for debugging. It logs every value it receives:

// DebugLogBinder supports every mode and every data type.
// Add it in the Inspector next to a regular binder
// to watch the values.

Implements IAnyBinder and IAnyReverseBinder, accepts any data type.


See also