СЕНТЯБРЬ 2015

ТОМ 30, НОМЕР 9

Мобильные приложения, подключенные к облаку — Создание приложения Xamarin с поддержкой аутентификации и автономной работы

Крейг Брокшмидт

Исходный код можно скачать по ссылке

Продукты и технологии:

Microsoft Azure, SQLite, ASP.NET, OAuth, Visual Studio, Visual Studio Online, Team Foundation Server, Xamarin, Xamarin.Forms

В статье рассматриваются:

  • создание кросс-платформенного мобильного приложения с помощью Xamarin.Forms;
  • вход через социальные сети для аутентификации пользователей в серверной части приложения;
  • применение базы данных SQLite для поддержки автономного кеша серверных данных на мобильном клиенте;
  • настройка среды сборки для приложений Xamarin, используя Visual Studio Online и Team Foundation Server.

Как было отмечено в первой части этой серии статей «Создание веб-сервиса с помощью Azure Web Apps и WebJobs», опубликованной в прошлом номере (msdn.microsoft.com/magazine/mt185572), многие мобильные приложения сегодня подключены к одному или более веб-сервисам, которые предоставляют важные и интересные данные. Хотя нетрудно просто напрямую вызывать эти сервисы через REST API и обрабатывать ответы на клиенте, такой подход может оказаться слишком дорогостоящим в плане энергопотребления, сетевого трафика и ограничений, связанных с регулированием нагрузки (throttling limitations) различными сервисами. Производительность также может пострадать из-за слабого аппаратного обеспечения на клиентской стороне. Поэтому имеет смысл передать основную работу собственной серверной части, как мы и демонстрируем на примере проекта Altostratus, обсуждаемого в этой серии статей.

Серверная часть Altostratus, рассмотренная в первой части, периодически собирает и нормализует «обсуждения» («conversations») из StackOverflow и Twitter (просто для использования двух разных источников данных) и сохраняет их в базе данных Microsoft Azure. То есть серверная часть может напрямую обслуживать именно те данные, которые нужны клиентам, что позволяет нам масштабировать серверную часть в Azure для подстройки к любому количеству клиентов без попадания под регулирование нагрузки оригинальными провайдерами. Нормализуя данные на серверной стороне под необходимые клиенту, мы также оптимизируем обмен данными через Web API, экономя драгоценные ресурсы мобильных устройств.

В этой статье мы обсудим детали клиентского приложения (рис. 1). Мы начнем с архитектуры приложения, чтобы подготовить общий контекст, а затем перейдем к нашему использованию Xamarin и Xamarin.Forms, аутентификации в серверной части, созданию автономного кеша (offline cache) и сборке с помощью Xamarin в Team Foundation Server (TFS) и Visual Studio Online.

Мобильное приложение Xamarin, выполняемое на планшете с Android (слева), на смартфоне с Windows Phone (в середине) и на iPhone (справа)
Рис. 1. Мобильное приложение Xamarin, выполняемое на планшете с Android (слева), на смартфоне с Windows Phone (в середине) и на iPhone (справа)

Архитектура клиентского приложения

Клиентское приложение содержит три основные страницы, или представления: Configuration, Home и Item, чьи классы основаны на тех же именах (рис. 2). Дополнительная страница Login не имеет UI и является просто хостом для веб-страниц OAuth-провайдера. При использовании базовой структуры Model-View-ViewModel (MVVM) каждой основной странице, кроме дополнительной страницы Login, сопоставляется класс модели представления для обработки отношений привязки (binding relationships) между представлениями и классами модели представления, которые отражают элементы (items), категории и конфигурационные параметры (включая список провайдеров аутентификации).

Архитектура клиентского приложения Altostratus, показывающая имена основных классов (имена файлов в проекте соответствуют именам этих классов, если не указано иное)
Рис. 2. Архитектура клиентского приложения Altostratus, показывающая имена основных классов (имена файлов в проекте соответствуют именам этих классов, если не указано иное)

ConfigurationPage ConfigurationPage
Provider List Список провайдеров
Log In/Off Вход/выход
Limit Slider Ползунок ограничения
Category Toggles Переключатели категории
Configuration ViewModel ConfigurationViewModel
HomePage HomePage
Items ListView (grouped) Элементы ListView (сгруппированные)
HomeViewModel HomeViewModel
Sync Синхронизация
ItemPage ItemPage
ItemViewModel ItemViewModel
Links open default browser Ссылки открываются браузером по умолчанию
Data binding to model Привязка данных к модели
LoginPage OAuth flow Retrieve settings LoginPage
Поток управления OAuth
Получение параметров
DataModel (model.cs) + DataAccessLayer (dataaccess.cs) DataModel (model.cs) + DataAccessLayer (dataaccess.cs)
Model always draws from local storage Модель всегда опирается на локальное хранилище
sync.cs sync.cs
webapi.cs webapi.cs
Sync refreshes local cache Локальный кеш обновляется при синхронизации
Pre-Populated SQLite Database (altostratus.db3) Заранее заполненная база данных SQLite (altostratus.db3)
Copy database file on first run (SQLite_*.cs in platform projects) Копирование файла базы данных при первом запуске (SQLite_*.cs в проектах для конкретных платформ)
Local SQLite Database Локальная база данныхSQLite
Back End Серверная часть

(Примечание Разработчики часто предпочитают выделять XAML-представления в библиотеку портируемых классов [PCL], отделяемую от моделей представлений и других классов, что позволяет дизайнерам независимо работать над представлениями в таких инструментах, как Blend. Мы не пошли на такое разделение, чтобы сохранить простоту структуры проекта. Кроме того, в тот период времени Blend не работал с элементами управления в Xamarin.Forms.)

