Портирование приложения Windows 8.1 на Windows Phone 8.0 с разбором проблем
На примере простого Windows 8.1 приложения посмотрим насколько просто переносить приложения с WinRT (Windows 8.1) на Silverlight (WP8.0) и по ходу разберем несколько подводных камней.
Вы наверняка слышали о продвигаемом способе разработки Windows / Windows Phone приложений — Universal Apps. Подход здравый, но рыночная доля WP8.1 пока еще только начинает расти, а приложение нужно делать сейчас, поэтому остановимся на WP8.0 (Silverlight). Преимущества: поддержка девайсов как на WP8.0 так и на WP8.1, поддержка всех типов экранов без «черных полос» (в отличие от WP7 приложений), стабильные сторонние библиотеки т.д.
Кратко об исходном приложенииПодопытным кроликом будет выступать почти готовое одностраничное приложение Windows 8.1 для отображения основных курсов валют.Поскольку приложение бесплатное и без рекламы, функционал жестко лимитирован. Только самые необходимые функции: отображение среднего курса валют (USD, EUR, RUB), график флуктуации за последние 30 дней, отображение курсов по банкам, конвертер валют и поддержка Live Tiles.Серверную часть оставим за кадром т.к. там нет ничего интересного.
Структура проекта С прицелом на Universal Apps основные блоки вынесены в юзер контролы, классы данных вынесены в Portable Library, а код отвечающий за получение данных отделен от UI.Переносим код на Windows Phone 8.0 Создаем пустой проект, выкидываем все лишнее из MainPage, добавляем нужные референсы и NuGet пакеты. Первым делом добавляем как ссылки (Add → Existing Item → Add As Link) файлы классов для обработки данных из Windows 8 проекта.Проблемы: ◊ Ожидать что неймспейсы сойдутся не приходится (WinRT vs Silverlight), поэтому используем теплый ламповый #if #endif, получается что то типа такого: #if NETFX_CORE using Windows.Web.Http; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Networking.BackgroundTransfer; #endif #if WINDOWS_PHONE using System.Net.Http; #endif Должен отметить, что хоть мы и не используем Universal Apps, Visual Studio 2013 Update 2 сильно упрощает работу с #if #endif для разных платформ. Появился еще один выпадающий список прямо над кодом, позволяющий быстро переключить платформу без переоткрытия файла. IntelliSense больше не падает, Resharper не отваливается в самый не подходящий момент. Никаких больше «This document is opened by another project.» и т.п.◊ Поскольку данные мы загружаем с помощью HttpClient (позже будет понятно, почему именно HttpClient), который отсутствует в WP8.0, добавляем NuGet пакет Microsoft.Net.Http. Но и тут не без сюрпризов, без #if не обошлось:
var client = new HttpClient (); byte[] buff; #if NETFX_CORE var ibuff = await client.GetBufferAsync (uri); buff = ibuff.ToArray (); #endif #if WINDOWS_PHONE buff = await client.GetByteArrayAsync (uri); #endif Вопрос на засыпку, сколько официальных реализаций HttpClient вы знаете? ◊ Полученные данные мы сохраняем в памяти устройства, что бы было что показать следующий раз при отсутствии интернет подключения, но… ну вы поняли:
#if NETFX_CORE var file = await ApplicationData.Current.LocalFolder.CreateFileAsync (LOCAL_DATA_FILENAME, CreationCollisionOption.ReplaceExisting); await FileIO.WriteTextAsync (file, json); #endif #if WINDOWS_PHONE var fs = await ApplicationData.Current.LocalFolder.OpenStreamForWriteAsync (LOCAL_DATA_FILENAME, CreationCollisionOption.ReplaceExisting);
using (StreamWriter streamWriter = new StreamWriter (fs)) { await streamWriter.WriteAsync (json); } #endif Тут стоит помнить о разнице между LocalFolder и LocalCache (начиная с WP8.1), основное отличие в том, что LocalFolder в отличие от LocalCache подвержен встроенному beckup/restore (не путать с roaming) и как следствие ваши данные могут оказаться на совсем другом устройстве (или нескольких) со всеми вытекающими. Подробней как это работает можно посмотреть тут. В нашем случае LocalCache на WP8.0 не доступен, поэтому используем что имеем.◊ Отдельно стоит упомянуть поддержку шифрования данных, исторически сложилось что у меня уже был самописный кроссплатформенный бинарно совместимый класс шифрования используя AES, который кочует из проекта в проект. Описание этого класса выходит за рамки статьи, скажу только что под WP8.0 существует замечательный класс AesManaged, а под WinRT использую мапперы на нативную реализацию:
#if NETFX_CORE using Windows.Security.Cryptography; using Windows.Security.Cryptography.Core; using Windows.Storage.Streams; #else using System.Security.Cryptography; #endif Переносим UI на Windows Phone 8.0 После того как вспомогательный код перенесен и успешно компилируется, беремся за интерфейс. Т.к. телефон, планшет или десктоп это совсем не одно и то же. Поэтому и приложение будет выглядеть по-разному. Для этого Windows 8 и Windows Phone проекты будут иметь свои собственные MainPage, а юзер контролы мы будем добавлять как ссылки (Add As Link, в будущем все это просто переедет в Shared project). Плюс сами контролы будут адаптироваться под ситуацию, например ширину доступного пространства (не забывает про snapped mode в Windows 8).
Проблемы: ◊ Первое что нужно сделать, это разделить неймспейсы в code behind классах с помощью все тех же #if #endif, пример приводить не буду, просто пользуемся Shift+Alt+F10 на всем что подчеркнуто красным.◊ Так сложилось что в WP8.0 нет события DataContextChanged, поэтому немного меняем логику, что бы от него избавится.
◊ Поскольку в WP8.0 нет встроенных стилей из WinRTшного XAMLа, таких как «SymbolThemeFontFamily», «BaseTextBlockStyle», «BodyTextBlockStyle» и др. создаем отдельный ResourceDictionary в WP8.0 проекте. Открываем «c:\Program Files (x86)\Windows Kits\8.1\Include\winrt\xaml\design\generic.xaml» и выдираем все что нам нужно и кладем в только что созданный ResourceDictionary, попутно заменяя или выкидывая то что не поддерживается на телефоне (CharacterEllipsis → WordEllipsis, SemiLight → Light, Typography.* → c:\NUL).Потом мержим этот справочник в App.xaml:
public object Convert (object value, Type targetType, object parameter #if NETFX_CORE , string language #endif #if WINDOWS_PHONE , System.Globalization.CultureInfo culture #endif ) ◊ Следующее с чем пришлось столкнуться, это разница объявления сторонних неймспейсов в XAML. В WinRT неймспейсы описываются через «using:», а в Silverlight через «clr-namespace:», пример:
xmlns: Common=«using: BXFinanceDashboard.Common»
xmlns: Common=«clr-namespace: BXFinanceDashboard.Common»
Решений этой проблемы не так много, и ни одного хорошего, #ifdef для XAML нет. В таких случаях, исходя из конкретной ситуации, можно создавать необходимые контролы в коде, заморачиваться с кстомными build action, выносить все что можно в стили и/или разделять интерфейс на общие и платформо-зависимые файлы. На крайний случай copy-paste.Но в нашем случае не все так сложно, т.к. в большинстве случаев объявление неймспейса было необходимо что бы подключить конверторы. Поэтому я просто создал по одному ResourceDictionary в каждом проекте и объявил конверторы там. И поскольку приложение не большое, подключил его глобально в App.xaml. Плюс в том, что конверторы не создаются повторно, но нужно помнить, что имена теперь глобальные и не могут повторяться.На всякий случай приведу листинг, WinRT (Windows 8.1):
Кстати не забывайте использовать именно глифы (особенно в кнопках на апп баре), а не свои картинки. Задолбаетесь генерить десяток картинок под разные DPI, для примера скрин другого приложения с Nokia 1520:
Адаптируем приложение под телефон Приложение собралось, хорошо, но останавливаться не стоит. Все-таки мобильный это не то же самое что планшет или десктоп (кстати приложение на десктопе само обновляется если висит на экране, удобно для всяких информационных панелей).Layout Для переключения графика курсов, списка банков и конвертера валют берем более привычный на Windows Phone контрол Pivot. Включаем трей (прятать трей без веской причины плохо), подгоняем верхний отступ, что бы трей не съедал много места. Благо в WP8 с треем меньше проблем чем на WP7, прыгает реже, но не забываем что в коде цвет трея можно указывать не раньше Page.Loaded. Что б избежать некрасивой полоски между треем и страницей ствим прозрачность 0.99.Поскольку экран телефона уже по ширине, необходимо что бы все основные контролы адаптировались соответственно (так же это полезно для snapped mode на десктопе).
Список банков автоматически отображает только одну валюту если места недостаточно, а внизу появляется переключатель. Причем важным требованием здесь было, что бы прокрутка списка оставалась на месте при переключении валют. Т.е. что бы не приходилось заново листать до необходимого банка.
Конвертер валют, в свою очередь, заворачивает некоторые элементы и выравнивается по ширине:
Добиться такого поведения можно несколькими способами, например, используя состояния (states), но в этот раз я просто прибиндил свойства HorizontalAlignment к самому контролу, главное разбросать элементы гридами, а не StackPanel. Пример: