Skip to content

Avatarchik/UnityMvvmToolkit

Β 
Β 

Repository files navigation

UnityMvvmToolkit

A package that brings data-binding to your Unity project.

git-main

πŸ“– Table of Contents

πŸ“ About

The UnityMvvmToolkit allows you to use data binding to establish a connection between the app UI and the data it displays. This is a simple and consistent way to achieve clean separation of business logic from UI. Use the samples as a starting point for understanding how to utilize the package.

Key features:

  • Runtime data-binding
  • UI Toolkit & uGUI integration
  • Multiple-properties binding
  • Custom UI Elements support
  • Compatible with UniTask
  • Mono & IL2CPP support*

Samples

The following example shows the UnityMvvmToolkit in action using the Counter app.

CounterView
<UXML>
    <BindableContentPage binding-theme-mode-path="ThemeMode" class="counter-screen">
        <VisualElement class="number-container">
            <BindableCountLabel binding-text-path="Count" class="count-label count-label--animation" />
        </VisualElement>
        <BindableThemeSwitcher binding-value-path="ThemeMode, Converter={ThemeModeToBoolConverter}" />
        <BindableCounterSlider increment-command="IncrementCommand" decrement-command="DecrementCommand" />
    </BindableContentPage>
</UXML>

Note: The namespaces are omitted to make the example more readable.

CounterViewModel
public class CounterViewModel : ViewModel
{
    private int _count;
    private ThemeMode _themeMode;

    public CounterViewModel()
    {
        IncrementCommand = new Command(IncrementCount);
        DecrementCommand = new Command(DecrementCount);
    }

    public int Count
    {
        get => _count;
        set => Set(ref _count, value);
    }

    public ThemeMode ThemeMode
    {
        get => _themeMode;
        set => Set(ref _themeMode, value);
    }

    public ICommand IncrementCommand { get; }
    public ICommand DecrementCommand { get; }

    private void IncrementCount() => Count++;
    private void DecrementCount() => Count--;
}
Counter Calculator ToDoList
UnityMvvmCounter.mp4
UnityMvvmCalc.mp4
UnityMvvmToDoList.mp4

You will find all the samples in the samples folder.

🌡 Folder Structure

.
β”œβ”€β”€ samples
β”‚   β”œβ”€β”€ Unity.Mvvm.Calc
β”‚   β”œβ”€β”€ Unity.Mvvm.Counter
β”‚   β”œβ”€β”€ Unity.Mvvm.ToDoList
β”‚   └── Unity.Mvvm.CounterLegacy
β”‚
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ UnityMvvmToolkit.Core
β”‚   └── UnityMvvmToolkit.UnityPackage
β”‚       ...
β”‚       β”œβ”€β”€ Core      # Auto-generated
β”‚       β”œβ”€β”€ Common
β”‚       β”œβ”€β”€ External
β”‚       β”œβ”€β”€ UGUI
β”‚       └── UITK
β”‚
β”œβ”€β”€ UnityMvvmToolkit.sln

βš™οΈ Installation

You can install UnityMvvmToolkit in one of the following ways:

1. Install via Package Manager

The package is available on the OpenUPM.

  • Open Edit/Project Settings/Package Manager

  • Add a new Scoped Registry (or edit the existing OpenUPM entry)

    Name      package.openupm.com
    URL       https://package.openupm.com
    Scope(s)  com.cysharp.unitask
              com.chebanovdd.unitymvvmtoolkit
    
  • Open Window/Package Manager

  • Select My Registries

  • Install UniTask and UnityMvvmToolkit packages

2. Install via Git URL

You can add https://github.com/ChebanovDD/UnityMvvmToolkit.git?path=src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit to the Package Manager.

If you want to set a target version, UnityMvvmToolkit uses the v*.*.* release tag, so you can specify a version like #v0.1.0. For example https://github.com/ChebanovDD/UnityMvvmToolkit.git?path=src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit#v0.1.0.

IL2CPP restriction

The UnityMvvmToolkit uses generic virtual methods under the hood to create bindable properties, but IL2CPP in Unity 2021 does not support Full Generic Sharing this restriction will be removed in Unity 2022.

To work around this issue in Unity 2021 you need to change the IL2CPP Code Generation setting in the Build Settings window to Faster (smaller) builds.