Модель данных всегда заполняет эти объекты из локальной базы данных SQLite, которая в свою очередь заранее заполняется при первом запуске приложения. Синхронизация с серверной частью, которая происходит в модели данных, — прямолинейный процесс извлечения новых данных (как можно меньшего объема для минимизации сетевого трафика), который обновляет базу данных, очищает любые устаревшие данные и указывает модели данных обновить свои объекты. Это приводит к обновлению UI из-за привязки данных к моделям представлений.

Как мы обсудим позже, синхронизация инициируется рядом событий: нажатием кнопки обновления в UI, изменением конфигурации, аутентификацией в серверной части (которая извлекает ранее сохраненную конфигурацию), возобновлением работы приложения не менее чем после 30 минут и т. д. Конечно, синхронизация — основной вид коммуникации с серверной частью через ее Web API, но серверная часть также предоставляет API для регистрации пользователя, получения параметров для аутентифицированного пользователя и обновления этих параметров, когда аутентифицированный пользователь изменяет конфигурацию.

Xamarin.Forms для кросс-платформенного клиента

Вы наверняка не раз читали в «MSDN Magazine», что Xamarin позволяет использовать C# и Microsoft .NET Framework для создания приложений под Android, iOS и Windows, причем большое количество кода является общим для этих платформ. (Обзор нашей конфигурации для разработок см. в разделе «Сборка с использованием Xamarin в TFS и Visual Studio Online» далее в этой статье.) Инфраструктура Xamarin.Forms еще больше увеличивает долю этого кода, предлагая общее UI-решение для XAML/C#. Благодаря Xamarin.Forms в проекте Altostratus более 95% его кода является общим в одной PCL. По сути, специфичным для конкретной платформы является только код, отвечающий за запуск (он содержится в шаблонах проектов) и рендеринг страниц входа, имеющий дело с элементом управления «веб-браузер», а также несколько строк кода для копирования заранее заполняемой базы данных SQLite в подходящее место локального хранилища для чтения и записи.

Заметьте, что проект Xamarin также можно сконфигурировать на использование общего проекта вместо PCL. Однако компания Xamarin рекомендует применять PCL, и ее основное преимущество в случае Altostratus в том, что мы используем ту же PCL в консольном Win32-приложении, которое создает предварительно заполняемую базу данных. То есть мы не дублируем никакой код базы данных для этой цели, и программа-инициализатор всегда находится в синхронизированном состоянии с остальной частью приложения.

Имейте в виду, что общий код ненамного уменьшает усилия, необходимые для тщательного тестирования приложения на каждой целевой платформе; эта часть процесса будет отнимать примерно столько же времени, как и при написании каждого приложения как «родного» для его платформы. Кроме того, поскольку Xamarin.Forms довольно новый инструмент, вы можете обнаружить специфичные для платформы ошибки или какие-то отклонения от нормального поведения, которые вам придется как-то обходить в своем коде. Подробнее о том, какие проблемы были выявлены нами при написании Altostratus, см. нашу публикацию по ссылке bit.ly/1g5EF4j.

Если вы столкнулись со странными проблемами, первым делом изучите базу данных Xamarin по ошибкам (bugzilla.xamarin.com). Если вы не увидите там своей проблемы, сообщите о ней на форумах Xamarin (forums.xamarin.com), сотрудники Xamarin оперативно откликаются на этих форумах.

Работа с любым видом данных, специфичных для конкретного пользователя, подразумевает аутентификацию этого пользователя.

Даже с учетом сказанного решение отдельных проблем такого рода требует куда меньше работы, чем изучение всех деталей UI-уровня каждой платформы. И поскольку Xamarin.Forms сравнительно нова, выявление подобных проблем помогает делать эту инфраструктуру все более надежной.

Корректировки, специфичные для платформ

В Xamarin.Forms иногда нужны корректировки то для одной платформы, то для другой, скажем, для тонкой настройки разметки. (Ряд примеров вы найдете в великолепной книге Чарльза Петцольда [Charles Petzold] «Programming Mobile Apps with Xamarin.Forms» [bit.ly/1H8b2q6].) Вам также может понадобиться обработка некоторых несогласованностей в поведении, например когда элемент webview генерирует свое первое событие Navigating (и вновь см. нашу публикацию по ссылке bit.ly/1g5EF4j).

Для этой цели в Xamarin.Forms есть API-функция Device.OnPlatform<T>(iOS_value, Android_value, Windows_value) и соответствующий XAML-элемент. Как вы, видимо, догадываетесь, OnPlatform возвращает разные значения в зависимости от текущей исполняющей среды. Так, следующий XAML-код скрывает элементы управления входом на странице Configuration в Windows Phone, поскольку компонент Xamarin.Auth пока не поддерживает эту платформу, и мы всегда работаем без аутентификации (configuration.xaml):

<StackLayout Orientation="Vertical">
  <StackLayout.IsVisible>
    <OnPlatform x:TypeArguments="x:Boolean" Android="true"
      iOS="true" WinPhone="false" />
  </StackLayout.IsVisible>

  <Label Text="{ Binding AuthenticationMessage }"
    FontSize="Medium" />
  <Picker x:Name="providerPicker"
    Title="{ Binding ProviderListLabel }"
    IsVisible="{ Binding ProviderListVisible }" />
  <Button Text="{ Binding LoginButtonLabel}"
    Clicked="LoginTapped" />
</StackLayout>

Кстати, сама Xamarin состоит в основном из компонентов, которые абстрагируют общую функциональность «родных» платформ; многие из этих компонентов используются Xamarin.Forms для UI. Некоторые компоненты изначально встроены в Xamarin, тогда как другие, в том числе разработанные сообществом, можно получить с сайта components.xamarin.com. Кроме Xamarin.Auth, Altostratus использует Connectivity Plugin (tinyurl.com/xconplugin), чтобы выводить индикатор и отключать кнопку обновления при автономной работе устройства.

Мы обнаружили, что между подключением устройства к сети или отключением от нее (отражаемой свойством IsConnected этого плагина) и срабатыванием соответствующего события всегда наблюдается небольшая задержка. Это означает, что между переходом устройства в автономный режим работы и отключением кнопки Refresh может пройти несколько секунд. Для обработки этого мы используем событие команды Refresh для проверки состояния свойства IsConnected плагина. Если устройство отключено от сети, мы сразу же делаем кнопку обновления недоступной, но выставляем флаг, который сообщает обработчику ConnectivityChanged автоматически начинать синхронизацию, когда подключение к сети будет восстановлено.

Altostratus также использует Xamarin.Auth (tinyurl.com/xamauth) для обработки деталей аутентификации через OAuth, о которой мы поговорим в следующем разделе. Здесь подвох в том, что данный компонент в настоящее время поддерживает только iOS и Android, но не Windows Phone, и в цели нашего проекта не входило решение этой проблемы. К счастью, клиентское приложение нормально работает и без аутентификации — просто параметры пользователя не сохраняются в облаке и обмен данными с серверной частью оптимизируется не полностью. Когда компонент будет обновлен для поддержки Windows, нам потребуется лишь удалить тег OnPlatform в показанном ранее XAML, чтобы сделать видимыми элементы управления входом.

Аутентификация в серверной части

В приложении Altostratus в целом мы хотели продемонстрировать механизмы, участвующие в сохранении предпочтений конкретных пользователей на серверной стороне, чтобы серверная часть могла автоматически применять эти предпочтения при обработке HTTP-запросов. Конечно, для этого приложения мы могли бы добиться того же результата, просто используя параметры URI в запросах, но такой пример не был бы основой для более сложных сценариев. Сохранение предпочтений на сервере также позволяет переносить их между всеми устройствами пользователя.

Работа с любым видом данных, специфичных для конкретного пользователя, подразумевает аутентификацию этого пользователя. Она отличается от авторизации. Аутентификация — это способ идентификации пользователя и подтверждения того, что он является тем, кем заявляет себя. Тогда как авторизация относится к выдаче разрешений, заданных для любого конкретного пользователя (например, права администратора, обычного пользователя или гостя).

Для аутентификации мы используем вход через социальные сети вроде Google и Facebook, не реализуя свою систему удостоверений (и серверная часть имеет API, через который клиент получает список провайдеров для отображения в UI на странице Configuration). Основное преимущество входа через социальные сети в том, что мы полностью избавляемся от обработки удостоверений и необходимости соответствующей защиты; серверная часть хранит только адрес электронной почты как имя пользователя, а клиент управляет лишь маркером доступа в период выполнения. Всю черную работу берет на себя провайдер, включая проверку постовых адресов, извлечение паролей и т. д.

Конечно, не у каждого есть учетная запись в какой-либо социальной сети, а некоторые пользователи вообще избегают социальных сетей по соображениям безопасности. Кроме того, вход через социальные сети не годится для специализированных бизнес-приложений; в таких случаях мы рекомендуем Azure Active Directory. Однако для наших целей это был логичный выбор, так как нам просто требовались хоть какие-то средства аутентификации индивидуального пользователя.

После аутентификации пользователь авторизуется на сохранение своих предпочтений в серверной части. Если бы мы хотели реализовать другие уровни авторизации (например, чтобы можно было изменять параметры, сохраненные другими пользователями), серверная часть могла бы сверять имя пользователя по базе данных, где хранятся разрешения для разных пользователей.

Применение OAuth2 для входа через социальные сети в ASP.NET Web API OAuth2 (bit.ly/1SxC1AM) является инфраструктурой авторизации, которая позволяет выдавать права доступа к ресурсам без использования общих удостоверений. В ней определяется несколько «потоков удостоверений» (credential flows), которые указывают, как передаются удостоверения между различными сущностями. В ASP.NET Web API применяется так называемый неявный поток выдачи прав доступа (implicit grant flow), где мобильное приложение ни собирает удостоверения, ни хранит никаких секретов. Эта работа выполняется провайдером OAuth2 и библиотекой ASP.NET Identity (asp.net/identity) соответственно.

Чтобы разрешить вход через социальные сети, нужно зарегистрировать свое приложение у каждого интересующего вас провайдера, используя их порталы для разработчиков. (В этом контексте понятие «приложение» распространяется на любые клиенты, в том числе мобильные и веб-клиенты, и не относится исключительно к мобильному приложению.) После регистрации провайдер выдает вам уникальный идентификатор клиента и секрет. Некоторые примеры см. по ссылке bit.ly/1BniZ89.

С помощью этих значений мы инициализируем промежуточный уровень ASP.NET Identity, как показано на рис. 3.

Рис. 3. Инициализация промежуточного уровня ASP.NET Identity

var fbOpts = new FacebookAuthenticationOptions
{
  AppId = ConfigurationManager.AppSettings["FB_AppId"],
  AppSecret = ConfigurationManager.AppSettings["FB_AppSecret"]
};
fbOpts.Scope.Add("email");
app.UseFacebookAuthentication(fbOpts);

var googleOptions = new GoogleOAuth2AuthenticationOptions()
{
  ClientId = ConfigurationManager.AppSettings[
    "GoogleClientID"],
  ClientSecret = ConfigurationManager.AppSettings[
    "GoogleClientSecret"]
};
app.UseGoogleAuthentication(googleOptions);

Строки вроде «FB_AppID» являются ключами, которые относятся к параметрам окружения и конфигурации веб-приложения, где и хранятся реальные идентификаторы и секреты. Это позволяет обновлять их без перекомпиляции и повторного развертывания приложения.

Применение Xamarin.Auth для обработки потока удостоверений В целом, процесс аутентификации через социальные сети включает множество обменов информацией между приложением, серверной частью и провайдером. К счастью, большую часть всего этого берет на себя компонент Xamarin.Auth (доступный в магазине компонентов Xamarin). Это включает работу с элементом управления «браузер» и предоставление точек подключения обратных вызовов, чтобы приложение могло отреагировать на окончание авторизации.

Xamarin.Auth заставляет клиентское приложение выполнять некоторые части процесса авторизации, а это означает, что приложение должно хранить идентификатор клиента и секрет. Это немного отличается от процесса в ASP.NET, но в Xamarin.Auth хорошо проработанная иерархия классов. Класс Xamarin.Auth.OAuth2Authenticator наследует от WebRedirectAuthenticator, который обеспечивает нам нужную базовую функциональность и требует написать лишь небольшое количество дополнительного кода, который помещается в файлы LoginPageRenderer.cs в проектах для Android и iOS (напомню, что Xamarin.Auth пока не поддерживает Windows). Подробнее о том, что мы делаем здесь, см. нашу публикацию в блоге по ссылке tinyurl.com/kboathalto.

Тогда в клиентском приложении просто содержится класс LoginPage, производный от базового ContentPage из Xamarin.Forms. Этот класс предоставляет два метода — CompleteLoginAsync и CancelAsync, — которые вызываются из кода LoginPageRenderer в зависимости от того, что именно делает пользователь в веб-интерфейсе провайдера.

Передача аутентифицированных запросов После успешного входа клиентское приложение получает маркер доступа (access token). Чтобы выдать аутентифицированный запрос, ему достаточно включить этот маркер в заголовок Authorization:

GET http://hostname/api/UserPreferences HTTP/1.1
Authorization: Bearer I6zW8Dk...
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0;
  rv:11.0) like Gecko

Здесь Bearer указывает на авторизацию с помощью маркеров на предъявителя (bearer tokens); за ним следует длинная, непрозрачная строка маркера.

В целом, процесс аутентификации через социальные сети включает множество обменов информацией между приложением, серверной частью и провайдером.

Мы используем библиотеку System.Net.Http.HttpClient для всех REST-запросов с собственным обработчиком сообщений, который добавляет в каждый запрос заголовок аутентификации. Обработчики сообщений являются компонентами-плагинами, позволяющими просматривать и модифицировать HTTP-запрос и сообщения-ответы. Чтобы узнать больше, см. по ссылке bit.ly/1MyMMB8.

Обработчик сообщений реализуется в классе AuthenticationMessageHandler (webapi.cs) и устанавливается при создании экземпляра HttpClient:

_httpClient = HttpClientFactory.Create(
  handler, new AuthenticationMessageHandler(provider));

Интерфейс ITokenProvider — это просто способ для обработчика получить маркер доступа от приложения (это реализовано в классе UserPreferences в model.cs). Метод SendAsync вызывается для каждого HTTP-запроса; как показано на рис. 4, он добавляет заголовок Authorization, если провайдер маркера предоставляет таковой для использования.

Рис. 4. Добавление заголовка Authorization

public interface ITokenProvider
{
  string AccessToken { get; }
}

class AuthenticationMessageHandler : DelegatingHandler
{
  ITokenProvider _provider;

  public AuthenticationMessageHandler(ITokenProvider provider)
  {
     _provider = provider;
  }

  protected override Task<HttpResponseMessage>
    SendAsync(HttpRequestMessage request,
    System.Threading.CancellationToken cancellationToken)
  {
    var token = _provider.AccessToken;
    if (!String.IsNullOrEmpty(token))
    {
      request.Headers.Authorization =
        new System.Net.Http.Headers.AuthenticationHeaderValue(
        "Bearer", token);
    }
    return base.SendAsync(request, cancellationToken);
  }
}

Azure Offline Sync

Альтернативой реализации собственного автономного кеша является Azure Offline Sync для планшетов — часть Azure Mobile Services. Это напрочь исключает необходимость написания любого кода синхронизации и очень эффективно для передачи изменений от клиента серверу. Однако, поскольку при этом используется Table Storage, вы лишаетесь реляционной модели данных, доступной при использовании SQLite.

На серверной стороне, как описывалось в первой части этой статьи, если с запросом принимается маркер, этот маркер используется для получения предпочтений пользователя и их автоматического применения к остальной части запроса. Например, если пользователь задал предел обсуждений равным 25 вместо 100 по умолчанию, запрос вернет максимум 25 элементов, сэкономив сетевой трафик.

Создание автономного кеша для серверных данных

Крупное преимущество мобильного приложения над мобильным веб-сайтом — гибкость поддержки автономного использования. Мобильное приложение, по определению, всегда присутствует на клиентском устройстве и может использовать самые разнообразные варианты хранилищ данных, такие как SQLite, для поддержания автономного кеша данных, не полагаясь на механизмы на основе браузера.

UI мобильного клиента Altostratus, по сути, работает с данными, которые поддерживаются в локальной базе данных SQLite, что обеспечивает полную функциональность приложения в отсутствие сетевого соединения. Когда сеть становится доступной, фоновые процессы получают текущие данные от серверной части и обновляют базу данных. При этом обновляются объекты модели данных, размещенные поверх базы данных, что в свою очередь инициирует обновление UI через механизм связывания с данными (вы можете увидеть это, вернувшись к рис. 2). Таким образом, архитектура очень похожа на таковую для серверной части, которая обсуждалась в первой части, где постоянно работающие WebJob собирают, нормализуют и сохраняют данные в базе данных SQL Server, чтобы Web API мог обслуживать запросы напрямую из базы данных.

Поддержка автономного режима для Altostratus включает три разные задачи:

  • размещение файла заранее заполненной базы данных прямо в пакете приложения, что предоставить рабочие данные при первом же запуске без обязательного сетевого подключения;
  • реализацию процессов односторонней синхронизации для каждой части модели данных, доступной в UI: обсуждений (элементов), категорий, провайдеров аутентификации и предпочтений пользователя;
  • подключение синхронизации к соответствующим триггерам помимо кнопки Refresh в UI.

Давайте рассмотрим каждую из них по порядку.

Создание заранее заполненной базы данных Вполне возможна ситуация, когда пользователь устанавливает приложение из онлайнового магазина, но откладывает его запуск на более позднее время, а устройство оказывается отключенным от сети. Вопрос в том, хотите ли вы, чтобы ваше приложение сообщало «Извините, пока не будет подключения к сети ничего полезного сделать не удастся»? Или вы все же предпочитаете интеллектуально обрабатывать такие случаи?

Чтобы продемонстрировать второй подход, клиент Altostratus содержит заранее заполненную базу данных SQLite прямо в пакете приложения под каждую платформу (находящуюся в папке resources/raw проекта для каждой платформы). При первом запуске клиент копирует этот файл базы данных в хранилище для чтения и записи на устройстве, а затем работает с данными исключительно в этом хранилище. Поскольку процесс копирования файла уникален на каждой платформе, мы используем Xamarin.Forms.DependencyService для разрешения в период выполнения специфической реализации определенного нами интерфейса, который мы назвали ISQLite. Это происходит в конструкторе DataAccessLayer (DataAccess.cs), который потом вызывает ISQLite.GetDatabasePath для получения специфичного для платформы места хранения скопированного файла базы данных для чтения и записи, как показано на рис. 5.

Рис. 5. Получение специфичного для платформы места хранения файла базы данных

public DataAccessLayer(SQLiteAsyncConnection db = null)
{
  if (db == null)
  {
    String path = DependencyService.Get<ISQLite>()
      .GetDatabasePath();
    db = new SQLiteAsyncConnection(path);

    // Альтернатива – использовать синхронный SQLite API:
    // database = SQLiteConnection(path);
  }

  database = db;
  _current = this;
}

Крупное преимущество мобильного приложения над мобильным веб-сайтом — гибкость поддержки автономного использования.

Для создания начальной базы данных решение Altostratus содержит небольшое консольное Win32-приложение DBInitialize. Оно использует ту же общую PCL, что и приложение, для работы с базой данных, поэтому никогда не возникает проблемы с наличием второй кодовой базы, принципиально отличной от первой. Однако DBInitialize не требуется использовать DependencyService: оно может создать файл напрямую и открыть соединение:

string path = "Altostratus.db3";
SQLiteAsyncConnection conn = new SQLiteAsyncConnection(path);
var dbInit = new DataAccessLayer(conn);

Отсюда DBInitialize вызывает DataAccessLayer.InitAsync, чтобы создать таблицы (приложение никогда не имеет с ними дела благодаря заранее заполненной базе данных), и использует другие методы DataAccessLayer для получения данных от серверной части. Заметьте, что при асинхронных вызовах DBInitialize просто использует .Wait, потому что это консольное приложение и ему незачем беспокоиться об отзывчивом UI:

DataModel model = new DataModel(dbInit);
model.InitAsync().Wait();
model.SyncCategories().Wait();
model.SyncAuthProviders().Wait();
model.SyncItems().Wait();

Чтобы пользователям было на что посмотреть, с помощью таймера оно выводит на экран точку через каждые полсекунды.

Заметьте, что вам понадобится всегда проверять заранее заполненную базу данных с помощью какого-нибудь инструмента вроде DB Browser for SQLite (bit.ly/1OCkm8Y), прежде чем помещать ее в проект. Есть вероятность сбоя одного или более веб-запросов, и тогда база данных будет недействительной. Вы могли бы встроить соответствующую логику в DBInitialize, чтобы оно удаляло базу данных и показывало ошибку. В нашем случае мы просто следим за сообщениями об ошибках и при необходимости заново запускаем эту программу.

Возможно, вы спросите: «Не устареет ли относительно быстро содержимое заранее заполненной базы данных? Не хотел бы я, чтобы пользователи моего приложения видели слишком устаревшие данные при первом же запуске!». Так, конечно, и будет, если вы не будете регулярно обновлять свое приложение. Поэтому вам стоит периодически передавать в магазин обновления приложения, включающие базу данных со достаточно актуальными данными (все зависит от природы ваших данных).

Если пользователь уже установил приложение, обновление не возымеет никакого эффекта, так как код, копирующий упакованный файл базы данных, проверяет, существует ли копия для чтения и записи, и, если да, просто использует ее. Однако, если при такой проверке контролируется лишь сам факт наличия файла, пользователь, который некоторое время не запускал приложение, в итоге получит более старые данные, чем находящиеся в недавно обновленном пакете. Поэтому вы могли бы сверять дату и время кеша базы данных с датой и временем содержимого пакета и перезаписывать кеш более новой копией. Однако мы не реализовали это в Altostratus, поскольку нам также нужно было сохранять информацию о предпочтениях пользователя из существующей базы данных.

Синхронизация автономного кеша с серверной частью Как уже отмечалось, клиент Altostratus всегда работает с кешем базы данных. Все веб-запросы к серверной части (кроме закачки новых предпочтений пользователя) происходят в контексте синхронизации кеша. Механизм для этого реализован как часть класса DataModel, а именно в виде четырех методов в sync.cs: SyncSettings (который делегирует в Configuration.ApplyBackendConfiguration в model.cs), SyncCategories, SyncAuthProviders и SyncItems. Очевидно, что рабочей лошадкой в этой группе является SyncItems, но о том, что запускает все это, мы поговорим в следующем разделе.

Заметьте, что Altostratus синхронизирует данные только в одном направлении — от серверной части в локальный кеш. Кроме того, поскольку мы знаем, что интересующие нас данные не меняются слишком быстро (учитывая расписание работы WebJob в серверной части), нас заботит прежде всего конечная согласованность с хранилищем данных на серверной стороне, а не обновление в течение минут или секунд.

Каждый процесс синхронизации, по сути, один и тот же: получаем текущие данные от серверной части, обновляем локальную базу данных новыми значениями, удаляем любые старые значения из базы данных, а затем заново заполняем объекты модели данных, чтобы инициировать обновление UI.

В случае элементов требуется чуть больше работы, потому что SyncItems может быть вызван из UI и нужно защититься от слишком возбужденных пользователей, которые повторно нажимают кнопку. Закрытое свойство DataModel.syncTask указывает, имеется ли активный процесс синхронизации элемента; SyncItems игнорирует повторный запрос, если syncTask отличен от null. Более того, поскольку запрос элемента может занять некоторое время и включать более крупные наборы данных, мы хотим иметь возможность отменить синхронизацию элемента, если устройство перейдет в автономный режим. Для этого мы сохраняем System.Threading.CancellationToken для задачи.

Закрытый метод SyncItemsCore (рис. 6) является сердцевиной этого процесса. Он получает метку времени последней синхронизации из базы данных и включает ее в веб-запрос.

Рис. 6. Закрытый метод SyncItemsCore

private async Task<SyncResult> SyncItemsCore()
{
  SyncResult result = SyncResult.Success;
  HttpResponseMessage response;

  Timestamp t = await DataAccessLayer.
    Current.GetTimestampAsync();
  String newRequestTimestamp = DateTime.UtcNow.ToString(
    WebAPIConstants.ItemsFeedTimestampFormat);
  response = await WebAPI.GetItems(t, syncToken);

  if (!response.IsSuccessStatusCode)
  {
    return SyncResult.Failed;
  }

  t = new Timestamp() { Stamp = newRequestTimestamp };
  await DataAccessLayer.Current.SetTimestampAsync(t);

  if (response.StatusCode ==
    System.Net.HttpStatusCode.NoContent)
  {
    return SyncResult.NoContent;
  }

  var items = await response.Content.ReadAsAsync<
    IEnumerable<FeedItem>>();
  await ProcessItems(items);

  // Синхронизация завершена,
  // обновляем источник данных ListView
  await PopulateGroupedItemsFromDB();

  return result;
}

Тем самым серверная часть возвращает только те элементы, которые являются новыми или обновленными со времени, указанной в метке. В итоге клиент получает лишь те данные, которые ему нужны, экономя трафик по потенциально ограниченному тарифному плану пользователя. Это также означает, что клиент выполняет меньше работы по обработке данных от каждого запроса, что также минимизирует сетевой трафик плюс уменьшает энергопотребление, экономя аккумуляторы. Обработка пяти обсуждений на категорию вместо, скажем, 50 при каждом запросе на первый взгляд не кажется чем-то существенным, а ведь дает сокращение на 90%. При 20–30 синхронизациях в день это легко может вылиться в дополнительные сотни мегабайт в месяц только для одного приложения. Короче говоря, ваши пользователи определенно оценят усилия по оптимизации трафика!

Получив ответ на запрос, метод ProcessItems добавляем все эти элементы в базу данных, выполняя некоторую чистку заголовков (например, удаляя закругленные кавычки) и извлекая первые 100 символов из тела ответа, чтобы показать описание в основном списке. Чистку заголовков можно было бы выполнять на серверной стороне, что еще немного сэкономило бы время обработки на клиенте. Мы предпочли оставить это на клиенте, поскольку в других сценариях может понадобиться подстройка, специфичная для конкретной платформы. Кроме того, мы могли бы заставить серверную часть создавать 100-символьные описания, что также немного уменьшило нагрузку на клиент, но привело бы к увеличению сетевого трафика. В случае данных, с которыми мы работает здесь, это, по-видимому, равноценный компромисс, а поскольку за UI в конечном счете отвечает клиент, лучше оставить клиенту контроль за этим этапом. (Дополнительные подробности на эту тему и некоторые другие соображения по UI см. в нашей публикации в блоге по ссылке tinyurl.com/kboathaltoxtra.)

После того как мы добавили элементы в базу данных, группы в модели данных обновляются вызовом PopulateGroupedItemsFromDB. Здесь важно понимать, что в базе данных может быть больше элементов, чем необходимо для текущего лимита обсуждений. PopulateGroupedItemsFromDB учитывает это, напрямую применяя это ограничение к запросу, адресованному базе данных.

