UnityMvvmToolkit

repository·main·Indexed 19 days ago

https://github.com/librastack/unitymvvmtoolkit

A data-binding framework for Unity (version 1.1.9) that enables clean separation of concerns between business logic (ViewModels) and UI, supporting both uGUI and UI Toolkit. Key features include runtime data-binding, support for custom UI elements, and integration with UniTask via AsyncCommand. It provides base classes like CanvasView and DocumentView, and utilizes IBindingContext, Property<T>, and Command to manage state and UI actions.

Tokens
9.1K
Snippets
24
Records
27
Agent score
19%

What's inside UnityMvvmToolkit

  1. Overview of UnityMvvmToolkit features

    main

    UnityMvvmToolkit is a package designed to bring data-binding to Unity projects, enabling a clean separation between business logic and UI.

    Key Features:

    • Runtime data-binding: Establish connections between UI and data at runtime.
    • Integration: Supports both UI Toolkit and uGUI.
    • Multiple-properties binding: Bind multiple properties to a single UI element.
    • Custom UI Elements: Support for custom UI components.
    • Compatibility: Works with UniTask and supports both Mono and IL2CPP (with specific settings for Unity 2021).
  2. Implement IBindingContext for ViewModels

    main

    The IBindingContext is a marker interface used for ViewModels. It signals to Views that the class contains observable properties available for data binding.

    If your ViewModel does not have a parameterless constructor, you must override the GetBindingContext method in your View class to handle manual instantiation (e.g., via dependency injection).

    public class CounterViewModel : IBindingContext
    {
        public CounterViewModel()
        {
            Count = new Property<int>();
        }
    
        public IProperty<int> Count { get; }
    }
  3. Use BindingContextProvider to scope binding contexts

    main

    The BindingContextProvider allows you to define a specific IBindingContext for a subtree of UI elements.

    • If a child element does not specify a context, it inherits the context of its parent (or the nearest BindingContextProvider).
    • Use the binding-context-path attribute to specify which property in the current context should be used as the new binding context for children.

    To avoid memory allocations for custom providers, you can create a specialized class inheriting from BindingContextProvider<T> using the [UxmlElement] attribute.

    <ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
        <!-- Uses MainViewModel -->
        <uitk:BindableLabel name="Label1" binding-text-path="Title" />
    
        <!-- Inherits MainViewModel -->
        <uitk:BindingContextProvider>
            <uitk:BindableLabel name="Label2" binding-text-path="Title" />
        </uitk:BindingContextProvider>
    
        <!-- Uses CustomViewModel via path -->
        <uitk:BindingContextProvider binding-context-path="CustomViewModel">
            <uitk:BindableLabel name="Label3" binding-text-path="Title" />
        </uitk:BindingContextProvider>
    </ui:UXML>
  4. Create custom bindable controls with Source Generators

    main

    To create a custom VisualElement that supports data binding with minimal boilerplate, use the [BindableElement] and [BindableProperty] attributes from the UnityMvvmToolkit.Generator.

    Workflow:

    1. Define your base VisualElement.
    2. Mark the class with [BindableElement] and make it partial.
    3. Mark the property you want to bind with [BindableProperty].
    4. Use partial methods like AfterSetBindingContext, AfterResetBindingContext, or On[PropertyName]ValueChanged to handle logic when the binding changes.

    Note: The UnityMvvmToolkit.Generator is a premium tool available via Patreon.

    [BindableElement]
    public partial class BindableImage : Image
    {
        [BindableProperty]
        private IReadOnlyProperty<Texture2D> _imageProperty;
    
        partial void AfterSetBindingContext(IBindingContext context, IObjectProvider objectProvider)
        {
            SetImage(_imageProperty?.Value);
        }
    
        partial void AfterResetBindingContext(IObjectProvider objectProvider)
        {
            SetImage(null);
        }
    
        partial void OnImagePropertyValueChanged([CanBeNull] Texture2D value)
        {
            SetImage(value);
        }
    }
  5. Implement UI Toolkit bindings with DocumentView

    main

    To use Unity's UI Toolkit, follow these steps:

    1. Create a View class: Inherit from DocumentView<TBindingContext>, where TBindingContext is your ViewModel type.
    2. Configure UXML: In your .uxml file, use the BindableLabel control (from the UnityMvvmToolkit.UITK.BindableUIElements namespace) and set the binding-text-path attribute to the name of your property.
    3. Setup Scene: Add a UI Document component to your scene, assign your .uxml as the Source Asset, and attach your DocumentView component to it.
    using UnityMvvmToolkit.UITK;
    
    public class MyFirstDocumentView : DocumentView<MyFirstViewModel>
    {
    }
    <ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
        <uitk:BindableLabel binding-text-path="Text" />
    </ui:UXML>
  6. Implement observable properties with Property<T>

    main

    Use Property<T> and ReadOnlyProperty<T> to create bindable properties in your ViewModel. These implement IProperty<T> and IReadOnlyProperty<T>, which expose a ValueChanged event for UI updates.

    Simple Property

    Directly instantiate a Property<T> in the constructor.

    Observable Property with Attributes

    You can use the [Observable] attribute on private fields to automatically map them to public binding paths. The toolkit automatically converts field names like _title or m_title to Title for the binding path.

    Wrapping non-observable models

    To wrap an existing data model (like a database entity), use [Observable(nameof(PropertyName))] on a private IProperty<T> field and relay the getter/setter to the underlying model.

    Using UnityMvvmToolkit.Generator

    If using the UnityMvvmToolkit.Generator package, you can use the [WithObservableBackingField] attribute on a property to automatically generate the observable backing field, significantly reducing boilerplate.

    // Simple Property
    public class CounterViewModel : IBindingContext
    {
        public CounterViewModel()
        {
            Count = new Property<int>();
        }
    
        public IProperty<int> Count { get; }
    }
    
    // Observable Property
    public class MyViewModel : IBindingContext
    {
        [Observable("Count")]
        private readonly IProperty<int> _amount = new Property<int>();
      
        [Observable]
        private readonly IProperty<string> _title = new Property<string>();
    }
  7. Install UnityMvvmToolkit via OpenUPM

    main

    You can install the package using the OpenUPM scoped registry.

    1. Open Edit/Project Settings/Package Manager.
    2. Add a new Scoped Registry with the following configuration:
      • Name: package.openupm.com
      • URL: https://package.openupm.com
      • Scope(s): com.cysharp.unitask and com.chebanovdd.unitymvvmtoolkit
    3. Open Window/Package Manager.
    4. Select My Registries and install both UniTask and UnityMvvmToolkit.
    Name: package.openupm.com
    URL: https://package.openupm.com
    Scope(s):
      com.cysharp.unitask
      com.chebanovdd.unitymvvmtoolkit
  8. Enable Async Commands and USS Transition support via UniTask

    main

    To use AsyncCommand and <AsyncCommand<T>>, you must add the UniTask package to your Unity project.

    Additionally, UnityMvvmToolkit provides extension methods for VisualElement that allow you to await Unity UI Toolkit (USS) transitions using UniTask. All transition extension methods include an optional timeoutMs parameter which defaults to 2500ms.

    // Requires UniTask package installed
    public async UniTask DeactivatePanel()
    {
        try
        {
            panel.style.opacity = 0;
            panel.style.paddingBottom = 0;
            
            // Await for the 'opacity' || 'paddingBottom' to end or cancel.
            await panel.WaitForAnyTransitionEnd();
            
            // Await for the 'opacity' & 'paddingBottom' to end or cancel.
            await panel.WaitForAllTransitionsEnd();
            
            // Await 150ms.
            await panel.WaitForLongestTransitionEnd();
    
            // Await 65ms.
            await panel.WaitForTransitionEnd(0);
            
            // Await for the 'padding-bottom' to end or cancel.
            await panel.WaitForTransitionEnd(new StylePropertyName("padding-bottom"));
            
            // Await for the 'paddingBottom' to end or cancel using nameof.
            await panel.WaitForTransitionEnd(nameof(panel.style.paddingBottom));
            
            // Await using a custom ITransitionPredicate.
            await panel.WaitForTransitionEnd(new TransitionAnyPredicate());
        }
        finally
        {
            panel.visible = false;
        }
    }
  9. Optimize performance by warming up object pools

    main

    UnityMvvmToolkit uses object pooling to minimize runtime memory allocations. To prevent allocations during critical execution time, you can "warm up" objects (pre-allocate and rent them) by overriding GetObjectProvider in your view classes.

    Use the IObjectProvider fluent API to warm up ViewModels, Value Converters, or entire assemblies.

    public abstract class BaseView<TBindingContext> : DocumentView<TBindingContext>
            where TBindingContext : class, IBindingContext
    {
        protected override IObjectProvider GetObjectProvider()
        {
            return new BindingContextObjectProvider(new IValueConverter[] { new IntToStrConverter() })
                // Finds and warms up all classes from calling assembly that implement IBindingContext.
                .WarmupAssemblyViewModels()
                // Finds and warms up all classes from a specific assembly that implement IBindingContext.
                .WarmupAssemblyViewModels(Assembly.GetExecutingAssembly())
                // Warms up a specific ViewModel class.
                .WarmupViewModel<CounterViewModel>()
                .WarmupViewModel(typeof(CounterViewModel))
                // Creates a specific number of instances for a Value Converter to avoid runtime allocation.
                .WarmupValueConverter<IntToStrConverter>(5);
        }
    }
  10. Implement virtualized lists with BindableListView

    main

    The BindableListView is the most efficient way to display large lists in UI Toolkit because it uses virtualization (creating VisualElements only for visible items).

    Requirements:

    1. Data Model: Individual items must implement ICollectionItem.
    2. Item Template: You must provide a VisualTreeAsset for the item template by overriding GetCollectionItemTemplates in your DocumentView.
    3. Binding: Bind the binding-items-source-path to an ObservableCollection in your ViewModel.

    Note: BindableListView and BindableScrollView are provided for UI Toolkit only.

    // 1. Item ViewModel
    public class UserItemViewModel : ICollectionItem
    {
        [Observable(nameof(Name))] 
        private readonly IProperty<string> _name = new Property<string>();
    
        public int Id { get; }
        public string Name
        {
            get => _name.Value;
            set => _name.Value = value;
        }
    }
    
    // 2. List View implementation
    public class UserListView : BindableListView<UserItemViewModel>
    {
        public new class UxmlFactory : UxmlFactory<UserListView, UxmlTraits> {}
    }
    
    // 3. Main View with Template mapping
    public class UsersView : DocumentView<UsersViewModel>
    {
        [SerializeField] private VisualTreeAsset _userItemViewAsset;
    
        protected override IReadOnlyDictionary<Type, object> GetCollectionItemTemplates()
        {
            return new Dictionary<Type, object>
            {
                { typeof(UserItemViewModel), _userItemViewAsset }
            };
        }
    }
    <!-- UsersView.uxml -->
    <ui:UXML ...>
        <UserListView binding-items-source-path="Users" />
    </ui:UXML>
  11. Install UnityMvvmToolkit via Git URL

    main

    You can add the package directly to the Unity Package Manager using its Git URL. To target a specific version, append the release tag using the #v*.*.* syntax.

    Base URL: https://github.com/ChebanovDD/UnityMvvmToolkit.git?path=src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit

    Example with version tag (v1.0.0): https://github.com/ChebanovDD/UnityMvvmToolkit.git?path=src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit#v1.0.0

    https://github.com/ChebanovDD/UnityMvvmToolkit.git?path=src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit#v1.0.0
  12. Use Command and Command<T> for UI actions

    main

    Command and Command<T> implement ICommand and allow you to bind ViewModel methods to UI elements (like buttons). They support wrapping standard Action or Action<T> delegates.

    To trigger a command from UI Toolkit (UXML), use the command attribute on a bindable element.

    Example:

    public class CounterViewModel : IBindingContext
    {
        public CounterViewModel()
        {
            Count = new Property<int>();
            IncrementCommand = new Command(IncrementCount);
        }
    
        public IProperty<int> Count { get; }
        public ICommand IncrementCommand { get; }
    
        private void IncrementCount() => Count.Value++;
    }

    UXML:

    <uitk:BindableButton command="IncrementCommand" />
    <ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
        <uitk:BindableButton command="IncrementCommand" />
    </ui:UXML>