Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

191 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bindings

dotnet-test Releases openupm GitHub license

English | 日本語

Overview

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.

Requirements

  • Unity 6000.0 or later
  • Unity uGUI 2.0.0 or later

Installation

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

Quick Start

1. Define a ViewModel

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.

Generated Code

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
}

2. Set up the Binder in the Scene

  1. Add a Binder component to a GameObject in your scene.
  2. In the Inspector, assign the generated CounterView instance to the Views list.
  3. Call binder.Initialize(viewModel) to associate the ViewModel, then call binder.Run() (or enable Run On Start) to bind the UI.

Using the Binder Inspector

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.

Add View – namespace selection

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.

Add View – View selection

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.

Assign UI components

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.

Preview and Invoke

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);
    }
}

VContainer Support

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);
    }
}

Attributes

[ViewModel]

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.

[Required]

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.

[Schema(bindingPath, id, format, tooltip)]

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.

GameObject active state binding

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.

RectTransform calculated size 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.

Messaging with IMvvmSubscriber<T>

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:

  1. Define a message type (typically a readonly struct).
  2. Call _publisher.Publish(new MyMessage(...)) from the ViewModel.
  3. 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.

Analyzer Diagnostics

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.

License

This library is released under the MIT license.

About

MVVM-based automated UI binding framework built on uGUI.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages