Шел 2013 год. За доллар давали 30 рублей, а я устроился в компанию 2ГИС разрабатывать под Windows Phone. Мне удалось поучаствовать в запуске почти готового к тому времени приложения 2ГИС, которое в скором времени стало доступно нашим пользователям в Marketplace.
Была у этого приложения одна досадная особенность: оно работало на нашем WebAPI, и, соответственно, требовало подключения к Интернету. Поэтому почти сразу возникла необходимость научить 2ГИС под WP работать офлайн. А заодно решить другие насущные проблемы.
Причины всё переписать
Старое приложение, несмотря на то что его опубликовали совсем недавно, не очень хорошо справлялось с требованиями пользователей и бизнеса. Требования выглядели примерно так.
Быстрая доставка данных
Предположим, 2ГИС узнал какую-нибудь новую информацию о вашем городе. Для того чтобы поделиться этой информацией с пользователями, мы проделывали такие вот действия.
- Учили приложение отображать новые данные.
- Все тестировали.
- Публиковали новую версию приложения в сторе.
- Выпускали обновление данных для города.
Только после этого вы скачивали новую версию приложения, и узнавали, наконец, сколько стоит расчесать бороду в ближайшем барбершопе. Это я еще не говорю о проблемах версионирования данных и приложения, совместимости и всем таком. Быстрая доставка данных означает, что мы хотели бы вообще исключить из этого процесса пункты 1—3, и сделать тем самым обладателей бороды чуть более счастливыми.
Быстрое появление новых фич
Допустим, мы придумали новый алгоритм поиска, который ищет лучше и быстрее. Хотелось, чтобы этот алгоритм появился в продукте без существенных трудозатрат со стороны команды Windows Phone.
Офлайн
Как я уже говорил, для прежней версии приложения нужен был Интернет.
Некоторых наших пользователей это слегка расстраивало, и поэтому новый 2ГИС для WP обязательно должен был работать офлайн.
Архитектура нового 2ГИС под Windows Phone
Ну вот, наша немногочисленная, но дружная команда уяснила основные требования, зарядилась с утра пораньше позитивом от свежих отзывов в сторе и начала разрабатывать. Первым делом, конечно же, архитектуру.
Стоит сказать, что в 2ГИС мобильные приложения делают довольно давно, и для других платформ их сменилось уже несколько версий. Когда мы разрабатывали архитектуру, учли накопленный компанией опыт и требования, озвученные выше. Вот что получилось.
Кроссплатформенное ядро
Верхний блок на картинке — это кроссплатформенное ядро, основа всего, точка входа во все наши алгоритмы и сервисы. Наш офлайновый бэкенд, которому мы задаем вопросы и получаем ответы. Эта штука обеспечивает нам работу без подключения к сети. Мы называем его кроссплатформенным, потому что можем собирать его не только под WP, но и под Windows, OS X, Linux, iOS и Android.
В соответствии с требованием быстро добавлять новые фичи, все новое, что появляется в 2ГИС, появляется в ядре и автоматически попадает в новый 2ГИС для WP (как и во все продукты, которые это ядро используют). Кроссплатформенное ядро разрабатывается на C++ очень умными ребятами из специальной команды по разработке кроссплатформенного ядра. Они молодцы, но больше я о них ничего писать не буду, потому что статья не про них, а про про Windows Phone.
UI
Самый нижний блок на диаграмме — это UI, фронтэнд, та часть, с которой работает пользователь. Она разрабатывается с помощью нативных для платформы инструментов (C# и XAML), чтобы максимально легко и качественно обеспечить пользователю привычный опыт взаимодействия с его любимой платформой. На момент написания этой статьи разработчикам было доступно несколько типов приложений под WP: Silverlight Application, Windows Phone (не Silverlight!) Application, Universal Application. Но когда мы только начинали работать над новым 2ГИС, был только Silverlight, поэтому неудивительно, что мы выбрали его.
Промежуточный слой
Для того чтобы подружить C++ и C#, научить ядро общаться с приложением, а приложение с ядром, необходим некий промежуточный слой в виде Windows Runtime Component, написанный на C++/CX.
Трехмерная карта на OpenGL
Продолжая разговор об офлайне, нельзя не сказать о нашей карте, которая тоже работает без подключения к интернету. Карта в 2ГИС — это довольно узнаваемый элемент. Она трехмерная, кроссплатформенная (тоже собирается под разные ОС) и написана на OpenGL.
К сожалению, OpenGL не поддерживается на WP, поэтому нам приходится использовать Angle для трансляции OpenGL вызовов в DirectX. Хочу отметить, что с интеграцией карты мы натерпелись много разного, но в итоге удалось-таки ее запустить на WP. Конечно, использование Angle накладывает свой отпечаток на производительность, но мы стараемся свести это влияние к минимуму.
Инструменты
Как в любом другом приложении, написанном на C#/XAML, все внутреннее устройство фронтэнда 2ГИС для WP подчиняется паттерну MVVM.
У любого программиста, использующего MVVM, рано или поздно возникает потребность
- Открываем любимый браузер. Например, Internet Exporer.
- Заходим на сайт caliburnmicro.com.
- Качаем Caliburn.Micro, устанавливаем и радуемся.
Если серьезно, то в 2013 году популярных MVVM фрэймворков, работающих на WP8 было не очень много. Фактически выбор стоял между Prism, MVVM Light и Caliburn.Micro. Prism слишком монструозный, подходящий больше для больших энтерпрайз приложений, MVVM Light, напротив, слишком уж light, и хотелось чего-то большего. А вот Caliburn.Micro нам пришелся по душе по следующим причинам.
Поддержка навигации
Обычно навигация между страницами в silverlight приложении выглядит примерно так:
NavigationService.Navigate(new Uri("/GroupPage.xaml?name=Administrators", UriKind.Relative));
Это не очень красиво, легко ошибиться при вводе строк, т.к. отсутствует типизация. Сам параметр name необходимо доставать внутри события onNavigated страницы в таком виде.
var name = NavigationContext.QueryString["name"];
Caliburn.Micro позволяет ту же самую задачу решить следующим образом.
NavigationService.UriFor<GroupPageViewModel>()
.WithParam(x => x.Name, "Administrators")
.Navigate();
Код выглядит красиво, присутствует контроль типов, а параметр Name сразу записывается в соответствующее свойство ViewModel при навигации.
Поддержка сохранения состояния
Caliburm.Micro предлагает интересную инфраструктуру для сохранения состояния приложения. Например, для того чтобы определить стратегию сохранения состояния для GroupPageViewModel, достаточно определить вот такой класс.
public class GroupPageViewModelStorage : StorageHandler<GroupPageViewModel>
{
public override void Configure()
{
Property(x => x.Name)
.InPhoneState()
.RestoreAfterActivation();
}
}
Код выше означает, что если система внезапно выгрузит ваше приложение, пока оно неактивно, то свойство Name у GroupPageViewModel переживет эту неприятность и будет сохранено, а затем восстановлено при повторной активации приложения.
Встроенная поддержка IoC контейнера
Сердцем Caliburn.Micro является встроенный IoC контейнер, с помощью которого реализуется паттерн dependency injection. При навигации между страницами ViewModel’и получают все необходимые сервисы с помощью constructor/property injection, и это чрезвычайно удобно. Всячески рекомендую.
Для WP — поддержка Pivot
Caliburn.Micro предоставляет инфраструктуру, при которой каждая страница контрола Pivot может быть отдельной View со своей отдельной ViewModel. Это очень удобно, позволяет декомпозировать логику и относительно просто наладить ленивую загрузку данных для вкладок Pivot.
Специальные методы во вьюмоделях, привязанные к жизненному циклу страницы
Класс Screen — базовый класс для большинства ваших ViewModel’ей имеет очень удобные методы OnInitialize, OnActivate, OnDeactivate и т.п., которые вызываются фрэймворком в момент создания экземпляра ViewModel’и при навигации на соответствующую страницу или при уходе с нее. Вы можете переопределять эти методы в своих ViewModel’ях и выполнять там какой-нибудь полезный код.
Открытый исходный код
Caliburn.Micro имеет открытый исходный код. Если вам чего-то не хватает во фрэймворке, всегда можно это дописать самому.
Низкий порог вхождения
Начать пользоваться Caliburn.Micro легко. Кроме того, он довольно легковесный, весь код Caliburn.Micro вполне реально изучить за день-два.
Сохранение состояния приложения
Стоит сказать, что мы несколько доработали механизм сохранения состояния приложения, используемый в Caliburn.Micro. По умолчанию Caliburn использует стандартные механизмы XML-сериализации, используемые в WP. Мы добавили поддержку бинарной сериализации с помощью SharpSerializer. Получилось удобно, быстро, и можно сериализовать практически все что угодно.
Быстрая доставка данных
Итак, кроссплатформенное ядро обеспечивает нам быстрое появление новых фич и работу в офлайне, а что с быстрой доставкой данных? Как новые данные о городе могут появиться в приложении, если приложение ничего об этих данных не знает?
Ответ в том, что вместе с новыми данными мы должны поставить и новый UI для их отображения. В нашем случае это XAML-шаблоны. Обычно XAML-шаблоны живут внутри самого приложения и поставляются вместе с ним, но мы хотим распространять XAML-ресурсы отдельно и совершенно независимо от самого приложения.
Я не уверен, что кто-то еще так делает, но, по большому счету, здесь нет ничего фантастического. Мы какое-то время экспериментировали с различными вариантами и остановились на очень простой, на мой взгляд, схеме, о которой я постараюсь вам сейчас очень упрощенно рассказать.
Предположим, что отдельно от приложения мы распространяем вот такие данные, которые хотим отобразить.
{
"data": "Windows Phone"
}
Тут ничего особенного — это просто json.
Также мы распространяем (тоже отдельно от приложения) XAML-шаблон, необходимый для отображения этих данных.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<DataTemplate x:Key="TestIcon">
<Path Width="42"
Height="41"
Stretch="Fill"
Fill="{StaticResource PhoneForegroundBrush}"
Data="F1 M 17,23L 34,20.7738L 34,37L 17,37L 17,23 Z M 34,55.2262L 17,53L 17,39L 34,39L 34,55.2262 Z M 59,17.5L 59,37L 36,37L 36,20.5119L 59,17.5 Z M 59,58.5L 36,55.4881L 36,39L 59,39L 59,58.5 Z " />
</DataTemplate>
<DataTemplate x:Key="EntryPoint">
<StackPanel>
<ContentPresenter ContentTemplate="{StaticResource TestIcon}"
Margin="0,0,0,12" />
<TextBlock Text="{Binding [data]}" />
</StackPanel>
</DataTemplate>
</ResourceDictionary>
Здесь стоит отметить несколько моментов.
- Этот шаблон на стороне приложения мы будем загружать в виде строки и парсить с помощью XamlReader.Load(). Из этого естественным образом вытекает то, что не любой XAML можно так использовать. Правильный XAML не должен содержать никакого code behind, никаких ссылок на x:Class, подписок на события и т.п.
- Под предыдущее требование отлично подходят DataTemplates, поэтому мы будем использовать их.
- Мы используем ResourceDictionary как контейнер для нескольких шаблонов.
- Обратите внимание, как мы распространяем иконку TestIcon, — тоже в виде DataTemplate. А отображаем ее с помощью ContentPresenter.
- Свойство Text текстового блока привязано к некоторому свойству data посредством привязки к индексатору некоторой ViewModel’и (о которой после). Обратите внимание, что в нашем json’е есть элемент с точно таким же именем, и это неспроста.
Допустим, уже в самом приложении у нас есть вот такая View.
<phone:PhoneApplicationPage x:Class="DynamicXaml.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DynamicXaml"
mc:Ignorable="d">
<phone:PhoneApplicationPage.DataContext>
<local:MainPageViewModel />
</phone:PhoneApplicationPage.DataContext>
<ContentControl Content="{Binding DynamicData}"
ContentTemplate="{StaticResource EntryPoint}" />
</phone:PhoneApplicationPage>
В этой View элемент ContentControl является точкой входа для отображения динамических данных. Здесь есть важное соглашение: ContentControl знает, что должен отобразить шаблон с ключом EntryPoint, и именно такой шаблон есть в нашем XAML’е, который мы распространяем отдельно. Собственно, имя ключа — это единственное, что приложение знает о шаблоне, который будет показывать.
Соответственно, для View определена ViewModel, которая реализует некоторую тестовую магию по загрузке динамического содержимого.
public class MainPageViewModel
{
public MainPageViewModel()
{
// Загружаем с секретных серверов 2ГИС данные о городе.
string json = LoadDynamicData();
DynamicData = (JObject)JsonConvert.DeserializeObject(json);
// Загружаем шаблон для отображения данных.
string xaml = LoadDynamicXaml();
// Парсим xaml.
var resources = (ResourceDictionary)XamlReader.Load(xaml);
// Добавляем шаблоны в ресурсы приложения.
foreach (DictionaryEntry entry in resources)
{
Application.Current.Resources.Add(entry.Key, entry.Value);
}
}
public JObject DynamicData { get; private set; }
}
Поясню несколько моментов.
- В примере я использовал Json.Net для того, чтобы распарсить json и получить из него некоторый объект, пригодный для привязки данных. На самом деле для этих целей мы используем DynamicDataContext, но в данном примере для простоты используется JObject.
- У JObject есть индексатор, принимающий строку — имя json-элемента, и возвращающий значение этого элемента.
- Именно поэтому в шаблоне текст текстового блока привязывается к индексатору [data], который возвращает значение элемента data из json. Так обычный json становится вьюмоделью для нашего шаблона.
Если собрать этот пример и запустить его на своем любимом телефоне под управлением Windows Phone, можно увидеть такую картинку.
Вот так легко и непринужденно мы только что отобразили в приложении данные, о которых приложение вообще ничего не знает. И как отображать эти данные приложение тоже не знает — вся информация загружается с серверов 2ГИС.
Итоги
Мы сделали настоящий 2ГИС для WP: у нас есть трехмерная карта, подробный справочник организаций, кроссплатформенное ядро, а xaml-шаблоны мы распространяем независимо от приложения. И все это работает без подключения к Интернету. Если вы по каким-то причинам все еще не установили новый 2ГИС на ваши смартфоны с Windows Phone, самое время это сделать.
Комментарии (41)
Shersh
02.06.2015 12:44+1Надеюсь реальный код далеко от того, что дано в примере :)
Синхронный блокирующий UI вызов в ctor'е, добавление в dictionary ресурсов без проверки на существование ключа, парсинг XAML без try..catch.
Viacheslav01
02.06.2015 12:46Доставка новых возможностей без публикации приложения удивила еще на девконе.
На самом деле все самое интересное реально за бортом!
На самом деле для этих целей мы используем DynamicDataContext
прощай скорость…volokhin Автор
02.06.2015 12:57Но почему? Там же обычный биндинг на индексатор.
Viacheslav01
02.06.2015 14:07Видимо дело в предвзятом отношении к Dynamic, когда все это счастье появилось я поигрался, потестировал и зарекся использовать без ну очень сильной и невыносимой нужды.
QtRoS
02.06.2015 13:21Кстати, на Qt/QML довольно легко может быть решена задача динамического обновления возможностей ПО без переустановки, надо попробовать применить где-нибудь…
Viacheslav01
02.06.2015 14:06Какими средствами это решается?
QtRoS
02.06.2015 14:34Компоненты UI в Qt Quick можно создавать из файлов (и даже из текстовой строки) в рантайме, и это довольно просто и удобно несмотря на то, что первоначально язык QML предлагает декларативный подход к описанию UI.
XogN
02.06.2015 13:18+1Прочитал и стало грустно.
Именно отсутствие возможности работать оффлайн в 2ГИС для Windows Phone стало для меня одной из причин отказаться от этой платформы.
Iron_Butterfly
02.06.2015 13:26-3Под WP пишете, а версию для BlackBerry OS10 совсем забросили. Сто лет уже не обновляли и оптимизацию для Passport так и не сделали, несмотря на огромное количество реквестов. Отвратительный custom service!
DobroeZlo
02.06.2015 13:34+1Про монстроузность Призма это вы зря. Конечно начинать с него не стоит (для этого есть mvvm.light), но после нескольких проектов Призм становится как брат родной и уже знаешь прекрасно что тебе надо, а что не надо.
dordzhiev
02.06.2015 13:36Но когда мы только начинали работать над новым 2ГИС, был только Silverlight, поэтому неудивительно, что мы выбрали его.
Но судя по переходам страниц у вас WinRT XAML. Да и скачав appx со Store, я открыл первый попавшийся xbf файл и увидел «Windows.UI.Xaml.Controls».volokhin Автор
02.06.2015 13:58Вы абсолютно правы, сейчас у нас WinRT XAML. Начинали мы в 2013 году с Silverlight, но примерно осенью 2014 перешли на Universal Application. Но это уже совсем другая история.
Viacheslav01
02.06.2015 14:09Герои я уже два месяца времени закопал в перенос, конец только показался на горизонте.
volokhin Автор
02.06.2015 14:14Крепитесь, впереди переход на Windows 10 :)
Viacheslav01
02.06.2015 14:46И если они сделают нормальную поддержку BLE Advertising мне будет не отвертеться от переноса :)
Но мне кажется переход будет менее болезненным, так как SL -> WRT это смена платформы со всеми вытекающими.
А WRT 8.1 -> WRT 10 вроде как новая версия, ну я очень надеюсь на это :)
Если не подводит память вы в перенос 3 месяца закопали, скоро догоню :)
suratovvlad
02.06.2015 20:18то есть скоро можно ждать полноценного универсального приложения для планшета на полной винде + переход на win10? :)
p.s. спасибо за интересную статью!
dobriykot
02.06.2015 16:51+1Все это очень хорошо, но почему 2гис так долго загружается на WP? Если вдруг карту надо посмотреть очень срочно (автобус подъезжает), это невозможно сделать. Телефон Lumia 640 XL.
Dywar
02.06.2015 20:20У меня 640 3G DS, загружается нормально, как и другие приложения. Это я говорю, потому что практически ничего другого на телефоне не использую, только свои Unity3D приложения :D
Доволен тем что получилось, очень рад что 2ГИС стал дружелюбней к WP. Жду Windows 10 и вас на нем.
gaploid
02.06.2015 21:18-1Под Universal App будете переписывать и делать версию для планшета?
volokhin Автор
03.06.2015 06:21Технически 2GIS для WP уже сейчас Universal Application. Но UI под планшеты еще только предстоит сделать. И здесь, как мне кажется, стоит дождаться Windows 10 для смартфонов, там есть интересные фичи про адаптивный layout.
Viacheslav01
03.06.2015 13:10Мне почему-то кажется, что что все равно два лэйаута независимых, как бы все радужно не преподносилось :(
ad1Dima
03.06.2015 14:34А на планшете все равно придется поддерживать приложение в маленьком окне. Так что…
suratovvlad
03.06.2015 16:27так весь сдк уже доступен же с эмуляторами и проч. проч. проч.
volokhin Автор
04.06.2015 06:59Доступен… Мы бы конечно хотели заняться переходом на Windows 10 вот прямо сейчас, но это просто вопрос приоритетов при ограниченных ресурсах: вместо того, чтобы сейчас заниматься десяткой, разумнее потратить время на фичи для пользователей WP8.1, которых пока что больше, чем пользователей Windows 10 для смартфонов.
ad1Dima
Вероятно это ещё одно место просадки производительности, так как при обычной компиляции, это все превращается в промежуточный BAML
Viacheslav01
Сдается мне что в Silverlight не превращается.
volokhin Автор
На silverlight xaml не компилировался совсем, сейчас для uni app действительно компилируется в .xbf. Влияет на время запуска приложения — при старте приходится парсить файлы. Недавно замеряли: на NL720 выходит около 2 сек. Скоро сделаем отложенную загрузку ресурсов и сэкономим 2 секунды на старте.