Благодаря Visual Studio 2012 в вашем распоряжении имеется великолепный набор инструментов для создания приложений Windows 8 и Windows Phone 8. А значит, есть смысл исследовать, сколько кода ваших приложений можно сделать общим для их версий под Windows Store и Windows Phone.
Приложения Windows Store можно писать, используя несколько разных языков программирования: XAML с C#, Visual Basic, C++ и даже HTML5 с JavaScript.
Приложения Windows Phone 8 обычно пишутся на XAML в сочетании с C# или Visual Basic, но Windows Phone 8 SDK теперь позволяет создавать Direct3D-приложения с применением XAML и C++. Хотя Windows Phone 8 SDK также предоставляет шаблон для приложений на основе HTML5, на самом деле они все равно базируются на XAML с простым добавлением элемента управления WebBrowser, в котором размещаются веб-страницы с поддержкой HTML5.
В этой статье я рассмотрю три стратегии совместного использования кода между приложениями Windows Store и Windows Phone: Portable Class Libraries (PCL), компоненты Windows Runtime (WinRT) (и компоненты Windows Phone Runtime) и вариант Add as Link в Visual Studio. Дополнительные сведения по разделению кода между приложениями Windows Store и Windows Phone вы найдете в Dev Center (aka.ms/sharecode).
Стоит отметить, что, хотя у приложений Windows Store и Windows Phone много схожего (например, активные плитки), это все же разные платформы со своей спецификой.
Архитектура
Архитектурные принципы, позволяющие увеличить долю общего кода, — в целом, те же, что и способствующие разделению обязанностей (separation of concerns). Если вы уже применяете шаблоны, которые обеспечивают разделение обязанностей, например Model-View-ViewModel (MVVM) или Model-View-Controller (MVC), то сможете легко добиться совместного использования кода; то же самое относится и к случаю, когда в вашей архитектуре задействованы шаблоны встраивания зависимостей (dependency injection). Вы определенно должны подумать о применении этих шаблонов, проектируя архитектуру новых приложений, чтобы максимально повысить уровень совместного использования кода. В существующих приложениях вы, возможно, захотите выполнить рефакторинг архитектуры, чтобы содействовать разделению обязанностей и тем самым более высокой степени совместного использования кода. Разделение обязанностей, обеспечиваемое MVVM или MVC, дает дополнительные преимущества, такие как возможность параллельной работы проектировщиков и разработчиков. Первые проектируют структуру программы и UI с помощью инструментария вроде Expression Blend, а вторые пишут код в Visual Studio, рождающий эту программу на свет.
Portable Class Libraries
PCL-проект в Visual Studio 2012 обеспечивает кросс-платформенную разработку, позволяя выбирать целевые инфраструктуры, которые будут поддерживаться конечными сборками. Шаблон PCL-проект, введенный в Visual Studio 2010 как дополнительная надстройка, теперь включается в Visual Studio Professional 2012 и выше.
Итак, какой же код можно сделать общим в рамках PCL?
Библиотеки PCL называются так потому, что они позволяют совместно использовать портируемый код, и для того, чтобы он был портируемым, его нужно писать как управляемый на C# или Visual Basic. Так как PCL дает на выходе один двоичный файл, портируемый код не использует директивы условной компиляции; вместо этого средства, специфичные для конкретной платформы, абстрагируются с помощью интерфейсов или абстрактных базовых классов. Когда портируемому коду нужно взаимодействовать с кодом, специфичным для платформы, применяются шаблоны встраивания зависимостей, через которые предоставляются реализации, специфичные для платформ. В результате компиляции PCL дает единую сборку, на которую можно ссылаться из любого проекта, основанного на целевых инфраструктурах.
На рис. 1 показан один из рекомендованных подходов к архитектуре, обеспечивающих создание общего кода с применением PCL. В случае шаблона MVVM модели (models) и модели представлений (view models) содержатся внутри PCL; там же находятся абстракции любых специфичных для платформы средств. Приложения Windows Store и Windows Phone предоставляют стартовую логику, представления и реализации любых абстракций специфичных для платформы средств. Хотя проектировочный шаблон MVVM не обязателен для написания портируемого кода, этот шаблон разделения обязанностей способствует созданию четкой и расширяемой архитектуры.
Рис. 1. Создание общего кода с помощью проектировочного шаблона MVVM
Windows Store App | Приложение Windows Store |
Startup | Стартовая логика |
Views | Представления |
Platform-Specific Functionality | Специфичная для платформы функциональность |
Portable Class Library | Portable Class Library |
View Models | Модели представлений |
Models | Модели |
Platform Functionality Abstractions | Специфичные для платформы абстракции |
Windows Phone App | Приложение Windows Phone |
Reference | Ссылка |
Диалог Add Portable Class Library в Visual Studio 2012 позволяет выбрать целевые инфраструктуры, которые будут поддерживаться конечной сборкой.
Чтобы код был портируемым, его нужно писать как управляемый на C# или Visual Basic.
Поначалу вы могли решить, что должны установить флажок для Silverlight 5, но для совместного использования кода между приложениями Windows Store и Windows Phone это не обязательно. По сути, выбор Silverlight 5 означает, что ваш портируемый код не будет использовать преимущества некоторых очень полезных новых типов, таких как класс CallerMemberNameAttribute, введенный в Microsoft .NET Framework 4.5.
Если вы уже занимались разработкой для Windows Phone, то скорее всего знакомы с классом MessageBox, который позволяет выводить сообщения пользователю. Приложения Windows Store для той же цели используют класс MessageDialog из Windows Runtime. Давайте рассмотрим, как абстрагировать эту специфичную для платформ функциональность в PCL.
Интерфейс IMessagingManager на рис. 2 абстрагирует специфичную для платформы функциональность, относящуюся к выводу сообщений. Этот интерфейс предоставляет перегруженный метод ShowAsync, который принимает сообщение и его заголовок, а потом отображает пользователю.
Рис. 2. Интерфейс IMessagingManager
/// <summary> /// Предоставляет абстракцию специфичных для платформы /// средств вывода сообщений пользователю /// </summary> public interface IMessagingManager { /// <summary> /// Выводит указанное сообщение средствами, /// специфичными для платформы /// </summary> /// <param name="message"> /// The message to be displayed to the user.</param> /// <param name="title">The title of the message.</param> /// <returns>A <see cref="T:MessagingResult"/> /// value representing the user's response.</returns> Task<MessagingResult> ShowAsync(string message, string title); /// <summary> /// Выводит указанное сообщение средствами, /// специфичными для платформы /// </summary> /// <param name="message"> /// The message to be displayed to the user.</param> /// <param name="title">The title of the message.</param> /// <param name="buttons"> /// The buttons to be displayed.</param> /// <returns>A <see cref="T:MessagingResult"/> /// value representing the user's response.</returns> Task<MessagingResult> ShowAsync(string message, string title, MessagingButtons buttons); }
Метод ShowAsync перегружен, чтобы вы могли при необходимости задавать кнопки, отображаемые вместе с сообщением. Перечисление MessagingButtons предоставляет независимую от платформы абстракцию для вывода кнопки OK или кнопок OK и Cancel, или кнопок Yes и No (рис. 3).
Рис. 3. Перечисление MessagingButtons
/// <summary> /// Указывает кнопки, включаемые в отображаемое сообщение /// </summary> public enum MessagingButtons { /// <summary> /// Показывает только кнопку OK /// </summary> OK = 0, /// <summary> /// Показывает кнопки OK и Cancel /// </summary> OKCancel = 1, /// <summary> /// Показывает кнопки Yes и No /// </summary> YesNo = 2 }
Нижележащие целочисленные значения перечисления MessagingButtons были намеренно сопоставлены с перечислением MessageBoxButton из Windows Phone, чтобы безопасно привести MessagingButtons к MessageBoxButton.
ShowAsync — это асинхронный метод, который возвращает Task<MessagingResult>, указывающее кнопку, выбранную пользователем для удаления сообщения с экрана. Перечисление MessagingResult (рис. 4) также является аппаратно-независимой абстракцией.
Рис. 4. Перечисление MessagingResult
/// <summary> /// Представляет результат сообщения, /// отображаемого пользователю /// </summary> public enum MessagingResult { /// <summary> /// Это значение в настоящее время не используется /// </summary> None = 0, /// <summary> /// Пользователь щелкнул кнопку OK /// </summary> OK = 1, /// <summary> /// Пользователь щелкнул кнопку Cancel /// </summary> Cancel = 2, /// <summary> /// Пользователь щелкнул кнопку Yes /// </summary> Yes = 6, /// <summary> /// Пользователь щелкнул кнопку No /// </summary> No = 7 }
В этом примере интерфейс IMessagingManager и перечисления MessagingButtons и MessagingResult являются портируемыми, а значит, и общим кодом внутри PCL.
Абстрагировав специфичную для платформ функциональность в PCL, вы должны предоставить реализации интерфейса IMessagingManager, специфичные для приложений Windows Store и Windows Phone. На рис. 5 показана реализация для приложений Windows Phone, а на рис. 6 — для приложений Windows Store.
Рис. 5. MessagingManager — реализация для Windows Phone
/// <summary> /// Реализация для Windows Phone интерфейса /// <see cref="T:IMessagingManager"/> /// </summary> internal class MessagingManager : IMessagingManager { /// <summary> /// Инициализирует новый экземпляр класса /// <see cref="T:MessagingManager"/> /// </summary> public MessagingManager() { } /// <summary> /// Отображает указанное сообщение, /// используя специфичные для платформы средства /// </summary> /// <param name="message"> /// The message to be displayed to the user.</param> /// <param name="title">The title of the message.</param> /// <returns>A <see cref="T:MessagingResult"/> /// value representing the users response.</returns> public async Task<MessagingResult> ShowAsync(string message, string title) { MessagingResult result = await this.ShowAsync(message, title, MessagingButtons.OK); return result; } /// <summary> /// Отображает указанное сообщение, /// используя специфичные для платформы средства /// </summary> /// <param name="message"> /// The message to be displayed to the user.</param> /// <param name="title">The title of the message.</param> /// <param name="buttons"> /// The buttons to be displayed.</param> /// <exception cref="T:ArgumentException"/> The specified /// value for message or title is <c>null</c> or empty. /// </exception> /// <returns>A <see cref="T:MessagingResult"/> /// value representing the users response.</returns> public async Task<MessagingResult> ShowAsync( string message, string title, MessagingButtons buttons) { if (string.IsNullOrEmpty(message)) { throw new ArgumentException( "The specified message cannot be null or empty.", "message"); } if (string.IsNullOrEmpty(title)) { throw new ArgumentException( "The specified title cannot be null or empty.", "title"); } MessageBoxResult result = MessageBoxResult.None; // Определяем, связан ли вызвавший поток с Dispatcher if (App.RootFrame.Dispatcher.CheckAccess()) { result = MessageBox.Show(message, title, (MessageBoxButton)buttons); } else { // Асинхронно выполняем в потоке, // с которым сопоставлен Dispatcher App.RootFrame.Dispatcher.BeginInvoke(() => { result = MessageBox.Show(message, title, (MessageBoxButton)buttons); }); } return (MessagingResult) result; } }
Рис. 6. MessagingManager — реализация для Windows Store
/// <summary> /// Реализация для Windows Store интерфейса /// <see cref="T:IMessagingManager"/> /// </summary> internal class MessagingManager : IMessagingManager { /// <summary> /// Инициализирует новый экземпляр класса /// <see cref="T:MessagingManager"/> /// </summary> public MessagingManager() { } /// <summary> /// Отображает указанное сообщение, /// используя специфичные для платформы средства /// </summary> /// <param name="message"> /// The message to be displayed to the user.</param> /// <param name="title">The title of the message.</param> /// <returns>A <see cref="T:MessagingResult"/> /// value representing the users response.</returns> public async Task<MessagingResult> ShowAsync(string message, string title) { MessagingResult result = await this.ShowAsync(message, title, MessagingButtons.OK); return result; } /// <summary> /// Отображает указанное сообщение, /// используя специфичные для платформы средства /// </summary> /// <param name="message"> /// The message to be displayed to the user.</param> /// <param name="title">The title of the message.</param> /// <param name="buttons"> /// The buttons to be displayed.</param> /// <exception cref="T:ArgumentException"/> The specified /// value for message or title is <c>null</c> or empty. /// </exception> /// <exception cref="T:NotSupportedException"/> /// The specified <see cref="T:MessagingButtons"/> value /// is not supported.</exception> /// <returns>A <see cref="T:MessagingResult"/> /// value representing the users response.</returns> public async Task<MessagingResult> ShowAsync( string message, string title, MessagingButtons buttons) { if (string.IsNullOrEmpty(message)) { throw new ArgumentException( "The specified message cannot be null or empty.", "message"); } if (string.IsNullOrEmpty(title)) { throw new ArgumentException( "The specified title cannot be null or empty.", "title"); } MessageDialog dialog = new MessageDialog(message, title); MessagingResult result = MessagingResult.None; switch (buttons) { case MessagingButtons.OK: dialog.Commands.Add(new UICommand("OK", new UICommandInvokedHandler((o) => result = MessagingResult.OK))); break; case MessagingButtons.OKCancel: dialog.Commands.Add(new UICommand("OK", new UICommandInvokedHandler((o) => result = MessagingResult.OK))); dialog.Commands.Add(new UICommand("Cancel", new UICommandInvokedHandler((o) => result = MessagingResult.Cancel))); break; case MessagingButtons.YesNo: dialog.Commands.Add(new UICommand("Yes", new UICommandInvokedHandler((o) => result = MessagingResult.Yes))); dialog.Commands.Add(new UICommand("No", new UICommandInvokedHandler((o) => result = MessagingResult.No))); break; default: throw new NotSupportedException( string.Format("MessagingButtons.{0} is not supported.", buttons.ToString())); } dialog.DefaultCommandIndex = 1; // Определяем, связан ли вызвавший поток с Dispatcher if (Window.Current.Dispatcher.HasThreadAccess) { await dialog.ShowAsync(); } else { // Асинхронно выполняем в потоке, // с которым сопоставлен Dispatcher await Window.Current.Dispatcher.RunAsync( CoreDispatcherPriority.Normal, async () => { await dialog.ShowAsync(); }); } return result; } }
Чтобы вывести сообщение, версия класса MessagingManager для Windows Phone использует специфичный для платформы класс MessageBox. Нижележащие целочисленные значения перечисления MessagingButtons были намеренно сопоставлены с перечислением MessageBoxButton из Windows Phone, чтобы вы могли безопасно приводить MessagingButtons к MessageBoxButton. Точно так же нижележащие целочисленные значения перечисления MessagingResult обеспечивают безопасное преобразование в перечисление MessageBoxResult.
Версия класса MessagingManager для Windows Store на рис. 6 использует с той же целью класс MessageDialog из Windows Runtime. Нижележащие целочисленные значения перечисления MessagingButtons были намеренно сопоставлены с перечислением MessageBoxButton из Windows Phone, чтобы вы могли безопасно приводить MessagingButtons к MessageBoxButton.
Встраивание зависимостей
При архитектуре приложения, продемонстрированной на рис. 1, IMessagingManager предоставляет специфичную для платформы абстракцию для вывода сообщений пользователям. Теперь я задействую шаблоны встраивания зависимостей, чтобы включить специфичные для платформ реализации этой абстракции в портируемый код. В примере на рис. 7 HelloWorldViewModel использует встраивание конструктора, чтобы включить специфичную для платформы реализацию интерфейса IMessagingManager. Затем метод HelloWorldViewModel.DisplayMessage использует встроенную реализацию для отображения сообщения пользователю. Если вы хотите узнать больше о встраивании зависимостей, советую прочитать книгу Марка Симанна (Mark Seemann) «Dependency Injection in .NET» (Manning Publications, 2011, bit.ly/dotnetdi).
Рис. 7. Портируемый класс HelloWorldViewModel
/// <summary> /// Предоставляет портируемую модель представления /// приложению Hello World /// </summary> public class HelloWorldViewModel : BindableBase { /// <summary> /// Сообщение, отображаемое диспетчером сообщений /// </summary> private string message; /// <summary> /// Заголовок сообщения, отображаемого диспетчером сообщений /// </summary> private string title; /// <summary> /// Специфичный для платформы экземпляр интерфейса /// <see cref="T:IMessagingManager"/> /// </summary> private IMessagingManager MessagingManager; /// <summary> /// Инициализирует новый экземпляр класса /// <see cref="T:HelloWorldViewModel"/> /// </summary> public HelloWorldViewModel(IMessagingManager messagingManager, string message, string title) { this.messagingManager = MessagingManager; this.message = message; this.title = title; this.DisplayMessageCommand = new Command(this.DisplayMessage); } /// <summary> /// Получает команду отображения сообщения /// </summary> /// <value>The display message command.</value> public ICommand DisplayMessageCommand { get; private set; } /// <summary> /// Выводит сообщение, используя специфичный /// для платформы диспетчер сообщений /// </summary> private async void DisplayMessage() { await this.messagingManager.ShowAsync( this.message, this.title, MessagingButtons.OK); } }
Компоненты Windows Runtime
Компоненты Windows Runtime позволяют совместно использовать непортируемый код между приложениями Windows Store и Windows Phone. Однако эти компоненты не являются совместимыми на уровне двоичного кода, поэтому вам придется создать эквивалентные проекты компонентов Windows Runtime и Windows Phone Runtime для использования кода на обеих платформах. Хотя эти проекты нужно включать в решение как для компонентов Windows Runtime, так и для компонентов Windows Phone Runtime, они компилируются из одинаковых файлов исходного кода на C++.
Благодаря возможности использования неуправляемого C++-кода между приложениями Windows Store и Windows Phone компоненты Windows Runtime являются великолепным выбором для написания операций с интенсивными вычислениями на C++, который обеспечивает наибольшее быстродействие.
API-определения внутри компонентов Windows Runtime предоставляются в метаданных, содержащихся в файлах .winmd. С помощью этих метаданных языковые проекции (language projections) позволяют определять на применяемом вами языке, как в нем используются такие API. В табл. 1 перечислены поддерживаемые языки для создания и использования компонентов Windows Runtime. На момент написания этой статьи создавать оба типа компонентов можно было только на C++.
Табл. 1. Создание и использование компонентов Windows Runtime
Платформа | Создаются | Используются |
Компоненты Windows Runtime | C++, C#, Visual Basic | C++, C#, Visual Basic, JavaScript |
WКомпоненты Windows Phone Runtime | C++ | C++, C#, Visual Basic |
В следующем примере я продемонстрирую, как C++-класс, написанный для вычисления чисел Фибоначчи, можно использовать между приложениями Windows Store и Windows Phone. На рис. 8 и 9 показаны реализации класса FibonacciCalculator на C++/CX (Component Extensions).
Рис. 8. Fibonacci.h
#pragma once namespace MsdnMagazine_Fibonacci { public ref class FibonacciCalculator sealed { public: FibonacciCalculator(); uint64 GetFibonacci(uint32 number); private: uint64 GetFibonacci(uint32 number, uint64 p0, uint64 p1); }; }
Рис. 9. Fibonacci.cpp
#include "pch.h" #include "Fibonacci.h" using namespace Platform; using namespace MsdnMagazine_Fibonacci; FibonacciCalculator::FibonacciCalculator() { } uint64 FibonacciCalculator::GetFibonacci(uint32 number) { return number == 0 ? 0L : GetFibonacci(number, 0, 1); } uint64 FibonacciCalculator::GetFibonacci( uint32 number, uint64 p0, uint64 p1) { return number == 1 ? p1 : GetFibonacci( number - 1, p1, p0 + p1); }
На рис. 10 вы видите структуру решения в Visual Studio Solution Explorer для примеров в этой статье и можете сами убедиться, что в обоих компонентах содержатся одни и те же файлы исходного кода на C++.
Рис. 10. Visual Studio Solution Explorer
Вариант Add as Link в Visual Studio
Добавляя существующий элемент в проект Visual Studio, вы, вероятно, замечали небольшую стрелку справа от кнопки Add. Если щелкнуть эту стрелку, у вас появится выбор: Add или Add as Link. Если вы выберете Add по умолчанию для файла, этот файл будет скопирован в проект, и на диске (а также в системе контроля версий, если она применяется) окажутся две независимые копии данного файла. Если же вы выберете Add as Link, на диске и в системе контроля версий будет только один экземпляр файла, что может быть чрезвычайно полезно для управлениями его версиями. При добавлении существующих файлов в проекты Visual C++ это поведение по умолчанию, и в диалоге Add Existing Item другой вариант для кнопки Add не появится. Подробнее о совместном использовании кода через Add as Link см. в Dev Center по ссылке bit.ly/addaslink.
Windows Runtime API не являются портируемыми, и поэтому их нельзя совместно использовать для разных платформ в PCL. Windows 8 и Windows Phone 8 предоставляют подмножество Windows Runtime API, поэтому вы можете писать код с применением этого подмножества и в дальнейшем разделять его между приложениями двух типов, используя Add as Link. Все детали по этому общему подмножеству Windows Runtime API см. в Dev Center по ссылке bit.ly/wpruntime.
Заключение
В Windows 8 и Windows Phone 8 есть разные способы совместного использования кода между этими двумя платформами. В этой статье я рассмотрел, как сделать общим портируемый код, используя PCL с совместимостью на уровне двоичного кода, и как можно абстрагировать специфичную для платформ функциональность. Затем я продемонстрировал, как совместно использовать непортируемый неуправляемый код с помощью компонентов Windows Runtime. Наконец, мы кратко обсудили вариант Add as Link в Visual Studio.
Применительно к архитектуре я обратил ваше внимание на то, что шаблоны, способствующие разделению обязанностей, такие как MVVM, могут быть полезны для поддержки совместного использования кода и что шаблоны встраивания зависимостей позволяют задействовать в общем коде специфичную для платформ функциональность. Более подробное руководство по совместному использованию кода между приложениями Windows Store and Windows Phone вы найдете в Windows Phone Dev Center по ссылке aka.ms/sharecode, а полезное приложение-пример PixPresenter — по ссылке bit.ly/pixpresenter.