Unity UI Toolkit: MVVM ннада?

Изображёные приложения вы можете найти на GitHub проекта в папке примеровИзображёные приложения вы можете найти на GitHub проекта в папке примеров

Ни для кого не секрет, что Unity сейчас активно работают над новой системой создания пользовательского интерфейса UI Toolkit. Это инструмент разработки интерфейсов вдохновлённый стандартными подходами веб-разработки.

Пользовательский интерфейс состоит из двух основных частей:

  • UXML документ — язык разметки, основанный на HTML и XML, определяет структуру пользовательского интерфейса.

  • Unity Style Sheets (USS) — таблицы стилей, похожи на каскадные таблицы стилей CSS, применяют визуальные стили и поведение к пользовательскому интерфейсу.

И всё бы хорошо, но какого было моё удивление, что, проделав такую работу, они не предоставили механизма связывания данных, работающего в runtime. Формально, механизм связывания есть, но он работает только при создании интерфейсов для редактора.

А мне так хотелось вновь прикоснуться к WPF и MVVM, но в контексте Unity, что было решено разработать собственный механизм data-binding’а. Вдохновлялся я .NET Community Toolkit, так что если вы уже работали с этим набором инструментов, для вас всё будет максимально знакомо.

В результате получилась библиотека которая позволяет реализовать:

  • Связывание данных работающее в runtime

  • Привязку нескольких свойств у одного UI элемента

  • Поддержку кастомных UI элементов

  • Совместима с UniTask для реализации асинхронных команд

Библиотека содержит набор стандартных классов, которые облегчают создание приложений с использованием шаблона MVVM.

В этот набор входят:

Также реализован базовый набор UI элементов:

Давайте рассмотрим работу библиотеки на примере простого HelloWorld'а.

Для этого добавим UnityMvvmToolkit в проект:

  1. Откройте Edit/Project Settings/Package Manager

  2. Добавьте новый Scoped Registry

Name      package.openupm.com
URL       https://package.openupm.com
Scope(s)  com.chebanovdd.unitymvvmtoolkit
  1. Откройте Window/Package Manager

  2. Выберите My Registries

  3. Установите пакет UnityMvvmToolkit

Первым делом создадим нашу ViewModel:

using UnityMvvmToolkit.Core;

public class MyFirstViewModel : ViewModel
{
    public string Text { get; } = "Hello World";
}

Далее добавим на сцену UI Document выбрав GameObject/UI Toolkit/UI Document.

Добавление UI документа на сценуДобавление UI документа на сцену

Затем создадим файл MyFirstView.uxml. Это будет наша View.

Создание UXML файлаСоздание UXML файла

После того как файл MyFirstView.uxml будет создан. Откройте его в UI Builder и добавьте UI элемент BindableLabel, установив в поле Binding Text Path значение Text (название нашего свойства из ViewModel).

UI BuilderUI Builder

Всё, наша View готова. Если открыть её в редакторе кода, то там будет примерно такое содержание:


    

Заключительным шагом будет создание класса MyFirstDocumentView, который установит нашу ViewModel в качестве BindingContext'а для созданной View.

using UnityMvvmToolkit.UITK;

public class MyFirstDocumentView : DocumentView
{
}

Этот класс необходимо будет довавить к UI Document’у на сцене и задать нашу View там же.

Конфигурирование UI документа на сценеКонфигурирование UI документа на сцене

Запустив проект, мы увидим наш Hello World.

Получившийся результатПолучившийся результат

Библиотека получилась довольно гибкой и расширяемой. В чём можно убедиться, изучив приложение Counter из папки примеров, где в UI используются только кастомные элементы пользовательского интерфейса.

Вот так выглядит CounterView:


    
        
            
        
        
        
    

А так 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--;
}

В заключение немного технической информации. Под капотом UnityMvvmToolkit использует reflection, но получение и установка значений свойств реализована через делегаты.

public static class PropertyInfoExtensions
{
    public static Func CreateGetValueDelegate(
        this PropertyInfo propertyInfo)
    {
        return (Func) Delegate.CreateDelegate(typeof(Func),
            propertyInfo.GetMethod);
    }

    public static Action CreateSetValueDelegate(
        this PropertyInfo propertyInfo)
    {
        return (Action) Delegate.CreateDelegate(typeof(Action),
            propertyInfo.SetMethod);
    }
}

Этот подход позволяет избежать упаковки (boxing) и распаковки (unboxing) для типов значений, что значительно улучшает производительность. В частности, он примерно в 65 раз быстрее, чем тот, который использует стандартные методы GetValue и SetValue, и вообще не приводит к выделению памяти.

Все исходники, примеры и документацию можно найти на GitHub, также пакет опубликован на площадке OpenUPM. Если у вас есть идеи, предложения или желание поучавствовать в разработке, добро пожаловать в дискуссии.

А что вы думаете о таком подходе создания пользовательских интерфейсов в Unity?

© Habrahabr.ru