В этой статье описываются основные принципы и механизмы фреймворка MvvmCross, который облегчает создание слабосвязанных, поддерживаемых мобильных решений. Существует множество проверенных методов организации проектов создания индивидуальной архитектуры для каждого проекта на основе Общего Проекта (Shared Project) или Переносимой Библиотеки Классов (Portable Class Library).
Однако эффективнее использовать готовое кроссплатформенное каркасное решение, которое определит структуру и основные механизмы его работы, предоставит набор библиотек, компонентов и плагинов, что ускоряет процесс разработки. Среди множества популярных фреймворков, таких как Xamarin.Forms, ReactiveUI, FreshMvvm или Prism, особого внимания заслуживает MvvmCros. Он предлагает хороший компромисс между высококачественным UX (User Experience) и количеством кода, используемого в проектах.
Что такое MvvmCross?
MvvmCros — это фреймворк, облегчающий создание кроссплатформенных приложений, соответствующих шаблону проектирования MVVM (Model-View-ViewModel). Он поддерживает многие популярные типы проектов .NET, такие как:
- Xamarin.Android
- Xamarin.iOS
- Xamarin.Mac
- WinRT (Windows 8.1, Windows Phone 8.1)
- Universal Windows Platform (UWP) (Windows 10)
- Windows Presentation Foundation (WPF)
Он также предоставляет механизмы привязки данных для платформ, изначально использующих шаблон проектирования MVC (Model-View-Controller).
Приложения MvvmCross обычно образуются из нескольких составляющих:
- Основной проект, построенный на основе переносимой библиотеки классов (PCL), содержащий все модели представления, модели и интерфейсы сервисов для последующей реализации на платформах. Базовый PCL несет в себе бизнес-логику, обработку базы данных и уровень доступа к веб-службам.
- Собственные проекты для каждой платформы, содержащие пользовательский интерфейс и реализацию специфичных для платформы служб. Рекомендуется создать дополнительный проект PCL в чтобы отделить бизнес-логику приложения от уровня доступа к данным.
Чтобы начать использование фреймворка MvvmCross, нужно создать решение, содержащее все необходимые проекты: как минимум одну библиотеку PCL и собственные проекты для каждой платформы, которые вы планируете поддерживать. Далее необходимо добавить в каждый из них зависимости MvvmCross из Nuget репозитория.
Основные элементы фрейморка
Каждое приложение MvvmCross имеет определенные элементы: класс App, класс Setup и модели представления.
В примере ниже показан типичный пример класса App. В каждом приложении MvvmCross есть ровно одна реализация класса App, которая наследуется от класса MvxApplication. Метод Initialize регистрирует точку входа: первую модель представления, которая будет создана после входа в приложение (в данном случае ProductsViewModel). Initialize также регистрирует типы, вводимые в общей части.
public class App : MvvmCross.Core.ViewModels.MvxApplication { public override void Initialize() { CreatableTypes() .EndingWith("Service") .AsInterfaces() .RegisterAsLazySingleton(); RegisterAppStart; Mvx.RegisterType( () => new ProductWebRepository("http://webservice/api/product/")); } }
MvvmCross предоставляет статический класс Mvx, который функционирует как контейнер для внедрения зависимостей и отвечает за управление реализациями, зарегистрированными как в общей части, так и в платформенно-зависимых проектах в классе Setup.
Класс Setup является своего рода загрузчиком для MvvmCross и присутствует в каждом проекте для конкретной платформы. Примером может служить проект Xamarin.Android. Основная задача этого класса — создать экземпляр класса App, а также настроить фреймворк под специфику приложения.
public class Setup : MvxAndroidSetup { public Setup(Context applicationContext) : base(applicationContext) { } protected override IMvxApplication CreateApp() { return new Core.App(); } protected override IMvxTrace CreateDebugTrace() { return new DebugTrace(); } protected override void InitializePlatformServices() { base.InitializePlatformServices(); Mvx.RegisterType<_ICallerService2c_e280af_DroidCallerService>(); Mvx.RegisterType<_IEmailService2c_e280af_DroidEmailService>(); Mvx.RegisterType<_IPopupService2c_e280af_DroidPopupService>(); } }
Класс MvxAndroidSetup, от которого наследуется класс Setup, предоставляет ряд виртуальных методов, которые необходимо переопределить, чтобы, среди прочего, зарегистрировать все службы платформы (ссылаясь на собственный API). Специфичные для платформы сервисы используются в общей части для выполнения специфичных для каждой платформы инструкций – механизм инверсии управления (Inversion of Control).
Еще одним важным элементом решения MvvmCross является модель представления, которая функционирует как контейнер для свойств и команд, отвечающих за изменение состояния и сохранение связанного с ним представления.
public class ProductsViewModel : MvxViewModel { private IProductRepository productRepository; private List products; private bool isAddButtonEnabled; private IMvxCommand adddProductCommand; public List Products { get { return products; } set { SetProperty(ref products, value); } } public bool IsAddButtonEnabled { get { return isAddButtonEnabled; } set { isAddButtonEnabled = value; RaisePropertyChanged(() => IsAddButtonEnabled); } } public IMvxCommand AddProductCommand { get { adddProductCommand = adddProductCommand ?? new MvxCommand(() => ShowViewModel()); return adddProductCommand; } } public ProductsViewModel(IProductRepository productRepository) { this.productRepository = productRepository; } ... }
Базовый класс представленного фрагмента модели представления содержит реализацию интерфейсов INotifyPropertyChanged, INotifyCollectionChanged и таких методов, как SetProperty или RaisePropertyChanged. Эти интерфейсы и методы позволяют системе обновлять элементы представления: при изменении определенных свойств они запускают событие, которое передает это изменение через пользовательский интерфейс.
Команды, реализованные с использованием класса MvxCommand, управляют отдельными действиями, выполняемыми пользователем, например, изменение представления. MvxViewModel предоставляет множество полезных методов для таких функций, как навигация между моделями представления (ShowViewModel) или управление жизненным циклом представления. Задача контейнера Mvx — автоматически внедрять зависимости в созданные модели представления.
Определение пользовательского интерфейса: как создавать представления
Механизм привязки данных является естественным элементом экосистемы Windows (WPF, WinRT и UWP), поэтому в Windows метод создания представлений использует нативный подход. На платформах Android и iOS нативной моделью является MVC, где ключевую роль играют контроллеры (iOS) и действия/фрагменты (Android).
Исключительным преимуществом фреймворка MvvmCross является тот факт, что в отличие от Xamarin.Forms все представления, макеты, виджеты определяются полностью нативно, с использованием нативных механизмов и инструментов.
Xamarin.Android
В случае Android мы создаем файлы xml или axml, которые используют только собственный API для создания макетов для отдельных представлений. MvvmCross предоставляет атрибут local: MvxBind, который можно использовать для привязки свойств элементов представления (виджетов, макетов) с соответствующими свойствами модели представления согласно приведенному примеру ниже.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/ res/android" xmlns:local="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:padding="20dp"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginBottom="20dp"> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" android:text="Add Product" local:MvxBind="Click AddProductCommand; Enabled IsAddButtonEnabled" /> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" android:text="Remove All" local:MvxBind="Click RemoveAllProductsCommand; Enabled IsRemoveAllButtonEnabled" /> </LinearLayout> <MvxListView android:layout_width="fill_parent" android:layout_height="wrap_content" android:divider="@null" android:scrollbars="none" android:footerDividersEnabled="false" android:overScrollFooter="@android:color/transparent" local:MvxItemTemplate="@layout/view_product_item" local:MvxBind="ItemsSource Products" /> </LinearLayout>
Платформа предоставляет дополнительный набор элементов управления пользовательского интерфейса. Одним из них является виджет (MvxListView), отображающий список элементов. MvxListView указывает шаблон ячейки заданного списка с помощью свойства local:MvxItemTemplate. Внедрение адаптера не требуется, поэтому мы избегаем лишнего кода в платформенно-зависимом проекте.
[Activity] public class ProductsActivity : MvxActivity { protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); SetContentView(Resource.Layout.layout_products_activity); } }
В MvvmCross контроллеры используются только для загрузки представления и привязки его к правильной модели представления. Если приложение требует предоставления определенных функций/действий на данной платформе, они могут быть реализованы в контроллерах. Но это уменьшит объем кода, которым можно поделиться, а возможные различия или несоответствия в поведении приложений сделают отладку и внедрение дополнительных изменений более трудоемкими.
Xamarin.iOS
Создание представления для системы iOS начинается с добавления класса контроллера вместе с файлом с расширением xib, представляющим представление. Редактируйте xib-файлы с помощью Xamarin Studio (среда, предоставляемая Xamarin) или инструмента Xcode Interface Builder, встроенного в Xcode.
После упорядочивания представления, удерживая клавишу CTRL, перетащите элементы, которые вы хотите связать с моделью представления, в соответствующий файл заголовка. Изменения будут автоматически синхронизированы с языком C#, а точнее с классом созданного контроллера (листинг 6). Через свойства контроллера, помеченные атрибутом Outlet, можно получить доступ к отдельным элементам пользовательского интерфейса.
[Register ("ProductsViewController")] partial class ProductsViewController { [Outlet] UIKit.UIButton AddProductButton { get; set; } [Outlet] UIKit.UITableView ProductList { get; set; } [Outlet] UIKit.UIButton RemoveAllButton { get; set; } }
После загрузки представления необходимо создать набор, который связывает свойства помеченные атрибутом Outlet со свойствами данной модели представления. Метод Bind принимает объект (обычно это outlet), метод For указывает свойство этого объекта, которое будет связано со свойством модели представления, указанной методом To. Если метод For пропущен, для данного объекта будет использовано свойство по умолчанию.
public partial class ProductsViewController : BaseViewController { public ProductsViewController() : base(nameof(ProductsViewController), null) { } public override void ViewDidLoad() { base.ViewDidLoad(); InitializeBinding(); } private void InitializeBinding() { var set = this.CreateBindingSet<_ProductsViewController2c_ ProductsViewModel="">(); var source = new ProductListDataSource(ProductList); ProductList.Source = source; set.Bind(source).For(v => v.ItemsSource).To(vm => vm.Products); set.Bind(AddProductButton).To(vm => vm.AddProductCommand); set.Bind(AddProductButton).For(v => v.Enabled).To(vm => vm.IsAddButtonEnabled); set.Bind(RemoveAllButton).To(vm => vm.RemoveAllProductsCommand); set.Bind(RemoveAllButton).For(v => v.Enabled).To(vm => vm.IsRemoveAllButtonEnabled); set.Apply(); } }
Расширенная привязка данных – создание конвертеров и пользовательских привязок
Часто составные представления требуют дополнительного преобразования связанных данных, т. е. изменения типа или формата свойств модели представления, которые связываются со свойством данного элемента управления пользовательского интерфейса. Для этой цели можно определить конвертер, реализующий абстрактный класс MvxValueConverter.
public class BoolToTextColorConverter : MvxValueConverter { protected override UIColor Convert(bool value, Type targetType, object parameter, CultureInfo culture) { return value ? UIColor.Red : UIColor.Black; } }
Как применить определенный конвертер? В Xamarin.iOS применение определенного конвертера сводится к запросу метода WithConversion, который предполагает наличие экземпляра преобразователя.
set.Bind(EmailTextField).For(x => x.TextColor) .To (x => x.IsErrorVisible) .WithConversion(new BoolToTextColorConverter());
В Xamarin.Android существует возможность связать свойство с именем конвертера в разметке представления.
<EditText
style="@style/EmailEditText"
local:MvxBind="TextColor BoolToTextColor(IsErrorVisible)" />
Конкретное свойство модели представления определяет изменение нескольких свойств виджета или требует изменения виджета, что может быть выполнено только путем запроса вызова для него одного или нескольких методов. В таком случае становится необходимым механизм, позволяющий регистрировать пользовательские привязки данных.
public class TextViewWithHtmlBinding : MvxConvertingTargetBinding { public TextViewWithHtmlBinding(TextView textView) : base(textView) { } public override Type TargetType { get { return typeof(TextView); } } protected override void SetValueImpl(object target, object value) { var textView = target as TextView; textView.MovementMethod = LinkMovementMethod.Instance; textView.SetText(Html.FromHtml((string)value), TextView.BufferType.Spannable); } }
Первым шагом является создание класса, наследуемого от класса MvxConvertingTargetBinding. В представленном примере определена привязка для текстового значения, содержащего разметку HTML. Чтобы их правильно интерпретировать, необходимо использовать метод SetText с соответствующими параметрами и изменить свойство MovementMethod виджета TextView, что невозможно сделать эффективно, основываясь на привязках по умолчанию.
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry) { registry.RegisterCustomBindingFactory("TextWithHtml", x => new TextViewWithHtmlBinding(x)); base.FillTargetFactories(registry); }
Затем необходимо переопределить метод FillTargetFactories в классе Setup, зарегистрировав созданную привязку под выбранным именем с соответствующим типом элемента управления пользовательского интерфейса. Весь процесс аналогичен системе Xamarin.iOS. Зарегистрированную привязку можно успешно использовать во всем приложении так же, как стандартные предопределенные привязки данных.
Изменение стандартной схемы навигации
Каждый пакет MvvmCross для конкретной платформы содержит презентер по умолчанию, реализующий интерфейс IMvxViewPresenter. Презентер отвечает за предоставление схемы навигации между определенными представлениями. По умолчанию он использует механизм рефлексии для связывания контроллеров с соответствующими им моделями представлений, поэтому ключевым элементом является присвоение имен контроллерам, которые соответствуют именам моделей представлений, используемых в проекте PCL.
Если по какой-то причине мы хотим изменить схему навигации по умолчанию, достаточно переопределить соответствующие методы стандартного презентера, а после этого вернуть его в методе CreateViewPresenter в классе Setup. Мы сталкиваемся с такой ситуацией, например, когда есть необходимость связать определенную группу моделей представления с фрагментами, отображаемыми в области основной деятельности — всплывающей навигации.
public class DroidPresenter : MvxAndroidViewPresenter { public override void Close(IMvxViewModel viewModel) { Activity.FinishAffinity(); } public override void Show(MvxViewModelRequest request) { base.Show(request); } }
Затем необходимо переопределить презентер по умолчанию в классе Setup:
protected override IMvxAndroidViewPresenter CreateViewPresenter() { return new DroidPresenter(); }
Плагины и дополнительные компоненты
Платформа MvvmCross предоставляет большое количество подключаемых модулей и библиотек, доступных на GitHub, а также в виде пакетов NuGet. Это, например, компоненты для упрощения работы с базой данных, доступности сети, соединений, местоположений, операций с файлами, отправки электронной почты, интеграции с социальными сетями, загрузки и хранения данных в кэш-памяти.Чтобы воспользоваться нужным модулем, необходимо добавить соответствующий пакет во все проекты в решении, а затем использовать доступный API в общей части. Также есть возможность создавать собственные внутренние плагины или развивать существующие в соответствии с доступной инструкцией.
Заключение
В этой статье представлены только избранные, наиболее значимые механизмы фреймворка MvvmCross. Обсуждаемое решение постоянно развивается, предоставляя все новые и новые возможности. Это, несомненно, лучшее решение для сложных и требовательных бизнес-приложений Xamarin.
Благодаря нативным методам построения пользовательского интерфейса мы можем предоставить отличный UX и в то же время поделиться значительной частью всего решения, включая тестируемую бизнес-логику приложения.