Craig Shoemaker

下载代码示例

用 JavaScript 构建的 Windows 应用商店应用程序允许具备 HTML 和 JavaScript 技能的任何人创建 Windows 本机应用程序,但 JavaScript 并不总是解决每个问题的最佳选择。 应用程序的某些行为可以使用 Visual Basic 或 C++ 以更面向对象的方式更好地实现。 而且,代码的某些内容可能适合于在多个需要从 UI 层获取数据的 Windows 运行时(WinRT)组件中进行重用。 在以上每一种情况中,理解将数据从 JavaScript 传递给 WinRT 组件再返回给 UI 这一机制非常重要。

在 Web 中,数据经常是以 JSON 对象的形式从客户端传递给服务器再返回。 在不同的背景下,ASP.NET Web 窗体和 ASP.NET MVC 等框架都具有模型绑定器等功能,或至少是服务器端对 JSON 对象解析的某种“自动化魔术”处理功能。 WinRT 组件包含了提供解析 JSON 的功能的对象,但支持级别较低,如要实现简化程度更高的交互,需要由您进行某些显式处理。

本文演示了如何对传入 WinRT 组件中的 JSON 字符串进行可靠解析,以便冻结强类型对象并将结果返回至 UI。

与 WinRT 组件交互的限制

在讨论解析 JSON 对象的细节之前,您必须先熟悉与 WinRT 组件交互的要求与限制。 MSDN 帮助主题“用 C# 和 Visual Basic 创建 Windows 运行时组件”( bit.ly/WgBBai ) 详细讨论了 WinRT 组件声明方法参数和返回类型所需的前提条件。 WinRT 组件可接受的类型主要包括基元类型和少量集合类型,因此尝试将原始 JSON 对象传递到组件中是不允许的行为。 将 JSON 对象传入托管组件中的最好办法是首先对 JSON 对象进行序列化(使用 JSON.stringify 方法),因为字符串在这些类中受到完全支持。

在托管代码中解析 JSON 对象

Windows.Data.Json 命名空间包括多个旨在以强类型方式使用 JSON 对象的不同类,例如 JsonValue、JsonArray 和 JsonObject 类。 JsonValue 类表示一个以字符串、数字、布尔型、数组或对象形式公开的 JSON 值(详情请参阅 bit.ly/14AcTmF )。 解析 JSON 字符串需要将原始字符串传递给 JsonValue,这样后者才能返回 JsonObject 的一个实例。

JsonObject 类表示一个完整的 JSON 对象,并包含对源对象操作的方法。 通过 JsonObject 类可以添加和删除成员,从成员中提取数据,对每个成员进行迭代处理,甚至将对象再次序列化。 有关 JsonObject 的更多详情可参阅 bit.ly/WDWZkG

JsonArray 类表示一个 JSON 数组,其中又包含大量用于控制数组的方法,例如对增加和删除数组元素以及进行迭代的方法。 有关 JsonArray 类接口的详细信息,可参阅 bit.ly/XVUZo1

我们来看以下用 JavaScript 表示的 JSON 对象,可作为了解如何开始试用这些类的示例:

firstName: "Craig"

在试图将该对象传递给 WinRT 组件之前,必须首先用 JSON.stringify 函数将该对象序列化为字符串。 请注意对象被序列化后发生了什么——同一个对象以完全不同的方式表示,如下所示:

'_backingData': { 'firstName': 'Craig' 'firstName': 'Craig', 'backingData': { 'firstName':'Craig'}

这可能令您大吃一惊,因为 Web 浏览器中的同一个函数调用只是将对象序列化为字符串,并不附加任何内容。 JSON 字符串结构的这一变化对从对象中如何提取数据造成了影响。

要在 WinRT 组件中读取这一数据,第一步是尝试将传入字符串作为 JsonValue 实例进行解析。 如果解析成功,则可以从根实例 JsonValue 请求获取 JsonObject。 在这种情况下,JsonValue 是由调用 stringify 函数创建的根对象,JsonObject 可授予您访问以 JavaScript 创建的原始对象的权限。

以下代码描述在 JsonObject 对象可用后,如何使用 GetNamedString 方法将“firstName”成员的值提取到变量中:

JsonValue root;
JsonObject jsonObject;
string firstName;
if (JsonValue.TryParse(jsonString, out root))
  jsonObject = root.GetObject();
  if (jsonObject.ContainsKey("firstName"))
    firstName = jsonObject.GetNamedString("firstName");

采用相似的方法以访问布尔型和数值型成员—有 GetNamedBoolean 和 GetNamedNumber 方法可用。 下一步是实现 JsonObject 的扩展方法,以便轻松访问 JSON 数据。

JsonObject 的扩展方法

JsonObject 类的默认实现提供了一些底层行为,而运用某些能够处理不完美格式以及当源字符串找不到成员时可避免出现异常的简单方法,可以极大地改善这些默认行为。 换言之,JavaScript 创建的对象注定会碰到可能导致异常的格式化或结构化问题。 将以下扩展方法添加到 JsonObject 类将有助于缓解这些问题。

要添加的第一个扩展方法名为 GetStringValue。 图 1 显示该方法的具体实现,首先进行检查以确保成员已存在于对象中。 为此,key 参数即为 JSON 对象属性的名称。 确认成员存在后,即可用 TryGetValue 方法来尝试访问来自 JsonObject 实例的数据。 如果成功找到该值,则将其实现 IJsonValue 接口的对象返回。

图 1 GetStringValue 扩展方法的实现

public static string GetStringValue(this JsonObject jsonObject, 
  string key)
  IJsonValue value;
  string returnValue = string.Empty;
  if (jsonObject.ContainsKey(key))
    if (jsonObject.TryGetValue(key, out value))
      if (value.ValueType == JsonValueType.String)
        returnValue = jsonObject.GetNamedString(key);
      else if (value.ValueType == JsonValueType.Number)
        returnValue = jsonObject.GetNamedNumber(key).ToString();
      else if (value.ValueType == JsonValueType.Boolean)
        returnValue = jsonObject.GetNamedBoolean(key).ToString();
  return returnValue;

IJsonValue 接口包含只读的 ValueType 属性,后者负责公开表示对象数据类型的给定 JsonValueType 枚举值。 询问 ValueType 之后,即使用相应的类型化方法将数据从该对象中提取出来。

GetStringValue 方法能够识别布尔型和数值型值,以防出现格式不正确的 JSON 对象。 以上代码实现过程也可以更加严格:当遇到 JSON 对象未严格按照预期类型进行格式化时,放弃解析或抛出错误。不过,上例中的代码可以使解析处理过程更加灵活,防止出现错误。

下一个扩展方法如图 2 所示,实现的功能是提取布尔值。 在本例中,GetBooleanValue 方法支持将布尔值表示为字符串(例如,“1”或“true”表示真值等)或数字(例如,“1”表示真,以及“0”或“false”)。

图 2 GetBooleanValue 扩展方法的实现

public static bool? GetBooleanValue(this JsonObject jsonObject, 
  string key)
  IJsonValue value;
  bool? returnValue = null;
  if (jsonObject.ContainsKey(key))
    if (jsonObject.TryGetValue(key, out value))
      if (value.ValueType == JsonValueType.String)
        string v = jsonObject.GetNamedString(key).ToLower();
        if (v == "1" || v == "true")
          returnValue = true;
        else if (v == "0" || v == "false")
          returnValue = false;
      else if (value.ValueType == JsonValueType.Number)
        int v = Convert.ToInt32(jsonObject.GetNamedNumber(key));
        if (v == 1)
          returnValue = true;
        else if (v == 0)
          returnValue = false;
      else if (value.ValueType == JsonValueType.Boolean)
        returnValue = value.GetBoolean();
  return returnValue;

由于基于数值的扩展方法设置为返回可为空的类型,因此在本例中,GetDoubleValue 返回一个可为空的双精度值。 本例中的纠正行为试图将字符串转换成可能的相应数值(参见图 3)。

图 3 GetDoubleValue 扩展方法的实现

public static double? GetDoubleValue(this JsonObject jsonObject, 
  string key)
  IJsonValue value;
  double? returnValue = null;
  double parsedValue;
  if (jsonObject.ContainsKey(key))
    if (jsonObject.TryGetValue(key, out value))
      if (value.ValueType == JsonValueType.String)
        if (double.TryParse(jsonObject.GetNamedString(key), 
          out parsedValue))
          returnValue = parsedValue;
      else if (value.ValueType == JsonValueType.Number)
        returnValue = jsonObject.GetNamedNumber(key);
  return returnValue;

由于 JsonObject 类内置的提取数字方法返回双精度值,而数据值通常以整数形式表示,因此以下代码展示了 GetIntegerValue 方法如何将 GetDoubleValue 方法封装起来,并将结果转换成整数:

public static int? GetIntegerValue(this JsonObject jsonObject, 
  string key)
  double? value = jsonObject.GetDoubleValue(key);
  int? returnValue = null;
  if (value.HasValue)
    returnValue = Convert.ToInt32(value.Value);
  return returnValue;

添加工厂支持

现在,扩展后的 JsonObject 类包含将数据提取为基元类型的高层支持,下一步是将此支持运用到工厂类中,后者负责接收传入的 JSON 字符串,并返回一个冻结的域对象实例。

以下代码描述如何在系统中对 Person 建模:

internal class Person
  public int Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public bool? IsOnWestCoast { get; set; }

以下代码显示 Person­Factory 类中的 Create 方法,该方法接受字符串参数:

public static Person Create(string jsonString)
  JsonValue json;
  Person person = new Person();
  if (JsonValue.TryParse(jsonString, out json))
    person = PersonFactory.Create(json);
  return person;

图 4 显示接受 JsonValue 参数的 Create 方法。 综合运用这些 Create 方法,即可实现原始字符串的接收并返回 Person 类的实例,其中每个成员都包含预期数据。 为了对 JSON 数组提供支持,可将这些方法分开并重载,我们将在下一节讨论该内容。

图 4 接受 JsonValue 参数的 PersonFactory 的 Create 方法

public static Person Create(JsonValue personValue)
  Person person = new Person();
  JsonObject jsonObject = personValue.GetObject();
  int? id = jsonObject.GetIntegerValue("id");
  if (id.HasValue)
    person.Id = id.Value;
  person.FirstName = jsonObject.GetStringValue("firstName");
  person.LastName = jsonObject.GetStringValue("lastName");
  bool? isOnWestCoast = jsonObject.GetBooleanValue("isOnWestCoast");
  if (isOnWestCoast.HasValue)
    person.IsOnWestCoast = isOnWestCoast.Value;
  return person;

添加数组支持

有时,数据以对象数组而非单个对象的形式存在。 在这种情况下,必须利用 JsonArray 类尝试将字符串作为数组进行解析。 图 5 显示传入字符串如何解析为数组,以及随后将每个元素传递至 Create 方法以便最终解析到模型中。 请注意,首先要创建 Person 列表的新实例,以便在字符串不能解析到对象数组中时返回空数组,这将有助于防止出现意外异常。

图 5 PersonFactory 的 CreateList 方法

public static IList<Person> CreateList(string peopleJson)
  List<Person> people = new List<Person>();
  JsonArray array = new JsonArray();
  if (JsonArray.TryParse(peopleJson, out array))
    if (array.Count > 0)
      foreach (JsonValue value in array)
        people.Add(PersonFactory.Create(value));
  return people;

添加支持类

下一步是创建一个负责使用工作类并对返回的模型实例结果进行有意义处理的对象。 图 6 演示如何使用单个 JSON 对象及 JSON 数组字符串,以及随后将其作为强类型对象进行处理。

图 6 ContactsManager 的实现(无异步支持)

using System.Collections.Generic;
public sealed class ContactsManager
  private string AddContact(string personJson)
    Person person = PersonFactory.Create(personJson);
    return string.Format("{0} {1} is added to the system.",
      person.FirstName,
      person.LastName);
  private string AddContacts(string personJson)
    IList<Person> people = PersonFactory.CreateList(personJson);
    return string.Format("{0} {1} and {2} {3} are added to the system.",
      people[0].FirstName,
      people[0].LastName,
      people[1].FirstName,
      people[1].LastName);

支持异步交互

此类对 WinRT 组件内部方法的调用应异步进行,因为 JSON 消息有可能增加到任意大小,从而导致应用程序延迟。

以下代码包含添加到 ContactsManager 类的方法,用来支持对 AddContact 方法进行异步调用:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
public IAsyncOperation<string> AddContactAsync(string personJson)
  return Task.Run<string>(() =>
    return this.AddContact(personJson);
  }).AsAsyncOperation();