Однако мы не хотим, чтобы со временем база данных постоянно расширялась из-за хранения набора элементов, которые никогда не будут отображаться. Поэтому SyncItems вызывает метод DataAccessLayer.ApplyConversationLimit, чтобы удалить старые элементы из базы данных до того количества, которое соответствует указанному лимиту. Поскольку размер любого индивидуального элемента в наборе данных Altostratus сравнительно мал, мы используем максимальный лимит обсуждений, равный 100, несмотря на текущую настройку, заданную пользователем. Благодаря этому, если пользователь увеличит лимит, нам не придется снова запрашивать данные от серверной части. Однако, если бы у нас были гораздо большие элементы данных, то, вероятно, имело бы смысл агрессивно очищать базу данных от лишних элементов и при необходимости запрашивать их снова.

Триггеры синхронизации Кнопка Refresh в UI, очевидно, является основным способом запуска синхронизации элементов, но когда выполняются другие процессы синхронизации? И есть ли другие триггеры для синхронизации элементов?

Ответить на эти вопросы, глядя на код, не так просто, поскольку все вызовы методов Sync* происходят в одном месте — в методе HomeViewModel.Sync. Однако этот метод и дополнительная точка входа, HomeViewModel.CheckTimeAndSync, вызываются из множества других мест. Вот краткое описание того, когда, где и как вызовы Sync параметризуются значением из перечисления SyncExtent.

  • При запуске конструктор HomeViewModel вызывает Sync(SyncExtent.All), используя принцип «выстрелил и забыл» (fire-and-forget), поэтому синхронизация происходит полностью в фоновом режиме. Здесь этот принцип означает лишь то, что значение, возвращаемое из асинхронного метода, сохраняется в локальной переменной, чтобы подавить предупреждение компилятора по поводу отсутствия await.
  • Внутри обработчика события ConnectivityChanged плагина Connectivity мы вызываем Sync, если устройство перешло в автономный режим, когда был выдан предыдущий вызов (используя тот же лимит, что и при том вызове).
  • Если пользователь заходит на страницу Configuration и вносит изменения в активные категории, лимит обсуждений или входит в серверную часть, чтобы применить настройки, хранящиеся там, этот факт запоминается установкой флага DataModel.Configuration.HasChanged. Когда пользователь возвращается на начальную страницу, обработчик HomePage.OnAppearing вызывает HomeViewModel.CheckRefresh, который проверяет флаг HasChanged и при необходимости вызывает Sync(SyncExtent.Items).
  • Событие App.OnResume (app.cs) вызывает CheckTimeAndSync, который определяет, что нужно синхронизировать с учетом того, сколько времени приложение находилось в приостановленном состоянии. Очевидно, что эти условия сильно зависят от природы ваших данных и операций, выполняемых серверной частью.
  • Наконец, кнопка Refresh приводит к вызову CheckTimeAndSync с флагом, который всегда заставляет выполнять, как минимум, синхронизацию элементов. Кнопка Refresh использует CheckTimeAndSync, так как возможно, хоть и редко, что пользователь мог оставить приложение работающим на переднем плане в течение более получаса или даже дня, в каковом случае кнопка Refresh должна инициировать и остальные виды синхронизации, как это делается при возобновлении работы приложения.

Преимущество объединения всего в HomeViewModel.Sync заключается в том, что в соответствующие моменты он может устанавливать открытое свойство HomeViewModel.IsSyncing. Это свойство связано через механизм привязки данных со свойствами IsVisible и IsRunning класса Xamarin.Forms.ActivityIndicator в Home.xaml. Простые манипуляции этим флагом позволяют управлять видимостью этого индикатора.

Сборка с использованием Xamarin в TFS и Visual Studio Online

Для проекта Altostratus мы использовали среду разработки, достаточно распространенную при кросс-платформенной работе: ПК под управлением Windows с эмуляторами и соответствующими устройствами для Android и Windows Phone наряду с локальным компьютером Mac OS X с эмулятором iOS и связанными устройствами iOS (рис. 7). В такой среде вы можете выполнять все задачи по разработке и отладке непосредственно в Visual Studio на ПК, используя компьютер Mac OS X для удаленной сборки и отладки под iOS. Тогда приложения iOS, готовые к отправке в магазин, можно передавать и с Mac.

Распространенная среда кросс-платформенной разработки для проектов Xamarin, а также для тех, кто использует другие технологии вроде Visual Studio Tools for Apache Cordova
Рис. 7. Распространенная среда кросс-платформенной разработки для проектов Xamarin, а также для тех, кто использует другие технологии вроде Visual Studio Tools for Apache Cordova

Surface Pro 3 (Windows 8.1) Visual Studio with Xamarin Android SDK, Windows SDK Including Emulators Surface Pro 3 (Windows 8.1)
Visual Studio с Xamarin Android SDK, Windows SDK, включая эмуляторы
Windows Phone 8.1 (Nokia 1520, HTC Trophy) Windows Phone 8.1 (Nokia 1520, HTC Trophy)
Android 4.4 (Asus MemoPad) Android 4.4 (Asus MemoPad)
MacBook Pro (OS X) iOS Simulator MacBook Pro (OS X) iOS Simulator
iOS 7 (iPhone 4s) iOS 7 (iPhone 4s)
iOS 8 (iPad2) iOS 8 (iPad2)
Visual Studio Online Source Control, Work Items, Team Room, ASP.NET Build Note: The Visual Studio Online Hosted Build Controller Supports Xamarin Visual Studio Online
Контроль версий исходного кода, рабочие элементы, Team Room, ASP.NET Build
Примечание: размещенный в Visual Studio Online контроллер сборки поддерживает Xamarin
Local Build Server Локальный сервер сборки
TFS Express TFS Express
Xamarin Android SDK Windows SDK Xamarin Android SDK Windows SDK
Mac OS X Build Host for iOS (Not Configured for Our Project) Mac OS X Build Host for iOS (не сконфигурирован для нашего проекта)

