JavaScript

使用 JavaScript 在 Windows 应用商店应用程序中构建和使用控件

Chris Sells
Brandon Satrom

下载代码示例

在前一篇文章“使用 JavaScript 在 Windows 应用商店应用程序中进行数据绑定”中,我们探讨了 Windows JavaScript 库 (WinJS) 及其对数据绑定的支持。 在使用数据绑定时,数据绑定通常位于控件环境中,这就是为什么我们用了这么多时间来深入讨论 ListView 控件的管理和维护(您可以在 msdn.microsoft.com/magazine/jj651576 中查看这篇文章)。 在本文中,我们将快速介绍一下在您作为 JavaScript 程序员构建 Windows Store 应用程序时可供您使用的控件。 并且,如果这些控件无法满足您的需要,我们将会介绍如何构建您自己的应用程序。

当使用 JavaScript 在 Windows 应用商店应用程序中构建控件时,您可以访问若干系列中的控件:

  • HTML5 元素: 从 HTML5 元素是可重复使用的 UI 和行为块这个意义上来说,HTML5 元素属于控件,例如 <progress /> 和 <a />。
  • WinRT 控件: 作为投影到 JavaScript 中的 Windows 运行时 (WinRT) 类公开的控件,例如 Windows.UI.Popups.PopupMenu。
  • WinJS 控件: 作为 JavaScript 类实现的控件,例如 WinJS.UI.ListView。
  • CSS 样式: CSS 提供若干样式,这些样式允许您对内容项进行布局,就好像它们是控件容器一样,例如列计数: 4.

在本文中,我们将重点讨论前三个类别的控件。

HTML5 元素

因为使用 JavaScript 构建的 Windows 应用商店应用程序基于 Web 技术,因此所有 HTML5 元素都会正常运行,如图 1 所示。

HTML5 Controls Available to Your Windows Store Apps Built with JavaScript
图 1 可用于使用 JavaScript 构建的 Windows 应用商店应用程序的 HTML5 控件

HTML5 元素的详细信息不在本文的介绍范围之内,但我们建议您参阅相关 HTML5 文档以便了解更多信息。 此外,用于创建图1 的示例在随附的源代码下载中提供。

WinRT 控件

Windows 运行时提供所有类型领域中的所有类型功能,但就控件而言,它仅提供两个:

  • 消息对话框: 具有可选标题的消息。
  • 弹出菜单: 限制为六个或更少项的菜单。

使用 MessageDialog 函数调用该消息对话框:

var popups = Windows.UI.Popups;
var mb = new popups.MessageDialog("and welcome to my message box!", "Hello!");
mb.showAsync();

MessageDialog 类具有一个 showAsync 方法,该方法返回一个承诺,就像使用 JavaScript 构建的 Windows 应用商店应用程序已经拥有的所有其他异步操作一样。 但在本例中,我们忽略该约定,因为我们通常不关心对话框何时消失。 图 2 显示结果(使用“MessageBox”,这是以前用于 MessageDialog 的术语)。

The WinRT Message Dialog
图 2 WinRT 消息对话框

以类似方式使用 PopupMenu 类:

var popups = Windows.UI.Popups;
var menu = new popups.PopupMenu();
menu.commands.push(new popups.UICommand("one", null, 1));
menu.commands.push(new popups.UICommand("two", null, 2));
menu.showAsync({ x: 120, y: 360 }).done(function (e) {
  // Do something with e.label and/or e.id
  ...
});

在这个示例中,在创建 PopupMenu 对象后,我们提供两个 UICommand 对象,每个对象都具有一个标签以及可选的回调和 ID 参数。 我们没有将回调用于此示例中的每个命令,因为我们在“done”完成方法中捕获事件参数。 一个弹出菜单看上去符合您的期望,如图 3 中所示。

The WinRT Pop-up Menu
图 3 WinRT 弹出菜单

请记住,在撰写本文时,上下文菜单限制为仅六个项。

WinJS 控件

尽管 HTML5 控件丰富且各不相同,但在万维网联合会决定添加新的元素标签且浏览器供应商决定实现新标签之前,该控件集将是不可扩展的。 同样,WinRT 控件集也不可扩展(尽管您可以构建非 UI Windows 运行时组件)。 对于使用 Windows 应用商店应用程序专门构建的控件的可扩展集合,其优势是 WinJS 提供的集合。

WinJS 控件是在 JavaScript 中实现的控件,在构造函数中提供某个签名:

function MyControl(element,
    options) {...}

元素参数是 HTML 文档对象模型 (DOM) 元素,旨在充当控件内容的宿主,通常为 div。 选项参数是 JavaScript 对象,用于提供可选的配置参数,例如 ListView itemDataSource 属性。

若要看到 WinJS 控件的实际效果,我们来看看旨在充当 DatePicker 控件宿主的 div:

<div id="datePickerDiv"></div>

这种情况下,我们可以轻松地按如下所示创建 DatePicker:

var datePicker = new WinJS.UI.DatePicker(datePickerDiv);

输出是全新的 DatePicker 控件,如图 4 中所示。

Output of the DatePicker Control
图 4 DatePicker 控件的输出

如果我们想要配置一个控件,可以传入一组选项:

var datePicker = new WinJS.UI.DatePicker(datePickerDiv, { current: "6/2/1969" });

就 DatePicker 而言,当前选项允许我们设置当前显示的日期,如图 5 中所示。

Setting the Currently Displayed Date
图 5 设置当前显示的日期

一旦您获取了与 HTML5 元素相关联的控件后,就可以使用 winControl 属性获取该控件:

var datePicker = datePickerDiv.winControl; // Magical well-known property name
datePicker.current = "5/5/1995"; // Now we're talking to the control

此外,一旦您获取了该控件后,就可以返回到具有元素属性的关联的 HTML5 元素:

var datePickerDiv = datePicker.element;
datePickerDiv.style.display = "none";
// Now we're talking to the HTML element

除了允许以编程方式进行创建以外,每个控件还通过 data-win-control 和 data-win-options 属性提供声明性创建方式,正如我们所看到的:

<div id="datePicker2"
  data-win-control="WinJS.UI.DatePicker" data-win-options=
  "{current: '6/2/1969'}" ></div>

data-win-control 属性是要调用的构造函数的名称。 与在要求调用 WinJS.Binding.processAll 以便对 data-win-bind 属性进行分析(参见我们之前的文章)的数据绑定中一样,data-win-control 属性需要调用 WinJS.UI.processAll 以便对其进行分析以及创建控件。 因此,您将在所有生成的项目模板代码中都看到对 WinJS.UI.processAll 的调用。

data-win-options 字符串被分析为功能较弱的 JavaScript 对象初始化语法。 例如,您将注意到我们直接传递了该字符串,而不是通过创建 Date 对象为 DatePicker 控件设置当前选项。 其原因在于,选项分析器不理解“new”关键字 — 它只处理静态数据。 但是,因为 DatePicker 和其他 WinJS 控件应该以声明方式创建,所以,针对选项分析器的限制(在此例中针对 DatePicker)对它们特别放宽了要求,这意味着采用一个字符串并且将其分析为供您使用的 Date 对象。

每个控件都具有不同的选项集,并且我们将告知您要参考的文档,以便了解哪些控件具有哪些选项。 图 6 显示了内置 WinJS 控件的列表。

图 6 WinJS 控件

名称 说明
应用程序栏 在工具栏中显示应用程序级别命令 WinJS.UI.AppBar
日期挑选器 显示用于挑选日期的 UI WinJS.UI.DatePicker
翻转视图 翻阅一组内容,一次一项 WinJS.UI.FlipView
弹出窗口 显示具有任意内容的覆盖面 WinJS.UI.Flyout
列表视图 在列表或网格中以分组或未分组的形式显示项的集合 WinJS.UI.ListView
评级 显示用于对某些内容(例如影片)进行评级的 UI WinJS.UI.Rating
语义缩放 提供一个 UI 以便从一个 ListView 缩放到另一个,例如,缩小到组列表的分组的 ListView WinJS.UI.SemanticZoom
设置弹出窗口 提供一个 UI 以便配置应用程序设置 WinJS.UI.SettingsFlyout
时间挑选器 显示用于挑选时间的 UI WinJS.UI.TimePicker
切换开关 显示用于在两个选项之间进行挑选的 UI WinJS.UI.ToggleSwitch
工具提示(丰富) 显示具有任意 HTML 内容的工具提示 WinJS.UI.Tooltip
视图框 提供缩放到可用空间的合理的固定大小的区域 WinJS.UI.ViewBox

图 7 显示运行中的 WinJS 控件。

The WinJS Controls in Action
图 7 运行中的 WinJS 控件

您可以在您的 Windows 应用商店应用程序中任意组合和搭配 HTML5 控件、WinRT 控件和 WinJS 控件。

或者,如果您在 HTML5、Windows Runtime 或 WinJS 提供的列表上找不到想要的控件,则可以构建您自己的控件。

自定义控件

如前所述,WinJS 控件只是提供以下形式的构造函数的一种函数:

function MyControl(element, options) {...}

构建此类控件就是实现一个函数以便在作为第一个参数传入的父元素下创建该 HTML,并且使用作为第二个参数传入的选项对象。 例如,假设我们想要构建一个很小的时钟控件,如图 8 中所示。

A Custom Clock Control
图 8 自定义时钟控件

假设我们将 div 设置为包含我们的时钟控件:

<div id="clockControl1"></div>

与内置 WinJS 控件一样,我们想要能够创建一个自定义控件的示例,如下所示:

var clock = new Samples.UI.ClockControl(clockControl1, { color: 'red' });
clock.color = 'red'; // Can set options as part of construction or later

我们为该自定义控件挑选的名称是来自 Samples.UI 命名空间的 ClockControl。 与之前一样,创建该控件就是传入包含元素 (clockControl1) 以及选项的名称/值对的可选集合。 如果以后在该控件的生存期中我们想要更改该控件的选项之一,我们应该能够通过设置单独的属性值来这样做。

我们还想要能够以声明方式创建自定义控件:

<script src="/js/clockControl.js"></script>
  ...
<div id="clockControl2"
    style="width: 200px; height: 200px;"    
    data-win-control="Samples.UI.ClockControl"
    data-win-options="{color: 'red'}">  </div>

在实现过程中,我们想要确保设置 winControl 和元素属性,确保私有成员被适当标记并且可以相应处理事件。 在我们深入探讨该 ClockControl 的实现方式时,我们将介绍 WinJS 是如何帮助我们实现这些功能的。

控件类首先,我们将需要确保 ClockControl 处于正确的命名空间中。 大多数流行语言都具有命名空间的概念,作为将类型、函数和值分隔成单独的命名区域以免发生冲突的一种方法。 例如,如果 Microsoft 在 WinJS 2.0 中提供 ClockControl 类型,它将位于 WinJS.UI 命名空间中,以便不会与 Samples.UI 冲突。 在 JavaScript 中,命名空间只是具有构造函数、函数和值的另一个对象,您可以按如下所示进行填充:

// clockControl.js
(function () {
  // The hard way
  window.Samples = window.Samples || {};
  window.Samples.UI = window.Samples.UI || {};
  window.Samples.UI.ClockControl = 
    function(element, options) { ...
};
})();

这将会顺利运行。 但是,定义命名空间(以及嵌套命名空间)很简单,WinJS(像许多 JavaScript 库一样)提供一个快捷方式:

