Шаблон Model-View-ViewModel (MVVM) стал эталонным для любого приложения XAML (Windows Presentation Foundation [WPF], Windows 8, Windows Phone и Silverlight). Впервые появившийся в WPF, он разделяет обязанности, обеспечивает тестируемость и др. Самое лучшее в том, что вы можете использовать его для любых других технологий — даже тех, где XAML не применяется, например в ASP.NET, JavaScript и т. д.
Xamarin позволяет разрабатывать приложения Android или iOS на C#. Эти приложения имеют свои модели разработки, но благодаря инфраструктуре MvvmCross вы можете перенести шаблон MVVM и на эти платформы. В этой статье я дам все, что нужно для понимания MvvmCross и того, как использовать ее в своих приложениях Android и iOS.
Краткий обзор MVVM
О шаблоне MVVM написано много статей, поэтому я не стану тратить время на его подробный обзор. Шаблон MVVM состоит из трех частей: Model (соответствует данным, которые вы хотите отображать на экране и манипулировать ими), View (компонент презентационного уровня и UI) и ViewModel (который использует Model и отображает ее во View через связывание с данными, а также реагирует на действия пользователя). Схема MVVM показана на рис. 1.
Увеличить
Рис. 1. Обзор шаблона Model-View-ViewModel
Data Binding | Связывание с данными |
Model | Model |
ViewModel | ViewModel |
View | View |
User Input | Пользовательский ввод |
При разработке на основе технологий Microsoft легко увидеть, что MVVM обеспечивает повторное использование. А как насчет других технологий? Например, Android и iOS?
Конечно, вы можете по-прежнему реализовать собственные шаблоны или методологии, но они не предоставят вам некоторые из наиболее эффективных средств MVVM вроде связывания с данными или тестируемости. Одно из крупнейших преимуществ следования шаблону MVVM заключается в том, что ViewModel легко поддаются тестированию. Это также позволяет вам помещать кросс-платформенный код в ViewModel. В ином случае этот код содержался бы в специфичном для платформе классе, скажем, в контроллере.
Xamarin и MvvmCross решают эту проблему и предлагают единообразный способ использования MVVM на других платформах. Но, прежде чем рассматривать применение MVVM на других платформах, я ненадолго отвлекусь на то, чтобы рассказать о Xamarin.
Xamarin для приложений Android/iOS
Xamarin — это набор инструментов, дающих на выходе высокопроизводительный скомпилированный код с полным доступом ко всем «родным» API. Он позволяет создавать «родные» приложения, способные задействовать аппаратно-специфичные возможности. Все, что вы делаете на Objective-C или Java, можно делать на C# в Xamarin.
Хотя для разработки приложений можно использовать Xamarin Studio, также применимы Visual Studio и любые другие современные среды разработки на C#. К ним относятся Team Foundation Server (для контроля версий исходного кода) и такие плагины, как Resharper, GhostDoc и др.
С точки зрения разработчика, Xamarin предлагает три основных продукта: Xamarin.Mac, Xamarin.iOS (MonoTouch.dll) и Xamarin.Android (Mono.Android.dll). Все они построены поверх Mono — версии Microsoft .NET Framework с открытым исходным кодом. На самом деле изначально Mono был создан Мигелем де Икасой (Miguel De Icaza), соучредителем и нынешним техническим директором Xamarin.
В iOS выделенный компилятор транслирует приложения, написанные на C#, непосредственно в «родной» ARM-код. В случае Android процесс похож на компиляцию и выполнение в .NET. Исходный код компилируется в код на промежуточном языке (IL). Когда этот код запускается на устройстве, выполняется JIT-компиляция IL-кода в «родной» для этого устройства. Это имеет смысл, потому что приложения Android пишутся на Java, внутренняя архитектура которой сходна с таковой в .NET Framework. Наглядное представление процесса компиляции для iOS и Android показано на рис. 2.
Увеличить
Рис. 2. Компиляция в «родной» код с помощью Xamarin
Bindings | Привязки |
ARM BINARY | Исполняемый файл для ARM |
Runs Natively | Выполняется как «родной» |
Большую часть времени вам не придется беспокоиться об управлении памятью, выделении ресурсов и т. п., поскольку все управляется исполняющей средой, которую предоставляет Xamarin. Однако бывают случаи, где вы должны понимать, что происходит, например при interop-вызовах в Objective-C. В таких случаях ваш управляемый класс на самом деле обертывает некоторые дорогостоящие ресурсы вроде UIImage в iOS. Подробнее на эту тему см. по ссылке bit.ly/1iRCIa2.
При разработке на основе технологий Microsoft легко увидеть, что MVVM обеспечивает повторное использование.
Для приложений iOS нужно учесть некоторые специфические моменты. Хотя Xamarin Studio для Mac предоставляет все, что нужно для разработки под iOS, пользователям Visual Studio на ПК все равно потребуется установить Mac с инструментарием Xamarin. Это дает возможность компилировать приложения по сети и тестировать их в iOS Simulator или на устройстве iOS.
Xamarin позволяет компилировать приложения iOS и Android, написанные на C# или F#, но использующие традиционный шаблон Model-View-Controller. Если вы хотите улучшить тестируемость, возможности в сопровождении и портируемости, вам нужен какой-то способ переноса шаблона MVVM на эти платформы. Знакомьтесь с MvvmCross.
MvvmCross для приложений Xamarin
MvvmCross — кросс-платформенная инфраструктура MVVM с открытым исходным кодом, разработанная Стюартом Лоджем (Stuart Lodge). Она доступна для приложений Windows Phone, Windows 8, iOS, Android и WPF. MvvmCross вводит шаблон MVVM на платформы, где ранее его не было, например на iOS и Android.
Она также поддерживает связывание с данными в представлениях. Это мощный функционал, обеспечивающий разделение обязанностей (separation of concerns). View будет использовать разные ViewModel, чтобы обеспечивать нужные поведения в приложении. MvvmCross даже находит эти ViewModel в выделенном проекте, чтобы вы могли легко ссылаться на них и повторно использовать в других приложениях.
Это самое важное в MvvmCross. Находя разные ViewModel в Portable Class Library (PCL), вы можете добавлять их как ссылки в любые другие проекты. Конечно, это не единственная интересная особенность MvvmCross. Эта инфраструктура также имеет архитектуру на основе плагинов, поддерживает встраивание зависимостей (dependency injection, DI) и др.
Применение MvvmCross в Android/iOS
Применять MvvmCross легко, потому что она состоит всего из нескольких NuGet-пакетов, которые вы добавляете в свои проекты. После этого нужно проделать несколько простых операций перед запуском приложения. Эти операции в iOS отличаются от таковых в Android, но все равно весьма схожи. Проект Core содержит ваши ViewModel и класс App. Он инициализирует сервисы и определяет ViewModel, которая будет запущена при старте приложения:
public class App : MvxApplication
{
public override void Initialize()
{
this.CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
this.RegisterAppStart<HomeViewModel>();
}
}
В своем приложении iOS или Android вы должны создать файл Setup.cs. Он будет ссылаться на проект Core и даст знать исполняющей среде, как создать экземпляр приложения:
public class Setup : MvxAndroidSetup
{
public Setup(Context applicationContext) :
base(applicationContext)
{
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
}
Таким образом, файл Setup создает экземпляр приложения, используя файл приложения. Последний указывает исполняющей среде загрузить конкретный ViewModel при запуске, применяя метод RegisterAppStart.
Каждое представление специфично для каждого приложения. Это единственная часть, которая изменяется. В Android класс наследует от MvxActivity (стандартные операции в Android наследуют от Activity). В случае iOS представления наследуют от MvxViewController (стандартный ViewController в iOS наследует от UIViewController):
[Activity(ScreenOrientation = ScreenOrientation.Portrait)]
public class HomeView : MvxActivity
{
protected override void OnViewModelSet()
{
SetContentView(Resource.Layout.HomeView);
}
}
MvvmCross нужно знать ViewModel, с которым сопоставлен View. Вы можете делать это по умолчанию благодаря соглашению по именованию. Кроме того, вы можете легко менять его, переопределяя свойство ViewModel в View или используя MvxViewForAttribute:
[Activity(ScreenOrientation = ScreenOrientation.Portrait)]
[MvxViewFor(typeof(HomeViewModel))]
public class HomeView : MvxActivity
{ ... }
В iOS и Android представления проектируются, используя разные подходы. В iOS View определяется в коде на C#. В Android вы также можете использовать код на C#. Но лучше применять формат AXML (XML-формат, используемый для описания UI в Android). Из-за этих различий связывание с данными на каждой платформе определяется по-разному.
В iOS вы создаете BindingDescriptionSet, представляющий связь между View и ViewModel. В нем вы указываете, какой элемент управления вы хотите связать с тем или иным свойством до применения привязки:
var label = new UILabel(new RectangleF(10, 10, 300, 40));
Add(label);
var textField = new UITextField(
new RectangleF(10, 50, 300, 40));
Add(textField);
var set = this.CreateBindingSet<HomeView,
Core.ViewModels.HomeViewModel>();
set.Bind(label).To(vm => vm.Hello);
set.Bind(textField).To(vm => vm.Hello);
set.Apply();
Одно из крупнейших преимуществ следования шаблону MVVM заключается в том, что ViewModel легко поддаются тестированию.
В Android при использовании AXML вы можете задействовать новый XML-атрибут MvxBind для выполнения связывания с данными:
<TextView xmlns:local="http://schemas.android.com/apk/res-auto"
android:text="Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tripitem_title"
local:MvxBind="Text Name"
android:gravity="center_vertical"
android:textSize="17dp" />
Атрибут MvxBind принимает параметры, которые указывают связываемое свойство элемента управления и свойство ViewModel, используемое в качестве источника. Если вы разработчик на XAML, имейте в виду, что в MvvmCross режим связывания по умолчанию — TwoWay. В XAML режим связывания по умолчанию — OneWay. Инфраструктура MvvmCross понимает несколько пользовательских XML-атрибутов, доступных в MvxBindingAttributes.xml (рис. 3).
Рис. 3. Содержимое файла MvxBindingAttributes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-stylable name="MvxBinding">
<attr name="MvxBind" format="string"/>
<attr name="MvxLang” format="string"/>
</declare-styleable>
<declare-stylable name="MvxControl">
<attr name="MvxTemplate" format="string"/>
</declare-styleable>
<declare-styleable name="MvxListView">
<attr name="MvxItemTemplate" format= "string"/>
<attr name="MvxDropDownItemTemplate" format="string"/>
</declare-stylable>
<item type="id" name="MvxBindingTagUnique">
<declare-styleable name="MvxImageView">
<attr name="MvxSource" format="string"/>
</declare-stylable>
</resources>
Контент этого файла прост, но очень важен. Данный файл указывает атрибуты, которые вы можете использовать в AXML-файлах. В операции связывания можно применять атрибут MvxBind или MvxLang. Кроме того, вы можете задействовать и некоторые новые элементы управления (MvxImageView, MvxListView). Каждый из них поддерживает собственные специализированные атрибуты, как видно на рис. 4.
Рис. 4. Новые элементы управления имеют собственные атрибуты
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="2">
<Mvx.MvxListView
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind=
"ItemsSource Trips;ItemClick SelectTripCommand"
local:MvxItemTemplate="@layout/tripitemtemplate" />
</LinearLayout>
Это должно быть знакомо разработчикам на XAML. Свойства ItemsSource и ItemClick связываются с некоторыми свойствами источника данных (в нашем случае — ViewModel). MvxItemTemplate определяет интерфейс для каждого элемента в ListView.
Возможно, вы ожидали, что синтаксис для iOS будет сильно отличаться, но на деле он весьма похож. Синтаксис, используемый в AXML-файле, — просто привязка в текстовом формате, которую можно сопоставить и с C#-версией. Команда в следующем примере, которая выбирает элемент из списка, является объектом ICommand:
public ICommand<Trip> SelectTripCommand { get; set; }
Реализация этого интерфейса предоставляется MvvmCross с помощью класса MvxCommand:
private void InitializeCommands()
{
this.SelectTripCommand = new MvxCommand<Trip>(
trip => this.ShowViewModel<TripDetailsViewModel>(trip),
trip => this.Trips != null &&
this.Trips.Any() && trip != null);
}
Распространенная проблема при применении шаблона MVVM — преобразования типов. Это происходит, когда вы определяете свойство с типом, который нельзя напрямую использовать в UI. Например, у вас может быть свойство image как байтовый массив, но вы хотите задействовать его для свойства source элемента управления image. В XAML эта задача может быть решена с помощью интерфейса IValueConverter, который сопоставляет значения между View и ViewModel.
В MvvmCross процесс довольно похож благодаря интерфейсу IMvxValueConverter и двум его методам: Convert и ConvertBack. Этот интерфейс позволяет выполнять преобразования, аналогичные таковым в XAML, но с учетом кросс-платформенных факторов. Имейте в виду, что этому интерфейсу свойственны те же недостатки, что и XAML. Он принимает объект как параметр и возвращает его как значение. Поэтому требуется приведение типов. Для оптимизации MvvmCross предлагает обобщенный класс MvxValueConverter, который принимает параметры и возвращает типы:
public class ByteArrayToImageConverter :
MvxValueConverter<byte[], Bitmap>
{
protected override Bitmap Convert(byte[] value,
Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
var options = new BitmapFactory.Options
{ InPurgeable = true };
return BitmapFactory.DecodeByteArray(
value, 0, value.Length, options);
}
}
Ссылаться на класс преобразований легко. В iOS используйте метод WithConversion в текучем (fluent) синтаксисе:
var set = this.CreateBindingSet<HomeView,
Core.ViewModels.HomeViewModel>();
set.Bind(label).To(vm => vm.Trips).WithConversion(
"ByteArrayToImage");
set.Apply();
В Android ссылайтесь на класс преобразований напрямую в AXML-файле:
<ImageView
local:MvxBind="Bitmap Image,Converter=ByteArrayToImage"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
Классы преобразования (конвертеры) распознаются по их именам через механизм отражения. По умолчанию инфраструктура будет искать любой тип, содержащий в своем имени слово «converter». Кроме того, можно вручную зарегистрировать конвертеры, переопределив метод FillValueConverters в классе Setup.
Все, что вы делаете на Objective-C или Java, можно делать на C# в Xamarin.
MvvmCross предоставляет простой, облегченный DIcontainer. В этом контейнере можно регистрировать классы и интерфейсы, используя несколько шаблонов, в том числе регистрацию singleton-объектов, динамическую регистрацию и т. д.:
Mvx.RegisterType<ISQLiteConnectionFactory,
SQLiteConnectionFactory>();
Mvx.RegisterSingletong<ISQLiteConnectionFactory,
SQLiteConnectionFactory>();
Разрешение типов в контейнере может происходить двумя способами. Вы можете использовать метод Mvx.Resolve для явного разрешения типа. Кроме того, поддерживается встраивание конструктора (constructor injection), что позволяет MvvmCross использовать механизм отражения и автоматически разрешать параметры при создании объекта:
private readonly ISQLiteConnectionFactory _sqlFactory;
public DataAccessLayerService(
ISQLiteConnectionFactory sqlFactory)
{
this._sqlFactory = sqlFactory;
}
Встраивание конструктора можно использовать для сервисов, а также для ViewModel. Это важно понимать, так как любые сервисы, разработанные для вашего приложения и помещенные в проект Core, по умолчанию являются кросс-платформенными.
Вы также можете задействовать сервисы, которые кажутся кросс-платформенными, но реализации которых специфичны для конкретной платформы. Например, создание снимка камерой, получение координат пользователя, работа с базой данных и т. д. Благодаря встраиванию конструктора ViewModel могут получить интерфейс, где реализация специфична для конкретной платформы.
MvvmCross вводит шаблон MVVM на платформы, где ранее его не было, например на iOS и Android.
Для расширения механизма встраивания и поддержки специфичного для платформ кода MvvmCross предоставляет систему плагинов. Эта система позволяет создавать и встраивать новую функциональность в период выполнения. Каждый плагин является сервисом с интерфейсом, который имеет конкретную реализацию для каждой платформы. Система плагинов регистрирует интерфейс и реализацию. Приложения используют плагины с помощью DI, которое также дает возможность разработчикам плагинов предоставлять упрощенные «фальшивые» реализации на период тестирования и разработки.
С точки зрения разработчика, написание плагина столь же просто, как и написание интерфейса. Вы создаете класс, реализующий соответствующий интерфейс, и загрузчик плагина. Последний является классом, который реализует интерфейс IMvxPluginLoader. Он регистрирует интерфейс и реализацию плагина (используя Mvx.RegisterType) при вызове его метода EnsureLoaded.
В настоящее время существует уже много плагинов. Они предоставляют такой функционал, как доступ к файлам, электронную почту, преобразование JSON и др. Большинство из них вы найдете через NuGet. Но будьте осторожны: некоторые плагины не содержат реализаций для всех платформ. Обращайте внимание на эту деталь, планируя использование какого-либо плагина. Даже если в плагине отсутствует поддержка вашей платформы, возможно, вы сумеете легко реализовать недостающее, следуя шаблону, и тогда вам не придется создавать новый плагин с нуля. В этом случае будет неплохо, если вы поделитесь своей реализацией с владельцем данного плагина, чтобы и другие могли воспользоваться ею.
MvvmCross — ценная инфраструктура. Принимайте ее во внимание при разработке мобильных приложений — даже на платформах Android и iOS. Шаблон MVVM в сочетании с привязкой к данным и с плагинами предоставляет мощную систему для создания портируемого кода, удобного в сопровождении.