Мы применяли Visual Studio Online для коллективной работы в группе и контроля версий исходного кода и сконфигурировали этот продукт на сборку с непрерывной интеграцией (continuous integration, CI) как для серверной части, так и для клиента Xamarin. Если бы мы начали этот проект сегодня, мы имели бы возможность воспользоваться новейшей системой сборки Visual Studio Online для сборки приложений Xamarin непосредственно в размещенном в ней контроллере сборки. Подробности см. в нашей публикации в блоге (tinyurl.com/kboauthxamvso). Однако в начале 2015 года размещенный контроллер сборки еще не имел такой поддержки. К счастью, для этого достаточно (честно!) задействовать локальную машину, выполняющую TFS как контроллер сборки для Visual Studio Online. На этом сервере мы установили бесплатную редакцию TFS Express наряду с Xamarin и необходимыми SDK для платформ Android и Windows, поместив Android SDK в такую папку, как c:\android-sdk, к которой можно обращаться по учетной записи сборки (build account). (Его установщик по умолчанию помещает SDK в хранилище текущего пользователя, к которому нельзя обратиться по этой учетной записи.) Это обсуждается в документации Xamarin «Configuring Team Foundation Server for Xamarin» по ссылке bit.ly/1OhQPSW.

После полного конфигурирования сервера сборки выполните следующие операции для установления соединения с Visual Studio Online (см. «Deploy and Configure a Build Server» по ссылке bit.ly/1RJS4QL).

  1. Откройте TFS Administration Console.
  2. В панели навигации слева раскройте имя сервера и выберите Build Configuration.
  3. Под сервисом сборки щелкните Properties, чтобы открыть диалог Build Service Properties.
  4. Щелкните Stop the service в верхней части диалога.
  5. Под Communications в поле ниже Provide Build Services for Project Collection введите URL своего набора Visual Studio Online, например https://<ваша_учетная_запись>.visualstudio.com/defaultcollection.
  6. Щелкните кнопку Start внизу диалога, чтобы перезапустить сервис.

И это все! Когда вы создадите определение сборки в Visual Studio Team Explorer, машина с TFS, подключенная к Visual Studio Online, появится в списке доступных контроллеров сборки. При выборе этого варианта сборки, отправляемые в очередь из Visual Studio или при передаче кода в систему контроля версий, будут направляться вашей машине с TFS.

Заключение

Надеемся, вам понравилось наше обсуждение проекта Altostratus, и вы сочтете его код полезным для ваших мобильных приложений, подключенных к облаку. Наша цель в этом проекте заключалась в том, чтобы предоставить ясный пример кросс-платформенного мобильного приложения с собственной серверной частью, способной выполнять значительные объемы работы в интересах клиента, тем самым снимая с него большую нагрузку. Располагая постоянно выполняемой серверной частью, собирающей данные в интересах клиентов, мы значительно сократили сетевой трафик, генерируемый клиентами (и соответственно влияющий на тарифные планы). За счет нормализации данных из разных источников мы свели к минимуму обработку данных на клиенте, что обеспечивает экономное расходование заряда аккумуляторов. Применив аутентификацию пользователя в серверной части, мы продемонстрировали, как можно сохранять там пользовательские предпочтения и автоматически применять их к клиентам, что опять же обеспечивает оптимизацию сетевого трафика и требований к обработке. Мы понимали, что для наших конкретных требований есть более простые способы добиться того же эффекта, но хотели создать пример, который был бы масштабируемым в более сложных сценариях.

Мы с радостью послушаем, что вы думаете об этом проекте. Дайте нам знать!


Крейг Брокшмидт (Kraig Brockschmidt) — старший редактор документации разработок в Microsoft по кросс-платформенным мобильным приложениям. Автор книги «Programming Windows Store Apps with HTML, CSS and JavaScript» (два издания) от Microsoft Press, ведет блог на сайте kraigbrockschmidt.com.

Майк Уоссон (Mike Wasson) — разработчик контента в Microsoft. В течение многих лет писал документацию по мультимедийным Win32 API. В настоящее время пишет о Microsoft Azure и ASP.NET.

Рик Андерсон  (Rick Anderson) — старший редактор документации разработок в Microsoft по ASP.NET MVC, Microsoft Azure и Entity Framework. Следите за его заметками в twitter.com/RickAndMSFT.

Эрик Райтан (Erik Reitan) — старший разработчик контента в Microsoft, уделяет основное внимание Microsoft Azure и ASP.NET. Следите за его заметками в twitter.com/ReitanErik.

Том Дейкстра (Tom Dykstra) — старший разработчик контента в Microsoft, уделяет основное внимание Microsoft Azure и ASP.NET.

Выражаем благодарность за рецензирование статьи экспертам Микелю Колье (Michael Collier), Брейди Гастеру (Brady Gaster), Джону де Хэвилленду (John de Havilland), Райену Джонсу (Ryan Jones), Виджею Рамакришнану (Vijay Ramakrishnan) и Пранаву Растоги (Pranav Rastogi).