Любая система, которая часто используется в проекте, со временем обречена на эволюцию. Так случилось и с нашей системой реактивного связывания reactive bindings.
Что это за система? Она позволяет нам связывать данные на префабе с данными в коде. У нас есть ViewModel, лежащая на префабе. В ней есть некие ключи с разными типами. Соответственно, вся остальная логика, которая у нас привязана к UI, привязана к этим ключам и их изменениям. То есть, если у нас есть некая логическая переменная, меняя ее в коде, мы можем менять любые состояния UI автоматически.

Использование reactive bindings принесло нам как множество новых возможностей, так и ряд зависимостей. Для связи переменных кода и ViewModel, лежащей на префабе, нам необходимо было соответствие строковых имен. Это приводило к тому, что в результате неосторожной правки префаба или ошибки мерджа могли быть утеряны какие-то из этих связей, а ошибка замечалась уже на этапе поздних тестов в виде отвалившегося UI-функционала.
Росла частота использования системы — росло число подобных сложностей.
Два основных неудобства, с которыми мы столкнулись:
Эта статья — о том, как мы дополнили систему и тем самым закрыли эти потребности.
Но обо всем по порядку.
В наших reactive bindings доступ к полям происходит по связке «тип поля-строковый путь» во ViewModel. Отсюда повсеместно мы имели подобный код:
То есть, посредством GetString/GetInteger/GetBoolean и т. д. мы получаем ссылку на поле в модели и пишем/читаем значения.
В чем проблема этой системы? А в том, что чем больше полей в модели — тем больше «строк» в коде. Читать и поддерживать подобный стиль становится весьма проблематично.
Контролировать соответствие типов/путей в коде и в реальной ViewModel — та еще боль. Если c UI-префабом работает больше одного человека, может возникнуть неявный мердж, в результате которого какие-то ключи могут «потеряться». Об этом мы узнаем только на этапе поздних тестов, когда UI перестает работать корректно.
Задача заключалась в том, чтобы получить систему, которая уберет из нашего кода ненужные строковые константы и предоставит явный контракт взаимодействия.
Второй подзадачей являлось получение инструмента, который позволит валидировать эти значения, чтобы мы могли быть на 100% уверены, что текущая ViewModel на префабе соответствует текущему контракту и содержит все необходимые поля.
Желаемый формат работы выглядел примерно так:
В проекте есть отличный механизм для валидирования и вывода информации о возможных ошибках в редакторе — модуль Validate. И есть возможность использовать кодогенерацию (T4). Все это мы задействовали, чтобы решить поставленную задачу.
Теперь ближе к коду.
Раньше описание работы с элементами у нас было в следующем стиле:
Здесь нужно понимать, что презентер взаимодействует только со своей частью полей, которые нужны конкретно для его задачи. И для проверки валидности ViewModel необходимо проверить соответствие полей у каждого из презентеров, которые используют данную ViewModel, а это та еще задачка.
Стало же все выглядеть так:
ViewModel приписывается атрибут ExpectReactiveContract, который получает параметры контракта. Пример контракта выглядит следующим образом:
В этом варианте есть явное типизированное поле. Сверху атрибутом Bind описывается строка, которая связывает это поле с ViewModel.
Способ использования теперь стал каноничным: мы берем структуру (контракт) и устанавливаем новое значение одному из полей (в примере это DroneId).
В результате в основном коде у нас нет никаких лишних строковых констант, нет дополнительной логики связывания полей, которая бы смущала при написании. Он стал намного чище, ведь мы работаем с обычными структурами. Все это получается за счет существования контракта, который сам знает, каким образом из той ViewModel, которая была ему передана, получить данные пол�� и, привязываясь к ней, попутно выполнить валидацию.
Для описания контракта используются два основных атрибута: Bind и SchemaContract. Bind отвечает за связывание поля структуры с полем во ViewModel. Атрибут получает ключ и опциональное поле IsRequired, говорящее о том, действительно ли во ViewModel необходимо иметь конкретный ключ или ничего не произойдет, если его упустить.
При помощи Bind мы передаем строковые ключи и используем этот атрибут для передачи информации в кодогенератор:
Атрибут SchemaContract служит с целью указания контракта для элементов коллекции:
Итак, теперь у нас есть контракт. Дело осталось за малым: написать механизм, который позволит его полноценно использовать. Необходимо как-то заполнить значения всех полей. Для этого мы реализуем специализированный класс — резолвер.
Резолвер — класс, который может проинициализировать поля структуры (контракта). Он и выполняет роль связывания между контрактом и ViewModel на префабе.
Резолверы имеют простую структуру и хорошо подходят для кодогенерации:
Темплейт для генерации:
Класс BindViewModelResolver — partial и имеет генерируемую часть. Задача метода resolve — найти нужный резолвер для контракта и с его помощью выполнить связывание между логической и префабной частью.
Также есть метод ResolveWithReflection (fallback), который выполняет данное связывание через рефлексию. Это сделано на случай, если у нас отсутствует сгенерированный резолвер. Рефлексия работает медленнее, поэтому мы стараемся ее избегать.
Сами резолверы лежат в словаре по типам. Этот список резолверов и описан в сгенерированной части. А сама она выглядит так:
Темплейт для генерируемой части:
Итак, теперь у нас есть инструмент создания резолверов. Осталось создать инструмент для его вызова. А это задача генератора.
Генератор проходится по assemblies и выискивает контракты-наследники IBindViewModel. Найдя контракт, он проходит по нему и заполняет информацию для генерации. Текущая информация состоит из имени переменной, типа, пути для связывания и прочего. Затем подготовленная информация передается непосредственно в T4-генератор.
Код для сбора информации:
Передача информации и запуск T4-генератора:
Как результат — теперь мы можем инициализировать контракт следующим образом:
Пример сгенеренного резолвера для DroneInfoViewModel:
Напоследок — в паре слов о валидаторах.
Чтобы включить валидацию для модели, нужно всего лишь прописать атрибут ExpectReactiveContract:
При наличии ошибок в редакторе будет выведено предупреждение вида:

