Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/LibraStack/UnityMvvmToolkit

Brings data-binding to your Unity project
https://github.com/LibraStack/UnityMvvmToolkit

data-binding mvvm-architecture ui unity

Last synced: 4 months ago
JSON representation

Brings data-binding to your Unity project

Lists

README

        

# UnityMvvmToolkit

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

![git-main](https://user-images.githubusercontent.com/28132516/187478087-909fc50b-778b-4827-8090-c5a66d7b6b11.png)

## :open_book: Table of Contents

- [About](#pencil-about)
- [Samples](#samples)
- [Folder Structure](#cactus-folder-structure)
- [Installation](#gear-installation)
- [IL2CPP restriction](#il2cpp-restriction)
- [Introduction](#ledger-introduction)
- [IBindingContext](#ibindingcontext)
- [CanvasView](#canvasviewtbindingcontext)
- [DocumentView](#documentviewtbindingcontext)
- [Property](#propertyt--readonlypropertyt)
- [Command](#command--commandt)
- [AsyncCommand](#asynccommand--asynccommandt)
- [PropertyValueConverter](#propertyvalueconvertertsourcetype-ttargettype)
- [ParameterValueConverter](#parametervalueconverterttargettype)
- [Quick start](#watch-quick-start)
- [How To Use](#joystick-how-to-use)
- [Data-binding](#data-binding)
- [Create custom control](#create-custom-control)
- [Source code generator](#source-code-generator)
- [External Assets](#link-external-assets)
- [UniTask](#unitask)
- [Performance](#rocket-performance)
- [Memory allocation](#memory-allocation)
- [Contributing](#bookmark_tabs-contributing)
- [Discussions](#discussions)
- [Report a bug](#report-a-bug)
- [Request a feature](#request-a-feature)
- [Show your support](#show-your-support)
- [License](#balance_scale-license)

## :pencil: 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](https://github.com/Cysharp/UniTask)
- Mono & IL2CPP support[*](#il2cpp-restriction)

### Samples

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

CounterView

```xml







```

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

CounterViewModel

```csharp
public class CounterViewModel : IBindingContext
{
public CounterViewModel()
{
Count = new Property();
ThemeMode = new Property();

IncrementCommand = new Command(IncrementCount);
DecrementCommand = new Command(DecrementCount);
}

public IProperty Count { get; }
public IProperty ThemeMode { get; }

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

private void IncrementCount() => Count.Value++;
private void DecrementCount() => Count.Value--;
}
```


Counter
Calculator
ToDoList











> You will find all the samples in the `samples` folder.

## :cactus: 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

## :gear: Installation

You can install UnityMvvmToolkit in one of the following ways:

1. Install via Package Manager



The package is available on the [OpenUPM](https://openupm.com/packages/com.chebanovdd.unitymvvmtoolkit/).

- 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 `#v1.0.0`. For example `https://github.com/ChebanovDD/UnityMvvmToolkit.git?path=src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit#v1.0.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](https://blog.unity.com/technology/feature-preview-il2cpp-full-generic-sharing-in-unity-20221-beta) 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](https://user-images.githubusercontent.com/28132516/187468236-4b455b62-48ef-4e9c-9a3f-83391833c3c0.png)

## :ledger: 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:
- [IBindingContext](#ibindingcontext)
- [CanvasView\](#canvasviewtbindingcontext)
- [DocumentView\](#documentviewtbindingcontext)
- [Property\ & ReadOnlyProperty\](#propertyt--readonlypropertyt)
- [Command & Command\](#command--commandt)
- [AsyncCommand & AsyncCommand\](#asynccommand--asynccommandt)
- [PropertyValueConverter\](#propertyvalueconvertertsourcetype-ttargettype)
- [ParameterValueConverter\](#parametervalueconverterttargettype)
- [IProperty\ & IReadOnlyProperty\](#propertyt--readonlypropertyt)
- [ICommand & ICommand\](#command--commandt)
- [IAsyncCommand & IAsyncCommand\](#asynccommand--asynccommandt)
- [IPropertyValueConverter\](#propertyvalueconvertertsourcetype-ttargettype)
- [IParameterValueConverter\](#parametervalueconverterttargettype)

### IBindingContext

The `IBindingContext` is a base interface for ViewModels. It is a marker for Views that the class contains observable properties to bind to.

Here's an example of a simple ViewModel.

```csharp
public class CounterViewModel : IBindingContext
{
public CounterViewModel()
{
Count = new Property();
}

public IProperty Count { get; }
}
```

> **Note:** In case your ViewModel doesn't have a parameterless constructor, you need to override the `GetBindingContext` method in the View.

### CanvasView\

The `CanvasView` is a base class for `uGUI` views.

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](#propertyvalueconvertertsourcetype-ttargettype) & [parameter](#parametervalueconverterttargettype) value converters

```csharp
public class CounterView : CanvasView
{
// Override the base viewmodel instance creation.
// Required in case the viewmodel doesn't have a parameterless constructor.
protected override CounterViewModel GetBindingContext()
{
return _appContext.Resolve();
}

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

// Define a collection item templates.
protected override IReadOnlyDictionary GetCollectionItemTemplates()
{
return _appContext.Resolve>();
}
}
```

### DocumentView\

The `DocumentView` is a base class for `UI Toolkit` views.

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](#propertyvalueconvertertsourcetype-ttargettype) & [parameter](#parametervalueconverterttargettype) value converters

```csharp
public class CounterView : DocumentView
{
// Override the base viewmodel instance creation.
// Required in case the viewmodel doesn't have a parameterless constructor.
protected override CounterViewModel GetBindingContext()
{
return _appContext.Resolve();
}

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

// Define a collection item templates.
protected override IReadOnlyDictionary GetCollectionItemTemplates()
{
return _appContext.Resolve>();
}
}
```

### Property\ & ReadOnlyProperty\

The `Property` and `ReadOnlyProperty` provide a way to bind properties between a ViewModel and UI elements.

Key functionality:
- Provide a base implementation of the `IBaseProperty` interface
- Implement the `IProperty` & `IReadOnlyProperty` interface, which exposes a `ValueChanged` event

#### Simple property

Here's an example of how to implement a simple property.

```csharp
public class CounterViewModel : IBindingContext
{
public CounterViewModel()
{
Count = new Property();
}

public IProperty Count { get; }
}
```

```xml

```

> **Note:** You need to define `IntToStrConverter` to convert int to string. See the [PropertyValueConverter](#propertyvalueconvertertsourcetype-ttargettype) section for more information.

#### Observable property

```csharp
public class MyViewModel : IBindingContext
{
[Observable("Count")]
private readonly IProperty _amount = new Property();

// The field name will be used if you don't provide a property name.
// Names '_title' and 'm_title' will be auto-converted to 'Title'.
[Observable]
private readonly IProperty _title = new Property();
}
```

```xml


```

> **Note:** You need to define `IntToStrConverter` to convert int to string. See the [PropertyValueConverter](#propertyvalueconvertertsourcetype-ttargettype) section for more information.

You can use the `Observable` attribute even on `public` properties to override the binding path.

```csharp
public class MyViewModel : IBindingContext
{
[Observable("PreviousPropertyName")]
public IReadOnlyProperty NewPropertyName { get; }
}
```

#### Wrapping a non-observable model

A common scenario, for instance, when working with database items, is to create a wrapping "bindable" model that relays properties of the database model, and raises the property changed notifications when needed.

```csharp
public class UserViewModel : IBindingContext
{
private readonly User _user;

[Observable(nameof(Name))]
private readonly IProperty _name = new Property();

public UserViewModel(User user)
{
_user = user;
_name.Value = user.Name;
}

public string Name
{
get => _user.Name;
set
{
if (_name.TrySetValue(value))
{
_user.Name = value;
}
}
}
}
```

```xml

```

To achieve the same result, but with minimal boilerplate code, you can automatically create an observable backing field using the `[WithObservableBackingField]` attribute from [UnityMvvmToolkit.Generator](https://github.com/LibraStack/UnityMvvmToolkit.Generator).

```csharp
public partial class UserViewModel : IBindingContext
{
private readonly User _user;

public UserViewModel(User user)
{
_user = user;
_name.Value = user.Name;
}

[WithObservableBackingField]
public string Name
{
get => _user.Name;
set
{
if (_name.TrySetValue(value))
{
_user.Name = value;
}
}
}
}
```

Generated code

`UserViewModel.BackingFields.g.cs`

```csharp
partial class UserViewModel
{
[global::System.CodeDom.Compiler.GeneratedCode("UnityMvvmToolkit.Generator", "1.0.0.0")]
[global::UnityMvvmToolkit.Core.Attributes.Observable(nameof(Name))]
private readonly global::UnityMvvmToolkit.Core.Interfaces.IProperty _name = new global::UnityMvvmToolkit.Core.Property();
}
```

Waiting for the [partial properties](https://github.com/dotnet/csharplang/issues/6420) support to make it even shorter.

```csharp
public partial class UserViewModel : IBindingContext
{
private readonly User _user;

public UserViewModel(User user)
{
_user = user;
_name.Value = user.Name;
}

[WithObservableBackingField]
public partial string Name { get; set; }
}
```

> **Note:** The [UnityMvvmToolkit.Generator](https://github.com/LibraStack/UnityMvvmToolkit.Generator) is available exclusively for my [patrons](https://patreon.com/DimaChebanov).

#### Serializable ViewModel

A common scenario, for instance, when working with collection items, is to create a "bindable" item that can be serialized.

```csharp
public class ItemViewModel : ICollectionItem
{
[Observable(nameof(Name))]
private readonly IProperty _name = new Property();

public int Id { get; set; }

public string Name
{
get => _name.Value;
set => _name.Value = value;
}
}
```

```xml

```

The `ItemViewModel` can be serialized and deserialized without any issues.

The same result, but using the `[WithObservableBackingField]` attribute from [UnityMvvmToolkit.Generator](https://github.com/LibraStack/UnityMvvmToolkit.Generator).

```csharp
public partial class ItemViewModel : ICollectionItem
{
public int Id { get; set; }

[WithObservableBackingField]
public string Name
{
get => _name.Value;
set => _name.Value = value;
}
}
```

Generated code

`ItemViewModel.BackingFields.g.cs`

```csharp
partial class ItemViewModel
{
[global::System.CodeDom.Compiler.GeneratedCode("UnityMvvmToolkit.Generator", "1.0.0.0")]
[global::UnityMvvmToolkit.Core.Attributes.Observable(nameof(Name))]
private readonly global::UnityMvvmToolkit.Core.Interfaces.IProperty _name = new global::UnityMvvmToolkit.Core.Property();
}
```

> **Note:** The [UnityMvvmToolkit.Generator](https://github.com/LibraStack/UnityMvvmToolkit.Generator) is available exclusively for my [patrons](https://patreon.com/DimaChebanov).

### Command & Command\

The `Command` and `Command` 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` interface, which exposes a `RaiseCanExecuteChanged` method to raise the `CanExecuteChanged` event
- Expose constructor taking delegates like `Action` and `Action`, which allow the wrapping of standard methods and lambda expressions

The following shows how to set up a simple command.

```csharp
using UnityMvvmToolkit.Core;
using UnityMvvmToolkit.Core.Interfaces;

public class CounterViewModel : IBindingContext
{
public CounterViewModel()
{
Count = new Property();

IncrementCommand = new Command(IncrementCount);
}

public IProperty Count { get; }

public ICommand IncrementCommand { get; }

private void IncrementCount() => Count.Value++;
}
```

And the relative UI could then be.

```xml


```

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](#propertyvalueconvertertsourcetype-ttargettype) section for more information.

### AsyncCommand & AsyncCommand\

The `AsyncCommand` and `AsyncCommand` 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` 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.

```csharp
public class ImageViewerViewModel : IBindingContext
{
[Observable(nameof(Image))]
private readonly IProperty _image;
private readonly IImageDownloader _imageDownloader;

public ImageViewerViewModel(IImageDownloader imageDownloader)
{
_image = new Property();
_imageDownloader = imageDownloader;

DownloadImageCommand = new AsyncCommand(DownloadImageAsync);
}

public Texture2D Image => _image.Value;

public IAsyncCommand DownloadImageCommand { get; }

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

With the related UI code.

```xml




```

> **Note:** The `BindableImage` is a custom control from the [create custom control](#create-custom-control) section.

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

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

To allow the same async command to be invoked concurrently multiple times, set the `AllowConcurrency` property of the `AsyncCommand` to `true`.

```csharp
public class MainViewModel : IBindingContext
{
public MainViewModel()
{
RunConcurrentlyCommand = new AsyncCommand(RunConcurrentlyAsync) { AllowConcurrency = true };
}
}
```

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

```csharp
public class MyViewModel : IBindingContext
{
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, this method will perform no action.
MyAsyncCommand.Cancel();
}
}
```

If a command supports cancellation and the `AllowConcurrency` property is set to `true`, all running commands will be canceled.

> **Note:** You need to import the [UniTask](https://github.com/Cysharp/UniTask) package in order to use async commands.

### PropertyValueConverter\

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` abstract class and then implement the `Convert` and `ConvertBack` methods.

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

public class ThemeModeToBoolConverter : PropertyValueConverter
{
// 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.

```csharp
public class MyView : DocumentView
{
protected override IValueConverter[] GetValueConverters()
{
return new IValueConverter[] { new ThemeModeToBoolConverter() };
}
}
```

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

```xml






```

### ParameterValueConverter\

Parameter value converter allows to convert a command parameter.

Built-in parameter value converters:
- ParameterToIntConverter
- ParameterToFloatConverter

By default, the converter is not needed if your command has a `string` parameter type.

```csharp
public class MyViewModel : IBindingContext
{
public MyViewModel()
{
PrintParameterCommand = new Command(PrintParameter);
}

public ICommand PrintParameterCommand { get; }

private void PrintParameter(string parameter)
{
Debug.Log(parameter);
}
}
```

```xml



```

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

```csharp
public class ParameterToIntConverter : ParameterValueConverter
{
public override int Convert(string parameter)
{
return int.Parse(parameter);
}
}
```

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

```csharp
public class MyView : DocumentView
{
protected override IValueConverter[] GetValueConverters()
{
return new IValueConverter[] { new ParameterToIntConverter() };
}
}
```

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

```csharp
public class MyViewModel : IBindingContext
{
public MyViewModel()
{
PrintParameterCommand = new Command(PrintParameter);
}

public ICommand PrintParameterCommand { get; }

private void PrintParameter(int parameter)
{
Debug.Log(parameter);
}
}
```

```xml






```

## :watch: Quick start

Once the `UnityMVVMToolkit` is installed, create a class `MyFirstViewModel` that implements the `IBindingContext` interface.

```csharp
using UnityMvvmToolkit.Core;
using UnityMvvmToolkit.Core.Interfaces;

public class MyFirstViewModel : IBindingContext
{
public MyFirstViewModel()
{
Text = new ReadOnlyProperty("Hello World");
}

public IReadOnlyProperty Text { get; }
}
```

#### UI Toolkit

The next step is to create a class `MyFirstDocumentView` that inherits the `DocumentView` class.

```csharp
using UnityMvvmToolkit.UITK;

public class MyFirstDocumentView : DocumentView
{
}
```

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

```xml

```

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](https://user-images.githubusercontent.com/28132516/187613060-e20a139d-72fc-4088-b8d5-f9a01f5afa5b.png)

#### Unity UI (uGUI)

For the `uGUI` do the following. Create a class `MyFirstCanvasView` that inherits the `CanvasView` class.

```csharp
using UnityMvvmToolkit.UGUI;

public class MyFirstCanvasView : CanvasView
{
}
```

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

Canvas Inspector

![canvas-inspector](https://user-images.githubusercontent.com/28132516/187613633-2c61c82e-ac25-4319-8e8d-1954eb4be197.png)

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](https://user-images.githubusercontent.com/28132516/187614103-ad42d000-b3b7-4265-96a6-f6d4db6e8978.png)

## :joystick: How To Use

### Data-binding

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

The included UI elements are:
- [BindableLabel](#bindablelabel)
- [BindableTextField](#bindabletextfield)
- [BindableButton](#bindablebutton)
- [BindableDropdownField](#bindabledropdownfield)
- [BindableListView](#bindablelistview)
- [BindableScrollView](#bindablescrollview)
- [BindingContextProvider](#bindingcontextprovider)

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

#### BindableLabel

The `BindableLabel` element uses the `OneWay` binding by default.

```csharp
public class LabelViewModel : IBindingContext
{
public LabelViewModel()
{
IntValue = new Property(55);
StrValue = new Property("69");
}

public IReadOnlyProperty IntValue { get; }
public IReadOnlyProperty StrValue { get; }
}

public class LabelView : DocumentView
{
protected override IValueConverter[] GetValueConverters()
{
return new IValueConverter[] { new IntToStrConverter() };
}
}
```

```xml


```

#### BindableTextField

The `BindableTextField` element uses the `TwoWay` binding by default.

```csharp
public class TextFieldViewModel : IBindingContext
{
public TextFieldViewModel()
{
TextValue = new Property();
}

public IProperty TextValue { get; }
}
```

```xml

```

#### BindableButton

The `BindableButton` can be bound to the following commands:
- [Command & Command\](#command--commandt)
- [AsyncCommand & AsyncCommand\](#asynccommand--asynccommandt)
- [AsyncLazyCommand & AsyncLazyCommand\](#asynclazycommand--asynclazycommandt)

To pass a parameter to the viewmodel, see the [ParameterValueConverter](#parametervalueconverterttargettype) section.

#### BindableDropdownField

The `BindableDropdownField` allows the user to pick a choice from a list of options. The `BindingSelectedItemPath` attribute is optional.

```csharp
public class DropdownFieldViewModel : IBindingContext
{
public DropdownFieldViewModel()
{
var items = new ObservableCollection
{
"Value 1",
"Value 2",
"Value 3"
};

Items = new ReadOnlyProperty>(items);
SelectedItem = new Property(items[0]);
}

public IReadOnlyProperty> Items { get; }
public IProperty SelectedItem { get; }
}
```

```xml

```

#### BindableListView

The `BindableListView` control is the most efficient way to create lists. It uses virtualization and creates VisualElements only for visible items. 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 `UI Document` named `UserItemView.uxml` for the individual items in the list.

```xml

```

Create a `UserItemViewModel` class that implements `ICollectionItem` to store user data.

```csharp
public class UserItemViewModel : ICollectionItem
{
[Observable(nameof(Name))]
private readonly IProperty _name = new Property();

public UserItemViewModel()
{
Id = Guid.NewGuid().GetHashCode();
}

public int Id { get; }

public string Name
{
get => _name.Value;
set => _name.Value = value;
}
}
```

Create a `UserListView` that inherits the `BindableListView` abstract class.

```csharp
public class UserListView : BindableListView
{
public new class UxmlFactory : UxmlFactory {}
}
```

Create a `UsersViewModel`.

```csharp
public class UsersViewModel : IBindableContext
{
public UsersViewModel()
{
var users = new ObservableCollection
{
new() { Name = "User 1" },
new() { Name = "User 2" },
new() { Name = "User 3" },
};

Users = new ReadOnlyProperty>(users);
}

public IReadOnlyProperty> Users { get; }
}
```

Now we need to provide an item template for the `UserItemViewModel`. Create a `UsersView` as follows.

```csharp
public class UsersView : DocumentView
{
[SerializeField] private VisualTreeAsset _userItemViewAsset;

protected override IReadOnlyDictionary GetCollectionItemTemplates()
{
return new Dictionary
{
{ typeof(UserItemViewModel), _userItemViewAsset }
};
}
}
```

Starting with Unity 2023, you can select an ItemTemplate directly in the UI Builder.

UI Builder Inspector

![collection-item-template](https://github.com/LibraStack/UnityMvvmToolkit/assets/28132516/2dba3a31-7ca9-45c3-a704-5f847262449c)

Finally, create a main `UI Document` named `UsersView.uxml` with the following content.

```xml

```

#### BindableScrollView

The `BindableScrollView` has the same binding logic as the `BindableListView`. It does not use virtualization and creates VisualElements for all items regardless of visibility.

#### BindingContextProvider

The `BindingContextProvider` allows you to provide a custom `IBindingContext` for all child elements.

Let's say we have the following binding contexts.

```csharp
public class MainViewModel : IBindingContext
{
[Observable]
private readonly IReadOnlyProperty _title;

[Observable]
private readonly IReadOnlyProperty _customViewModel;

public MainViewModel()
{
_title = new ReadOnlyProperty("Main Context");
_customViewModel = new ReadOnlyProperty(new CustomViewModel());
}
}
```

```csharp
public class CustomViewModel : IBindingContext
{
[Observable]
private readonly IReadOnlyProperty _title;

public CustomViewModel()
{
_title = new ReadOnlyProperty("Custom Context");
}
}
```

To provide the `CustomViewModel` as a binding context for certain elements, we have to use the `BindingContextProvider` as the parent for those elements.

```xml





```

In this example, `Label1` and `Label2` will display the text "Main Context", while `Label3` will display the text "Custom Context".

We can create a `BindingContextProvider` for a specific `IBindingContext` to avoid allocating memory for a new `PropertyCastWrapper` class. Let's create a `CustomViewModelProvider` element.

```csharp
[UxmlElement]
public partial class CustomViewModelProvider : BindingContextProvider
{
}
```

> **Note:** We use a [UxmlElement](#source-code-generator) attribute to create a custom control.

Now we can use the `CustomViewModelProvider` just like the default `BindingContextProvider`.

```xml





```

### Create custom control

Let's create a `BindableImage` UI element.

First of all, create a base `Image` class.

```csharp
public class Image : VisualElement
{
public void SetImage(Texture2D image)
{
style.backgroundImage = new StyleBackground(image);
}

public new class UxmlFactory : UxmlFactory {}
}
```

Then create a `BindableImage` class and implement the data binding logic.

```csharp
public class BindableImage : Image, IBindableElement
{
private PropertyBindingData _imagePathBindingData;
private IReadOnlyProperty _imageProperty;

public string BindingImagePath { get; private set; }

public void SetBindingContext(IBindingContext context, IObjectProvider objectProvider)
{
_imagePathBindingData ??= BindingImagePath.ToPropertyBindingData();

_imageProperty = objectProvider.RentReadOnlyProperty(context, _imagePathBindingData);
_imageProperty.ValueChanged += OnImagePropertyValueChanged;

SetImage(_imageProperty.Value);
}

public void ResetBindingContext(IObjectProvider objectProvider)
{
if (_imageProperty == null)
{
return;
}

_imageProperty.ValueChanged -= OnImagePropertyValueChanged;

objectProvider.ReturnReadOnlyProperty(_imageProperty);

_imageProperty = null;

SetImage(null);
}

private void OnImagePropertyValueChanged(object sender, Texture2D newImage)
{
SetImage(newImage);
}

public new class UxmlFactory : UxmlFactory { }

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

Now you can use the new UI element as following.

```csharp
public class ImageViewerViewModel : IBindingContext
{
public ImageItemViewModel(Texture2D image)
{
Image = new ReadOnlyProperty(image);
}

public IReadOnlyProperty Image { get; }
}
```

```xml

```

### Source code generator

The best way to speed up the creation of custom `VisualElement` is to use source code generators. With this powerful tool, you can achieve the same great results with minimal boilerplate code and focus on what really matters: programming!

Let's create the `BindableImage` control, but this time using source code generators.

For a visual element without bindings, we will use a [UnityUxmlGenerator](https://github.com/LibraStack/UnityUxmlGenerator).

```csharp
[UxmlElement]
public partial class Image : VisualElement
{
public void SetImage(Texture2D image)
{
style.backgroundImage = new StyleBackground(image);
}
}
```

Generated code

`Image.UxmlFactory.g.cs`

```csharp
partial class Image
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityUxmlGenerator", "1.0.0.0")]
public new class UxmlFactory : global::UnityEngine.UIElements.UxmlFactory
{
}
}
```

For a bindable visual element, we will use a [UnityMvvmToolkit.Generator](https://github.com/LibraStack/UnityMvvmToolkit.Generator).

```csharp
[BindableElement]
public partial class BindableImage : Image
{
[BindableProperty]
private IReadOnlyProperty _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);
}
}
```

Generated code

`BindableImage.Bindings.g.cs`

```csharp
partial class BindableImage : global::UnityMvvmToolkit.Core.Interfaces.IBindableElement
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
private global::UnityMvvmToolkit.Core.PropertyBindingData? _imageBindingData;

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public void SetBindingContext(global::UnityMvvmToolkit.Core.Interfaces.IBindingContext context,
global::UnityMvvmToolkit.Core.Interfaces.IObjectProvider objectProvider)
{
BeforeSetBindingContext(context, objectProvider);

if (string.IsNullOrWhiteSpace(BindingImagePath) == false)
{
_imageBindingData ??=
global::UnityMvvmToolkit.Core.Extensions.StringExtensions.ToPropertyBindingData(BindingImagePath!);
_imageProperty = objectProvider.RentReadOnlyProperty(context, _imageBindingData!);
_imageProperty!.ValueChanged += OnImagePropertyValueChanged;
}

AfterSetBindingContext(context, objectProvider);
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public void ResetBindingContext(global::UnityMvvmToolkit.Core.Interfaces.IObjectProvider objectProvider)
{
BeforeResetBindingContext(objectProvider);

if (_imageProperty != null)
{
_imageProperty!.ValueChanged -= OnImagePropertyValueChanged;
objectProvider.ReturnReadOnlyProperty(_imageProperty);
_imageProperty = null;
}

AfterResetBindingContext(objectProvider);
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
private void OnImagePropertyValueChanged(object sender, global::UnityEngine.Texture2D value)
{
OnImagePropertyValueChanged(value);
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
partial void BeforeSetBindingContext(global::UnityMvvmToolkit.Core.Interfaces.IBindingContext context,
global::UnityMvvmToolkit.Core.Interfaces.IObjectProvider objectProvider);

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
partial void AfterSetBindingContext(global::UnityMvvmToolkit.Core.Interfaces.IBindingContext context,
global::UnityMvvmToolkit.Core.Interfaces.IObjectProvider objectProvider);

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
partial void BeforeResetBindingContext(global::UnityMvvmToolkit.Core.Interfaces.IObjectProvider objectProvider);

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
partial void AfterResetBindingContext(global::UnityMvvmToolkit.Core.Interfaces.IObjectProvider objectProvider);

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
partial void OnImagePropertyValueChanged(global::UnityEngine.Texture2D value);
}
```

`BindableImage.Uxml.g.cs`

```csharp
partial class BindableImage
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
private string BindingImagePath { get; set; }

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
public new class UxmlFactory : global::UnityEngine.UIElements.UxmlFactory
{
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
public new class UxmlTraits : global::BindableUIElements.Image.UxmlTraits
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
private readonly global::UnityEngine.UIElements.UxmlStringAttributeDescription _bindingImagePath = new()
{ name = "binding-image-path", defaultValue = "" };

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("UnityMvvmToolkit.Generator", "1.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public override void Init(global::UnityEngine.UIElements.VisualElement visualElement,
global::UnityEngine.UIElements.IUxmlAttributes bag,
global::UnityEngine.UIElements.CreationContext context)
{
base.Init(visualElement, bag, context);

var control = (BindableImage) visualElement;
control.BindingImagePath = _bindingImagePath.GetValueFromBag(bag, context);
}
}
}
```

As you can see, using [UnityUxmlGenerator](https://github.com/LibraStack/UnityUxmlGenerator) and [UnityMvvmToolkit.Generator](https://github.com/LibraStack/UnityMvvmToolkit.Generator) we can achieve the same results but with just a few lines of code.

> **Note:** The [UnityMvvmToolkit.Generator](https://github.com/LibraStack/UnityMvvmToolkit.Generator) is available exclusively for my [patrons](https://patreon.com/DimaChebanov).

## :link: External Assets

### UniTask

To enable [async commands](#asynccommand--asynccommandt) support, you need to add the [UniTask](https://github.com/Cysharp/UniTask) package to your project.

In addition to async commands **UnityMvvmToolkit** provides extensions to make [USS transition](https://docs.unity3d.com/Manual/UIE-Transitions.html)'s awaitable.

For example, your `VisualElement` has the following transitions.
```css
.panel--animation {
transition-property: opacity, padding-bottom;
transition-duration: 65ms, 150ms;
}
```

You can `await` these transitions using several methods.
```csharp
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`).

## :rocket: Performance

### Memory allocation

The **UnityMvvmToolkit** uses object pools under the hood and reuses created objects. You can warm up certain objects in advance to avoid allocations during execution time.

```csharp
public abstract class BaseView : DocumentView
where TBindingContext : class, IBindingContext
{
protected override IObjectProvider GetObjectProvider()
{
return new BindingContextObjectProvider(new IValueConverter[] { new IntToStrConverter() })
// Finds and warmups all classes from calling assembly that implement IBindingContext.
.WarmupAssemblyViewModels()
// Finds and warmups all classes from certain assembly that implement IBindingContext.
.WarmupAssemblyViewModels(Assembly.GetExecutingAssembly())
// Warmups a certain class.
.WarmupViewModel()
// Warmups a certain class.
.WarmupViewModel(typeof(CounterViewModel))
// Creates 5 instances to rent 'IProperty' without any allocations.
.WarmupValueConverter(5);
}
}
```

## :bookmark_tabs: Contributing

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

### Discussions

Use [discussions](https://github.com/ChebanovDD/UnityMvvmToolkit/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](https://github.com/ChebanovDD/UnityMvvmToolkit/issues/new?assignees=ChebanovDD&labels=bug&template=bug_report.md&title=).

> Please browse [existing issues](https://github.com/ChebanovDD/UnityMvvmToolkit/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](https://github.com/ChebanovDD/UnityMvvmToolkit/issues/new?assignees=ChebanovDD&labels=enhancement&template=feature_request.md&title=).

> 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 :star: if this project helped you!

Buy Me A Coffee

## :balance_scale: License

Usage is provided under the [MIT License](LICENSE).