Благодаря 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.