Skip to main content

Custom Binder

How to bind a component the StarterKit does not know about.

You learn: ComponentFloatMonoBinder<T>, [GenerateSerializableBinder], [AddBinderContextMenu], non-silent validation, a scene without a [View] class.

Assumes: Todo List.

Scene: Scenes/Custom Binder.unity.

FileRole
Scripts/Components/HealthBar.csA plain UI component with a Value property.
Scripts/Binders/HealthBarValueMonoBinder.csThe binder.
Scripts/ViewModels/HeroViewModel.csThe ViewModel the scene binds to, edited inside ViewInitializer.

What we build

Hero
[████████████░░░░░░░░] 70%
[ Hit ] [ Heal ]

HealthBar is an ordinary project component: a filled Image, a label and a color gradient. The task is to bind its Value to Health on the ViewModel.

A component without MVVM

public sealed class HealthBar : MonoBehaviour
{
[SerializeField] private Image _fill;
[SerializeField] private TMP_Text _label;
[SerializeField] private Gradient _gradient = new();

public float Value
{
get => _value;
set
{
_value = Mathf.Clamp01(value);
_fill.fillAmount = _value;
_fill.color = _gradient.Evaluate(_value);
_label.text = $"{Mathf.RoundToInt(_value * 100f)}%";
}
}
}

The component knows nothing about the framework. The binder is an adapter on the outside, not a dependency on the inside.

The binder

[GenerateSerializableBinder]
[AddBinderContextMenu(typeof(HealthBar))]
[AddComponentMenu("Aspid/MVVM/Binders/Samples/Health Bar Binder – Value")]
public class HealthBarValueMonoBinder : ComponentFloatMonoBinder<HealthBar>
{
protected sealed override float Property
{
get => CachedComponent.Value;
set => CachedComponent.Value = this.SafeClamp01(value);
}
}

All you write is Property. The rest comes from the base and the attributes:

ElementWhat it gives
ComponentFloatMonoBinder<T>IBinder<float> plus int, long, double, a converter slot, a cached component, OneWayToSource support
[GenerateSerializableBinder]the generator emits HealthBarValueBinder, a serializable twin for fields of [View] classes
[AddBinderContextMenu(typeof(HealthBar))]an "Add Binder" entry in the HealthBar component's context menu
[AddComponentMenu]the place in the Add Component menu
this.SafeClamp01(value)a value outside 0..1 is not swallowed: an error is logged and the nearest valid value applied

The class is not sealed: the generated twin and project subclasses must be able to extend it.

Which base to pick

Property typeBase
float / intComponentFloatMonoBinder<T> / ComponentIntMonoBinder<T>
UnityEngine.Object (Sprite, Material, …)ComponentObjectMonoBinder<T, TObject>
anything elseComponentMonoBinder<T, TValue>
convert and pass onCasterMonoBinder<TFrom, TTo>

Scene

There is no [View] class. A plain MonoView lists binders by id (Health, HitCommand, HealCommand), and ViewInitializer holds a [Serializable] HeroViewModel edited right in the Inspector. One screen, zero View scripts.

[ViewModel]
[Serializable]
public sealed partial class HeroViewModel
{
[OneWayBind]
[SerializeField] [Range(0f, 1f)] private float _health = 1f;

[RelayCommand]
private void Hit() => Health = Mathf.Max(0f, Health - 0.15f);

[RelayCommand]
private void Heal() => Health = Mathf.Min(1f, Health + 0.25f);
}

Summary

ConceptWhere
Binder = Property over CachedComponentHealthBarValueMonoBinder
Serializable twin from the generator[GenerateSerializableBinder]
Component context menu[AddBinderContextMenu]
Validation that speaks upSafeClamp01
MonoView + ViewInitializer without codescene Custom Binder

Full binder rules are in Binders.

Text uses TextMeshPro (part of com.unity.ugui). The sample ships its own font asset in Fonts/ (Liberation Sans, OFL), so it does not depend on the fonts from TMP Essentials.