картинка для привлечения внимания

Шел 2013 год. За доллар давали 30 рублей, а я устроился в компанию 2ГИС разрабатывать под Windows Phone. Мне удалось поучаствовать в запуске почти готового к тому времени приложения 2ГИС, которое в скором времени стало доступно нашим пользователям в Marketplace.

Была у этого приложения одна досадная особенность: оно работало на нашем WebAPI, и, соответственно, требовало подключения к Интернету. Поэтому почти сразу возникла необходимость научить 2ГИС под WP работать офлайн. А заодно решить другие насущные проблемы.

Причины всё переписать


Старое приложение, несмотря на то что его опубликовали совсем недавно, не очень хорошо справлялось с требованиями пользователей и бизнеса. Требования выглядели примерно так.

Быстрая доставка данных


Предположим, 2ГИС узнал какую-нибудь новую информацию о вашем городе. Для того чтобы поделиться этой информацией с пользователями, мы проделывали такие вот действия.

  1. Учили приложение отображать новые данные.
  2. Все тестировали.
  3. Публиковали новую версию приложения в сторе.
  4. Выпускали обновление данных для города.

Только после этого вы скачивали новую версию приложения, и узнавали, наконец, сколько стоит расчесать бороду в ближайшем барбершопе. Это я еще не говорю о проблемах версионирования данных и приложения, совместимости и всем таком. Быстрая доставка данных означает, что мы хотели бы вообще исключить из этого процесса пункты 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, рано или поздно возникает потребность написать свой начать использовать какой-нибудь MVVM фрэймворк. Представляю вам очень краткую инструкцию по выбору MVVM фрэймворка, не претендующую на истину в последней инстанции.

  1. Открываем любимый браузер. Например, Internet Exporer.
  2. Заходим на сайт caliburnmicro.com.
  3. Качаем 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)


  1. ad1Dima
    02.06.2015 12:36
    +2

    загружать в виде строки и парсить с помощью XamlReader.Load()

    Вероятно это ещё одно место просадки производительности, так как при обычной компиляции, это все превращается в промежуточный BAML


    1. Viacheslav01
      02.06.2015 12:52

      Сдается мне что в Silverlight не превращается.


    1. volokhin Автор
      02.06.2015 12:52

      На silverlight xaml не компилировался совсем, сейчас для uni app действительно компилируется в .xbf. Влияет на время запуска приложения — при старте приходится парсить файлы. Недавно замеряли: на NL720 выходит около 2 сек. Скоро сделаем отложенную загрузку ресурсов и сэкономим 2 секунды на старте.


  1. QtRoS
    02.06.2015 12:37
    +3

    А можно попозже про ядро и «ядреных» парней все же написать?


  1. Shersh
    02.06.2015 12:44
    +1

    Надеюсь реальный код далеко от того, что дано в примере :)
    Синхронный блокирующий UI вызов в ctor'е, добавление в dictionary ресурсов без проверки на существование ключа, парсинг XAML без try..catch.


    1. volokhin Автор
      02.06.2015 12:46
      +1

      Конечно, код очень сильно упрощен, чтобы не отвлекать от сути.


  1. Viacheslav01
    02.06.2015 12:46

    Доставка новых возможностей без публикации приложения удивила еще на девконе.
    На самом деле все самое интересное реально за бортом!

    На самом деле для этих целей мы используем DynamicDataContext
    прощай скорость…


    1. volokhin Автор
      02.06.2015 12:57

      Но почему? Там же обычный биндинг на индексатор.


      1. Viacheslav01
        02.06.2015 14:07

        Видимо дело в предвзятом отношении к Dynamic, когда все это счастье появилось я поигрался, потестировал и зарекся использовать без ну очень сильной и невыносимой нужды.


    1. QtRoS
      02.06.2015 13:21

      Кстати, на Qt/QML довольно легко может быть решена задача динамического обновления возможностей ПО без переустановки, надо попробовать применить где-нибудь…


      1. Viacheslav01
        02.06.2015 14:06

        Какими средствами это решается?


        1. zsilas
          02.06.2015 14:22
          +2

          Как правило, с помощью средств динамической загрузки элементов (Loader QML)


          1. Viacheslav01
            02.06.2015 14:43

            Глянул быстрым взглядом, похоже на то, что автор описал :)


        1. QtRoS
          02.06.2015 14:34

          Компоненты UI в Qt Quick можно создавать из файлов (и даже из текстовой строки) в рантайме, и это довольно просто и удобно несмотря на то, что первоначально язык QML предлагает декларативный подход к описанию UI.


      1. zsilas
        02.06.2015 14:18
        +1

        Кстати 2GIS Android пошел именно этим путем


  1. XogN
    02.06.2015 13:18
    +1

    Прочитал и стало грустно.
    Именно отсутствие возможности работать оффлайн в 2ГИС для Windows Phone стало для меня одной из причин отказаться от этой платформы.


    1. Woodroof
      02.06.2015 15:06

      Хотите вернуться? ;)


      1. ad1Dima
        02.06.2015 15:11

        Лучше сначала 10ку дождаться…


      1. XogN
        02.06.2015 15:44

        Увы, уже давно купил себе другой аппарат :)


  1. Iron_Butterfly
    02.06.2015 13:26
    -3

    Под WP пишете, а версию для BlackBerry OS10 совсем забросили. Сто лет уже не обновляли и оптимизацию для Passport так и не сделали, несмотря на огромное количество реквестов. Отвратительный custom service!


  1. DobroeZlo
    02.06.2015 13:34
    +1

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


  1. dordzhiev
    02.06.2015 13:36

    Но когда мы только начинали работать над новым 2ГИС, был только Silverlight, поэтому неудивительно, что мы выбрали его.

    Но судя по переходам страниц у вас WinRT XAML. Да и скачав appx со Store, я открыл первый попавшийся xbf файл и увидел «Windows.UI.Xaml.Controls».


    1. volokhin Автор
      02.06.2015 13:58

      Вы абсолютно правы, сейчас у нас WinRT XAML. Начинали мы в 2013 году с Silverlight, но примерно осенью 2014 перешли на Universal Application. Но это уже совсем другая история.


      1. Viacheslav01
        02.06.2015 14:09

        Герои я уже два месяца времени закопал в перенос, конец только показался на горизонте.


        1. volokhin Автор
          02.06.2015 14:14

          Крепитесь, впереди переход на Windows 10 :)


          1. Viacheslav01
            02.06.2015 14:46

            И если они сделают нормальную поддержку BLE Advertising мне будет не отвертеться от переноса :)

            Но мне кажется переход будет менее болезненным, так как SL -> WRT это смена платформы со всеми вытекающими.
            А WRT 8.1 -> WRT 10 вроде как новая версия, ну я очень надеюсь на это :)

            Если не подводит память вы в перенос 3 месяца закопали, скоро догоню :)


          1. ad1Dima
            02.06.2015 14:54
            -1

            надеюсь, хоть тогда вы поддержите планшеты.


      1. suratovvlad
        02.06.2015 20:18

        то есть скоро можно ждать полноценного универсального приложения для планшета на полной винде + переход на win10? :)

        p.s. спасибо за интересную статью!


        1. volokhin Автор
          03.06.2015 06:14

          Все это будет, но сроки пока что не ясны. Для начала нужно дождаться Windows 10 для смартфонов.


          1. ad1Dima
            03.06.2015 11:42

            Она осенью будет, а win 10 уже совсем скоро


  1. dobriykot
    02.06.2015 16:51
    +1

    Все это очень хорошо, но почему 2гис так долго загружается на WP? Если вдруг карту надо посмотреть очень срочно (автобус подъезжает), это невозможно сделать. Телефон Lumia 640 XL.


    1. volokhin Автор
      03.06.2015 06:17
      +1

      Справедливое замечние. Над временем запуска будем еще работать.


  1. Dywar
    02.06.2015 20:20

    У меня 640 3G DS, загружается нормально, как и другие приложения. Это я говорю, потому что практически ничего другого на телефоне не использую, только свои Unity3D приложения :D
    Доволен тем что получилось, очень рад что 2ГИС стал дружелюбней к WP. Жду Windows 10 и вас на нем.


  1. gaploid
    02.06.2015 21:18
    -1

    Под Universal App будете переписывать и делать версию для планшета?


    1. volokhin Автор
      03.06.2015 06:21

      Технически 2GIS для WP уже сейчас Universal Application. Но UI под планшеты еще только предстоит сделать. И здесь, как мне кажется, стоит дождаться Windows 10 для смартфонов, там есть интересные фичи про адаптивный layout.


      1. Viacheslav01
        03.06.2015 13:10

        Мне почему-то кажется, что что все равно два лэйаута независимых, как бы все радужно не преподносилось :(


        1. ad1Dima
          03.06.2015 14:34

          А на планшете все равно придется поддерживать приложение в маленьком окне. Так что…


          1. Viacheslav01
            03.06.2015 15:59

            Да уж заинтриговать у них получилось :)


      1. suratovvlad
        03.06.2015 16:27

        так весь сдк уже доступен же с эмуляторами и проч. проч. проч.


        1. volokhin Автор
          04.06.2015 06:59

          Доступен… Мы бы конечно хотели заняться переходом на Windows 10 вот прямо сейчас, но это просто вопрос приоритетов при ограниченных ресурсах: вместо того, чтобы сейчас заниматься десяткой, разумнее потратить время на фичи для пользователей WP8.1, которых пока что больше, чем пользователей Windows 10 для смартфонов.


  1. flight
    03.06.2015 12:19

    Не смог протестировать, т.к. нет Киева, хотя есть Днепропетровск и Донецк.