Instruction

build-settings

πŸ“’ Introduction

The package contains a collection of standard, self-contained, lightweight types that provide a starting implementation for building apps using the MVVM pattern.

The included types are:

ViewModel

The ViewModel is a base class for objects that are observable by implementing the INotifyPropertyChanged interface. It can be used as a starting point for all kinds of objects that need to support property change notification.

Key functionality:

  • Provides a base implementation for INotifyPropertyChanged, exposing the PropertyChanged event
  • Provides a series of Set methods that can be used to easily set property values from types inheriting from ViewModel, and to automatically raise the appropriate events

Note: In case your viewmodel doesn't have a parameterless constructor, you need to override the GetBindingContext method in the view.

Simple property

Here's an example of how to implement notification support to a custom property.

public class CounterViewModel : ViewModel
{
    private int _count;

    public int Count
    {
        get => _count;
        set => Set(ref _count, value);
    }
}

The provided Set<T>(ref T, T, string) method checks the current value of the property, and updates it if different, and then also raises the PropertyChanged event automatically. The property name is automatically captured through the use of the [CallerMemberName] attribute, so there's no need to manually specify which property is being updated.

Wrapping a model

To inject notification support to models, that don't implement the INotifyPropertyChanged interface, ViewModel provides a dedicated Set<TModel, T>(T, T, TModel, Action<TModel, T>, string) method for this.

public class UserViewModel : ViewModel
{
    private readonly User _user;

    public UserViewModel(User user)
    {
        _user = user;
    }

    public string Name
    {
        get => _user.Name;
        set => Set(_user.Name, value, _user, (user, name) => user.Name = name);
    }
}

CanvasView<TBindingContext>

The CanvasView<TBindingContext> is a base class for uGUI view's.

Key functionality:

  • Provides a base implementation for Canvas based view
  • Automatically searches for bindable UI elements on the Canvas
  • Allows to override the base viewmodel instance creation
  • Allows to define property & parameter value converters
  • Allows to provide a custom bindable elements factory
public class CounterView : CanvasView<CounterViewModel>
{
    // Override the base viewmodel instance creation.
    // Required in case the viewmodel doesn't have a parameterless constructor.
    protected override CounterViewModel GetBindingContext()
    {
        return _appContext.Resolve<CounterViewModel>();
    }

    // Define 'property' & 'parameter' value converters.
    protected override IValueConverter[] GetValueConverters()
    {
        return _appContext.Resolve<IValueConverter[]>();
    }

    // Provide a custom bindable elements factory.
    protected override IBindableElementsFactory GetBindableElementsFactory()
    {
        return _appContext.Resolve<IBindableElementsFactory>();
    }
}

DocumentView<TBindingContext>

The DocumentView<TBindingContext> is a base class for UI Toolkit view's.

Key functionality:

  • Provides a base implementation for UI Document based view
  • Automatically searches for bindable UI elements on the UI Document
  • Allows to override the base viewmodel instance creation
  • Allows to define property & parameter value converters
  • Allows to provide a custom bindable elements factory
public class CounterView : DocumentView<CounterViewModel>
{
    // Override the base viewmodel instance creation.
    // Required in case the viewmodel doesn't have a parameterless constructor.
    protected override CounterViewModel GetBindingContext()
    {
        return _appContext.Resolve<CounterViewModel>();
    }

    // Define 'property' & 'parameter' value converters.
    protected override IValueConverter[] GetValueConverters()
    {
        return _appContext.Resolve<IValueConverter[]>();
    }

    // Provide a custom bindable elements factory.
    protected override IBindableElementsFactory GetBindableElementsFactory()
    {
        return _appContext.Resolve<IBindableElementsFactory>();
    }
}

Command & Command<T>

The Command and Command<T> are ICommand implementations that can expose a method or delegate to the view. These types act as a way to bind commands between the viewmodel and UI elements.

Key functionality:

  • Provide a base implementation of the ICommand interface
  • Implement the ICommand & ICommand<T> interface, which exposes a RaiseCanExecuteChanged method to raise the CanExecuteChanged event
  • Expose constructor taking delegates like Action and Action<T>, which allow the wrapping of standard methods and lambda expressions

