Windows 8

在 Windows Phone 8 与 Windows 8 应用程序之间共享代码

Doug Holland

下载代码示例

Visual Studio 2012 为构建 Windows 8 和 Windows Phone 8 应用程序提供了一套出色的工具。 因此,可以进行适当的探究,以了解可在应用程序的 Windows 应用商店版本与 Windows Phone 版本之间共享多少代码。

您可以采用多种不同语言编写 Windows 应用商店应用程序:XAML 搭配 C#、Visual Basic、C++,甚至是 HTML5 搭配 JavaScript。

通常采用 XAML 搭配 C# 或 Visual Basic 来编写 Windows Phone 8 应用程序,但 Windows Phone 8 SDK 现已支持采用 XAML 和 C++ 来编写 Direct3D 应用程序。 虽然 Windows Phone 8 SDK 还为基于 HTML5 的应用程序提供了模板,但它们只是基于 XAML 并借助 WebBrowser 控件托管基于 HTML5 的 Web 页面。

在本文中,我将探讨三种用于在 Windows 应用商店与 Windows Phone 应用程序之间共享代码的策略: 可移植类库 (PCL)、Windows 运行时 (WinRT) 组件(以及 Windows Phone 运行时组件)和 Visual Studio 的“添加为链接”选项。 您可以在开发者中心 (aka.ms/sharecode) 找到有关在 Windows 应用商店与 Windows Phone 应用程序之间共享代码的更多指导信息。

值得注意的是,虽然 Windows 应用商店应用程序与 Windows Phone 应用程序之间有许多相似之处(如动态磁贴),但是它们毕竟是不同的平台,因此应有针对性地专门设计 UX。

体系结构

一般而言,那些提倡问题分割的体系结构原则能够提高可共享代码的比例。 如果已经采用了可促进问题分割的模式,如模型-视图-视图模型 (MVVM) 或模型-视图-控制器 (MVC),实现代码共享较为容易,此外,在体系结构中采用依赖关系注入模式也有助于实现代码共享。 在进行新应用程序的体系结构设计时,请务必考虑采用此类模式,以提高可实现的代码共享水平。 对于现有应用程序,可以考虑重构体系结构,以促进问题分割,进而促进代码共享。 MVVM 或 MVC 除可实现问题分割外,还具备其他一些优势,例如:允许设计者和开发者同时工作。 在设计者借助 Expression Blend 等工具进行 UX 设计的同时,开发者可在 Visual Studio 中编写 UX 的具体实现代码。

可移植类库

Visual Studio 2012 中的 PCL 项目可实现跨平台开发,它允许为生成的程序集选择要支持的目标框架。 在 Visual Studio 2010 中作为可选加载项引入的 PCL 项目模板现已包含在 Visual Studio Professional 2012 及更高版本中。

那么,在 PCL 中可以共享哪些代码呢?

PCL 之所以被称为 PCL,就是因为它可实现可移植代码的共享,为使代码能够移植,其必须为采用 C# 或 Visual Basic 编写的托管代码。 PCL 只生成单个二进制文件,因此,可移植代码不使用条件编译指令,而是借助接口或抽象基类将平台特定的功能抽象出来。 当可移植代码需要与平台特定代码交互时,可使用依赖关系注入模式提供抽象的平台特定实现。 编译后,PCL 会生成单个程序集,以供基于目标框架的任意项目引用。

图 1 显示一个用 PCL 实现共享代码的建议体系结构方法。 在 MVVM 模式中,视图模型和模型与任意平台特定功能的抽象一起包含在 PCL 中。 Windows 应用商店和 Windows Phone 应用程序提供了任意平台特定功能抽象的启动逻辑、视图和实现。 虽然 MVVM 设计模式并非实现可移植代码的必要条件,但是该模式提倡的问题分割可产生干净、可扩展的体系结构。

Sharing Code Using the MVVM Design Pattern
图 1 借助 MVVM 设计模式共享代码

可在 Visual Studio 2012 中的“添加可移植类库”对话框中选择生成的程序集将支持的目标框架。

最初,您可能以为应该选中 Silverlight 5 复选框,但对于在 Windows 应用商店和 Windows Phone 应用程序之间共享代码来说,这并不是必需的。 事实上,选中 Silverlight 5 将使可移植代码无法利用某些非常有用的新类型,如 Microsoft .NET Framework 4.5 中引入的 CallerMemberNameAttribute 类。

如果您从事过 Windows Phone 的开发工作,一定会很熟悉用于向用户显示消息的 MessageBox 类。 Windows 应用商店应用程序使用 Windows 运行时 MessageDialog 类来实现这一目的。 下面介绍如何在 PCL 中抽象这一平台特定功能。

图 2 中的 IMessagingManager 接口抽象了平台特定功能,以向用户显示消息。 IMessagingManager 接口提供一个重载的 ShowAsync 方法,它接收将向用户显示的消息的内容和标题。

图 2 IMessagingManager 接口

/// <summary>
/// Provides an abstraction for platform-specific user messaging capabilities.
/// </summary>
public interface IMessagingManager
{
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </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>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </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 枚举提供一个平台无关的抽象,用于显示“确定”按钮、“确定”与“取消”按钮,或“是”与“否”按钮(请参阅图 3)。

图 3 MessagingButtons 枚举

/// <summary>
/// Specifies the buttons to include when a message is displayed.
/// </summary>
public enum MessagingButtons
{
  /// <summary>
  /// Displays only the OK button.
  /// </summary>
  OK = 0,
  /// <summary>
  /// Displays both the OK and Cancel buttons.
  /// </summary>
  OKCancel = 1,
  /// <summary>
  /// Displays both the Yes and No buttons.
  /// </summary>
  YesNo = 2
}

MessagingButtons 枚举底层的整数值用于映射至 Windows Phone MessageBoxButton 枚举,以便将 MessagingButtons 枚举安全地转换为 MessageBoxButton 枚举。

ShowAsync 是一个异步方法,返回一个指示用户在关闭消息时所单击按钮的 Task­<MessagingResult>。 MessagingResult 枚举(参阅图 4)也是一个平台无关的抽象。

图 4 MessagingResult 枚举

/// <summary>
/// Represents the result of a message being displayed to the user.
/// </summary>
public enum MessagingResult
{
  /// <summary>
  /// This value is not currently used.
  /// </summary>
  None = 0,
  /// <summary>
  /// The user clicked the OK button.
    /// </summary>
    OK = 1,
    /// <summary>
    /// The user clicked the Cancel button.
    /// </summary>
    Cancel = 2,
    /// <summary>
    /// The user clicked the Yes button.
    /// </summary>
    Yes = 6,
   /// <summary>
  /// The user clicked the No button.
  /// </summary>
  No = 7
}

在此示例中,IMessagingManager 接口以及 Messaging­Buttons 和 MessagingResult 枚举是可移植的,因此可在 PCL 中共享。

在 PCL 中抽象出平台特定功能后,需要提供 IMessagingManager 接口针对 Windows 应用商店和 Windows Phone 应用程序的平台特定实现。 图 5 显示针对 Windows Phone 应用程序的实现,图 6 显示针对 Windows 应用商店应用程序的实现。

图 5 MessagingManager—Windows Phone 实现

/// <summary>
/// Windows Phone implementation of the <see cref="T:IMessagingManager"/> interface.
/// </summary>
internal class MessagingManager : IMessagingManager
{
  /// <summary>
  /// Initializes a new instance of the <see cref="T:MessagingManager"/> class.
  /// </summary>
  public MessagingManager()
  {
  }
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </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>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </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;
    // Determine whether the calling thread is the thread
    // associated with the Dispatcher.
    if (App.RootFrame.Dispatcher.CheckAccess())
    {
      result = MessageBox.Show(message, title, 
        (MessageBoxButton)buttons);
    }
    else
    {
      // Execute asynchronously on the thread the Dispatcher is associated with.
      App.RootFrame.Dispatcher.BeginInvoke(() =>
      {
        result = MessageBox.Show(message, title, 
          (MessageBoxButton)buttons);
      });
    }
    return (MessagingResult) result;
  }
}