AddContactAsync 方法接受 JSON 字符串参数并随即启动一个 Task,该 Task 负责运行 AddContact 方法。 一旦 Task 完成运行,即向 JavaScript Promise 发送响应,IAsyncOperation 接口支持将为该过程提供便利。 可在随付的代码下载中获得为 AddContact 和 AddContacts 提供异步支持的 ContactsManager 类的完整源代码。

在 JavaScript 中使用 Promise 模式

最后一个步骤是在 JavaScript 中使用 ContactsManager 类,并使用 promise 模式发起对该类的调用。 本例中使用的方法是为了实现视图模型,负责将已建模的数据传递给 WinRT 组件,随后等待响应。 用于传递给组件的数据在图 7 中定义,其中包含一个 JSON 对象及数组。

图 7 JSON 数据源

var _model = {
  contact: {
    id: 1000,
    firstName: "Craig",
    lastName: "Shoemaker"
  contacts: [
      id: 1001,
      firstName: "Craig",
      lastName: "Shoemaker",
      isOnWestCoast: "true"
      id: 1002,
      firstName: "Jason",
      lastName: "Beres",
      isOnWestCoast: "0"

该视图模型如图 8 所示,其中包含模型的成员以及一组由 WinRT 组件返回的消息的成员。 Windows JavaScript 库 (WinJS) 绑定框架用于绑定消息,这些消息由对 HTML 元素的响应返回。 在随附的代码下载中提供了页面模块的完整列表,从中可以查看各个部分如何结合为一体。

图 8 使用 ContactsManager 的视图模型

var _vm = {
  ViewModel: WinJS.Binding.as({
    model: _model,
    contactMsg: "",
    contactsMsg: "",
    addContact: function () {
      var mgr = ParseJSON.Utility.ContactsManager();
      var jsonString = JSON.stringify(_vm.ViewModel.model.contact);
      mgr.addContactAsync(jsonString).done(function (response) {
        _vm.ViewModel.contactMsg = response;
    addContacts: function () {
      var mgr = ParseJSON.Utility.ContactsManager();
      var jsonString = JSON.stringify(_vm.ViewModel.model.contacts);
        mgr.addContactsAsync(jsonString).done(function (response) {
          _vm.ViewModel.contactsMsg = response;

请注意,如果想在数据绑定过程中将 add­Contact 或 addContacts 函数绑定到按钮,必须运行 WinJS.Utilities.requireSupportedForProcessing 函数,并向其传递一个指向视图模型中相应函数的引用。

最后一步是向 HTML 中添加适当元素和属性以支持绑定操作。 通过设置 data-win-bindsource="Application.Pages.Home.View­Model",将 div 元素作为绑定元素的主要绑定容器。然后为 data-win-bind 提供适当的值,将标题元素与其数据成员绑定到一起:

<section aria-label="Main content" role="main">
  <div data-win-bindsource=
    "Application.Pages.Home.ViewModel">
    <h2 data-win-bind="innerText: contactMsg"></h2>
    <h2 data-win-bind="innerText: contactsMsg"></h2>
</section>

这就是我们想要的结果! 通过用 JavaScript 构建 Windows 应用商店应用程序,您将有机会利用现有 Web 技能来构建现代的本机 UI 应用程序,但在两个平台之间存在许多差异因素。 通过 Windows.Data.Json 命名空间可获得对 JSON 数据解析的底层支持,但也可以对现有对象进行几项扩展,实现更丰富的支持。

Craig Shoemaker是一位软件开发人员、播客、博客作者和技术推广人员。他还是 Code 杂志、MSDN 和 Pluralsight 的作者。他是个收藏爱好者,闲暇时光醉心于他众多的藏品。请关注他的 Twitter:twitter.com/craigshoemaker

衷心感谢以下技术专家对本文的审阅:Christopher Bennage (Microsoft)、Kraig Brockschmidt (Microsoft) 和 Richard Fricks (Microsoft)。
Christopher Bennage 是 Microsoft 的模式和实施方案团队的开发人员。 他在 Microsoft 的工作是发现、收集和支持那些令开发者赏心悦目的实施方案。 他最近感兴趣的技术领域包括 JavaScript 和(偶尔)游戏开发。 他的博客地址是 dev.bennage.com

Kraig Brockschmidt 自 1988 年起就在 Microsoft 工作,他的工作重心是通过写作、培训、演讲和直接指导为开发人员提供帮助。 他是 Windows Ecosystem 团队的高级程序经理,与核心合作伙伴在构建 Windows 应用商店应用程序领域进行协作,并将由此获得的知识提供给更广泛的开发者社区。 他最近推出了《Programming Windows 8 Apps in HTML, CSS, and JavaScript》(Microsoft 出版社发行的免费电子书),他的博客地址是 kraigbrockschmidt.com/blog

Richard Fricks 在过去的二十年里一直与开发社区展开协作,他最近帮助设计了 Media 的 Windows 运行时 API 策略,并协助开发社区采用 Windows 8 的新功能。 他目前是 Windows Scenario 采用团队的程序经理。