The following shows how to set up a simple command.

using UnityMvvmToolkit.Core;
using UnityMvvmToolkit.Core.Interfaces;

public class CounterViewModel : ViewModel
{
    private int _count;

    public CounterViewModel()
    {
        IncrementCommand = new Command(IncrementCount);
    }

    public int Count
    {
        get => _count;
        set => Set(ref _count, value);
    }

    public ICommand IncrementCommand { get; }

    private void IncrementCount() => Count++;
}

And the relative UI could then be.

<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
    <uitk:BindableLabel binding-text-path="Count" />
    <uitk:BindableButton command="IncrementCommand" />
</ui:UXML>

The BindableButton binds to the ICommand in the viewmodel, which wraps the private IncrementCount method. The BindableLabel displays the value of the Count property and is updated every time the property value changes.

Note: You need to define IntToStrConverter to convert int to string. See the PropertyValueConverter section for more information.

AsyncCommand & AsyncCommand<T>

The AsyncCommand and AsyncCommand<T> are ICommand implementations that extend the functionalities offered by Command, with support for asynchronous operations.

Key functionality:

  • Extend the functionalities of the synchronous commands included in the package, with support for UniTask-returning delegates
  • Can wrap asynchronous functions with a CancellationToken parameter to support cancelation, and they expose a DisableOnExecution property, as well as a Cancel method
  • Implement the IAsyncCommand & IAsyncCommand<T> interfaces, which allows to replace a command with a custom implementation, if needed

Let's say we want to download an image from the web and display it as soon as it downloads.

public class ImageViewerViewModel : ViewModel
{
    private readonly IImageDownloader _imageDownloader;
    private Texture2D _texture;

    public ImageViewerViewModel(IImageDownloader imageDownloader)
    {
        _imageDownloader = imageDownloader;
        DownloadImageCommand = new AsyncCommand(DownloadImageAsync);
    }

    public Texture2D Image
    {
        get => _texture;
        private set => Set(ref _texture, value);
    }

    public IAsyncCommand DownloadImageCommand { get; }

    private async UniTask DownloadImageAsync(CancellationToken cancellationToken)
    {
        Image = await _imageDownloader.DownloadRandomImageAsync(cancellationToken);
    }
}

With the related UI code.

<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
    <BindableImage binding-image-path="Image" />
    <uitk:BindableButton command="DownloadImageCommand">
        <ui:Label text="Download Image" />
    </uitk:BindableButton>
</ui:UXML>

Note: The BindableImage is a custom control from the create custom control section.

To disable the BindableButton while an async operation is running, simply set the DisableOnExecution property of the AsyncCommand to true.

public class ImageViewerViewModel : ViewModel
{
    public ImageViewerViewModel(IImageDownloader imageDownloader)
    {
        ...
        DownloadImageCommand = new AsyncCommand(DownloadImageAsync) { DisableOnExecution = true };
    }
}

If you want to create an async command that supports cancellation, use the WithCancellation extension method.

public class MyViewModel : ViewModel
{
    public MyViewModel()
    {
        MyAsyncCommand = new AsyncCommand(DoSomethingAsync).WithCancellation();
        CancelCommand = new Command(Cancel);
    }

    public IAsyncCommand MyAsyncCommand { get; }
    public ICommand CancelCommand { get; }
    
    private async UniTask DoSomethingAsync(CancellationToken cancellationToken)
    {
        ...
    }
    
    private void Cancel()
    {
        // If the underlying command is not running, or
        // if it does not support cancellation, this method will perform no action.
        MyAsyncCommand.Cancel();
    }
}

If the command supports cancellation, previous invocations will automatically be canceled if a new one is started.

Note: You need to import the UniTask package in order to use async commands.

AsyncLazyCommand & AsyncLazyCommand<T>

The AsyncLazyCommand and AsyncLazyCommand<T> are have the same functionality as the AsyncCommand's, except they prevent the same async command from being invoked concurrently multiple times.

Let's imagine a scenario similar to the one described in the AsyncCommand sample, but a user clicks the Download Image button several times while the async operation is running. In this case, AsyncLazyCommand will ignore all clicks until previous async operation has completed.

Note: You need to import the UniTask package in order to use async commands.

PropertyValueConverter<TSourceType, TTargetType>

Property value converter provides a way to apply custom logic to a property binding.

Built-in property value converters:

  • IntToStrConverter
  • FloatToStrConverter

If you want to create your own property value converter, create a class that inherits the PropertyValueConverter<TSourceType, TTargetType> abstract class and then implement the Convert and ConvertBack methods.

public enum ThemeMode
{
    Light = 0,
    Dark = 1
}

public class ThemeModeToBoolConverter : PropertyValueConverter<ThemeMode, bool>
{
    // From source to target. 
    public override bool Convert(ThemeMode value)
    {
        return (int) value == 1;
    }

    // From target to source.
    public override ThemeMode ConvertBack(bool value)
    {
        return (ThemeMode) (value ? 1 : 0);
    }
}

Don't forget to register the ThemeModeToBoolConverter in the view.

public class MyView : DocumentView<MyViewModel>
{
    protected override IValueConverter[] GetValueConverters()
    {
        return new IValueConverter[] { new ThemeModeToBoolConverter() };
    }
}

Then you can use the ThemeModeToBoolConverter as in the following example.

<UXML>
    <!--Full expression-->
    <MyBindableElement binding-value-path="ThemeMode, Converter={ThemeModeToBoolConverter}" />
    <!--Short expression-->
    <MyBindableElement binding-value-path="ThemeMode, ThemeModeToBoolConverter" />
    <!--Minimal expression - the first appropriate converter will be used-->
    <MyBindableElement binding-value-path="ThemeMode" />
</UXML>

ParameterValueConverter<TTargetType>

Parameter value converter allows to convert a command parameter.

Built-in parameter value converters:

  • ParameterToStrConverter
  • ParameterToIntConverter
  • ParameterToFloatConverter

By default, the converter is not needed if your command has a ReadOnlyMemory<char> parameter type.

public class MyViewModel : ViewModel
{
    public MyViewModel()
    {
        PrintParameterCommand = new Command<ReadOnlyMemory<char>>(PrintParameter);
    }

    public ICommand<ReadOnlyMemory<char>> PrintParameterCommand { get; }

    private void PrintParameter(ReadOnlyMemory<char> parameter)
    {
        Debug.Log(parameter);
    }
}
<UXML>
    <BindableButton command="PrintParameterCommand, Parameter={MyParameter}" />
    <!--or-->
    <BindableButton command="PrintParameterCommand, MyParameter" />
</UXML>

If you want to create your own parameter value converter, create a class that inherits the ParameterValueConverter<TTargetType> abstract class and then implement the Convert method.

public class ParameterToIntConverter : ParameterValueConverter<int>
{
    public override int Convert(ReadOnlyMemory<char> parameter)
    {
        return int.Parse(parameter.Span);
    }
}

Don't forget to register the ParameterToIntConverter in the view.

public class MyView : DocumentView<MyViewModel>
{
    protected override IValueConverter[] GetValueConverters()
    {
        return new IValueConverter[] { new ParameterToIntConverter() };
    }
}

Then you can use the ParameterToIntConverter as in the following example.

public class MyViewModel : ViewModel
{
    public MyViewModel()
    {
        PrintParameterCommand = new Command<int>(PrintParameter);
    }

    public ICommand<int> PrintParameterCommand { get; }

    private void PrintParameter(int parameter)
    {
        Debug.Log(parameter);
    }
}
<UXML>
    <!--Full expression-->
    <BindableButton command="PrintIntParameterCommand, Parameter={5}, Converter={ParameterToIntConverter}" />
    <!--Short expression-->
    <BindableButton command="PrintIntParameterCommand, 5, ParameterToIntConverter" />
    <!--Minimal expression - the first appropriate converter will be used-->
    <BindableButton command="PrintIntParameterCommand, 5" />
</UXML>

⌚ Quick start

Once the UnityMVVMToolkit is installed, create a class MyFirstViewModel that inherits the ViewModel class.

using UnityMvvmToolkit.Core;

public class MyFirstViewModel : ViewModel
{
    private string _text;

    public MyFirstViewModel()
    {
        _text = "Hello World";
    }

    public string Text
    {
        get => _text;
        set => Set(ref _text, value);
    }
}

UI Toolkit