图 6 MessagingManager—Windows 应用商店实现

/// <summary>
/// Windows Store implementation of the <see cref="T:IMessagingManager"/> interface.
/// </summary>
internal class MessagingManager : IMessagingManager
{
  /// <summary>
  /// Initializes a new instance of the <see cref="T:MessagingManager"/> class.
  /// </summary>
  public MessagingManager()
  {
  }
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </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>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </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;
    // Determine whether the calling thread is the
    // thread associated with the Dispatcher.
    if (Window.Current.Dispatcher.HasThreadAccess)
    {
      await dialog.ShowAsync();
    }
    else
    {
      // Execute asynchronously on the thread the Dispatcher is associated with.
      await Window.Current.Dispatcher.RunAsync(
        CoreDispatcherPriority.Normal, async () =>
      {
        await dialog.ShowAsync();
      });
    }
    return result;
  }
}

MessagingManager 类的 Windows Phone 版本使用平台特定的 MessageBox 类显示消息。 MessagingButtons 枚举底层的整数值用于映射至 Windows Phone MessageBoxButton 枚举,以实现将 MessagingButtons 枚举安全地转换为 MessageBoxButton 枚举。 同样,MessagingResult 枚举底层的整数值可实现到 MessageBoxResult 枚举的安全转换。

图 6 中 MessagingManager 类的 Windows 应用商店版本使用 Windows 运行时 MessageDialog 类显示消息。 MessagingButtons 枚举底层的整数值用于映射至 Windows Phone MessageBoxButton 枚举,以实现将 MessagingButtons 枚举安全地转换为 MessageBoxButton 枚举。

依赖关系注入

对于图 1 中所示的应用程序体系结构,IMessagingManager 提供了用于向用户发送消息的平台特定抽象。 接下来,我将采用依赖关系注入模式将该抽象的平台特定实现注入到可移植代码中。 在图 7 的示例中,HelloWorldViewModel 使用构造函数注入方法注入 IMessagingManager 接口的平台特定实现。 随后,HelloWorldView­Model.DisplayMessage 方法使用所注入的实现向用户发送了消息。 要了解更多有关依赖关系注入的信息,建议您阅读 Mark Seemann 编著的《Dependency Injection in .NET》(Manning Publications,2011,bit.ly/dotnetdi)。

图 7 可移植的 HelloWorldViewModel 类

/// <summary>
/// Provides a portable view model for the Hello World app.
/// </summary>
public class HelloWorldViewModel : BindableBase
{
  /// <summary>
  /// The message to be displayed by the messaging manager.
  /// </summary>
  private string message;
  /// <summary>
  /// The title of the message to be displayed by the messaging manager.
  /// </summary>
  private string title;
  /// <summary>
  /// Platform specific instance of the <see cref="T:IMessagingManager"/> interface.
  /// </summary>
  private IMessagingManager MessagingManager;
  /// <summary>
  /// Initializes a new instance of the <see cref="T:HelloWorldViewModel"/> class.
  /// </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>
  /// Gets the display message command.
  /// </summary>
  /// <value>The display message command.</value>
  public ICommand DisplayMessageCommand
  {
    get;
    private set;
  }
  /// <summary>
  /// Displays the message using the platform-specific messaging manager.
  /// </summary>
  private async void DisplayMessage()
  {
    await this.messagingManager.ShowAsync(
      this.message, this.title, MessagingButtons.OK);
  }
}

Windows 运行时组件

通过 Windows 运行时组件,可以在 Windows 应用商店和 Windows Phone 应用程序之间共享不可移植的代码。 但是,这些组件并非二进制兼容,因此,还需要创建内容相似的 Windows 运行时和 Windows Phone 运行时组件项目,以利用跨这两个平台的代码。 虽然不得不在解决方案中包含 Windows 运行时组件和 Windows Phone 运行时组件项目,但是这两个项目均以相同的 C++ 源文件构建。