// clockControl.js
(function () {
  // The easy way
  WinJS.Namespace.define("Samples.UI", {
    ClockControl: function (element, options) { ...
};
  };
})();

WinJS.Namespace 命名空间中的 define 函数允许定义一个新的命名空间,从而为您正确处理包含点的名称的分析。 第二个参数是用于定义您要从该命名空间公开的构造函数、函数和值的对象,在我们的例子中就是 ClockControl 构造函数。

控件属性和方法 在我们的 ClockControl 类型上,我们想要公开方法和属性,例如颜色属性。 这些方法和属性可以是实例或静态的,并且它们可以是公共或私有的(至少作为“private”,因为 Java­Script 允许获取某一对象的成员)。 均通过正确使用构造函数的原型属性以及 JavaScript 的新增 Object.defineProperties 方法来支持所有这些概念。 WinJS 也通过 WinJS.Class 命名空间上的 define 方法为此提供快捷方式:

WinJS.Namespace.define("Samples.UI", {
  ClockControl: WinJS.Class.define(
    function (element, options) {...}, // ctor
  { // Properties and methods
    color: "black",
    width: { get: function () { ...
} },
    height: { get: function () { ...
} },
    radius: { get: function () { ...
} },
    _tick: function () { ...
},
    _drawFace: function () { ...
},
    _drawHand: function (radians, thickness, length) { ...
},
  })
});

WinJS.Class.define 方法采用充当构造函数的函数,但它也采用属性和方法的集合。 该 define 方法知道如何从提供的 get 和 set 函数创建属性。 进一步讲,它知道以下划线为前缀的属性或方法(例如 _tick)是“private”(私有的)。JavaScript 并不是真的支持传统意义上的 private 方法 — 也就是说,我们仍可以调用 _tick 方法。 但是,它们将不会在 Visual Studio 2012 IntelliSense 或 JavaScript for-in 循环中出现,这至少是表明它们不是用于 public(公共)的一个简便方法。

该构造函数设置成为 WinJS 控件所需的属性,如图 9 中所示。

图 9 该构造函数设置成为 WinJS 控件所需的属性

WinJS.Namespace.define("Samples.UI", {
  ClockControl: WinJS.Class.define(function (element, options) {
    // Set up well-known properties
    element.winControl = this;
    this.element = element;
    // Parse the options; that is, the color option
    WinJS.UI.setOptions(this, options);
    // Create the drawing surface
    var canvas = document.createElement("canvas");
    element.appendChild(canvas);
    this._ctx = canvas.getContext("2d");
    // Draw the clock now and every second
    setTimeout(this._tick.bind(this), 0);
    setInterval(this._tick.bind(this), 1000);
  },
  ...
});

该构造函数所做的第一件事情就是设置已知的 winControl 和元素属性,以便开发人员可以在承载的 HTML5 元素和 JavaScript 控件之间进行切换。

接下来,该构造函数将处理这些选项。 您应该记得,可以使用 HTML5 中的 data-win-options 属性将这些选项指定为一组名称/值对或一个字符串。 WinJS 将选项字符串分析成一个 JavaScript 对象,这样您可以只处理名称/值对。 如果愿意,可以提取单独的属性,例如我们的例子中的颜色属性。 但是,如果您的选项列表很长,则 WinJS.UI 命名空间中的 setOptions 方法将遍历选项对象中的所有属性,并且将其设置为您的控件上的属性。 例如,下列代码块是等效的:

// Setting each property one at a time
myControl.one = "one";
myControl.two = 2;
// Setting all properties at once
WinJS.UI.setOptions(myControl, {
  one: "one",
  two: 2,
});

在设置该控件的选项后,构造函数的工作就是创建完成工作所需的 HTML5 父元素的任何子元素。 在 ClockControl 这个例子中,我们使用 HTML5 canvas 元素和一个计时器。 此控件是用平淡无奇的 HTML 和 JavaScript 编写的,因此就不在这里展示了(但会在随附的代码下载中提供它)。

控件事件 除了方法和属性之外,控件常常会公开事件。 事件是来自您的控件的某种通知,用来指示发生了某种令人感兴趣的情况,例如用户单击了该控件或者该控件达到了会触发程序中其他某个类型行为的某个状态。 基于 HTML DOM 设置的示例,您将需要 addEventListener 和 removeEventListener 之类的方法,以便允许开发人员订阅您的控件公开的任何事件以及相应的 onmyevent 属性。

例如,如果我们想要每 5 秒就公开来自示例 ClockControl 的事件,则我们应该能够以编程方式订阅它:

// Do something every 5 seconds
window.clockControl_fiveseconds = function (e) {
  ...
};
var clock = new Samples.UI.ClockControl(...);
// This style works
clock.onfiveseconds = clockControl_fiveseconds;
// This style works, too
clock.addEventListener("fiveseconds", clockControl_fiveseconds);
Declaratively, we’d like to be able to attach to custom events, too:
<!-- this style works, three -->
<div data-win-control="Samples.UI.ClockControl"
  data-win-options="{color: 'white',
    onfiveseconds: clockControl_fiveseconds}" ...>
</div>

启用所有这三个样式要求两样东西: 用于管理事件订阅的方法(并且在事件发生时用于分配事件)以及各事件的属性。 两者均由 WinJS.Class 命名空间提供:

// clockControl.js
...
WinJS.Namespace.define("Samples.UI", {
  ClockControl: WinJS.Class.define(...);
});
// Add event support to ClockControl
WinJS.Class.mix(Samples.UI.ClockControl, 
  WinJS.UI.DOMEventMixin);
WinJS.Class.mix(Samples.UI.ClockControl,  
  WinJS.Utilities.createEventProperties("fiveseconds"));

WinJS.Class 的 mix 方法允许您混用现有对象所提供的属性和方法。 在这个示例中,来自 WinJS.UI 的 DOMEventMixin 提供三种方法:

// base.js
var DOMEventMixin = {
  addEventListener: function (type, listener, useCapture) {...},
  dispatchEvent: function (type, eventProperties) {...},
  removeEventListener: function (type, listener, useCapture) {...},
};

一旦我们混合来自 DOMEventMixin 的方法后,我们就可以通过将该 mix 方法用于 WinJS.Utilities 的 createEventProperties 方法创建的对象,为每个自定义事件都创建属性。 此方法将为您传入的每个逗号分隔的事件名称都生成事件方法组,并且在组的前面放置“on”前缀。 通过对 mix 方法的这两个调用提供的此组属性和方法,我们对我们的自定义控件进行了扩展,以便支持该 fiveseconds 事件。 为了从该控件内分配此类型的事件,我们调用 dispatchEvent 方法:

// clockControl.js
...
_tick: function () {
  var now = new Date();
  var sec = now.getSeconds();
  ...
// Fire the 5 second event
  if (sec % 5 == 0) {
    this.dispatchEvent("fiveseconds", { when: now });
  }
},
...

调用 dispatchEvent 将采用两个参数: 事件的名称以及在事件本身中提供的可选详细信息对象。 我们在传入单个“when”值,但 JavaScript 就是 Java­Script,我们可以传入任何我们想要的内容。 访问处理程序中的事件详细信息就是提取事件对象本身的 detail 值:

// Do something every 5 seconds
window.clockControl_fiveseconds = function (e) {
  var when = e.detail.when;
  ...
};

我们向您展示的定义 WinJS 控件的原则(在命名空间中定义某个类,设置 winControl 和元素属性,处理选项对象,定义属性和方法,以及定义和分配自定义事件)是 Microsoft 中的 WinJS 团队用来生成 WinJS 控件本身的完全相同的技术。 您可以通过阅读 WinJS 随附的 ui.js 文件,了解如何构建您所喜爱的控件的许多信息。

Chris Sells 是 Telerik 的开发人员工具事业部的副总裁。 他是“使用 JavaScript 构建 Windows 8 应用程序”(Addison-Wesley Professional, 2012) 的作者之一,本文是根据这篇文章改编的。 sellsbrothers.com 上提供了有关 Sells 和他的各种项目的详细信息。

Brandon Satrom 是 Telerik 的 Kendo UI 事业部的项目经理。 他是“使用 JavaScript 构建 Windows 8 应用程序”(Addison-Wesley Professional, 2012) 的作者之一,本文是根据这篇文章改编的。 请关注他的 Twitter:twitter.com/BrandonSatrom

衷心感谢以下技术专家对本文的审阅: Chris Anderson、Jonathan Antoine、Michael Weinhardt、Shawn Wildermuth 和 Josh Williams