Довольно часто приходится портировать существующее приложение на другие платформы или же изначально писать его сразу для нескольких платформ. В этой статье мы рассмотрим основы шаринга кода конкретно для трех платформ, но описанные принципы применимы для разработки кроссплатформенных приложений и на другие платформы, к примеру, с использованием Xamarin.
Так как материал был достаточно объемным, я решил разбить статью на две части. Сегодня рассмотрим основы шаринга для новичков, и если вы уже имеете базовые навыки, то смело можете пропустить эту статью и дождаться следующей, где мы на практике рассмотрим более сложные сценарии шаринга кода.
А сейчас я расскажу о двух основных способах шаринга кода: линковании файлов и библиотеке с общим кодом. Последовательно рассмотрим оба подхода, основные преимущества и недостатки каждого из них.
0. Копирование
Конечно же, нельзя начать с линкования, не сказав пару слов о еще одном популярном способе «шаринга» кода — копировании. Копирование и модификация кода под конкретную платформу самый простой способ «шаринга», и, скорее всего, местами мы частично будем использовать этот подход. Но рассматривать его как основной принцип шаринга не будем в связи с тем, что у него минусов больше чем плюсов. Плюсы подхода: быстро получаем работающий код для другой платформы и при этом наши изменения никак не могут повлиять на основную платформу. Минусы: сопровождение обходится дорого, так как приходится вносить однотипные изменения уже в каждую копию для каждой платформы.
1. Линкование
Самый простой, и, пожалуй, самый популярный способ написания кроссплатформенных приложений, это «линкование» файлов. Зачастую, когда у нас уже есть готовое приложение под WP7 и хочется получить такое же приложение под WP8 (или наоборот), есть большой соблазн просто переиспользовать существующий код и не вносить какие-либо изменения в существующую архитектуру приложения (если архитектура не была под это заточена).
Рассмотрим пошагово этот подход.
Для примера создадим очень простое WP8-приложение и на главной странице (MainPage.Xaml) добавим классический «калькулятор» — два текстовых поля для ввода чисел, кнопку сложения и поле для вывода результата.
<StackPanel>
<TextBlock Text="A" Style="{StaticResource PhoneTextLargeStyle}"></TextBlock>
<TextBox x:Name="TextBoxA" Text=""></TextBox>
<TextBlock Text="B" Style="{StaticResource PhoneTextLargeStyle}"></TextBlock>
<TextBox x:Name="TextBoxB" Text=""></TextBox>
<Button Content="Summ" Click="ButtonBase_OnClick"></Button>
<TextBlock x:Name="TextBlockResult" Text="" Style="{StaticResource PhoneTextLargeStyle}"></TextBlock>
</StackPanel>
И C# код
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
var valueA = Int32.Parse(TextBoxA.Text);
var valueB = Int32.Parse(TextBoxB.Text);
TextBlockResult.Text = (valueA + valueB).ToString();
}
Теперь нам надо добавить в Solultion новый проект для WP7 (File — Add — New project — Windows Phone App — Windows Phone 7).
После этого необходимо удалить MainPage.xaml из WP7 проекта и добавить в него ссылку на MainPage.xaml из WP8 проекта.
В контекстном меню на проекте WP7 (правой кнопкой мыши) выбираем Add – Existing Item.
Далее в папке с проектом для WP8 находим файл MainPage.xaml (только этот файл, .cs файл будет добавлен автоматически) и выбираем Add As Link.
В конечном итоге у вас должна получиться примерно такая картина в проекте:
По большому счету, у нас получился проект с общим кодом, и таким же образом мы можем слинковать, по необходимости, также картинки или app.xaml. Теперь мы можем вносить изменения в проект (к примеру, добавить валидацию ввода номера) и получить эти изменения сразу в обоих проектах.
Также рекомендую в свойствах указать одно и то же пространство имен для обоих проектов.
Рано или поздно мы столкнемся с особенностями каждой платформы. Эти особенности мы можем разделить с помощью символов компиляции с использованием директив #If / #else / #endif. Для примера допустим, что у нас для WP8- и Win8-проектов сохраняем данные используя ApplicationData, а для WP7 — IsolatedStorage.
Например, для WP8-проекта у нас есть следующий метод:
public async Task SaveText(string text)
{
var fileBytes = System.Text.Encoding.UTF8.GetBytes(text);
var local = ApplicationData.Current.LocalFolder;
var dataFolder = await local.CreateFolderAsync("DataFolder", CreationCollisionOption.OpenIfExists);
var file = await dataFolder.CreateFileAsync("DataFile.txt", CreationCollisionOption.ReplaceExisting);
using (var s = await file.OpenStreamForWriteAsync())
{
s.Write(fileBytes, 0, fileBytes.Length);
}
}
Соответственно, для WP7 может быть следующий код для сохранения строки:
public void SaveText(string text)
{
using (var local = IsolatedStorageFile.GetUserStoreForApplication())
{
using (var stream = local.CreateFile("DataFile.txt"))
{
using (var streamWriter = new StreamWriter(stream))
{
streamWriter.Write(text);
}
}
}
}
Как же нам максимально совместить наш код?
В первую очередь, необходимо унифицировать сигнатуру метода, т.е. или отказаться от async/await на WP8/Win8, или же поддержать async на WP7.
Конечно, от вкусностей мы отказываться не хотим, поэтому для начала установим Microsoft.Bcl на WP7 проект для поддержки async. Мы можем установить его как через менеджер пакетов Nuget (контекстное меню по нажатию на проект с WP7 — Manage NuGet Packages) или же через консоль Nuget следующей командой (открыть консоль в студии можно через Tools -Nuget Package Manager — Package Manager Consol):
Install-Package microsoft.Bcl
(Следует убедиться, что Default Project выбран проект для WP7).
Теперь у нас для сохранения текста под WP7 может быть следующий код:
public async Task SaveText(string text)
{
using (var local = IsolatedStorageFile.GetUserStoreForApplication())
{
using (var stream = local.CreateFile("DataFile.txt"))
{
using (var streamWriter = new StreamWriter(stream))
{
streamWriter.Write(text);
}
}
}
}
Получается одинаковый метод для WP7 и WP8-файлов. Вопрос в том, как совместить тело метода для проекта под WP7, WP8, Win8?
Это можно сделать с помощью символов компиляции. Добавить их в проект достаточно просто. В свойствах проекта во вкладке Build мы можем дописать необходимые символы компиляции (я, к примеру, предпочитаю символы WP7/WP8/WIN8).
Как видим, мы также можем разделить платформы Windows 8 и Windows Phone по уже прописанному по умолчанию символу WINDOWS_PHONE.
Теперь, когда у нас есть символы компиляции, мы можем разделить наш код для разных платформ следующим образом:
public async Task SaveText(string text)
{
#if WP7
using (var local = IsolatedStorageFile.GetUserStoreForApplication())
{
using (var stream = local.CreateFile("DataFile.txt"))
{
using (var streamWriter = new StreamWriter(stream))
{
streamWriter.Write(text);
}
}
}
#else
var fileBytes = System.Text.Encoding.UTF8.GetBytes(text);
var local = ApplicationData.Current.LocalFolder;
var file = await local.CreateFileAsync("DataFile.txt", CreationCollisionOption.ReplaceExisting);
using (var s = await file.OpenStreamForWriteAsync())
{
s.Write(fileBytes, 0, fileBytes.Length);
}
#endif
}
Обратите внимание, что в разных проектах выделение метода различается, так как серым подчеркивается код, который не будет компилироваться в этом проекте. В данном случае файл был открыт в WP7 проекте и если закрыть и открыть файл в проекте с WP8 то выделение будет инвертировано. Компилятор «увидит» код для WP8
2. Библиотека с общим кодом
2.1. Общая WP7-библиотека
В том случае, если у вас проекты только под WP7 и WP8, то можно выделить общий код в отдельную Windows Phone 7 библиотеку и использовать эту библиотеку для двух проектов. Для этого добавим новый проект (File — Add — New Project) Windows Phone Class Library, выберем платформу Windows Phone 7 и назовем в нашем примере с калькулятором, к примеру, CalculatorCore.
Для того, чтобы можно было добавить страницы в этот проект, необходимо вставить ссылку на библиотеку Microsoft.Phone:
Во вкладке Assemblies – Framework:
Следующим шагом необходимо добавить в проекты WP7 и WP8 ссылку на проект Calculator Core.
Для каждого из проектов в References аналогично добавляем ссылку на проект, которую можно найти во вкладке Solution:
Теперь мы можем перенести нашу страницу MainPage в эту общую библиотеку.
Если все сделали правильно, то структура вашего проекта будет выглядеть следующим образом:
Таким же образом можно перенести в общую библиотеку контент, локализацию, UserControl и т.д.
Для того что бы проект заработал остался последний шаг: чтобы проект мог найти эту страницу в другой сборке, необходимо добавить полную ссылку на нее в каждом проекте в файлы манифеста, (файл WMAppManifest.xml).
Т.е. вместо ссылки:
<DefaultTask Name ="_default" NavigationPage="MainPage.xaml"/>
Необходимо прописать полный путь:
<DefaultTask Name="_default" NavigationPage="CalculatorCore;component/MainPage.xaml"/>
Теперь, если мы запустим проект, то увидим нашу страницу. Таким образом, мы получили два разных проекта, где у нас есть один общий работающий код. В следующей статье разберем, как использовать особенности каждой платформы.
2.2. Библиотека Portable Library
Зачастую нам нужно «шарить» код между несколькими платформами WP7/WP8/Win8, у которых разные UI, но при этом есть общая логика.
В этом случае общая WP7-библиотека, конечно же, нам не подойдет. Вместо этого мы можем воспользоваться проектом типа Portable Library (этот тип проекта можно найти в шаблонах Windows а не Windows Phone):
Как видите, мы можем использовать общий код даже с Android и iOS, если используете, к примеру, Xamarin (В «чистом» виде студия предлагает без пунктов Android и iOS)
Здесь мы можем выбрать, какие типы проекта у нас будут, и при этом задействуем только те возможности платформы, которые есть во всех выбранных типах проектов.
Более подробно со списком поддерживаемых возможностей можно ознакомиться на сайте msdn. Или, если используете, Xamarin.
Здесь мы уже не будем подробно останавливаться на этой библиотеке, так как в следующей части большинство примеров будут связаны именно с Portable Library.
Заключение
На практике достаточно много людей и команд обращаются за консультацией на тему шаринга кода и наверное выборка не презентативна, но видел что большинство начинающих разработчиков предпочитает линкование файлов.
В целом, популярность шаринга линкованием файлов обусловлена простотой и скоростью внедрения этого подхода. В большинстве случаев не требуется проводить рефакторинг кода и можно брать существующий код, внося небольшие вставки символов компиляции, быстро использовать особенности платформы. Напротив, библиотека с общим кодом зачастую не дает возможности простого разделения особенностей платформы, и в следующей части мы подробно остановимся на этом моменте.
Есть два существенных недостатка у подхода с линкованием файлов, из-за чего я стараюсь его не применять:
- В больших проектах рефакторинг и сопровождение кода сильно усложняются, так как компилятор и студия просто не видят код, который разделен символом компиляции.
- При добавлении новой платформы или нового софта, для использования этого кода нам придется сопровождать все эти изменения повторно, зачастую внося правки во все символы компиляции. Сопровождение кода усложняется в разы.
Основным же недостатком подхода с общей библиотекой является то, что необходимо больше кода (хотя это с лихвой компенсируется удобством сопровождения). Кроме того, для проектов с низким качеством кода этот метод плохо подходит. Во время консультации разных команд я неоднократно сталкивался с тем, что для использования общей библиотеки командам приходилось проделывать довольно большую и объемную работу по «чистке» проекта. Но с другой стороны, необходимость думать о том, что код надо расшаривать и для этого необходимо выделить общую логику косвенно приводило к улучшению качества кода.
Автор: Atreides07