Windows 运行时组件能够在 Windows 应用商店和 Windows Phone 应用程序之间共享本机 C++ 代码,是采用 C++ 编写计算密集型操作以实现最佳性能的理想选择。

.winmd 文件中包含的元数据公开了 Windows 运行时组件中的 API 定义。 借助这些元数据,语言投射能够让所采用的语言自行决定如何在该语言中使用这些 API。 图8 列出了支持创建和使用 Windows 运行时组件的语言。 至本文撰写时为止,仅支持采用 C++ 创建这两类组件。

图 8 创建和使用 Windows 运行时组件

平台 Create 使用
Windows 运行时组件 C++、C#、Visual Basic C++、C#、Visual Basic、JavaScript
Windows Phone 运行时组件 C++ C++、C#、Visual Basic

下面的示例演示如何跨 Windows 应用商店和 Windows Phone 应用程序共享一个用于计算斐波纳契数的 C++ 类。 图 9图 10 显示了 C++/组件扩展 (CX) 中 FibonacciCalculator 类的实现。

图 9 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);
  };    
}

图 10 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);
}

图 11 是本文所含示例在 Visual Studio 解决方案资源管理器中的解决方案结构,可以看到,两个组件包含相同的 C++ 源文件。

Visual Studio Solution Explorer
图 11 Visual Studio 解决方案资源管理器

Visual Studio 的“添加为链接”功能

在 Visual Studio 中向某项目添加现有项目时,您可能会注意到,“添加”按钮右侧有一个小箭头。 单击该箭头时,Visual Studio 会显示用于选择“添加”还是“添加为链接”的选项。 如果选择默认选项(即添加文件),则文件会复制到目标项目,磁盘和源代码管理(如果使用的话)中将存在文件的两个副本。 如果选择“添加为链接”,则磁盘和源代码管理中将只存在文件的单个实例,这对于版本管理来说非常有用。 在 Visual C++ 项目中添加现有文件时,此为默认行为,因此,“添加现有项”对话框未在“添加”按钮上提供选项。 开发者中心提供了关于使用“添加为链接”共享代码的更多指导信息,请访问 bit.ly/addaslink

Windows 运行时 API 是不可移植的,因而无法在 PCL 中共享。 Windows 8 和 Windows Phone 8 公开了 Windows 运行时 API 的一个子集,因此,可针对该子集编写代码,然后借助“添加为链接”在两种应用程序间共享。 开发者中心提供了有关 Windows 运行时 API 共享子集的详细信息,请访问 bit.ly/wpruntime

总结

随着 Windows 8 和 Windows Phone 8 的发布,您可以开始探索在这两个平台间共享代码的方法。 本文探讨了如何借助二进制兼容的 PCL 共享可移植代码,以及如何抽象平台特定功能。 然后演示如何利用 Windows 运行时组件共享不可移植的本机代码。 最后,我围绕 Visual Studio 的“添加为链接”选项进行了讨论。

在体系结构方面,我发现,促进问题分割的模式(如 MVVM)有助于实现代码共享,而依赖关系注入模式能够使共享代码充分利用平台特定功能。 Windows Phone 开发者中心提供了有关在 Windows 应用商店和 Windows Phone 应用程序之间共享代码的更多指导信息 (aka.ms/sharecode),并提供了 PixPresenter 示例应用程序 (bit.ly/pixpresenter)。

Doug Holland 是 Microsoft 开发者和平台推广团队的一名高级体系结构推广专家。 过去几年,他携手战略合作伙伴将许多关注客户需求的应用程序带到了 Windows 和 Windows Phone 上。 他过去曾是 Visual C# MVP 和 Intel 黑带开发者,并编著了《Professional Windows 8 Programming: Application Development with C# and XAML》(Wrox, 2012)(可从 bit.ly/prowin8book 获取)。

衷心感谢以下技术专家对本文的审阅: Andrew Byrne (Microsoft)、Doug Rothaus (Microsoft) 和 Marian Laparu (Microsoft)