English | 日本語
Bindings is an MVVM-based automated UI binding framework for Unity built on uGUI.
It provides a Roslyn Source Generator that automatically generates ViewModel and View partial classes from annotated C# code, eliminating boilerplate and keeping UI bindings in sync with your data models.
Bind requests triggered by PublishRebindMessage() are batched internally and executed all at once just before the Canvas renders (Canvas.preWillRenderCanvases), avoiding redundant updates within a single frame.
- Unity 6000.0 or later
- Unity uGUI 2.0.0 or later
Open Window > Package Manager, select [+] > Add package from git URL, and enter the following URL:
https://github.com/AndanteTribe/Bindings.git?path=src/Bindings.Unity/Packages/jp.andantetribe.bindings
Annotate a partial class with [ViewModel]. Use [Required] to declare constructor parameters and [Schema] to declare UI bindings.
using Bindings;
using UnityEngine;
namespace MyApp
{
[ViewModel]
public partial class CounterViewModel
{
[Required]
private readonly CounterModel _model;
[SerializeField]
[Schema(PathResolver.TMPro.TMP_Text.text)]
private int _count;
[Schema(PathResolver.UnityEngine.UI.Button.onClick)]
public void Increment()
{
_count += 1;
PublishRebindMessage();
}
[Schema(PathResolver.UnityEngine.UI.Button.onClick)]
public void Decrement()
{
_count -= 1;
PublishRebindMessage();
}
partial void OnPostBind()
{
_model.Count = _count;
}
}
}The source generator automatically produces:
MyApp.CounterViewModel.g.cs— a partial ViewModel class with properties, constructor, and publisher wiring.MyApp.CounterView.g.cs— a sealed partial View class with serialized UI component fields and binding logic.
MyApp.CounterViewModel.g.cs
#nullable enable
namespace MyApp
{
#if UNITY_EDITOR
[global::System.Serializable]
#endif
public partial class CounterViewModel : global::Bindings.IViewModel
{
private readonly global::Bindings.IMvvmPublisher _publisher;
public int Count
{
get => _count;
set
{
_count = value;
PublishRebindMessage();
}
}
public CounterViewModel(global::MyApp.CounterModel model, global::Bindings.IMvvmPublisher publisher)
{
_model = model;
_publisher = publisher;
}
public void NotifyCompletedBind() => OnPostBind();
partial void OnPostBind();
[global::System.Runtime.CompilerServices.MethodImpl(
global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private void PublishRebindMessage()
{
_publisher.PublishRebindMessage<CounterViewModel>();
}
}
}MyApp.CounterView.g.cs
#nullable enable
namespace MyApp
{
[global::System.Serializable]
public sealed partial class CounterView : global::Bindings.IView<global::MyApp.CounterViewModel>
{
[global::System.NonSerialized]
private global::MyApp.CounterViewModel _viewModel = null!;
[global::UnityEngine.SerializeField]
private global::TMPro.TMP_Text _countText = null!;
[global::UnityEngine.SerializeField]
private global::UnityEngine.UI.Button _incrementButton = null!;
[global::UnityEngine.SerializeField]
private global::UnityEngine.UI.Button _decrementButton = null!;
void global::Bindings.IView<global::MyApp.CounterViewModel>.Initialize(
global::MyApp.CounterViewModel viewModel)
{
_viewModel = viewModel;
}
global::System.Threading.Tasks.ValueTask global::Bindings.IView.BindAsync(
global::System.Threading.CancellationToken _)
{
BindAll();
return default;
}
private void BindAll()
{
global::Bindings.TextMeshProExtensions.SetValue(_countText, _viewModel.Count);
_incrementButton.onClick.RemoveAllListeners();
_incrementButton.onClick.AddListener(_viewModel.Increment);
_decrementButton.onClick.RemoveAllListeners();
_decrementButton.onClick.AddListener(_viewModel.Decrement);
OnPostBind();
_viewModel.NotifyCompletedBind();
}
partial void OnPostBind();
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD || !DISABLE_DEBUGTOOLKIT
public sealed partial class CounterView : global::Bindings.IMvvmSubscriber<global::Bindings.DebugBindMessage>
{
void global::Bindings.IMvvmSubscriber<global::Bindings.DebugBindMessage>.OnReceivedMessage(
global::Bindings.DebugBindMessage message)
{
message.BindTo(this);
global::Bindings.TextMeshProExtensions.SetValue(_countText, _viewModel.Count);
OnPostBind();
_viewModel.NotifyCompletedBind();
}
}
#endif
}- Add a
Bindercomponent to a GameObject in your scene. - In the Inspector, assign the generated
CounterViewinstance to the Views list. - Call
binder.Initialize(viewModel)to associate the ViewModel, then callbinder.Run()(or enable Run On Start) to bind the UI.
The Binder component comes with a custom Unity Inspector that makes it easy to register Views without writing any setup code.
Step 1 — Add a View: namespace selection
Click the Add View button. A dropdown will appear — select the namespace that contains the View you want to register.
Step 2 — Add a View: View selection
After choosing a namespace, a list of all View types in that namespace is shown. Click the View you want to add.
Step 3 — Assign UI components
The selected View is now registered in the Views list. Each [SerializeField] declared in the View is shown as a slot in the Inspector — drag the corresponding UI GameObject onto each slot. To remove a View, click the − button in the top-right corner of its entry.
Step 4 — Preview
Click the Preview button below Add View to reveal the ViewModel associated with each registered View. Fill in values directly in the Inspector and click Invoke to apply the bindings and verify that the UI updates as expected.
public class GameEntry : MonoBehaviour
{
[SerializeField] private Binder _binder;
private void Start()
{
var model = new CounterModel();
var publisher = _binder; // Binder implements IMvvmPublisher
var viewModel = new CounterViewModel(model, publisher);
_binder.Initialize(viewModel);
}
}When VContainer is installed, Binder automatically supports DI injection — no extra setup is required. Simply register your ViewModels and the Binder in the container. VContainer will call Initialize(IReadOnlyList<IViewModel>) automatically, so no manual Initialize call is needed.
public class GameLifetimeScope : LifetimeScope
{
[SerializeField] private Binder _binder;
protected override void Configure(IContainerBuilder builder)
{
var model = new CounterModel();
builder.RegisterInstance(model);
builder.RegisterViewModel<CounterViewModel>(_binder);
builder.RegisterComponent(_binder);
}
}Applied to a partial class or struct to mark it as a ViewModel and trigger source generation.
| Parameter | Type | Default | Description |
|---|---|---|---|
requireBindImplementation |
bool |
false |
When true, skips auto-generating BindAsync. The user must implement it manually on the partial View class. |
Applied to a field or property to include it as a parameter in the generated constructor. The generated constructor will accept a parameter of the field's type and assign it.
Applied to a field or method to bind it to a UI component. Can be applied multiple times to the same member.
| Parameter | Type | Default | Description |
|---|---|---|---|
bindingPath |
string |
required | The UI component member to bind, expressed via PathResolver (e.g., PathResolver.TMPro.TMP_Text.text). |
id |
int |
-1 |
Groups multiple [Schema] entries onto the same View component field. Use id >= 0 for explicit grouping; -1 means auto-numbered. Values less than -1 trigger BND002. |
format |
string |
"" |
Format string applied when binding to TMPro.TMP_Text.text (e.g., "N0"). Ignored for other binding paths. |
tooltip |
string |
"" |
Tooltip text shown in the Unity Inspector on the generated View component field. |
If you specify UnityEngine.GameObject.activeSelf (or PathResolver.UnityEngine.GameObject.activeSelf) as the bindingPath, the generated View code will call GameObject.SetActive(bool) to update the GameObject active state. The generator maps the activeSelf binding to the proper runtime API because activeSelf is a read-only property — using SetActive ensures the active state is changed correctly.
Example:
[Schema(PathResolver.UnityEngine.GameObject.activeSelf)]
private bool _isVisible;The generated binding will call _isVisibleGameObject.SetActive(_viewModel.IsVisible); (or the equivalent generated helper) when applying the binding.
Use PathResolver.UnityEngine.RectTransform.rect.size to bind a Vector2 to the calculated size of a RectTransform while preserving its current anchors:
[Schema(PathResolver.UnityEngine.RectTransform.rect.size)]
private Vector2 _size;The generated View calls RectTransform.SetSizeWithCurrentAnchors once for the horizontal axis with Size.x and once for the vertical axis with Size.y. This binding is opt-in; PathResolver.UnityEngine.RectTransform.sizeDelta continues to assign RectTransform.sizeDelta directly.
In addition to automatic UI rebinding, a ViewModel can send arbitrary messages to Views that have opted in. This allows the ViewModel to push events (e.g., showing a dialog, playing an animation) without coupling it to the View type.
How it works:
- Define a message type (typically a
readonly struct). - Call
_publisher.Publish(new MyMessage(...))from the ViewModel. - Implement
IMvvmSubscriber<MyMessage>on the partial View class to handle the message.
// 1. Define the message
public readonly struct ShowDialogMessage
{
public readonly string Text;
public ShowDialogMessage(string text) => Text = text;
}
// 2. Publish from the ViewModel
[ViewModel]
public partial class MyViewModel
{
[Schema(PathResolver.UnityEngine.UI.Button.onClick)]
public void OnConfirm()
{
_publisher.Publish(new ShowDialogMessage("Are you sure?"));
}
}
// 3. Receive in the View (partial class alongside generated code)
public sealed partial class MyView : IMvvmSubscriber<ShowDialogMessage>
{
void IMvvmSubscriber<ShowDialogMessage>.OnReceivedMessage(ShowDialogMessage message)
{
// show dialog with message.Text
}
}Binder delivers the message to every View in its list that implements IMvvmSubscriber<T> for the given T.
| ID | Level | Condition |
|---|---|---|
BND001 |
Error | [ViewModel] class name does not contain "ViewModel"; neither ViewModel nor View source is generated. |
BND002 |
Error | [Schema] id is less than -1. |
BND003 |
Error | Multiple [Schema] entries assign conflicting tooltip values to the same View field. |
BND004 |
Error | A [ViewModel] type or one of its containing types has an accessibility that cannot be represented in generated source. |
BND005 |
Warning | A field whose type is annotated with [ViewModel] is marked with Unity's [SerializeField] or [SerializeReference]. Unity deserialization may leave generated runtime state uninitialized even when the type is marked [Serializable]; construct or assign the ViewModel at runtime instead. Applying [Serializable] to a ViewModel by itself does not report this diagnostic, allowing custom serialization scenarios. |
This library is released under the MIT license.