The next step is to create a class MyFirstDocumentView that inherits the DocumentView<TBindingContext> class.

using UnityMvvmToolkit.UITK;

public class MyFirstDocumentView : DocumentView<MyFirstViewModel>
{
}

Then create a file MyFirstView.uxml, add a BindableLabel control and set the binding-text-path to Text.

<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
    <uitk:BindableLabel binding-text-path="Text" />
</ui:UXML>

Finally, add UI Document to the scene, set the MyFirstView.uxml as a Source Asset and add the MyFirstDocumentView component to it.

UI Document Inspector

ui-document-inspector

Unity UI (uGUI)

For the uGUI do the following. Create a class MyFirstCanvasView that inherits the CanvasView<TBindingContext> class.

using UnityMvvmToolkit.UGUI;

public class MyFirstCanvasView : CanvasView<MyFirstViewModel>
{
}

Then add a Canvas to the scene, and add the MyFirstCanvasView component to it.

Canvas Inspector

canvas-inspector

Finally, add a Text - TextMeshPro UI element to the canvas, add the BindableLabel component to it and set the BindingTextPath to Text.

Canvas Text Inspector

canvas-text-inspector

πŸš€ How To Use

Data-binding

The package contains a set of standard bindable UI elements out of the box.

The included UI elements are:

Note: The BindableListView & BindableScrollView are provided for UI Toolkit only.

BindableLabel

The BindableLabel element uses the OneWay binding by default.

public class LabelViewModel : ViewModel
{
    private int _intValue;
    private string _strValue;

    public int IntValue
    {
        get => _intValue;
        set => Set(ref _intValue, value);
    }

    public string StrValue
    {
        get => _strValue;
        set => Set(ref _strValue, value);
    }
}

public class LabelView : DocumentView<LabelViewModel>
{
    protected override IValueConverter[] GetValueConverters()
    {
        return new IValueConverter[] { new IntToStrConverter() };
    }
}
<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
    <uitk:BindableLabel binding-text-path="StrValue" />
    <uitk:BindableLabel binding-text-path="IntValue" />
</ui:UXML>

BindableTextField

The BindableTextField element uses the TwoWay binding by default.

public class TextFieldViewModel : ViewModel
{
    private string _textValue;

    public string TextValue
    {
        get => _textValue;
        set => Set(ref _textValue, value);
    }
}
<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
    <uitk:BindableTextField binding-text-path="TextValue" />
</ui:UXML>

BindableButton

The BindableButton can be bound to the following commands:

To pass a parameter to the viewmodel, see the ParameterValueConverter section.

BindableListView

The BindableListView control is the most efficient way to create lists. Use the binding-items-source-path of the BindableListView to bind to an ObservableCollection.

The following example demonstrates how to bind to a collection of users with BindableListView.

Create a main UI Document named UsersView.uxml with the following content.

<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" ...>
    <uitk:BindableListView binding-items-source-path="Users" />
</ui:UXML>

Create a UI Document named UserEntry.uxml for the individual entries in the list.

<ui:UXML ...>
    <ui:Label name="NameLabel" />
</ui:UXML>

Create a UserItemData class to store user data.

public class UserItemData
{
    public string Name { get; set; }
}

Create a UserItemController class to display the data of a user instance in the UI of the list entry. It needs to access the label for the user name and set it to display the name of the given user instance.

public class UserItemController
{
    private readonly Label _nameLabel;

    public TaskItemController(VisualElement userEntryAsset)
    {
        _nameLabel = userEntryAsset.Q<Label>("NameLabel");
    }

    public void SetData(UserItemData userItemData)
    {
        _nameLabel.text = userItemData.Name;
    }
}

Create a UserListViewWrapper that inherits the BindableListViewWrapper<TItem, TData> abstract class and implement the OnMakeItem and OnBindItem methods.

public class UserListViewWrapper : BindableListViewWrapper<UserItemController, UserItemData>
{
    public UserListViewWrapper(BindableListView listView, VisualTreeAsset itemAsset,
        IObjectProvider objectProvider) : base(listView, itemAsset, objectProvider)
    {
    }

    protected override UserItemController OnMakeItem(VisualElement itemAsset)
    {
        return new UserItemController(itemAsset);
    }

    protected override void OnBindItem(UserItemController item, UserItemData data)
    {
        item.SetData(data);
    }
}

Create a CustomBindableElementsFactory and override the Create method.

public class CustomBindableElementsFactory : BindableElementsFactory
{
    private readonly VisualTreeAsset _userEntryAsset;

    public CustomBindableElementsFactory(VisualTreeAsset userEntryAsset)
    {
        _userEntryAsset = userEntryAsset;
    }

    public override IBindableElement Create(IBindableUIElement bindableUiElement, IObjectProvider objectProvider)
    {
        return bindableUiElement switch
        {
            BindableListView listView => new UserListViewWrapper(listView, _userEntryAsset, objectProvider),

            _ => base.Create(bindableUiElement, objectProvider)
        };
    }
}

Create a UsersViewModel.

public class UsersViewModel : ViewModel
{
    public UsersViewModel()
    {
        Users = new ObservableCollection<UserItemData>
        {
            new() { Name = "User 1" },
            new() { Name = "User 2" },
            new() { Name = "User 3" },
        };
    }
    
    public ObservableCollection<UserItemData> Users { get; }
}

Create a UsersView with the following content.

public class UsersView : DocumentView<UsersViewModel>
{
    [SerializeField] private VisualTreeAsset _userEntryAsset;

    protected override IBindableElementsFactory GetBindableElementsFactory()
    {
        return new CustomBindableElementsFactory(_userEntryAsset);
    }
}

BindableScrollView

The BindableScrollView has the same binding logic as the BindableListView, except that the UserItemData class must implement the ICollectionItemData interface.

public class UserItemData : ICollectionItemData
{
    public Guid Id { get; } = Guid.NewGuid();

    public string Name { get; set; }
}

Create custom control

Let's create a BindableImage UI element.

First of all, create a base Image class.

public class Image : VisualElement
{
    public void SetImage(Texture2D image)
    {
        // To prevent memory leaks.
        style.backgroundImage.Release(); // Object.Destroy(background.value.texture);
        style.backgroundImage = new StyleBackground(image);
    }

    public new class UxmlFactory : UxmlFactory<Image, UxmlTraits> {}
    
    public new class UxmlTraits : VisualElement.UxmlTraits {}
}

Then create a BindableImage and define BindingImagePath property.

public class BindableImage : Image, IBindableUIElement
{
    public string BindingImagePath { get; set; }

    public new class UxmlFactory : UxmlFactory<BindableImage, UxmlTraits> {}

    public new class UxmlTraits : Image.UxmlTraits
    {
        private readonly UxmlStringAttributeDescription _bindingImageAttribute = new()
            { name = "binding-image-path", defaultValue = "" };

        public override void Init(VisualElement visualElement, IUxmlAttributes bag, CreationContext context)
        {
            base.Init(visualElement, bag, context);
            ((BindableImage) visualElement).BindingImagePath = _bindingImageAttribute.GetValueFromBag(bag, context);
        }
    }
}

The next step is to describe the data binding logic. To do that, create a BindableImageWrapper that inherits the BindablePropertyElement abstract class.

public class BindableImageWrapper : BindablePropertyElement
{
    private readonly BindableImage _bindableImage;
    private readonly IReadOnlyProperty<Texture2D> _imageProperty;

    public BindableImageWrapper(BindableImage bindableImage, IObjectProvider objectProvider) : base(objectProvider)
    {
        _bindableImage = bindableImage;
        _imageProperty = GetReadOnlyProperty<Texture2D>(bindableImage.BindingImagePath);
    }

    public override void UpdateValues()
    {
        _bindableImage.SetImage(_imageProperty.Value);
    }
}

The UnityMvvmToolkit contains two abstract classes BindableCommandElement and BindablePropertyElement that provide a methods for getting properties from the BindingContext.

Finally, tell the elements factory what to do with the new UI element.

public class CustomBindableElementsFactory : BindableElementsFactory
{
    public override IBindableElement Create(IBindableUIElement bindableUiElement, IObjectProvider objectProvider)
    {
        return bindableUiElement switch
        {
            BindableImage bindableImage => new BindableImageWrapper(bindableImage, objectProvider),

            _ => base.Create(bindableUiElement, objectProvider)
        };
    }
}

Don't forget to override the GetBindableElementsFactory method in the view.

public class ImageViewerView : DocumentView<ImageViewerViewModel>
{
    protected override IBindableElementsFactory GetBindableElementsFactory()
    {
        return new CustomBindableElementsFactory();
    }
}

Now you can use the new UI element as following.

public class ImageViewerViewModel : ViewModel
{
    private Texture2D _texture;

    public Texture2D Image
    {
        get => _texture;
        private set => Set(ref _texture, value);
    }
}
<UXML>
    <BindableImage binding-image-path="Image" />
</UXML>

πŸ”— External Assets

UniTask

To enable async commands support, you need to add the UniTask package to your project.

In addition to async commands UnityMvvmToolkit provides extensions to make USS transition's awaitable.

For example, your VisualElement has the following transitions.

.panel--animation {
    transition-property: opacity, padding-bottom;
    transition-duration: 65ms, 150ms;
}

You can await these transitions using several methods.

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 'paddingBottom' to end or cancel.
        await panel.WaitForTransitionEnd(new StylePropertyName("padding-bottom"));
        
        // Await for the 'paddingBottom' to end or cancel.
        // Uses ReadOnlySpan to match property names to avoid memory allocation.
        await panel.WaitForTransitionEnd(nameof(panel.style.paddingBottom));
        
        // Await for the 'opacity' || 'paddingBottom' to end or cancel.
        // You can write your own transition predicates, just implement a 'ITransitionPredicate' interface.
        await panel.WaitForTransitionEnd(new TransitionAnyPredicate());
    }
    finally
    {
        panel.visible = false;
    }
}

Note: All transition extensions have a timeoutMs parameter (default value is 2500ms).

πŸ“ˆ Benchmarks

The UnityMvvmToolkit uses delegates to get and set property values. This approach avoids boxing and unboxing for value types, and the performance improvements are really significant. In particular, this approach is ~65x faster than the one that uses standard GetValue and SetValue methods, and does not make any memory allocations at all.

Environment
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19041.1165 (2004/May2020Update/20H1)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK=5.0.301
  [Host]     : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT
  DefaultJob : .NET 5.0.7 (5.0.721.25508), X64 RyuJIT

Set & Get integer value

|              Method |        Mean |     Error |    StdDev |  Ratio |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------- |------------:|----------:|----------:|-------:|-------:|------:|------:|----------:|
| DirectPropertyUsage |   0.4904 ns | 0.0364 ns | 0.0358 ns |   1.00 |      - |     - |     - |         - |
|    UnityMvvmToolkit |   3.4734 ns | 0.0925 ns | 0.0865 ns |   7.13 |      - |     - |     - |         - |
|          Reflection | 225.5382 ns | 4.4920 ns | 4.8063 ns | 463.38 | 0.0176 |     - |     - |     112 B |

Complex binding

|           Method |       Mean |    Error |   StdDev | Ratio |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------- |-----------:|---------:|---------:|------:|-------:|------:|------:|----------:|
|   ManualApproach |   209.3 ns |  3.02 ns |  2.35 ns |  1.00 | 0.0458 |     - |     - |     288 B |
| UnityMvvmToolkit |   418.1 ns |  7.82 ns |  8.04 ns |  2.00 | 0.0458 |     - |     - |     288 B |
|       Reflection | 1,566.4 ns | 31.01 ns | 33.18 ns |  7.46 | 0.0725 |     - |     - |     464 B |

πŸ“‘ Contributing

You may contribute in several ways like creating new features, fixing bugs or improving documentation and examples.

Discussions

Use discussions to have conversations and post answers without opening issues.

Discussions is a place to:

  • Share ideas
  • Ask questions
  • Engage with other community members

Report a bug

If you find a bug in the source code, please create bug report.

Please browse existing issues to see whether a bug has previously been reported.

Request a feature

If you have an idea, or you're missing a capability that would make development easier, please submit feature request.

If a similar feature request already exists, don't forget to leave a "+1" or add additional information, such as your thoughts and vision about the feature.

Show your support

Give a ⭐ if this project helped you!

Buy Me A Coffee

βš–οΈ License

Usage is provided under the MIT License.

About

Brings data-binding to your Unity project

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages

  • C# 100.0%