Валидатор работает на основе рефлексии, пробегая по Bind-полям и проверяя их наличие в модели.
Наличие валидации принесло нам ряд преимуществ:
На текущий момент только одна большая фича в проекте сделана полностью на основе описанного метода и еще несколько старых функциональностей переведено на новые рельсы. Данный инструмент новый и точно еще подвергнется дополнениям и изменениям.
Что это за система? Она позволяет нам связывать данные на префабе с данными в коде. У нас есть ViewModel, лежащая на префабе. В ней есть некие ключи с разными типами. Соответственно, вся остальная логика, которая у нас привязана к UI, привязана к этим ключам и их изменениям. То есть, если у нас есть некая логическая переменная, меняя ее в коде, мы можем менять любые состояния UI автоматически.

Использование reactive bindings принесло нам как множество новых возможностей, так и ряд зависимостей. Для связи переменных кода и ViewModel, лежащей на префабе, нам необходимо было соответствие строковых имен. Это приводило к тому, что в результате неосторожной правки префаба или ошибки мерджа могли быть утеряны какие-то из этих связей, а ошибка замечалась уже на этапе поздних тестов в виде отвалившегося UI-функционала.
Росла частота использования системы — росло число подобных сложностей.
Два основных неудобства, с которыми мы столкнулись:
- Строковые ключи в коде;
- Нет проверки соответствия ключей в коде и ключей в модели.
Эта статья — о том, как мы дополнили систему и тем самым закрыли эти потребности.
Но обо всем по порядку.
В наших reactive bindings доступ к полям происходит по связке «тип поля-строковый путь» во ViewModel. Отсюда повсеместно мы имели подобный код:
Посмотреть код
public static class AbilitiesPresenter { private static readonly PropertyName MechAbilities = "mech/abilities"; private static readonly PropertyName MechAbilitiesIcon = "mech/abilities/icon"; private static readonly PropertyName MechAbilitiesName = "mech/abilities/name"; private static readonly PropertyName MechAbilitiesDescription = "mech/abilities/description"; public static void Present(IViewModel viewModel, List<AbilityInfo> data) { var collection = viewModel.GetMutableCollection(MechAbilities); collection.Fill(data, SetupAbilityItem); } private static void SetupAbilityItem(AbilityInfo data, IViewModel model) { model.GetString(MechAbilitiesIcon).Value = data.Icon; model.GetString(MechAbilitiesName).Value = data.Name; model.GetString(MechAbilitiesDescription).Value = data.Desc; } }
То есть, посредством GetString/GetInteger/GetBoolean и т. д. мы получаем ссылку на поле в модели и пишем/читаем значения.
В чем проблема этой системы? А в том, что чем больше полей в модели — тем больше «строк» в коде. Читать и поддерживать подобный стиль становится весьма проблематично.
Контролировать соответствие типов/путей в коде и в реальной ViewModel — та еще боль. Если c UI-префабом работает больше одного человека, может возникнуть неявный мердж, в результате которого какие-то ключи могут «потеряться». Об этом мы узнаем только на этапе поздних тестов, когда UI перестает работать корректно.
Задача заключалась в том, чтобы получить систему, которая уберет из нашего кода ненужные строковые константы и предоставит явный контракт взаимодействия.
Второй подзадачей являлось получение инструмента, который позволит валидировать эти значения, чтобы мы могли быть на 100% уверены, что текущая ViewModel на префабе соответствует текущему контракту и содержит все необходимые поля.
Желаемый формат работы выглядел примерно так:
- Для работы с ViewModel создается некий «контракт», в котором описаны все поля и строковые связи;
- Далее нам нужно вызвать некий механизм инициализации этого контракта;
- В редакторе во ViewModel мы должны иметь явные сообщения об ошибках при отсутствии каких-то полей в модели или во вложенной коллекции.
В проекте есть отличный механизм для валидирования и вывода информации о возможных ошибках в редакторе — модуль Validate. И есть возможность использовать кодогенерацию (T4). Все это мы задействовали, чтобы решить поставленную задачу.
Теперь ближе к коду.
Раньше описание работы с элементами у нас было в следующем стиле:
Посмотреть код
public static class AbilitiesPresenter { private static readonly PropertyName MechAbilities = "mech/abilities"; private static readonly PropertyName MechAbilitiesIcon = "mech/abilities/icon"; private static readonly PropertyName MechAbilitiesName = "mech/abilities/name"; private static readonly PropertyName MechAbilitiesDescription = "mech/abilities/description"; public static void Present(IViewModel viewModel, List<AbilityInfo> data) { var collection = viewModel.GetMutableCollection(MechAbilities); collection.Fill(data, SetupAbilityItem); } private static void SetupAbilityItem(AbilityInfo data, IViewModel model) { model.GetString(MechAbilitiesIcon).Value = data.Icon; model.GetString(MechAbilitiesName).Value = data.Name; model.GetString(MechAbilitiesDescription).Value = data.Desc; } }
Здесь нужно понимать, что презентер взаимодействует только со своей частью полей, которые нужны конкретно для его задачи. И для проверки валидности ViewModel необходимо проверить соответствие полей у каждого из презентеров, которые используют данную ViewModel, а это та еще задачка.
Стало же все выглядеть так:
Посмотреть код
namespace DroneDetails { public class DroneDetailScreenView : UIScreenViewWith3D<DroneViewUI3DScreen> { [ExpectReactiveContract(typeof(DroneInfoViewModel))] [ExpectNotNull] [SerializeField] private ViewModel _droneInfoModel; [ExpectReactiveContract(typeof(DroneScreenMainEventsModel))] [ExpectNotNull] [SerializeField] private ViewModel _droneScreenMainEventsModel; [ExpectReactiveContract(typeof(DroneScreenInfoModel))] [ExpectNotNull] [SerializeField] private ViewModel _droneScreenInfoModel; [ExpectReactiveContract(typeof(DroneSpawnInfoViewModel))] [ExpectNotNull] [SerializeField] private ViewModel _droneSpawnInfoViewModel; [ExpectReactiveContract(typeof(ScrollListViewModel))] [ExpectNotNull] [SerializeField] private ViewModel _scrollListViewModel; //…. } }
ViewModel приписывается атрибут ExpectReactiveContract, который получает параметры контракта. Пример контракта выглядит следующим образом:
public struct ConnectionStatusViewModel : IBindViewModel { //пример описания полей [Bind("connection/is-lost")] public IMutableProperty<string>IsConnectionLost; [Bind("mech/slots-count")] public IMutableProperty<int> SlotsCount; //задание контракта для элементов вложенной коллекции [Bind("current-drone-info/scheme-slots-info")] [SchemaContract(typeof(SchemeSlotInfoViewModel))] public IMutableCollection SchemeSlotsInfo; }
В этом варианте есть явное типизированное поле. Сверху атрибутом Bind описывается строка, которая связывает это поле с ViewModel.
private void OnPreviewDrone(int index) { _droneDetailModel.DroneScrollStateModel.SaveState(index); var droneId = _dronesListModel.GetDroneIdByIndex(index); _view.DroneInfoViewModel.DroneId.Value = droneId; //... }
Способ использования теперь стал каноничным: мы берем структуру (контракт) и устанавливаем новое значение одному из полей (в примере это DroneId).
В результате в основном коде у нас нет никаких лишних строковых констант, нет дополнительной логики связывания полей, которая бы смущала при написании. Он стал намного чище, ведь мы работаем с обычными структурами. Все это получается за счет существования контракта, который сам знает, каким образом из той ViewModel, которая была ему передана, получить данные пол�� и, привязываясь к ней, попутно выполнить валидацию.
Для описания контракта используются два основных атрибута: Bind и SchemaContract. Bind отвечает за связывание поля структуры с полем во ViewModel. Атрибут получает ключ и опциональное поле IsRequired, говорящее о том, действительно ли во ViewModel необходимо иметь конкретный ключ или ничего не произойдет, если его упустить.
При помощи Bind мы передаем строковые ключи и используем этот атрибут для передачи информации в кодогенератор:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.GenericParameter)] public class BindAttribute : Attribute { public string ViewModelPath { get; } public bool IsRequired { get; } public BindAttribute(string value, bool isRequired = true) { ViewModelPath = value; IsRequired = isRequired; } }
Атрибут SchemaContract служит с целью указания контракта для элементов коллекции:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.GenericParameter)] public class SchemaContractAttribute : Attribute { public System.Type[] BindViewModelContracts; public SchemaContractAttribute(params System.Type[]contracts) { BindViewModelContracts = contracts; } }
Итак, теперь у нас есть контракт. Дело осталось за малым: написать механизм, который позволит его полноценно использовать. Необходимо как-то заполнить значения всех полей. Для этого мы реализуем специализированный класс — резолвер.
Резолвер — класс, который может проинициализировать поля структуры (контракта). Он и выполняет роль связывания между контрактом и ViewModel на префабе.
Резолверы имеют простую структуру и хорошо подходят для кодогенерации:
Посмотреть код
// ------------------------------------------------------------------------------ // <auto-generated> // This code was generated by ViewModelBindingsGenerator // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> // ------------------------------------------------------------------------------ using PS.ReactiveBindings; using Test; namespace BindViewModel { public partial struct BindViewModelResolver { private static ConnectionStatusViewModel ResolveConnectionStatusViewModel(IViewModel viewModel) => new ConnectionStatusViewModel { IsConnectionLost = LookupProperty<IMutableProperty<string>>( "ConnectionStatusViewModel", viewModel, PropertyType.String, "connection/is-lost", true), SomeCollection = LookupProperty<IMutableCollection>( "ConnectionStatusViewModel", viewModel, PropertyType.Collection, "mech/tempCollection", true), }; } }
Темплейт для генерации:
Посмотреть код
<#@ template debug="false" hostspecific="false" language="C#" #> <#@ parameter name ="m_GenerationInfo" type="WarRobots.RBViewModelWrapperGenerator.BindViewModel.GenerationInfo"#> // ------------------------------------------------------------------------------ // <auto-generated> // This code was generated by ViewModelBindingsGenerator // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> // ------------------------------------------------------------------------------ using PS.ReactiveBindings; using <#=m_GenerationInfo.Namespace #>; namespace BindViewModel { public partial struct BindViewModelResolver { private static <#=m_GenerationInfo.ClassName #> Resolve<#=m_GenerationInfo.ClassName #>(IViewModel viewModel) => new <#=m_GenerationInfo.ClassName #> { <# foreach (var property in m_GenerationInfo.PropertiesInfo) { var requiredString = property.Required.ToString().ToLower(); #> <#=property.Name #> = LookupProperty<<#=property.PropertyTypeName #>>("<#=m_GenerationInfo.ClassName #>",viewModel, <#=property.ReactivePropertyTypeName #>, "<#=property.ViewModelPath #>", <#=requiredString #>), <# } #> }; } }
Класс BindViewModelResolver — partial и имеет генерируемую часть. Задача метода resolve — найти нужный резолвер для контракта и с его помощью выполнить связывание между логической и префабной частью.
Также есть метод ResolveWithReflection (fallback), который выполняет данное связывание через рефлексию. Это сделано на случай, если у нас отсутствует сгенерированный резолвер. Рефлексия работает медленнее, поэтому мы стараемся ее избегать.
Посмотреть код
public partial struct BindViewModelResolver { private static Dictionary<System.Type, IResolver> _resolvers; static partial void InitResolvers(); public static T Resolve<T>(IViewModel viewModel) where T : struct, IBindViewModel { InitResolvers(); if (_resolvers != null && _resolvers.ContainsKey(typeof(T))) { var resolver = (Resolver<T>) _resolvers[typeof(T)]; return resolver.Resolve(viewModel); } return ResolveWithReflection<T>(viewModel); } private class CanNotResolvePropertyException : System.Exception { public CanNotResolvePropertyException(string message) : base(message) { } } private interface IResolver { } private struct Resolver<T> : IResolver where T : struct, IBindViewModel { public delegate T ResolveDelegate(IViewModel viewModel); public ResolveDelegate Resolve; } private static Resolver<T> FromDelegate<T>(Resolver<T>.ResolveDelegate resolveDelegate) where T : struct, IBindViewModel => new Resolver<T> {Resolve = resolveDelegate}; private static T LookupProperty<T>( string holderName, IViewModel viewModel, PropertyType type, PropertyName id, bool required) where T : class, IReactive { T obj = viewModel.LookupProperty(id, type) as T; if (obj == null) { if (required) { throw new CanNotResolvePropertyException( $"{holderName} -> Can't resolve {id} path => \n PropertyType.{type} \n {id}" ); } Debug.LogWarning($"{holderName} -> Can't resolve {id} path => \n PropertyType.{type} \n {id}"); } return obj; } private static T ResolveWithReflection<T>(IViewModel viewModel) { var type = typeof(T); var fields = type.GetFields(); var resolvedStruct = System.Activator.CreateInstance(type); foreach (var field in fields) { var bindAttribute = field.GetCustomAttribute<BindAttribute>(); if (bindAttribute != null) { var viewModelPath = bindAttribute.ViewModelPath; var result = ResolveFieldValue(type.Name, field.FieldType, viewModelPath, viewModel, bindAttribute.IsRequired); field.SetValue(resolvedStruct, result); } } return (T) resolvedStruct; }
Сами резолверы лежат в словаре по типам. Этот список резолверов и описан в сгенерированной части. А сама она выглядит так:
Посмотреть код
public partial struct BindViewModelResolver { static partial void InitResolvers() { if (_resolvers != null) return; _resolvers = new Dictionary<System.Type, IResolver> { {typeof(DroneInfoViewModel), FromDelegate(ResolveDroneInfoViewModel)}, {typeof(DroneSchemeMetaphorViewModel), FromDelegate(ResolveDroneSchemeMetaphorViewModel)}, {typeof(DroneScreenInfoModel), FromDelegate(ResolveDroneScreenInfoModel)}, {typeof(DroneScreenMainEventsModel), FromDelegate(ResolveDroneScreenMainEventsModel)}, {typeof(DroneSpawnInfoViewModel), FromDelegate(ResolveDroneSpawnInfoViewModel)}, {typeof(DroneStoreItemViewModel), FromDelegate(ResolveDroneStoreItemViewModel)}, {typeof(HangarSlotViewModel), FromDelegate(ResolveHangarSlotViewModel)}, {typeof(SchemeSlotInfoViewModel), FromDelegate(ResolveSchemeSlotInfoViewModel)}, {typeof(ScrollListViewModel), FromDelegate(ResolveScrollListViewModel)}, {typeof(StateItemViewModel), FromDelegate(ResolveStateItemViewModel)}, {typeof(ConnectionStatusViewModel), FromDelegate(ResolveConnectionStatusViewModel)}, {typeof(TitanStateViewModel), FromDelegate(ResolveTitanStateViewModel)}, {typeof(MechStateViewModel), FromDelegate(ResolveMechStateViewModel)}, {typeof(ChipOfferItemViewModel), FromDelegate(ResolveChipOfferItemViewModel)}, {typeof(DroneOfferItemViewModel), FromDelegate(ResolveDroneOfferItemViewModel)}, }; } }
Темплейт для генерируемой части:
Посмотреть код
<#@ template debug="false" hostspecific="false" language="C#" #> <#@ parameter name ="m_GenerationInfos" type="System.Collections.Generic.List<WarRobots.RBViewModelWrapperGenerator.BindViewModel.GenerationInfo>"#> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="BindViewModel" #> // ------------------------------------------------------------------------------ // <auto-generated> // This code was generated by ViewModelBindingsGenerator // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> // ------------------------------------------------------------------------------ using System.Collections.Generic; <# List<string> namespaces = new List<string>(); foreach (var generationInfo in m_GenerationInfos) { if (!namespaces.Contains(generationInfo.Namespace)) { #> using <#=generationInfo.Namespace #>; <# namespaces.Add(generationInfo.Namespace); } } #> namespace BindViewModel { public partial struct BindViewModelResolver { static partial void InitResolvers() { if (_resolvers != null) return; _resolvers = new Dictionary<System.Type, IResolver> { <# foreach (var generationInfo in m_GenerationInfos) { #> {typeof(<#=generationInfo.ClassName #>), FromDelegate(Resolve<#=generationInfo.ClassName #>)}, <# } #> }; } } }
Итак, теперь у нас есть инструмент создания резолверов. Осталось создать инструмент для его вызова. А это задача генератора.
Генератор проходится по assemblies и выискивает контракты-наследники IBindViewModel. Найдя контракт, он проходит по нему и заполняет информацию для генерации. Текущая информация состоит из имени переменной, типа, пути для связывания и прочего. Затем подготовленная информация передается непосредственно в T4-генератор.
Код для сбора информации:
Посмотреть код
List<GenerationInfo> generationInfos = new List<GenerationInfo>(); Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { var types = assembly.GetTypes(); var iBindViewModelType = typeof(IBindViewModel); foreach (Type type in types) { if (type.IsValueType && iBindViewModelType.IsAssignableFrom(type)) { GenerationInfo generationInfo = new GenerationInfo {ClassName = type.Name, Namespace = type.Namespace}; var props = new List<PropertyInfo>(); var fields = type.GetFields(); foreach (var field in fields) { var bindAttribute = field.GetCustomAttribute<BindAttribute>(); if (bindAttribute != null) { var propertyInfo = new PropertyInfo(); propertyInfo.Name = field.Name; propertyInfo.ViewModelPath = bindAttribute.ViewModelPath; var propertyTypeNames = GetPropertyTypeName(field.FieldType); propertyInfo.ReactivePropertyTypeName = propertyTypeNames.ReactivePropertyTypeName; propertyInfo.PropertyTypeName = propertyTypeNames.PropertyTypeName; propertyInfo.ValueTypeName = propertyTypeNames.ValueTypeName; propertyInfo.Required = bindAttribute.IsRequired; props.Add(propertyInfo); } } generationInfo.PropertiesInfo = props; generationInfos.Add(generationInfo); } } }
Передача информации и запуск T4-генератора:
Посмотреть код
foreach (var gInfo in generationInfos) { var viewModelBindingsTemplateGenerator = new ViewModelBindingsTemplate { Session = new Dictionary<string, object> {["_m_GenerationInfoField"] = gInfo} }; viewModelBindingsTemplateGenerator.Initialize(); var generationData = viewModelBindingsTemplateGenerator.TransformText(); File.WriteAllText(fullOutputPath + gInfo.ClassName + ".cs", generationData); } var viewModelResolverTemplateGenerator = new ViewModelResolverTemplate() { Session = new Dictionary<string, object> {["_m_GenerationInfosField"] = generationInfos} }; viewModelResolverTemplateGenerator.Initialize(); var generationResult = viewModelResolverTemplateGenerator.TransformText(); File.WriteAllText(fullOutputPath + "BindViewModelResolverGenerated.cs", generationResult);
Как результат — теперь мы можем инициализировать контракт следующим образом:
Var DroneInfoViewModel = BindViewModelResolver.Resolve<DroneInfoViewModel>(_droneInfoModel);
Пример сгенеренного резолвера для DroneInfoViewModel:
Посмотреть код
public partial struct BindViewModelResolver { private static DroneInfoViewModel ResolveDroneInfoViewModel(IViewModel viewModel) => new DroneInfoViewModel { OnTopInfoClick = LookupProperty<IEvent>("DroneInfoViewModel",viewModel, PropertyType.Event, "current-drone-info/on-top-info-click", true), OnBottomInfoClick = LookupProperty<IEvent>("DroneInfoViewModel",viewModel, PropertyType.Event, "current-drone-info/on-bottom-info-click", true), DroneName = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/drone-name", true), DroneTier = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/drone-tier", true), VoltageCurrent = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/voltage-current", true), VoltageMax = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/voltage-max", true), VoltageRange = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/voltage-range", true), SpawnChargeCost = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/spawn-charge-cost", true), SpawnHardCost = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/spawn-hard-cost", true), BuyCurrency = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/buy-currency", true), BuyPriceValue = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/buy-price-value", true), SchemeSlotsInfo = LookupProperty<IMutableCollection>("DroneInfoViewModel",viewModel, PropertyType.Collection, "current-drone-info/scheme-slots-info", true), DroneId = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/drone-id", true), InLoadingState = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/in-loading-state", true), DroneExist = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-exist", true), DroneNoSlot = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-no-slot", true), DroneNoDrone = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-no-drone", true), IsDroneBlueprint = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-blueprint", true), }; }
Напоследок — в паре слов о валидаторах.
Чтобы включить валидацию для модели, нужно всего лишь прописать атрибут ExpectReactiveContract:
[ExpectReactiveContract(typeof(DroneInfoViewModel))] private ViewModel _droneInfoModel;
При наличии ошибок в редакторе будет выведено предупреждение вида:

Валидатор работает на основе рефлексии, пробегая по Bind-полям и проверяя их наличие в модели.
Наличие валидации принесло нам ряд преимуществ:
- уменьшилось время ручного тестирования;
- поиск ошибок стал проще;
- упростилась дальнейшая поддержка/переработка UI;
- стало стабильнее и легче переиспользование классов, описывающих работу с UI.
На текущий момент только одна большая фича в проекте сделана полностью на основе описанного метода и еще несколько старых функциональностей переведено на новые рельсы. Данный инструмент новый и точно еще подвергнется дополнениям и изменениям.
