HTML5-和-JavaScript-的-Windows8-开发高级教程-全-

HTML5 和 JavaScript 的 Windows8 开发高级教程(全)

原文:Pro Windows 8 Development with HTML5 and JavaScript

协议:CC BY-NC-SA 4.0

一、将 Windows 8 放在上下文中

Windows 8 代表着微软希望突破传统桌面计算市场,并在移动世界中产生影响,移动世界一直由安卓设备,当然还有苹果产品主导。

微软的计划是为用户提供跨设备的一致性,允许相同的应用对用户的数据进行操作,而不管用户手边有哪种设备或哪种设备。这对许多用户来说很有吸引力,它利用了微软最大的素材——在桌面计算市场的领先地位——来推动平板电脑和智能手机市场的销售、接受度和可信度。

传统的 Windows 桌面对于不同类型的设备之间的一致性来说不是一个好的模型,并且为更小的屏幕添加触摸支持和重新设计界面的尝试也没有好结果。试图将旧的 Windows 模式扩展到小型设备上是微软之前进军移动世界失败的部分原因。

这就是 Windows 应用的用武之地。微软决定创建一个新的应用模型,而不是延续现有的应用模型。Windows Store 应用,通常被称为应用,可以在任何可以运行 Windows 8 及其衍生产品(Windows Phone 8、Windows RT 等)的设备上使用。).更重要的是,Windows 应用在配有鼠标和键盘的大屏幕台式机上运行得和在中等大小的触摸屏平板电脑上一样好。Windows Store 应用与常规的 Windows 桌面应用大相径庭:它们充满了屏幕,没有标题栏和按钮,具有完全不同的外观和感觉。

微软的另一个重大突破是,你可以使用网络技术来创建应用,这是我写这本书的原因,也很可能是你阅读这本书的原因。通过拥抱 HTML、CSS 和 JavaScript,微软拥抱了一个全新的开发人员社区,他们可以将自己的 web 应用开发知识应用到 Windows 应用开发中。

images 微软使用了 Windows Store App 这个术语,我觉得这个术语很别扭,我无法让自己在整本书中使用它。相反,我会提到 Windows 应用,通常只是简单的应用。我会让你在心里插入你认为合适的微软官方名称。

将应用开发置于背景中

Windows 应用是微软努力在一系列不同设备类型上提供一致用户体验的核心,包括传统的台式电脑、平板电脑和智能手机。Windows 应用提供快速流畅的用户交互,支持触摸和键盘/鼠标输入,并紧密集成到微软的云服务中,允许用户在他们工作的任何地方和他们使用的任何设备上复制他们的数据。

应用与传统的 Windows 桌面应用非常不同。默认情况下,Windows 8 应用充满屏幕,是 chromeless (这意味着没有周围的窗口、标题栏或按钮),不能像桌面应用那样调整大小或重叠。用户不关闭应用,也没有关闭或退出按钮。一次只显示一个应用,因此不需要窗口或标题栏。

需要用户输入的关键对话框,比如文件选择器,也是全屏的,就像迷你应用一样。事实上,它们的外观和感觉很像你可能在 iPhone、iPad 或 Android 设备上看到的应用——这当然不是偶然的。通过 Windows 8,微软旨在获得利润丰厚的智能手机和平板电脑市场,并希望通过在包括普通个人电脑在内的各种平台上提供应用来利用其在桌面世界的主导地位。

应用受益于一系列集成服务,称为契约,这使得创建紧密集成到 Windows 平台的应用变得容易,并且可以与其他应用共享数据。如果你刚刚安装了 Windows 8,并且一直想知道 Charm Bar 上的一些图标是干什么用的,那就不要再想了。应用通过 Charm Bar 使用契约向用户提供服务。当您刚接触 Windows 时,这可能看起来是一个笨拙的工具,但它很快就会成为您的第二天性。

使用 JavaScript 和 HTML 开发 Windows 应用

微软 Windows 8 最大的改变之一是让 JavaScript 和 HTML 成为应用开发的一等公民。这是一件大事,原因有二:第一是微软在。NET 平台多年来一直不愿意为不属于. NET 平台的工具和语言开放 Windows 开发。NET 家族,比如 C#。Windows 8 彻底改变了这一点。

第二个原因是微软坚持标准。您用来编写 web 应用的 JavaScript 和 HTML 与您用来编写 Windows 应用的 JavaScript 和 HTML 是相同的。仍然有新的库和技术需要学习——因此有了这本书——但是如果你已经开发了一个 web 应用,那么你已经拥有了应用开发所需的大量知识和经验。我在第三章中演示了这一点,我将向你展示如何使用普通的 JavaScript 和 HTML 创建你的第一个应用。这种网络驱动的主题根深蒂固:用 JavaScript/HTML 编写的应用是使用 Internet Explorer 10 执行的(尽管这对用户来说并不明显,因为他们无法判断你使用了哪种技术来创建你的应用)。你不能改变使用哪个浏览器来执行你的应用,但过一段时间后,你就不会真的想这么做了——IE10 非常好,对新的 HTML5 和 CSS3 功能有一些很好的支持。(有一些特定于微软的扩展,但它们出现在 W3C 标准仍在开发中或特性非常特定于应用的情况下。)

使用 Visual Studio 开发应用

与常规的 web 开发不同,当使用 JavaScript 和 HTML 编写应用时,您不能选择自己的开发工具:您必须使用 Visual Studio 2012,这与针对任何微软平台的开发所需的工具相同。因此,坏消息是您必须学习一个新的开发环境,但好消息是 Visual Studio 非常优秀,微软已经花时间使 JavaScript 和 HTML 支持与我用于常规 web 开发的任何工具和编辑器一样好。不过,我不得不承认,从我写 C#应用和服务的时候起,我就对 Visual Studio 情有独钟,而且你可能会发现,当你努力学习一套新的工具一种新的应用开发时,学习曲线是陡峭的。这不是一本关于 Visual Studio 的书,但是在第二章中,我会给你一个基本特性的快速浏览,帮助你入门。

发布 Windows 应用

大多数 Windows 应用都是通过 Windows 商店销售的。例外的是为企业编写的应用,它们可以像传统的桌面应用一样安装(尽管这仅在面向企业的 Windows 8 版本中可用)。Windows Store 与任何其他应用商店非常相似,用户可以搜索应用,查看成功应用的排名,并获得应用的更新。而且,和其他应用商店一样,Windows Store 通过从你的应用销售中提成来运营。我将在本书第四部分的中解释作为开发者如何使用 Windows Store,但如果你记住通过 Windows Store 发布是应用开发的最终目标,这将会很有帮助。

这本书里有什么?

在本书中,我将向您展示如何使用您的 web 应用技术知识,并应用它们来创建丰富、流畅和动态的 Windows 应用。我首先向您展示这些 web 技术可以用来创建一个简单的应用,使用的方法与您在常规 web 应用部署中遇到的方法相同,然后向您展示可用于利用 Windows 8 和应用环境的不同技术、库和功能。

这本书是给谁的?

您是一名经验丰富的 web 开发人员,已经掌握了 JavaScript、HTML 和 CSS 的基础知识,并且希望为 Windows 8 开发应用。您希望基于您的 web 体验创建超越浏览器的应用,并以常规 web 应用无法提供的方式利用 Windows 平台的功能。

在我阅读这本书之前,我需要知道些什么?

您需要知道如何使用 HTML、CSS 和 JavaScript 编写一个简单的 web 应用。您需要理解 HTML 元素和属性、CSS 样式以及函数和事件等 JavaScript 概念。你不需要成为这些技术的专家,但是你需要一些经验。在这本书里,我没有提供关于 web 开发的介绍,如果你是 web 技术领域的新手,你将很难理解这些例子。

Windows 应用开发使用 HTML5 和 CSS3,但如果您了解 HTML5 规范的最新草案,这并不重要。HTML5 和 CSS3 中的新特性在很大程度上是进化的,对 HTML4 的良好理解将为您提供足够的基础,让您了解自己不知道的内容。

images 提示我在本书中最常使用的 HTML5 相关特性其实是新的 CSS3 布局特性,它可以轻松创建流畅的界面。你可以使用新元素和 API,但在大多数情况下你不需要这样做,一些关键特性通过特定于应用的 API 更方便地暴露出来。

如果我没有这样的经历呢?

你可能仍然会从这本书里得到一些好处,但是你会发现这很难,你将不得不自己找出许多应用开发所需的基本技术。我写了几本其他的书,你可能会觉得有用。如果你是 HTML 新手,请阅读HTML 5权威指南。这解释了创建常规 web 内容和基本 web 应用所需的一切。我解释了如何使用 HTML 标记和 CSS3(包括新的 HTML5 元素),以及如何使用 DOM API 和 HTML5 APIs(如果您不熟悉这门语言,还包括一个 JavaScript 初级读本)。如果你想了解更多关于实用 web 应用开发的知识,请阅读 Pro jQuery 。jQuery 是一个非常流行的 JavaScript 库,它简化了 web 应用的开发。我在本书中不使用 jQuery,但是通过学习如何有效地使用 jQuery,您将提高您对 web 开发各个方面的理解(并且由于您可以使用 jQuery 进行 Windows 应用开发,您花费的时间将对您以后有好处)。对于更高级的主题,请阅读 Pro JavaScript for Web Apps ,其中我描述了我在自己的 Web 开发项目中使用的开发技巧和技术。这三本书都是由出版社出版的。

我不需要知道什么?

你不需要有任何 Windows 桌面开发或其他微软技术(如 C#、XAML 或。NET 框架)。使用 web 技术开发应用建立在您已经用于 web 应用开发的基础上,虽然有很多东西需要学习,但您不必担心其他编程语言或标记。

但是我不需要知道 C#的高级特性吗?

不,说实话。微软在让 JavaScript 与 C#和其他语言平起平坐方面做得很好。NET 语言,使 HTML 成为 XAML 的一个很好的替代品(这是大多数语言中定义用户界面的方式。NET 应用)。当您深入应用开发时,您会意识到您正在使用与。网络语言。这一点很明显,只是因为一些对象和属性名有点奇怪——在所有其他方面,您甚至不知道是否支持其他语言。

我用 HTML/JavaScript 和 XAML/C#编写 Windows 应用已经有一段时间了,但我还没有发现任何可用的特性。这是 web 技术程序员无法实现的。HTML 和 JavaScript 是应用开发领域的一流技术。

我需要什么工具和技术?

应用开发需要两样东西:一台运行 Windows 8 和 Visual Studio 2012 的 PC。如果你认真对待应用开发,你需要购买一份 Windows 8,但如果你只是好奇,你可以从微软获得 90 天的试用期——我将在稍后的第二章中解释如何进行。

Visual Studio 2012 是微软的开发环境。好消息是微软推出了 Visual Studio 的基础版本,可以免费获得,这是我在本书中一直使用的版本。它有一个吸引人的名字:Visual Studio 2012 Express for Windows 8,我将在本章的后面告诉你如何获得它。

Visual Studio 的付费版本是可用的,您可以将任何不同的 Visual Studio 版本与本书一起使用。微软倾向于对企业集成、版本控制和测试管理等功能收费,虽然它们都是有用的功能,但没有一个是应用开发所必需的,我不以任何方式依赖它们。

这本书的结构是怎样的?

在本章中,我将向您介绍 Visual Studio,并向您展示如何创建一个简单的项目。我将简要介绍 Visual Studio 界面的关键部分,解释 Windows 应用开发项目中每个文件的外观,并向您展示如何使用 Visual Studio 附带的应用模拟器工具运行和测试应用。

在第三章中,我将向你展示如何构建你的第一个应用。我主要使用基本的 HTML、CSS 和 JavaScript 特性来演示您现有的 web 应用开发知识有多少可以直接应用到 Windows 应用开发中。你会对你能做的事情感到惊喜。当然,你没有购买专业水平的基础书籍,这本书的其余大部分向你展示了不同的技术和功能,将一个基本的应用转变为一个提供一流应用体验的应用。在接下来的章节中,我将简要描述您将在本书的其他部分学到的内容。

第二部分:核心开发

有一些核心功能,几乎所有的应用都可以从中受益。在本书的这一部分,我解释了这些基础技术,向您展示了如何让用户浏览您的应用的内容,如何使您的应用布局适应运行它的设备的功能和配置,以及如何最好地利用广泛的异步编程支持,这些支持贯穿几乎所有的 Windows 应用开发库。当你完成这本书的这一部分时,你将知道如何创建一个动态的、自适应的、响应迅速的应用。

第三部分:UI 开发

您可以使用标准的 HTML 元素,如buttoninput,为应用创建 UI,称为布局,但您也可以访问 WinJS UI 库,其中包含的界面控件为 Windows 应用提供了独特的外观和感觉。在本书的这一部分,我将带您浏览这些控件,解释何时应该使用它们,以及如何将它们应用于常规的 HTML 元素,并给出许多许多示例,以便您可以看到它们的运行。当你完成这本书的这一部分时,你将知道如何应用独特的 Windows 外观和感觉来创建漂亮的应用。

第四部分:平台整合

一旦应用结构和布局就绪,就可以开始将应用集成到 Windows 提供的功能和服务中。这包括使你的应用成为文件和数据搜索过程的一部分,使用文件系统,告诉 Windows 你的应用支持不同类型的文件和协议,打印以及在应用之间共享数据。我将在本书的这一部分涵盖所有这些主题,并向您展示如何为您的应用创建不同类型的通知,包括低调的 live tiles 和更具侵入性的 toast 通知。当你完成这本书的这一部分时,你将知道如何使你的应用成为一个一流的 Windows 公民,完全集成到更广泛的平台和你的用户工作流中。

第五部分:销售应用

在本书的最后一部分,我将向您展示如何准备一个应用,以及如何在 Windows 应用商店中发布它。在本书的这一部分结束时,你将会看到一个 Windows 应用的完整生命周期,从最初的基本实现到高级功能,最后是它的发布。

这本书里有很多例子吗?

这本书里有个例子,我展示了创建一流应用所需的每个关键特性。在某些情况下,我回过头来组合不同的特性,向您展示它们是如何协同工作的,这些组合的好处,以及有时可能出现的问题。这本书里的例子太多了,以至于我很难把所有的代码都放进章节里。为了方便起见,我用两种方式列出了 JavaScript 代码和 HTML 标记。第一次引入新文件时,我会向您展示完整的内容。你可以在清单 1-1 中看到一个这样的例子,这是摘自第六章的代码。

清单 1-1 。新文件的完整列表

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    });

WinJS.Namespace.define("ViewModel.UserData", {

});
})();`

当我修改一个文件时,我倾向于只显示被修改的部分,类似于清单 1-2 。粗体代码显示了我所做的与我正在演示的技术或特性相关的更改。

清单 1-2 。修改文件的部分列表

... WinJS.Namespace.define("ViewModel.UserData", { **    word: null,** **    wordLength: {** **        get: function() {** **            return this.word ? this.word.length : null;** **        }**     } }); ...

如果我自己也想效仿呢?

你可以从Apress.com下载本书中每个例子的完整代码。代码是按章节组织的,每个项目显示每个应用的完成状态,所以你可以看到完成的结果是什么样子,如果你愿意,可以跟着做。您可以在自己的项目中使用代码,或者使用示例作为模板来创建新的应用。

图像归属

在本书的例子中,我使用了大量的图像文件。感谢以下人员善意地允许使用他们的照片:霍里亚·瓦兰、大卫·肖特、 Geishaboy500 、田中久约、梅尔维·埃斯凯利宁、花式速度女王、艾伦“克雷吉 3000 、克雷吉、诺森古德梅拉路易丝

二、入门指南

在开始开发应用之前,您需要启动并运行 Windows 8 和开发工具。

在这一章中,我将带您完成正确软件的安装过程,并带您完成为 Windows 8 应用创建 Visual Studio 项目的过程,带您浏览它包含的文件和 Visual Studio 提供的工具。

准备就绪

你不需要一台特别高端的电脑来进行应用开发——任何满足最低 Windows 8 规格的电脑都可以。在开发中,更快的机器让开发变得更愉快,但是你不需要任何强大的东西。

images 提示如果你正在购买一台用于 Windows 8 应用开发的电脑,我建议你把大部分的钱花在获得你能找到的最大的屏幕上。对于编写应用来说,没有什么屏幕是太大的——对我来说,最棒的是能够并排编辑 JavaScript 文件、HTML 文件和 CSS 文件,并且仍然有足够的空间来容纳 Visual Studio 界面的其余部分(我将很快向您展示)。你的下一个重点应该是记忆。最不重要的组件是 CPU:今天的 CPU 都非常强大,即使是更便宜的型号也能够应付 Visual Studio 的应用开发。

如果你想在承诺之前看看应用开发是什么样的,你可以获得 Windows 8 的 90 天免费试用。你可以在这里获得 Windows 8 评估版:[msdn.microsoft.com/en-us/evalcenter](http://msdn.microsoft.com/en-us/evalcenter)。点击Windows and Platform Development部分的Release链接,你会找到你需要的东西。

您需要创建一个 Microsoft 帐户来获得评估,但无论如何您都需要一个帐户来设置为开发人员。Microsoft 帐户可以免费创建,如果您还没有帐户,可以按照说明进行操作。

images 提示如果你要从早期版本的 Windows 系统升级,微软会提供相当不错的折扣。如果你是学生或者你在一家和微软有协议的公司工作,你也可以得到一份便宜的拷贝。很少有人需要支付 Windows 的全零售价,问问周围的人看看你是否在微软无数计划中的一个之内总是值得的。

准备好 Visual Studio

您还需要一份 Visual Studio 2012。正如我在前一章提到的,微软使 Visual Studio 的一个版本完全免费。它没有一些付费版本包含的所有测试和集成功能,但你不需要这些来创建应用。在所有其他方面,Visual Studio 的免费版本功能齐全,并能满足您的所有需求。如果您有不同版本的 Visual Studio 2012,请不要担心。您不需要您的版本中的所有功能,但是本书中的所有说明和示例都可以工作,没有任何问题或修改。

您可以从[www.microsoft.com/visualstudio/11/en-us/products/express](http://www.microsoft.com/visualstudio/11/en-us/products/express)下载 Visual Studio 安装程序。有几种不同的风格可供选择,每一种都可以用来开发特定类型的应用。对于应用开发,您需要 Visual Studio Express 2012 for Windows 8。这些口味的名字非常相似,所以一定要下载正确的。

像安装任何其他应用一样安装 Visual Studio。虽然该软件可以免费使用,但您需要激活它,这需要您之前创建的 Microsoft 帐户。完成这个过程后,您将得到一个 Visual Studio 的激活码。你还需要一个开发者许可,这也是免费的。当您第一次启动 Visual Studio 2012 时,系统会提示您获取许可证,这只需要一秒钟的时间,并且同样需要您之前创建的 Microsoft 帐户。

可选设备

一台 Windows 8 PC 和 Visual Studio 是你编写应用所需要的全部,但是如果你有一台带有方位传感器的设备,一些应用功能就更容易理解了。正如您将在本书的第二部分中了解到的,对用户定位设备的方式做出响应是 Windows 8 的一项重要功能,虽然您可以使用 Visual Studio 模拟这种效果,但没有什么可以替代真实的设备。同样,一个带触摸屏的设备将让你开发和测试触摸屏交互——另一个关键的应用功能。这是你可以模拟的其他东西,但是模拟不能代替真实的东西。

images 提示我用的是戴尔 Latitude Duo,它是平板电脑和笔记本电脑的奇怪混合体。你可以买到便宜的二手货,它们有一个像样的触摸屏和一个方向传感器(尽管这种驱动程序必须从传感器制造商那里找到,而不是从戴尔那里)。我不建议在 Duo 上编码,这对于舒适来说有点太慢了,而且缺少 RAM,但它是一个相当不错的测试机器,我不会没有它。

开始使用

一旦你安装了 Windows 8 和 Visual Studio,你就可以开始了。在本节中,我将向您展示如何为一个应用创建一个 Visual Studio 项目,并简要介绍您将使用的工具和功能。如果您使用过另一种集成开发工具,您将会认识到 Visual Studio 的许多关键特性。

创建 Visual Studio 项目

要创建一个新的应用项目,您可以单击 Visual Studio 起始页上的New Project链接(当您第一次启动 Visual Studio 2012 时显示),或者从File菜单中选择New Project

images 注意实际上是FILE菜单,因为微软已经决定在 Visual Studio 中以大写字母显示菜单,尽管这给人的感觉是你的开发工具在对你大喊大叫。我将只是参考正常情况下的菜单。

你会看到New Project对话框,如图图 2-1 所示。Visual Studio 包括一些模板,可以帮助您开始不同种类的项目,这些模板显示在对话框的左侧。可用的模板集因您使用的 Visual Studio 版本而异。该图显示了可用于 Visual Studio Express 2012 for Windows 的模板,该模板支持四种用于创建应用的编程语言。对于每种语言,都有一些预先填充的模板,用于创建不同的项目。

images

图 2-1。Visual Studio 新建项目对话框窗口

我明白为什么微软包括这些模板,但它们是相当无用的。对于新程序员来说,面对一个空项目和一个闪烁的光标可能有点令人担忧,但是他们放入这些模板的代码并不是很好,并且除了最简单和最琐碎的项目之外,很少是你想要的那种东西。

images 提示 Visual Studio 支持彩色主题。大多数 Visual Studio 版本的默认主题是深色主题,主要是黑色,在屏幕截图中显示不出来。我已经切换到灯光主题,这就是为什么图中的New Project对话框看起来和你在屏幕上看到的不一样。我通过从Tools菜单中选择Options并改变Environment部分的Color Theme设置来改变主题。

导航到Templates > JavaScript部分的Blank App模板。Blank Template创建一个几乎为空的项目,其中只有运行应用所需的文件,而不会产生任何错误。这是我将在整本书中使用的模板,当我说我创建了一个新项目时,这将永远是我使用过的模板。

在名称字段中输入NoteFlash。这是你将在第三章中创建的第一个应用的名称。在这一点上,我不打算做任何严肃的开发,但在本章结束时,你会明白 Windows 8 应用项目中的移动部分是什么,然后你会看到它们如何在第三章中使用。

接下来,单击Browse按钮为您的项目选择一个位置。你把文件保存在哪里并不重要,只要你以后能再次找到它们。最后,单击OK按钮创建项目。Visual Studio 在生成文件和配置内容时会磨蹭一会儿,然后你会看到项目的初始视图,如图图 2-2 所示。

images

图 2-2。新项目的初始 Visual Studio 视图

在接下来的几节中,我将向您展示将在应用开发中使用的最重要的 Visual Studio 特性。其中一些可能有点显而易见,但请继续关注我,因为 Visual Studio 提供了一个紧密集成的开发环境,了解各个部分如何协同工作是很有用的。

运行项目

开始一个新项目的最好地方是运行它,看看它看起来怎么样。在开发过程中运行应用时,您有三种选择。第一种是在你用来写代码的同一台 PC 上运行应用,即本地机器。第二种方法是在 Visual Studio 附带的模拟器中运行应用。最后一个选项是在另一台设备上运行该应用。

在这三个选项中,模拟器是最有用的。在您正在使用的设备上运行应用的问题是,Windows 8 应用是全屏的,它们覆盖了 Visual Studio 和桌面的其余部分。你可以在应用和桌面之间切换,但这是一个笨拙的过程,尤其是当你试图调试某种问题或错误时。

在另一台设备上运行该应用可能非常有用。你需要在设备上下载并安装 Visual Studio 2012 的远程工具,你可以在[msdn.microsoft.com/en-gb/windows/apps/br229516](http://msdn.microsoft.com/en-gb/windows/apps/br229516)从微软获得。当我想测试某个功能或问题与我的开发机器所不具备的硬件功能(例如触摸屏)之间的关系时,我发现远程运行应用的能力非常有用。我不在常规开发中使用它,因为 Visual Studio 需要几秒钟来打包应用并将其传输到远程设备,过一会儿这就变得令人厌倦了。

images 提示你不能像使用传统的 Windows 桌面应用那样,将一个应用复制到另一个设备上。微软非常热衷于使用 Windows Store 来部署应用,在你准备好发布之前,最简单的设备测试方法是使用远程工具。

这就剩下第三个选项:Visual Studio 模拟器。这是我在开发过程中测试应用的方式,我建议你也这样做。该模拟器是真实 Windows 8 设备的忠实再现,您可以使用它来模拟一些重要的设备特征,包括设备方向、GPS 位置、触摸和手势输入以及不同的屏幕大小和分辨率。然而,它并不完美,而且有些应用功能你无法在模拟器中正确测试。我会在适当的章节中指出这些特性,这样你就知道如何使用其他方式来运行应用。

您可以使用 Visual Studio 菜单栏选择您希望的应用运行方式。默认情况下,Visual Studio 会在本地机器上运行一个新的项目,但是你可以通过点击图 2-3 中按钮旁边的箭头来改变。该按钮显示的文本反映了您所做的选择,因此它可能与图形不完全匹配,但您不会错过该按钮,因为它旁边有一个大的绿色箭头。(您需要单击以更改设置的箭头是按钮文本右侧的小向下箭头,而不是绿色箭头)。

images

图 2-3。选择 Visual Studio 运行应用的方式

从下拉菜单中选择Simulator选项,如图所示。做出选择后,单击绿色箭头或按钮文本启动应用。Visual Studio 将启动模拟器,安装您的应用,并开始运行它。你用来创建项目的Blank App Visual Studio 模板确实只生成了一个应用的基本结构,所以目前没有太多东西可看——只有一个黑色的屏幕,左上角有一小行文字写着Content goes here。暂时忽略这个应用,看看模拟器窗口右边的按钮,我已经在图 2-4 中显示了。你也可以通过从 Visual Studio Debug菜单中选择Start Debugging来启动应用——效果是一样的。我将在本章的后面回到 Visual Studio 调试器。

images 注意当我告诉你启动应用时,我的意思是你应该选择Start Debugging菜单项或点击工具栏上的按钮,以便使用调试器。有几个特性需要在没有调试器的情况下进行测试,但是我会在您使用它们的时候说明这一点。在所有其他情况下,您应该确保调试器正在运行。

images

图 2-4。Visual Studio 模拟器

我在模拟器旁边放大显示了按钮。这些按钮允许您使用模拟器以不同的方式与应用交互,并更改模拟设备的方向。

模拟器按钮按相关功能分组。第一组改变输入法,允许你用鼠标模拟触摸输入。当我向你展示如何处理触摸手势时,你会在第十七章中看到这些按钮是如何工作的。

第二组允许您模拟顺时针和逆时针旋转设备。我在第六章中使用这些按钮向你展示如何创建在设备方向改变时适应的布局。

第三组中唯一的按钮改变屏幕分辨率和像素密度,我在第六章中也探讨了这一点。第四组中也只有一个按钮,用于模拟 GPS 数据。在《??》第二十九章中,当我向你展示如何创建具有位置感知的应用时,你会看到这是如何使用的。最后一个按钮组可以让你截屏模拟器显示的任何内容。我在这本书里不用这些按钮。

模拟器使用提示

模拟器的工作方式是在您的开发机器上创建第二个桌面会话,并将其显示在一个窗口中。这是一个巧妙的技巧,但是它有一些值得了解的副作用。如果你有任何问题,有些是有用的,有些是值得注意的。

首先,当您使用 Visual Studio 启动一个应用时,应用包会在本地计算机上安装和执行,并显示在模拟器中。这意味着,如果您愿意,您可以导航到模拟器的开始屏幕或本地计算机上的真正开始屏幕,并在没有 Visual Studio 的情况下启动应用。如果你在运行应用时遇到问题——这种情况时有发生——你通常可以通过进入开始屏幕,找到你正在开发的应用,然后卸载它(右击应用的磁贴并选择Uninstall)来解决问题。

第二,一些提供与 Windows 功能集成的应用功能最容易从桌面测试,例如文件激活,我在第二十四章中描述了它。因为 Visual Studio 模拟器正在运行常规的 Windows 会话,所以当您第一次在模拟器中激活桌面时,您设置为自动运行的所有应用都会启动。我发现这导致了一些问题,特别是对于那些希望独占存储位置或管理硬件的应用。我最讨厌的例子是让我重新映射鼠标按钮的软件——当我在模拟器中切换到桌面时,该软件的第二个实例会自动启动,并使我的鼠标无法使用,直到我杀死其中一个进程(由于缺少可用的鼠标,这项工作变得更加复杂)。这些通常不是终端问题,但是如果您在使用模拟器时开始看到奇怪的问题,请记住它运行一个完整的 Windows 会话,包括所有好的和坏的方面。

最后,模拟器创建和显示 Windows 会话的方式意味着,如果在模拟器启动时连接到 VPN,它将不起作用。您可以在模拟器启动后激活 VPN ,一切都会好的。

控制应用执行

启动应用后,Visual Studio 界面会发生变化,为您提供控制其执行的选项。工具栏上出现一排按钮,如图图 2-5 所示。

images

图 2-5。在 Visual Studio 中控制应用执行的按钮

这些按钮可以暂停、停止和重启应用。如果您对项目进行了任何更改,重启按钮将确保在开始执行之前更新应用。第四个按钮(其图标是一个带有两个形成圆圈的箭头的闪电)是重新加载按钮。此按钮可快速重新加载任何已更改的文件,而无需完全停止和重新启动应用。这个特性在开发过程中很有用,例如,当我微调 CSS 布局时,我发现它最有用。如果您在项目中添加或移除文件,或者进行需要重新安装应用的更改(例如更改清单,我将很快介绍),则无法重新加载—在这些情况下,Visual Studio 将提示您执行重新加载。

images 提示按钮由 Visual Studio Debug菜单上执行相同功能的项目补充。我倾向于使用工具栏按钮,但它们之间没有区别。

探索项目

默认情况下,Solution Explorer位于 Visual Studio 窗口的右上角,尽管您可以移动它,甚至完全分离它。按照 Visual Studio 的说法,一个解决方案是一个或多个项目的包装。本书中的所有示例应用都包含在单个项目中,并且在很大程度上,解决方案是 Windows 8 应用之前的开发实践的延续。

Solution Explorer提供了对项目中所有文件的访问。Solution Explorer显示的大部分条目是 Visual Studio 在您创建项目时生成的文件夹和文件,其他条目是对您的应用所依赖的文件的引用。你可以在图 2-6 中看到所有文件的更多细节,我将在接下来的章节中解释每个文件的作用和它们的初始内容。

images

图 2-6。解决方案浏览器,显示 Visual Studio 为空白应用模板生成的文件

要编辑项目中的文件,只需在解决方案资源管理器窗口中双击其条目。您还可以使用图标行正下方的搜索栏在解决方案资源管理器中按名称搜索文件,这在大型复杂的项目中非常有用。(图标本身允许您导航和配置解决方案资源管理器)。

images 提示Solution Explorer窗口下面的Properties窗口。这个窗口在其他种类的开发项目中很重要,但是它对于 JavaScript 应用开发没有任何价值,您可以安全地关闭它,为Solution Explorer窗口腾出更多空间,这对大型复杂的项目很有用。您可以通过View菜单重新打开任何已关闭的窗口(通过Other Windows菜单项可以打开一些不常用的窗口)。

探索项目参考

Solution Explorer中显示的第一个条目是项目参考。共有六个参考文件,在图 2-7 中的Solution Explorer中可以看到完全展开的References项。

images

图 2-7。解决方案浏览器中显示的项目引用

引用由 JavaScript 和 CSS 文件组成。这是很重要的一点,因为它展示了 Windows 8 应用对 web 技术的支持是如何深入人心的——以至于用 JavaScript 和 HTML 编写的应用可以使用 Internet Explorer 10 来执行。用户并不知道浏览器被用来运行应用,但它是执行你的应用的引擎。

理解 CSS 引用

References部分中的 CSS 文件包含 JavaScript 应用使用的默认样式。有两个主题可以应用到您的应用中。ui-dark.css文件包含一个在深色背景上有浅色文本的应用的样式。ui-light.css文件包含一个应用的样式,该应用在浅色背景上有深色文本。大多数 Visual Studio 版本默认使用深色主题,并且在default.html文件中选择该主题,稍后我将介绍该主题。

大多数应用以其中一个主题开始,然后出于自定义或品牌化目的覆盖选定的样式,但您不必使用任何一个文件,您可以自己创建应用中使用的所有样式。然而,这样做需要大量的工作,并且通过使用一个预定义的主题作为起点,您可以受益于与其他 Windows 8 应用和默认应用外观的一致性。

了解 JavaScript 参考

base.jsui.js文件包含创建应用的部分 API。它们组成了 WinJS API ,其中包含了特定于 JavaScript 应用开发的对象和函数。还有 Windows API ,它包含所有可用于应用开发的编程语言之间共享的对象和函数。

WinJS API 包含许多有用的功能,我在本书的第二部分和第三部分中解释和演示了这些功能。由于这些都是 JavaScript 文件,你可以打开它们,查看它们的内容,设置调试器断点,并大致了解一下微软是如何实现不同特性的。(Windows API 不是用 JavaScript 编写的,也没有源代码形式。)

粗略地说,ui.js文件包含了 WinJS UI 控件的代码,这是本书第三部分的主题,您可以使用它从标准 HTML 元素创建复杂的用户界面控件,就像您使用 jQuery UI 这样的库一样(尽管 UI 控件是专门针对 JavaScript 应用开发的)。base.js文件包含用于创建应用基本结构的对象。我将在本书的第二部分中向您介绍这些对象,因为我将涉及导航和数据绑定等主题。

另外两个文件base.strings.jsui.strings.js,包含显示给用户的文本,针对不同的语言和地区进行了本地化。您不需要直接处理这些文件,因为它们的内容是通过base.jsui.js文件自动消费的。

探索默认文件

当您使用Blank App模板创建 Visual Studio 项目时,会生成三个默认文件,它们构成了应用的基本结构和初始内容。这些文件是default.html(在项目的根文件夹中)default.css(在css文件夹中)default.js(在js文件夹中)。这些文件也描述了一个 JavaScript Windows 8 应用项目的基本组织——根文件夹中的 HTML 文件、css文件夹中的 CSS 文件和js文件夹中的 JavaScript 文件。这种结构是一种建议,并不是强制的,你可以自由地以你喜欢的任何方式组织你的文件——你将在本书中看到我这样做,以使示例应用更容易理解。在接下来的几节中,我将向您展示每个文件的初始内容,并解释关键特征,指出我将在本书后面详细讨论的章节。在本章中我不打算修改这些文件,但是在第三章中,你将使用它们来构建你的第一个真正的应用。

images 注意你可以重命名或替换这些文件,但我在本书中使用它们作为示例应用,这样你就知道哪些文件是由 Visual Studio 创建的,哪些文件是我添加的。

了解默认 HTML 文件

default.html文件在应用启动时加载,负责导入default.cssdefault.js文件,这是使用标准的 HTML linkscript元素完成的。这些元素也用于导入References文件,为您的应用提供对 WinJS API 和浅色或深色主题 CSS 样式的访问。您可以在清单 2-1 中看到这些元素,它显示了 Visual Studio 生成的default.html文件的内容。

清单 2-1 。由 Visual Studio 创建的 default.html 文件的初始内容

`

<html> <head>     <meta charset="utf-8" />     <title>NoteFlash</title>

**    **
**    **
**    **
**         **
**    **
**    **

    

Content goes here

`

我已经突出显示了导入项目和引用文件的元素。当您构建一个应用时,您会想要创建新的 JavaScript 和 CSS 文件,并使用linkscript元素将它们纳入范围,就像在 web 应用中一样。这些文件是按照它们被指定的顺序加载的,这意味着你不能在一个不同的 JavaScript 文件中调用一个函数,除非那个文件的script元素已经被处理,并且在一个 CSS 文件中定义的样式可以被随后导入的文件中的样式覆盖。如果你想改变你的应用的主题,你可以改变文档中的第一个link元素,这样它就会导入ui-light.css文件。

images 提示对于大多数应用来说,default.html文件通常是一个占位符,其他内容将被导入其中,这意味着该文件往往会保持非常简短。我将在第五章中解释内容是如何导入的,以及这种方法如何适应应用开发。

了解默认 CSS 文件

当第一次创建时,default.css文件包含一些占位符,你可以在清单 2-2 中看到。这个文件中没有定义实际的样式,但是请记住,ui-dark.cssui-dark.css文件提供了许多默认的样式。甚至当你开始为你的应用定义自定义样式时,你会发现你最终使用的 CSS 比一个类似的 web 应用要少,因为主题文件包含了基本的东西。

清单 2-2 。Visual Studio 创建的 default.css 文件的初始内容

`body {
}

@media screen and (-ms-view-state: fullscreen-landscape) {
}

@media screen and (-ms-view-state: filled) {
}

@media screen and (-ms-view-state: snapped) {
}

@media screen and (-ms-view-state: fullscreen-portrait) {
}`

在应用中使用@media规则来创建适应屏幕方向和分配给应用的空间量的布局(应用可以全屏显示或分配给屏幕的大部分,称为填充模式,或分配给屏幕的一小部分,称为捕捉模式)。在第六章的中,我解释了所有这些是如何工作的,并提供了不同种类的适应性应用布局的演示。

了解默认的 JavaScript 文件

default.js文件为应用带来了活力,它使用标准的 HTML DOM 和各种特定于应用的 API 来管理布局并与 Windows 集成。Visual Studio 创建的代码,如清单 2-3 所示,并没有做很多事情,正如我在第十九章中解释的那样,需要修改才能真正有用。在第十九章之前,我将在我的示例应用项目中使用该文件的简化版本,届时我将详细解释应用的生命周期,并向您展示如何以一种有用且有意义的方式响应 Windows 发送的系统事件。

清单 2-3 。Visual Studio 创建的 default.js 文件的初始内容

`// For an introduction to the Blank template, see the following documentation:
// http://go.microsoft.com/fwlink/?LinkId=232509
(function () {
    "use strict";

WinJS.Binding.optimizeBindingReferences = true;

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !==
                activation.ApplicationExecutionState.terminated) {
                // TODO: This application has been newly launched. Initialize
                // your application here.
            } else {
                // TODO: This application has been reactivated from suspension.
                // Restore application state here.
            }
            args.setPromise(WinJS.UI.processAll());
        }
    };

app.oncheckpoint = function (args) {
        // TODO: This application is about to be suspended. Save any state
        // that needs to persist across suspensions here. You might use the
        // WinJS.Application.sessionState object, which is automatically
        // saved and restored across suspension. If you need to complete an
        // asynchronous operation before your application is suspended, call
        // args.setPromise().
    };

app.start();
})();`

您现在不必理解这个文件的内容——我将在本书的后面解释所有的对象和方法调用。然而,值得理解的是,应用是使用标准 JavaScript 编写的,因此您熟悉的基本对象和数据类型是可用的,标准 DOM API 也是如此,例如,您可以使用它来定位应用标记中的 HTML 元素并注册事件处理程序。在接下来的章节中,你会看到无数的例子。

理解清单

解决方案资源管理器中最后一个感兴趣的文件是package.appxmanifest,它是应用的清单文件。清单向 Windows 应用商店和 Windows 描述您的应用。清单中的一些信息提供了基本信息,例如用于在“开始”菜单上显示应用的图像。我会在第四章的中给你展示这些设置。

清单中的其他信息更复杂。例如,您使用清单来指示您的应用需要访问的文件系统位置,以及您的应用与哪些 Windows 服务集成。Windows 使用这些信息中的一部分来激活和启动您的应用以响应用户操作,而另一部分则在 Windows 应用商店中呈现给用户,以便他或她可以评估您的应用带来的安全风险。我在整本书中解释了更复杂的设置,但是你可以了解我在第二十二章中给出的例子(在那里我向你展示了如何声明你想要使用的文件系统位置)和在第二十三章 - 26 章中给出的例子,在那里我向你展示了如何实现 Windows 集成契约。

清单是一个 XML 文件,但是您不必直接处理 XML。如果双击package.appxmanifest文件,Visual Studio 将打开一个可视化编辑器,将清单显示为一系列要填写的表单,由一系列选项卡组织。你可以在图 2-8 的中看到其中一个标签。(如果您确实想要查看 XML,您可以右击Solution Explorer中的文件,并从弹出菜单中选择View Code

images

图 2-8。Visual Studio 清单编辑器的应用 UI 标签

images 提示Solution Explorer显示有一个文件我还没有提到:NoteFlash_TemporaryKey.pfx文件。此文件包含一个数字证书,用于在开发过程中自动签署应用包。在发布过程中,您用一个真实的证书替换这个文件,我在本书的第五部分中对此进行了描述。

Visual Studio 工具

在这一节中,我将简要介绍 Visual Studio 的不同部分,您将使用它们来开发应用。我不会详细介绍,因为 Visual Studio 就像任何其他 IDE 一样,您以前已经无数次使用过文本编辑器和调试器。本章的这一节向您展示了如何找到主要的 IDE 构造块,并指出了您自己可能还没有发现的有用功能。

Visual Studio 编辑器

双击Solution Explorer窗口中的文件,打开编辑器。编辑器做了所有你能想到的事情。编辑器可以适应文件格式,因此您可以无缝地编辑 HTML、CSS 和 JavaScript 文件,并且它可以自动完成所有这三种类型的文件。它对语法进行颜色编码,它知道你可以在 HTML 文件中嵌入 CSS 和 JavaScript,并做出相应的响应。它是一个非常好的程序员编辑器,它做了所有其他程序员编辑器做的事情。

您可以通过从 Visual Studio Options菜单中选择Tools来配置如何在编辑器中处理每种语言。展开Text Editor部分,您将看到一个适用于所有语言的General项和一组每种语言的项。您可以控制颜色编码、缩进、自动完成和许多其他特性,以根据您的喜好定制编辑器。

一个值得指出的特性是 Visual Studio 编辑器出色的文本搜索功能。键入Control+F会打开一个编辑器内搜索框,您可以使用它来定位当前文件中的项目。每个编辑器窗口都可以使用自己的搜索框进行独立搜索,或者您可以单击向下箭头来扩展搜索范围。我经常使用搜索功能,但当我使用其他 ide 或编辑器时,我很怀念它。

JavaScript 控制台

启动应用时,Visual Studio 界面会发生变化。其中一个变化就是会出现JavaScript Console窗口。(如果它不可见或者如果您不小心关闭了它,您可以从Visual Studio Debug菜单上的Windows项手动打开此窗口,但只能在应用运行调试器时打开。)

JavaScript Console窗口是最有用的 Visual Studio 功能之一,用于跟踪应用中的问题,它扮演着几个不同的角色。如果从 JavaScript 代码中调用console.log方法,您指定的字符串将会显示在JavaScript Console窗口中,正如您所期望的那样。

您还可以执行任意 JavaScript 代码,探索应用定义的变量和函数。作为一个简单的例子,我将修改default.html文件,使p元素具有一个id属性,如清单 2-4 所示。

清单 2-4 。向 p 元素添加 id 属性

`

<html>` `<head>     <meta charset="utf-8" />     <title>NoteFlash</title>


    
    
    


    
    

**    

Content goes here

** `

我突出显示了我修改的元素。启动应用,找到JavaScript Console窗口,在窗口底部的文本框中输入以下内容:


paraElem.style.fontSize = "40pt"


如果你点击回车键,你会看到模拟器窗口中显示的内容被调整为 40 磅,如图 2-9 所示。

images

图 2-9。使用 JavaScript 控制台窗口定位和配置 HTML 元素

在这个简单的例子中,有几个要点需要注意。首先,JavaScript 控制台为您提供了对应用实时状态的访问。你可以调用任何函数,读取或修改任何变量,改变任何样式。唯一的限制是 JavaScript 变量和函数需要成为全局名称空间的一部分——我将在第三章的中回到这个话题。

第二点是,Internet Explorer 允许您通过将 HTML 元素的id属性值视为全局变量来定位 HTML 元素。这不是一个标准功能,尽管其他一些浏览器也有类似的功能。我在这本书里经常使用这个特性,这意味着我不必使用冗长的 DOM API(尽管需要明确的是,如果你愿意,你可以在你的应用代码和窗口中使用 DOM API)。paraElem变量返回表示 HTML 文档中p元素的 DOM 对象,我可以使用标准的HTMLElement对象和属性来操作该元素——在本例中,更改style.fontSize属性的值来增加文本的大小。

在应用开发中使用浏览器怪癖

能够通过 HTML 元素的id属性值来定位它们是非常有用的,但是这不是标准的 DOM 特性,所以它被归类为浏览器怪癖。在 web 应用开发中,您将学会避免浏览器怪癖,因为您需要支持各种各样的浏览器,并且您不想将您的应用与特定于浏览器的功能绑定在一起。

在应用开发的世界里,事情是不同的。将用于运行 JavaScript Windows 8 应用的唯一浏览器是 Internet Explorer,安装新浏览器,如 Chrome 或 Firefox,对 Windows 8 应用没有任何影响。

这意味着您可以毫无畏惧地使用 Internet Explorer 的怪癖,尽管您可能会质疑这样做是否是好的做法。几个月来,我一直在这个问题上反反复复。最初,我坚持使用标准的 DOM API,尽管我觉得它冗长又烦人。我通过使用WinJS.Utilities特性让自己的生活变得更轻松,这就像是 jQuery 的简化版。(你可以在应用项目中使用 jQuery,但我不会在本书中使用——我必须承认,我一般不会在我的 Windows 8 应用项目中使用它。即使我是 jQuery 的超级粉丝,也没有太大的必要——如果你对我的 jQuery 粉丝身份有任何疑问,可以看看我的专业 jQuery 书籍,由 Apress 出版。)

不过,最终我发现我忽略了重要的一点:Windows 8 应用开发不是 web 应用开发,尽管它有着共同的根源和技术。我是出于习惯而避免那些方便省时的怪癖,而不是因为它们会带来问题。事实上,一些怪癖有助于避免问题——例如,我发现使用全局变量定位元素比使用 DOM API 更不容易出错。

你可以自由选择最适合你的方法——但我建议你审视自己的决定,以确保你不是不必要的教条主义者,就像我开始 Windows 8 应用开发时一样。

在本书中,我通过元素的全局属性来定位元素——有两个例外。当我想清楚地表明我在一个例子中定位一个 HTML 元素时,我使用 DOM API,当我想用 CSS 选择器选择多个元素时,我使用类似 jQuery 的WinJS.Utilities特性(我在第三章的中介绍了该特性,并在第十八章的中详细描述了该特性),这样我可以在一个步骤中对所有元素执行操作。

DOM Explorer

启动应用时出现的另一个窗口是DOM Explorer。这允许您探索应用的 HTML 结构,查看单个元素的框布局属性,并跟踪 CSS 优先规则如何应用于确定元素的样式。DOM Explorer窗口在 Visual Studio 的编辑器区域中显示为一个选项卡。我发现自己经常不经意地关闭DOM Explorer窗口——如果你也这样做,你可以从 Visual Studio Debug菜单的Windows项中重新打开它。

我觉得最有用的功能是能够通过在应用中点击 HTML 文档中的元素来定位它。这是 web 应用开发工具中非常标准的东西,能够在应用开发中使用也很好。在DOM Explorer窗口的顶部是Select Element按钮,我在图 2-10 中高亮显示了这个按钮。

images

图 2-10。使用 DOM 浏览器窗口的选择元素功能

点击此按钮并切换到模拟器窗口。当您移动到元素上时,您会看到它们被突出显示,并且它们的详细信息显示在DOM Explorer窗口中。单击您感兴趣的元素,使用 DOM Explorer 查看它的所有特征、布局细节和 CSS 样式。

调试器

Visual Studio 包含我最喜欢的调试器。我发现它比我用过的任何其他调试器都更快、更可靠,我喜欢它如此完美地集成到 Visual Studio 的其余部分的方式。我不会向您提供详细的调试教程,因为您已经知道如何在 web 应用项目中使用调试器,但是在接下来的部分中,我将向您展示 Visual Studio 提供的有用的附加功能。

在您自己的代码中设置断点

如果你想在你可以修改的代码中设置一个断点(相对于微软的 JavaScript 文件),最简单的方法是使用 JavaScript debugger关键字,如清单 2-5 所示,我已经将这种断点添加到了default.js文件中。

清单 2-5 。使用 debugger 关键字创建断点

`// For an introduction to the Blank template, see the following documentation:
// http://go.microsoft.com/fwlink/?LinkId=232509
(function () {
    "use strict";

WinJS.Binding.optimizeBindingReferences = true;

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !==                 activation.ApplicationExecutionState.terminated) {
                // TODO: This application has been newly launched. Initialize
                // your application here.
            } else {
                // TODO: This application has been reactivated from suspension.
                // Restore application state here.
            }
            args.setPromise(WinJS.UI.processAll());
**            for (var i = 0; i < 5; i++) {**
**                console.log("Counter: " + i);**
**                if (i > 2) {**
**                    debugger**
**                }**
**            }**
        }
    };

app.oncheckpoint = function (args) {
        // TODO: This application is about to be suspended. Save any state
        // that needs to persist across suspensions here. You might use the
        // WinJS.Application.sessionState object, which is automatically
        // saved and restored across suspension. If you need to complete an
        // asynchronous operation before your application is suspended, call
        // args.setPromise().
    };

app.start();
})();`

我在文件中添加了一个简单的for循环,以便稍后我可以演示另一个特性。当计数器的值大于 2 时,断点被触发,并且应用执行的控制被转移到调试器。启动 app,你会看到 Visual Studio 在文本编辑器中高亮显示执行点,如图图 2-11 所示。

images

图 2-11。激活用调试器关键字创建的断点

您使用标准的Step IntoStep OverStep Out命令来控制调试器的执行。Visual Studio 将这些作为工具栏上的按钮和Debug菜单中的项目提供。您可以通过键入F5或点击工具栏上的Continue按钮来恢复应用的正常执行。

在其他代码中设置断点

使用debugger关键字要求您能够修改文件,这与显示在Solution Explorer窗口的References部分的 JavaScript 文件不同。对于这种文件,您必须使用 Visual Studio 对断点的支持。通过右键单击希望调试器中断的语句,并从弹出的Breakpoint菜单中选择Insert Breakpoint,可以创建一个断点。断点在文件的空白处显示为红色圆圈。当您想要移除断点时,右键单击该语句并从断点菜单中选择Delete Breakpoint

Visual Studio 断点非常复杂,您可以创建仅在特定情况下触发的条件断点。尽管这个特性很好,但当我试图弄清楚微软是如何在base.jsui.js文件中实现一个特性时,我发现自己尽可能地使用debugger关键字并依赖 Visual Studio 断点。

监控变量

我将for循环放入清单 2-5 的代码中的原因是为了演示 Visual Studio 让您看到变量如何变化的方式——当您使用debugger关键字或 Visual Studio 断点时,这一功能的工作方式完全相同。

启动应用,当调试器取得控制权时,将鼠标悬停在源代码中的变量i上。您将看到显示的当前值。值的右边是一个小图钉图标——单击这个, Visual Studio 将创建一个显示当前值的小窗口。键入F5继续执行,等待下一次循环迭代再次触发调试器。你会看到值窗口被更新,显示变量的当前值,如图 2-12 所示。

images

图 2-12。使用 Visual Studio 监控变量值

如果单击窗口中的值,将出现一个文本框,允许您更改分配给变量的值。当您键入F5继续执行时,将使用新值。

使用 JavaScript 控制台

JavaScript Console窗口在调试时也非常有用。当应用的执行暂停时,JavaScript Console的范围是断点的上下文。这意味着您可以引用局部定义的变量和调用函数,而不仅仅是全局定义的。例如,启动应用,等待调试器接管控制权,在JavaScript Console窗口底部的文本框中键入字母i,然后按下Enter。您将看到显示的当前值。如果您使用控制台为变量赋值(例如,输入i = 5并按下Enter),您的值将在应用恢复执行时使用。

JavaScript Console也可以用来探索复杂的物体。例如,在文本框中键入args,然后按下Enter。单击响应左边的箭头,您将看到由args对象定义的属性和函数,该对象被传递给我放置了debugger关键字的函数。暂时不要担心这个对象——我将在第十九部分详细解释它的重要性。你可以在图 2-13 中看到JavaScript Console显示对象的方式。

images

图 2-13。用 JavaScript 控制台窗口探索一个对象

我经常使用这个功能。它不允许您通过单击值来修改值,但是您可以输入 JavaScript 语句,为对象或其属性分配新值,并且还可以执行其功能。

总结

在本章中,我创建了一个初始的 Visual Studio 项目,并使用它向您展示了正在开发的应用的结构,突出显示了对 Microsoft 库和默认 HTML、JavaScript 和 CSS 文件的引用。我还简要介绍了您将在应用开发中使用的 Visual Studio 关键特性。Visual Studio 是一个庞大而复杂的工具,但我向您展示的部分将让您在开始 Windows 8 应用开发时处于良好的地位。在下一章中,我将在我创建的项目的基础上,展示利用您现有的 web 应用开发技能和知识,您可以在应用开发中走多远。

三、您的第一款 Windows 8 应用

在这一章,我将开始使用 JavaScript 和 HTML 构建一个真正的应用。这不是一个特别复杂的应用,但它演示了许多你需要理解的基本技术,并为后面的章节奠定了基础。

示例应用名为NoteFlash,,是我为自己使用而构建的。我最近开始学习弹钢琴,这个过程的一部分是学习读谱。我一直很难根据音符的位置来识别它们,所以我创建了一个小应用,就像一副闪存卡一样工作。这种方法效果很好——我一天要看几次音符,我开始越来越擅长看谱了。(如果有一种基于软件的技术可以用来提高我的实际演奏水平,我就会一路领先到卡内基音乐厅。)

除了向你介绍 Windows 8 的世界,NoteFlash应用还将展示你现有的 HTML 和 JavaScript 知识在 Windows 8 中有多少用处。当然,我会教你专门针对 Windows 8 应用的技能和技术,但是当你经历创建NoteFlash应用的过程时,你会看到基本的 Windows 应用与基本的 web 应用有多少共同之处。

在这一章中,我将构建应用的基本功能,然后我将在第四章中添加一些润色和介绍一些附加功能。在这个过程中,我还将使用一些特定于应用的特性,我将在本书的后面深入讨论这些特性。

了解应用结构

如果你知道最终产品是什么样的,那么创建NoteFlash应用的过程会更有意义。该应用有两个页面。第一个允许用户选择她想要测试的一组音符,如图 3-1 所示。选项将在左侧音符、右侧音符或两者上进行测试。

images

图 3-1。笔记选择页面

用户点击图中所示的三个按钮中的一个,显示第二页,如图图 3-2 所示。每个音符都显示在五线谱上,用户按下按钮来识别音符。在图 3-2 中显示的音符是一个C,例如,用户可以按下C按钮。

images

图 3-2。闪存卡页面

当用户看到所有的注释时,页面布局会改变。笔记名称按钮被几个导航按钮取代,如图图 3-3 所示。Again按钮测试用户对同一音符的选择,Back按钮返回选择页面。

images

图 3-3。向用户呈现导航控件

正如我所说,这是一个简单的应用。在接下来的小节中,我将带您完成我用来创建它的过程。同时,我将介绍一些关键的 Windows 8 概念,如导航、异常处理和数据绑定。我会在第四章给你演示如何完成 app。

重温示例应用项目

我将使用的 Visual Studio 项目是我在第二章中创建的项目。这个项目是使用项目名NoteFlashBlank App模板生成的,并且只包含 Visual Studio 默认添加的默认 HTML、JavaScript 和 CSS 文件。我在这个项目中做了两处修改:我在default.html文件的 HTML 元素中添加了一个 id 属性,在default.js文件中添加了一个for循环和debugger关键字。我将很快更改这两个文件的内容,替换这些添加的内容。

创建导航基础设施

从展示成品的图中可以看出,NoteFlash应用有几个不同的内容页面向用户显示。大多数 Windows 应用依赖于单页导航的 ?? 模式,类似于许多网络应用所采用的方法。

在这个模型中,运行时加载一个 HTML 母版页,然后根据需要将其他页面的内容插入到母版页中。对于NoteFlash app,我的母版页会是default.html(项目创建时 Visual Studio 生成的那个)。你可以在清单 3-1 中看到default.html的内容,这里我对前一章做了一个小改动。

了解不同的内容模型

为了避免混淆,在描述 Windows 应用时使用正确的术语非常重要。大多数应用都有多个页面,这些页面使用母版页作为内容容器来显示,这被称为单页内容模型。这样做的好处是,无论显示什么内容,都可以保留应用的状态,这使得编写应用的 JavaScript 部分变得更加容易。这种方法的缺点是 HTML 标记和 CSS 可能会更复杂,因为您必须确保不会无意中创建一个样式,例如,该样式的范围太广,会影响其他内容文件中包含的元素。

另一种方法是将所有内容文件完全分开,包含在单独的 HTML、JavaScript 和 CSS 文件中。这种方法被称为多页面模型,它使创建应用的 HTML 和 CSS 部分变得更加容易,但也使 JavaScript 变得更加复杂,因为每次加载一页内容并显示给用户时,应用的状态都会重置。

最后一个变化是单页应用。这是一个非常简单的应用,它只包含一页内容,通常是default.html文件。不需要加载其他内容,也不需要担心应用状态。我在本书的一些章节中使用单页应用进行简单的演示和小助手应用。

在很大程度上,单页内容模型是最有用的方法。在创建 HTML,尤其是 CSS 时,它需要一些小心,但它使 JavaScript 变得更简单,而且——正如您将了解到的——Windows 应用的复杂性通常在于代码,而不是样式或标记。在第五章中,我将回到单页内容模型,以及使其更容易使用的 API 特性。

清单 3-1 。default.html 的内容

`

<html> <head>     <meta charset="utf-8" />     <title>NoteFlash</title>


    
    
    


    
    

**    
** `

我用一个 div 元素替换了 Visual Studio 添加到default.html中的p元素(我在第二章中为其添加了一个id属性),该 div 元素的id属性值为pageFrame。这个元素将是我在单页模型中导入内容的容器,允许我向用户显示不同的内容。

images 提示应用的初始页面不必如此简单——你可以将本地元素与其他页面的元素以任何适合你的应用的组合混合在一起。我倾向于将这个页面用于整个应用中的通用内容,当我添加一个名为 AppBar 的 Windows 8 专用控件时,你会在第七章中看到。

定义代码

正如我在《??》第二章中解释的,Visual Studio 在创建新项目时会创建js/default.js文件,并在default.html文件中添加一个script元素,以确保该文件在应用启动时被加载。在本书中,我将使用default.js文件作为我的主要 JavaScript 文件,本章也不例外。

你可以在清单 3-2 中看到我对default.js文件所做的修改。我已经简化了代码,删除了一些我在本书后面才会描述的特性,并删除了注释。

清单 3-2 。修改后的 default.js 文件

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;

window.$ = WinJS.Utilities.query;
    window.showPage = function (url, options) {
        WinJS.Utilities.empty(pageFrame);
        WinJS.UI.Pages.render(url, pageFrame, options);
    };

app.onactivated = function (args) {
        showPage("/pages/selectorPage.html");
    };

app.start();
})();`

关于这段代码有很多需要解释的地方,尽管只有几条语句。由于这是我向您展示的第一个真正的 Windows 应用 JavaScript 文件,我将在接下来的部分中首先解释该文件的结构和内容。

处理全局名称空间

JavaScript 开发中最常遇到的问题之一是全局名称空间冲突。默认情况下,任何在函数之外创建的或者定义时没有使用var关键字的 JavaScript 变量都是一个全局变量,这意味着它可以通过应用中的 JavaScript 代码进行访问。对于 web 应用和 Windows 应用来说都是如此。有意义的变量名只有这么多,定义两个类似于datauser的变量并出现问题只是时间问题,尤其是当一个应用依赖于不同程序员的库时。代码的不同部分对有争议的变量的值和意义有不同的期望,结果可能是从稍微奇怪的行为到数据损坏。

使用自动执行功能

在清单 3-2 中,你可以看到 Windows 应用的三个约定中的两个,旨在减少名称空间污染。第一个是所有的 JavaScript 语句都包含在一个自执行函数中。通过将一个函数放在圆括号中,然后添加另一对圆括号,函数在被定义后立即被执行:

**(function() {**     // *... statements go here ...* **})();**

使用自执行函数的好处是,函数中定义的任何变量都是函数范围的局部变量,当函数完成时将被删除,从而保持全局命名空间清晰。Visual Studio 为您创建的任何 JavaScript 文件中都添加了自执行函数,我在本书中广泛使用了它们。我建议您在自己的代码中采用这种做法,因为这是帮助减少全局名称空间问题的最简单的方法之一。

使用严格模式

有助于保持全局名称空间清晰的第二个约定是使用严格模式,这是通过将"use strict"放在函数中实现的,如下所示:

**(function() {**     "use strict";     // ... statements go here ... **})();**

严格模式对 JavaScript 的使用方式施加了一些限制。其中一个限制是,您不能通过省略var关键字来隐式创建全局变量:

... var color1 = "blue";  // OK—scope is local to function color2 = "red";       // Not OK—this is a global variable ...

如果在使用严格模式时定义了一个隐式全局变量,Windows JavaScript 运行时将生成错误。使用严格模式是可选的,但这是很好的实践,它禁用了一些更令人困惑和奇怪的 JavaScript 行为。您可以通过阅读位于[www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf)的 ECMAScript 语言规范的附录 C 来获得严格模式实施的变化的全部细节。

了解 Windows 应用命名空间

既然你已经知道管理全局名称空间的重要性,那么清单 3-2 中的第一个常规语句就有意义了:

... window.$ = WinJS.Utilities.query; ...

这种说法只是一种方便。如果你读过我的 Pro jQuery 的书,你就会知道我是 jQuery 的忠实粉丝,我习惯于使用$快捷键来查询文档中的元素。我在了解 Windows API侧栏中介绍的WinJS API(应用编程接口)包含WinJS.Utilities.query方法,该方法对当前文档进行基于 CSS 选择器的查询,并支持一些基本的类似 jQuery 的操作。它不是 jQuery,但是对于大多数简单的任务来说已经足够接近了,我将在本书中通篇使用它。

images 提示你可以继续为你的 Windows 应用使用 jQuery(或者任何你喜欢的 JavaScript 库)。我在本书中保持简单,只使用内置的工具,但由于 JavaScript Windows 应用是使用 Internet Explorer 10 执行的,所以几乎任何编写良好的 JavaScript 库都可以正常工作。只要留意 Windows 8 与网络应用不同的地方就行了。一个很好的例子是用来表示 Windows 应用生命周期的一组事件。例如,您应该优先使用这些函数,而不是 jQuery 定义的ready函数,因为它们允许您正确地集成到操作系统中。我在第十九章中解释了这些事件。您可能还想看看我在本书第三部分中描述的 WinJS UI 控件——其中一些依赖于其他 WinJS 特性,如果您为这些特性使用其他 jQuery 库,您将不会获得最佳的 UI 体验。

我希望我的$快捷方式全局可用,所以我必须将其显式定义为window对象的属性(一个鲜为人知的事实是,所有 JavaScript 全局对象实际上都是window属性)。通过将WinJS.Utilities.query函数分配给window.$,我保持在自执行函数和严格模式的约束之内——这些约定都不是为了阻止你故意定义全局变量,只是为了防止你不小心这样做。

除了自执行函数和严格模式,微软帮助减少全局名称空间污染的第三种方式是支持名称空间,比如WinJS.Utilities:

... window.$ = **WinJS.Utilities**.query; ...

名称空间允许您以结构化的方式将相关的对象和函数组合在一起。在第四章中,我将向你展示如何创建你自己的名称空间。WinJS名称空间包含整个WinJS API,其中包括一个名为Utilities的子名称空间,它是用于 DOM 操作的类似 jQuery 的对象和方法的主目录。Utilities名称空间包含了query方法,它允许我使用 CSS 选择器定位 HTML 元素。命名空间创建按用途或公共功能分组的对象层次结构。

与 C#等语言中的命名空间不同,Windows app JavaScript 命名空间不限制对其包含的代码的访问,所有函数和数据值都可以通过命名空间全局使用。Windows 应用 JavaScript 名称空间都是关于使用结构来保持全局名称空间清晰。

了解 WINDOWS API

使用 HTML 和 JavaScript 编写 Windows 8 应用时,您可以访问许多不同的 API。首先,也是最明显的,是 DOM 和标准 JavaScript APIs。这些都是可用的,因为 JavaScript Windows 应用是使用 Internet Explorer 10 运行的,这意味着您可以访问 web 浏览器的所有功能(至少是 IE10 提供的功能)。当我在本章中构建NoteFlash应用时,你会发现你可以使用标准 HTML 元素和普通 JavaScript 创建一个基本的 Windows 应用。

您还可以访问 Windows API。这些是通过以Windows开头的名称空间访问的对象。这个 API 中的对象与 C#、Visual Basic 和 C++程序员可用的对象是相同的,但它们的出现是为了便于在 JavaScript 中使用。微软在让 JavaScript 成为 Windows 应用开发的一流语言方面做得相当不错,Windows API 是这一特性的核心。

最后一个 API 是WinJS,它包含特定于使用 HTML 和 JavaScript 编写的 Windows 应用的对象。在某些情况下,这些对象使得使用 Windows API 变得更加容易,但是大多数情况下,它们提供了只有 JavaScript 程序员才需要的特性。一个很好的例子是增加标准 HTML 元素的附加 UI 控件(我在第三部分中描述过)。这些是特定于 JavaScript 的,因为其他语言使用可扩展应用标记语言(XAML)。您可以通过打开 Visual Studio 项目的References部分来阅读 WinJS API 的源代码。(Windows API 的源代码不可用。)

定义全局导航功能

我的default.html文档包含了pageFrame元素,我将在其中插入其他页面。我想将导航到其他页面的能力作为一个全局函数公开,如下所示:

... window.showPage = function (url, options) {     WinJS.Utilities.empty(pageFrame);     WinJS.UI.Pages.render(url, pageFrame, options); }; ...

我已经将showPage函数定义为window对象的一个属性,因此它是全局可用的。这个函数的好处是,它允许我避免将pageFrame元素名称硬编码到我创建的每个内容页面中。showPage函数的参数是我想要显示的页面的 URL 和应该传递给页面的任何状态信息。

images 提示在第七章中我介绍了导航特性,它简化了这一技术。

在这个函数内部,可以看到一些 WinJS API 调用。我想替换而不是添加pageFrame元素中的任何内容。我调用了WinJS.Utilities.empty方法,它移除了作为参数传递的元素的所有子元素。我将pageFrame元素称为一个全局变量,这是我在第二章讨论使用浏览器特有特性时描述的 Internet Explorer 特性。

一旦我删除了任何现有的内容,我就调用WinJS.UI.Pages.render方法。这个方法是更大的页面特性的一部分,例如,除了使用iframe元素之外,它还提供了一些有用的行为。然而,在其核心,render方法是 web 应用 Ajax 请求中常用的XMLHttpRequest对象的包装器。

images 提示我将在本章稍后向应用添加页面时向您展示WinJS.UI.Pages提供的一些功能,我将在第五章中深入介绍 API 的这一部分。你可以在第十八章的中读到关于WinJS.Utilities API 的内容。

显示初始页面

设置应用的最后一步是使用我的showPage全局函数在pageFrame元素中显示初始页面:

`...
var app = WinJS.Application;

app.onactivated = function (eventObject) {
**    showPage("/pages/selectorPage.html");**
};
app.start();
...`

WinJS.Application对象为 Windows 应用提供了基础——包括定义描述应用生命周期的事件。我将在第十九章中全面解释生命周期,但是通过向onactivated属性添加一个处理函数,我表达了对activated事件的兴趣,该事件在应用启动时发送。这使我有机会执行任何一次性的初始化任务——包括使用showPage函数显示我的初始页面。您可以忽略这个片段中的最后一个语句——对app.start方法的调用。目前你需要知道的是activated事件的处理函数是你初始化应用的地方(我在第十九章中解释了start方法,但是在那之前你不需要知道它是如何工作的,只需要知道它是触发分配给onactivated属性的函数所必需的)。

添加音符字体

对于NoteFlash应用,我需要能够显示音符。我发现的最简单的方法是使用一种叫做MusiQwik的字体,它是由 Robert Allgeyer 创建的,我已经将它包含在本书的源代码下载中。如果你愿意,你可以直接从luc.devroye.org/allgeyer/allgeyer.html下载字体。

我需要使字体成为 Visual Studio 项目的一部分。我创建了一个名为resources的项目文件夹,将字体下载文件中的MusiQwik.ttf文件复制到其中。因为这是我第一次在项目中添加文件夹,所以我会给你一步一步的指导。

首先,您必须确保调试器没有运行,因为当它运行时,Solution Explorer不允许您修改项目结构。点击工具栏上的停止按钮(带有红色方形图标的那个)或从Debug菜单中选择Stop Debugging来停止应用。

右击Solution Explorer窗口中的粗体NoteFlash条目,并从弹出菜单中选择AddNew Folder。一个新的文件夹将被添加到项目中,并且它的名字将被选中,这样你就可以很容易地更改它——在这个例子中是更改为resources。按回车键确认名称,您就已经创建并命名了文件夹。您现在可以将源代码下载中的MusiQwik.ttf文件复制到文件夹中。

定义应用范围的 CSS

下一步是编写 CSS,使音乐字体可以在我的 HTML 文件中使用。用于执行 JavaScript Windows 应用的 Internet Explorer 10 支持 CSS3 网络字体功能,该功能允许自定义字体。清单 3-3 显示了我添加到css/default.css文件中的 CSS,它定义了字体和相关的样式。我删除了 Visual Studio 添加的默认样式和media规则(你可以在第二章的中看到)。

清单 3-3 。css/default.css 文件的内容

`@font-face {
    font-family: 'Music';
    font-style: normal;
    font-weight: normal;
    src: url('/resources/MusiQwik.ttf');
}

*.music {
    font-family: Music, cursive;
    font-size: 200px;
    letter-spacing: 0px;
}

*.musicSmall {
    font-size: 100px;
}

*.musicDisabled {
    color: #808080;
}

pageFrame {

height: 100%;
}`

这都是标准的 CSS3。我使用@font-face规则来定义一个新的字体,使用MusiQwik.ttf作为字体的来源。这个规则产生了一个新的字体,叫做Music

我在样式中为music类使用了新的字体,这允许我将任何元素中的文本显示为一系列注释。musicSmallmusicDisabled类是我在整个应用中使用的常见样式。我会把它们和music类结合起来,创造出特定的效果。

我在default.css文件中定义了font-face规则和相关的样式,因为我在这里定义的任何东西都将自动地被加载并插入到default.html文件中的每个页面所使用。这并不是 Windows 特有的魔法——它之所以有效,是因为 Visual Studio 在创建导入css/default.css文件的文件时向default.html添加了一个link元素:

`...

...`

default.css中定义的样式将应用于我插入到default.html中的其他页面的元素,这使得default.css特别适合定义应用范围的样式和规则。

我不想过多地强调这一点,但这是一个很好的例子,说明 JavaScript Windows 应用模型在多大程度上依赖于 web 标准,因此,您的 HTML、CSS 和 JavaScript 知识能让您在应用开发的道路上走多远。即使当我开始添加更多 Windows 特有的功能时,应用仍然会受到底层 web 技术和标准的驱动。

添加选择器页面

既然导航母版页和通用样式已经就绪,我可以添加应用的内容页面了。我将从选择器页面开始,它允许用户选择她将被测试的笔记组。

我喜欢在 Visual Studio 项目中将我的内容页面分组在一起,所以我在项目中添加了一个pages文件夹。我在这个文件夹中添加了selectorPage.html,方法是在Solution Explorer中右键单击新添加的pages文件夹,从弹出菜单中选择Add images New Item,并使用Page Control项目模板。

Page Control模板是一个方便的 Visual Studio 特性,它可以在一个步骤中创建一个 HTML 文件、一个 JavaScript 文件和一个 CSS 文件。HTML 文件包含一个用于 CSS 文件的link元素和一个用于 JavaScript 文件的script元素,以便在使用 HTML 时自动加载代码和样式。

JavaScript 和 CSS 文件创建在与 HTML 文件相同的文件夹中,并自动命名,这意味着我的项目包含三个新文件:pages/selectorPage.htmlpages/selectorPage.csspages/selectorPage.js。你可以在图 3-4 中看到这些文件是如何在解决方案浏览器中显示的。

images

图 3-4。使用页面控制模板创建一组链接的 HTML、CSS 和 JavaScript 文件

新的 CSS 和 JavaScript 文件允许我区分特定页面的样式和代码,以及适用于整个应用的样式和代码(在default.cssdefault.js文件中定义)。清单 3-4 显示了我的selectorPage.html文件的内容,它包含了将要呈现给用户的 HTML 标记。

清单 3-4 。selectorPage.html 文件的内容

`

<html> <head>     <meta charset="utf-8" />     <title>selectorPage</title>


    
    
    

**    **
**    **

**    
** **        

Select Notes

**

**        

**
**            
'&!
**
**            
'¯F=!
**
**            

Left Hand

**
**        
**
**        
**
**            
'&F=!
**
**            
'¯F===!
**
**            

Both Hands

**
**        
**

**        

**
**            
'&F=!
**
**            
'¯======!
**
**            

Right Hand

**
**        
**
**    
**

`

我突出显示了将 CSS 和 JavaScript 文件带入上下文的linkscript元素,以及我对文件所做的更改。Windows 应用真的很像 web 应用,并且没有将 Visual Studio 创建的文件与 HTML 页面相关联的魔法——您负责确保您需要的一切都链接到 HTML,尽管 Visual Studio 在从其模板创建文件时会有很大帮助。

所有三个selectorPage文件的内容将被导入到应用的主导航结构中,这意味着由default.js文件定义的功能和属性可以在特定于页面的脚本中调用,而在default.css中定义的 CSS 类将被应用到各个 HTML 文件中的元素。

我的更改删除了 Visual Studio 放入body元素中的默认内容,代之以一个简单的结构,该结构将向用户提供要测试的笔记的选择。我已经应用了我在css/default.css文件中定义的样式来应用音符字体。奇怪的字符串在字体中显示为有意义的音符,但它们本身看起来非常奇怪。

images 提示在第五章我解释了为什么default . CSS 和 default.js 文件的内容总是可用,当我解释 Windows 应用如何处理导入的 HTML 内容时。

HTML5 语义元素和划分

HTML5 有趣的一点是增加了新元素类型,比如sectionarticle。当您希望一致地将语义结构应用于内容时,这些元素非常有用,这样内容区域的重要性就可以从包含它的元素类型中明显看出。这是对 HTML 4 方法的一大改进,在 HTML 4 方法中,语义通常是应用于div元素的表达类,这很难一致地应用,并且使得与采用不同类本体的第三方共享内容变得特别困难。这很好,尤其是当你处理大量内容的时候。

另外,新的 HTML 开发人员倾向于一种被称为 divitusdiv-itus 的行为,这是指过度使用div元素来为页面布局添加结构。结果是一长串用于微观管理元素布局的 CSS 样式。这导致内容难以阅读、难以维护,并且在进行更改时容易出现显示问题。

许多程序员将这两个问题混为一谈,并开始使用 HTML5 语义元素来减少标记中的div元素的数量。用article元素替换div元素根本没有用,除非article元素中的内容代表某种...嗯,。如果内容不是某种类型的文章,你会让世界上其他人更难处理你的内容,这与 HTML5 中新元素的意图完全相反。

解决divitus的方法是学习如何有效地使用 CSS,这样你需要更少的 CSS 类,结果是更少的div元素——用不恰当的语义元素替换div元素不会提高你的 HTML 的质量(但是用恰当的语义元素替换div元素确实会提高你的 HTML,并且会让你因为掌握了 HTML5 的语义特性而广受赞誉)。

需要澄清的是,div元素被认为是用来帮助创建布局的,因为div元素在语义上是中性的——当一项内容有清晰明显的含义时,应用语义元素,当没有含义时,应用div元素。这就是为什么div元素在 HTML5 规范中被称为最后的元素——如果有更适合你的内容的元素,那么就使用它——但是当你需要它们的时候不要特意避开div元素。

对于前面的章节和例子,这个问题有两个实际结果。首先,我没有使用新的语义元素——不是因为我不喜欢它们,而是因为我的示例应用通常重布局轻数据。这是示例应用的本质,在某种程度上,也是 Windows 应用中 HTML 的本质。因此,在本书中,你会在我的标记中看到很多div元素。

你还会看到,我大量使用了两个新的 CSS3 布局特性,以防止我的div用法变成一种分割。这两个特性是网格布局flexbox 布局,它们都允许我创建流畅灵活的布局,而不需要大量的div元素作为基础设施。我在这一章简单解释网格布局,在第四章简单解释 flexbox 布局。这两者都值得你关注,并且会比无情地(并且没有必要地)替换div元素对你的 HTML 的简洁和质量有更大的影响。

定义选择器页面 CSS

为了管理selectorPage.html文件中标记的布局,我使用了grid布局,这是 CSS3 的一个特性。网格布局的规范还没有最终确定,所以微软使用了特定于供应商的属性名(以-ms-开头,表示一个没有最终确定或不符合最终规范的特性)。你可以在清单 3-5 中看到我是如何应用 CSS grid的,它显示了selectorPage.css文件的内容。

images 提示 Visual Studio 默认缩进 CSS,不适合我的开发风格,对书籍页面布局没有帮助。我已经禁用了层次缩进功能(使用ToolsimagesOptionsimagesText EditorimagesCSSimagesFormatting),我在本书中展示的所有 CSS 都将是平面的。

清单 3-5 。selectorPage.css 文件

`#selectorFrame {
**    display: -ms-grid;**
**    -ms-grid-rows: 0.25fr 0.25fr 1fr 0.5fr;**
**    -ms-grid-columns: 0.5fr 1fr 1fr 1fr 0.5fr;**
    text-align: center;
    padding: 0px 20px;
}

div.musicButton {
    border: medium solid white;
    margin: 25px;
    padding: 10px;
}

div.musicButton:hover {
    background-color: #3D4C42;
}

div.musicButtonPressed {
    background-color: #6B997A;
}

prompt {

**    -ms-grid-column: 2;**
**    -ms-grid-row: 2;**
**    -ms-grid-column-span: 3;**     margin: 10px;
}

leftHand {

**    -ms-grid-column: 2;**
**    -ms-grid-row: 3;**
}

rightHand {

**    -ms-grid-column: 4;**
**    -ms-grid-row: 3;**
}

bothHands {

**    -ms-grid-column: 3;**
**    -ms-grid-row: 3;**
}`

这个文件包含了selectorPage内容的所有 CSS,所以我强调了那些与网格布局特性相关的属性。

使用 CSS 网格布局

您可能没有使用过网格布局,因为它相当新,并且没有在主流 web 浏览器中一致地实现。幸运的是,在开发 Windows 应用时,您只需要担心 Internet Explorer 10,这意味着您可以根据单个浏览器的功能来定制 HTML 和 CSS 功能的使用。在这一节中,我给你一个 CSS grid布局的快速概述。

要开始使用grid布局,您需要设置display属性,并为包含网格的元素指定行数和列数。我希望网格出现在selectorFrame元素中,所以我像这样应用属性:

`...

selectorFrame {

**    display: -ms-grid;**
**    -ms-grid-rows: 0.25fr 0.25fr 1fr 0.5fr;**
**    -ms-grid-columns: 0.5fr 1fr 1fr 1fr 0.5fr;**
    text-align: center;
    padding: 0px 20px;
}
...`

必须将display属性设置为-ms-grid-ms-grid-rows-ms-grid-columns属性指定了网格结构。这些可以指定为分数单位(表示为fr,代表可用空间的一部分),可用空间的百分比,或者使用固定的尺寸,如像素。有许多不同的方式来表达网格,但是我最常用的方法是清单中显示的方法:整个可用空间的一部分。我喜欢分数,因为我可以很容易地创建一个网格,在这个网格中,行和列相对于彼此表示和相对于容器大小表示。我也喜欢使用分数,因为它们可以很容易地在元素周围创造空间。

为了理解清单中的网格,考虑一下行。要获得整个单位的总数,将所有的fr值加在一起:0.25 + 0.25 + 1 + 0.5 = 2 个单位。为容器元素分配的空间是指定的小数单位数,因此 1 个fr单位等于容器高度的 50%。无论容器元素占据多少像素,这个比率都将保持不变,浏览器将放大和缩小每行占据的切片。浏览器使用我指定的比率分配可用空间。在我对柱子重复这个过程之后,我就有了一个网格。

下一步是使用- ms-grid-column-ms-grid-row属性在网格中放置项目,如下所示:

`...

prompt {

**    -ms-grid-column: 2;**
**    -ms-grid-row: 2;**
**    -ms-grid-column-span: 3;**
    margin: 10px;
}
...`

这些属性使用基于 1 的索引指定元素出现在哪一行和哪一列(即,要将一个项目放在第一行或第一列,使用值1)。要使一个项目占据多行或多列,使用-ms-grid-column-span-ms-grid-row-span属性。在这个片段中,我已经在第 2 行第 2 列找到了具有promptid值的元素,并指定它应该跨越 3 列。

另一个感兴趣的网格属性是-ms-grid-column-align,我在这个例子中没有使用它。该属性指定网格正方形内元素的对齐方式,可以设置为startendcenterstretch。如果您使用的是从左到右的语言,比如英语,那么startend值将元素左右对齐。center值使元素居中,stretch值调整元素的大小,使其完全填充分配给它的空间。您可以使用网格属性创建一些非常复杂的布局。

images 提示详见[www.w3.org/TR/css3-grid](http://www.w3.org/TR/css3-grid)的 CSS 网格规范,记住这还不是一个被认可的标准。你可以在[msdn.microsoft.com/en-us/library/windows/apps/hh453256.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/hh453256.aspx)阅读微软对网格 CSS 属性的描述。

添加音乐字体和风格

如果你查看清单 3-4 中文件的内容,你会发现一些元素的内容是一系列字符引用,就像这样:

`...

**'¯==F===!**
...`

这就是我如何表达音符和它周围的符号。您可以看到片段中的div元素具有musicmusicSmall样式,这意味着浏览器将应用我在本章前面的default.css文件中定义的自定义font face。并不是所有我想从字体中得到的符号都被映射成方便的字符,所以我混合使用了常规字符和 HTML 转义码来获得我需要的内容。你可以在图 3-1 中看到音乐字体和相关样式的效果,图中显示了selectorPage.html文件的最终外观。

定义选择器页面的 JavaScript 代码

selectorPage.html标记中的元素看起来像按钮,但它们只是div元素。为了让它们表现得像按钮,我需要使用 CSS 和 JavaScript 代码的组合。你已经看到了 CSS——在清单 3-5 中,我定义了div.musicButtondiv.musicButton:hover样式来设置基础样式,并在指针移动到三个div元素之一所占据的屏幕区域时做出响应。

为了补充这个基本行为,我必须切换到 JavaScript,我已经在pages/selectorPage.js文件中定义了它,以便遵循 Windows 应用惯例。你可以在清单 3-6 中看到代码。

清单 3-6 。selectorPage.js 文件的内容

`(function () {
    "use strict";

function handleMusicButtonEvents(e) {
        switch (e.type) {
            case "mousedown":
                this.style.backgroundColor = "#6B997A";
                break;
            case "mouseup":
                this.style.backgroundColor = "";
                break;
            case "click":
                showPage("/pages/flashCardsPage.html", this.id);
                break;
        }
    };

**    WinJS.UI.Pages.define("/pages/selectorPage.html", {**
**        ready: function (element, options) {**
**            var buttons = WinJS.Utilities.query("div.musicButton");**
**            ["mouseup", "mousedown", "click"].forEach(function (eventType) {**
**                buttons.listen(eventType, handleMusicButtonEvents);**
**            });**
**        }**
**    });**
})();`

在这个文件中,您可以看到导航模型的另一半。在default.js文件中,我使用了WinJS.UI.Pages.render方法来显示页面,比如selectorPage.html。在selectorPage.js中,我使用WinJS.UI.Pages.define方法在页面显示时做出响应。

方法的参数是页面的 URL 和一个为不同事件定义处理函数的对象。在这个例子中,我为ready事件定义了一个处理程序,每次显示页面时都会调用这个处理程序。(我在第五章中详细解释了这两个部分的功能。)

这个方法的操作方式有些微妙,第一个参数暗示了这一点:页面 URL。您不必在与页面关联的 JavaScript 文件中注册事件处理函数。您可以将它们放入任何已经整合到应用中的 JavaScript 代码中——对于我的简单应用,这意味着default.js或创建一个新文件并使用script元素导入代码。我在第五章的中深入研究了define方法,但是现在,知道这一点就足够了这是 Windows 应用模式,用于定义当指定文件中的标记被加载到应用中时应该执行的代码。

优化和加载 JAVASCRIPT

当编写一个常规的 web 应用时,有很多动机直接控制 JavaScript 代码的加载。您从缩小或最小化代码开始,以便它需要更少的带宽,您将多个文件连接在一起以减少浏览器必须建立的 HTTP 连接的数量,您使用内容交付网络(cdn)来获得流行的 JavaScript 库,希望提高性能,或者,理想情况下,从您需要的公共库的先前下载版本中受益。

如果您非常重视 web 应用的性能,您可以开始考虑使用 JavaScript 加载器来异步引入您的 JavaScript 代码。我在 web 应用中使用的两个加载器是YepNopeRequireJS。如果小心使用,一个好的 JavaScript 加载器确实可以减少用户盯着加载屏幕的时间,并且在使用像YepNope这样的条件加载器的情况下,避免下载特定用户、浏览器或平台不需要的脚本文件。

可以将这些相同的技术应用到你的 JavaScript Windows 应用中,但是这样做没有任何好处——更糟糕的是,你只会让你的应用更难测试和调试。

Windows 8 应用部署在本地,这意味着您的脚本文件加载速度非常快,快到您不太可能需要推迟执行来解决性能问题。同样,因为 JavaScript 文件是本地加载的,所以不需要缩减代码来减少带宽消耗。

有一些重要的 WinJS 特性和约定隐含地依赖于同步脚本执行,包括我在上一节中向您展示的define方法。这并不是说你不能解决这些情况——但是在这样做之前,你应该停下来问问自己你正在试图解决什么问题。自从 Windows 8 的最早发布版本以来,我一直在编写 Windows 应用,我还没有遇到任何可以通过添加 JavaScript 加载程序来解决的问题。可能有一些非常特殊的情况,加载器非常有用,但我还没有遇到过,但这些情况很可能很少发生,因此您不应该仅仅因为它是 web 应用开发工作流的一部分就自动将加载器添加到您的 Windows 应用项目中。

处理按钮点击

在我的ready事件处理程序中,我将handleMusicButtonEvents函数注册为clickmousedownmouseup事件的处理程序。我没有在标记中使用button元素,所以我通过应用和移除背景颜色来响应mousedownmouseup事件。这些是您在 web 应用开发过程中遇到的标准 DOM 事件,您可以像处理普通 HTML 页面一样处理它们。

我使用WinJS.Utilities.query方法找到了我想要的元素,这是我之前在default.js文件中别名为$的方法。我在这个文件中明确地使用了方法名,所以你可以清楚地看到发生了什么。query方法采用 CSS 选择器,并返回代表页面中匹配元素的标准 DOM HTMLElement对象的集合(这是调用该方法时应用的整个标记,而不仅仅是内容 HTML 文件中的内容)。匹配元素的集合可以像常规数组一样处理,但实际上是一个WinJS.Utilities.QueryCollection对象,它定义了许多有用的方法。其中一个方法是listen,它接受一个事件名称和一个处理函数,并对集合中的每个HTMLElements调用addEventListener方法。如果您是一名 jQuery 用户,您会认识到,WinJS.Utilities名称空间受 jQuery API 启发的程度。(我将向您介绍我使用的各种WinJS.Utilities特性,并在第十八章中详细讨论名称空间。)

images 提示您会注意到我使用标准的 DOM 事件来处理用户交互。这工作得非常好(我在本书中使用这些事件),但是如果你想处理触摸和手势输入,你需要使用微软特有的事件。我在第十七章中解释了这些事件是什么以及它们是如何工作的。对于常规的输入,比如处理按钮,即使用户触摸了屏幕而不是使用鼠标,常规的 DOM 事件也会起作用。

当用户点击或触摸其中一个元素时,我调用我之前在default.js文件中创建的全局showPage导航函数,传入所选元素的id,如下所示:

... showPage("/pages/flashCardsPage.html", **this.id**); ...

在这个showPage函数中,这个参数被传递给WinJS.UI.Pages.render方法,如下所示:

... window.showPage = function (url, options) {     var targetElem = document.getElementById("pageFrame");     WinJS.Utilities.empty(targetElem);     WinJS.UI.Pages.render(url, targetElem, options); }; ...

这是一种将信息从一个页面传递到另一个页面的简单技术,但是它需要预先的知识和页面之间的协调。这对于像这样的简单应用来说是好的,但是在实际项目中是有问题的。在本书的第二部分中,我向你展示了各种技术和特性,来帮助你去除这些依赖性,创建由松散耦合的组件组成的应用,这使得应用更容易编写和维护。

你可以在图 3-5 中看到该应用是如何出现的:允许用户选择她想要测试的音符的按钮状div元素整齐地显示在布局网格上,并对基本的鼠标交互做出响应(尽管单击时它们加载的页面尚不存在)。

images

图 3-5。允许用户选择一组音符

目前看起来不太像,但这一章的大部分内容都是关于 Windows 应用开发的上下文和背景。到目前为止,该应用本身仅由几个短文件和一些基本的 JavaScript 组成。下一章会加快速度,你会看到应用的其余功能很快到位。

总结

示例应用目前没有做太多工作,但是到目前为止,您已经了解了 Windows 应用开发中一些最基本的概念。您还了解了 Windows 为帮助您管理全局命名空间而提供的约定和功能。Windows 应用开发不是 web 应用开发,但是您现有的技能为您创建丰富流畅的 Windows 应用打下了良好的基础。在本章中,你看到了我如何使用标准 HTML 和 CSS 来创建应用的结构和布局,包括 CSS3 web 字体和grid布局(尽管使用了特定于供应商的前缀)以及标准 DOM 事件,如clickmouseover。在下一章中,我将添加到示例应用中并构建功能。

四、完成应用

在这一章中,我将完成我在第三章中开始的基础版本NoteFlash示例应用。我继续使用与常规 web 应用开发有很多共同之处的方法和技术,但我也开始融入更多特定于 Windows 的功能,这些功能可通过 WinJS API 获得。我简要概述了我使用的每个 Windows 应用的功能,并解释了在本书中可以获得更多详细信息的地方。

重温示例应用

在这一章中,我将直接从第三章的开始构建NoteFlash项目。正如您所记得的,我将应用的基本结构放在适当的位置,定义了导航功能,并定义了将在整个应用中应用的样式。我还使用了Page Control条目模板来生成一组相关的 HTML、CSS 和 JavaScript 文件,这些文件用于创建允许用户选择要测试的笔记的内容。你可以在图 4-1 中看到结果。在这一章中,我将创建额外的内容来根据用户的选择执行测试。

images

图 4-1。便签选择页面

定义 Notes 数据

构建示例应用的下一步是定义用户将要测试的笔记。为此,我在名为notes.jsjs文件夹中添加了一个新的 JavaScript 文件,其内容可以在清单 4-1 中看到。(右击Solution Explorer中的js文件夹,选择Add images New Item,使用JavaScript File项模板。)

images 提示Solution Explorer窗口中右键点击js文件夹,从弹出菜单中选择Add images New Item,添加一个 JavaScript 文件。从文件类型列表中选择JavaScript File,设置文件名为notes.js,点击Add按钮。

清单 4-1 。定义音符数据

`(function () {
    "use strict";

var Note = WinJS.Class.define(function (note, character, hand) {
        this.note = note;
        this.character = character;
        this.hand = hand;
    });

WinJS.Namespace.define("Notes", {
        leftHand: [
                new Note('C', 80, "left"), new Note('D', 81, "left"),
                new Note('E', 82, "left"), new Note('F', 83, "left"),
                new Note('G', 84, "left"), new Note('A', 85, "left"),
                new Note('B', 86, "left"), new Note('C', 87, "left"),
                new Note('D', 88, "left"), new Note('E', 89, "left"),
                new Note('F', 90, "left"), new Note('G', 91, "left"),
                new Note('A', 92, "left"), new Note('B', 93, "left"),
                new Note('C', 94, "left")
        ],
        rightHand: [
                new Note('C', 82, "right"), new Note('D', 83, "right"),
                new Note('E', 84, "right"), new Note('F', 85, "right"),
                new Note('G', 86, "right"), new Note('A', 87, "right"),
                new Note('B', 88, "right"), new Note('C', 89, "right"),
                new Note('D', 90, "right"), new Note('E', 91, "right"),
                new Note('F', 92, "right"), new Note('G', 93, "right"),
                new Note('A', 94, "right"),
        ],
    });

})();`

这个文件需要一些解释。我使用了 WinJS API 的两个有用的特性:名称空间。我将在接下来的章节中解释它们。

Windows JavaScript 类

JavaScript 是一种基于原型的面向对象语言,这意味着继承是通过克隆现有对象来工作的(这些对象被称为原型)。所有其他可用于 Windows 应用开发的语言都使用基于类的继承,其中对象的功能在单独的类中定义。对象是作为这些类的实例创建的。大多数主流编程语言使用基于类的继承,例如,如果你用 C#或 Java 编写过软件,你就会遇到类。

WinJS API 支持用 JavaScript 创建类。WinJS.Class.define方法最多接受三个参数:一个作为类构造函数的函数、一个包含类实例成员的对象和一个包含类静态成员的对象。WinJS.Class.derive方法允许您通过从现有的类派生来创建新的类。

images 提示微软曾表示,基于类的继承比标准的基于 JavaScript 原型的方法提供了性能优势,但我怀疑它更多地与 Windows API 必须向本地支持类的 JavaScript 语言公开的方式有关。

notes.js文件中,我定义了一个名为Note的基类,就像这样:

... **var Note = WinJS.Class.define(function (note, character, hand) {** **    this.note = note;** **    this.character = character;** **    this.hand = hand;** **});** ...

我只为这个类定义了构造函数,它有三个参数——音符(如C),在音乐字体中代表音符的字符,以及音符与哪只手相关(因为,当然,弹钢琴时要用两只手——这给我的音乐课带来了很多困难)。

一旦定义了一个类,就可以使用关键字new创建新的实例,如下所示:

... var myNote = new Note('C', 80, "left"); ...

这条语句创建了一个代表左手音符 C 的Note对象,由我在第三章中添加到项目中的音乐字体中的字符代码80表示。

关键字new是 JavaScript 的标准部分,但在 web 应用开发中并未广泛使用。WinJS.Class.define方法创建的类是非常基本的,它们缺少你在其他语言中期望的类的大部分特性。这种方法的主要好处是,它提供了一种机制,以一种可以在 JavaScript 中使用的方式公开 Windows API。

您不必在自己的 JavaScript 中使用类,但是理解这个特性是很重要的,因为微软已经在 WinJS API 中广泛使用了它,一旦您使用调试器单步调试代码,就会遇到它。我很少在自己的代码中使用类,因为我认为它们增加了许多基于类的继承的问题,却没有任何好处。

创建名称空间

正如我在第三章中解释的那样,名称空间是减少 Windows 应用中全局名称空间污染的技术之一(另外两个是自执行函数严格模式)。名称空间背后的思想是创建一个单独的全局变量并将数据值和函数附加到它上面,而不是使每个单独的值和函数都是全局的。

您已经看到了微软如何使用名称空间来构建 API,比如WinJS.UtilitiesWinJS.UI.Pages。如果您想使用query方法在 HTML 中搜索元素,您可以调用WinJS.Utilities.query。查询方法是WinJS.Utilities名称空间的一部分,该名称空间包含许多其他有用的函数。

名称空间可以是分层的。Utilities名称空间是WinJS名称空间的一部分。WinJS包含许多子名称空间,Utilities只是其中之一。在上一节中,我使用了WinJS.Class.define方法——该方法位于WinJS.Class名称空间中,而ClassUtilities的对等体。通过使用名称空间,微软将大量的功能打包到两个全局名称空间对象中:WinJSWindows

所有的名称空间都是:全局对象。例如,清单 4-2 展示了如何使用常规的 JavaScript 对象来重新创建WinJS.Utilities.query方法。

清单 4-2 。作为对象层次结构的名称空间

... var WinJS = {     Utilities: {         query: function (someArguments) {             // ...implementation goes here...         }     } }; ...

名称空间的层次性意味着您可以在名称空间层次结构的不同位置重用变量和方法的名称。例如,一个(假设的)WinJS.Database.query方法完全独立于WinJS.Utilities.query,尽管这两个方法都被称为query。这是名称空间的好处之一。如果所有的方法都是全局的,我会以像queryHTMLByIdqueryHTMLByTagName这样的名字结束,这和你在 DOM API 中看到的冗长的名字是一样的,其中所有的方法都是对等的。使用名称空间向代码添加结构意味着方法名称可能是有意义的,而方法操作的上下文来自其名称空间

您可以使用 WinJS API 来创建自己的名称空间,使用WinJS.Namespace名称空间的特性,我在notes.js文件中使用了如下的特性:

... **WinJS.Namespace.define("Notes"**, {     leftHand: [         new Note('C', 80, "left"), new Note('D', 81, "left"),         // ...other notes removed for brevity...     ],     rightHand: [         new Note('C', 82, "right"), new Note('D', 83, "right"),         // ...other notes removed for brevity...     ], **});** ...

WinJS.Namespace.define方法创建新的名称空间。第一个参数是要创建的命名空间的名称,第二个参数是其成员将被添加到命名空间的对象。

images 提示您可以通过将带点的名称作为第一个参数传递给 define 方法来创建名称空间的层次结构,比如MyData.Music.Notes。将自动创建层次结构中的每个级别。

在清单中,我创建了一个名为Notes的新名称空间,它包含两个数组:leftHandrightHand。每个数组包含一组Note对象,代表与那只手相关的音符序列,这些对象是使用我在本章前面描述的Note类创建的。

使用define方法创建名称空间在几个方面让生活变得更简单。首先,名称空间被自动添加到全局名称空间中。考虑到创建全局变量是多么容易(真的太容易了),这没什么大不了的,但这确实意味着我将能够在我的应用中的任何地方引用Notes.leftHandNotes.rightHand。这是我创建视图模型时所依赖的东西,我会在第八章的中解释。

使用define方法的第二个好处是它检查您正在创建的名称空间的部分是否已经存在。例如,如果我像这样定义了名称空间Notes:

... window.Notes = {    leftHand: [ ...notes... ] } ...

然后尝试添加到该命名空间,就像这样:

... window.Notes = {     rightHand: [ ...notes... ] } ...

我将最终只得到rightHand笔记,因为我的第二个window.Notes对象将完全取代第一个。然而,我可以使用define方法安全地添加名称空间,没有任何问题,如清单 4-3 所示。

清单 4-3 。通过多次调用 define 方法逐步构建名称空间

`(function () {
    "use strict";

var Note = WinJS.Class.define(function (note, character, hand) {
        this.note = note;
        this.character = character;
        this.hand = hand;
    });

**    WinJS.Namespace.define("Notes", {**
**        leftHand: [**
**                new Note('C', 80, "left"), new Note('D', 81, "left"),**
**                new Note('E', 82, "left"), new Note('F', 83, "left"),**
**                new Note('G', 84, "left"), new Note('A', 85, "left"),                 new Note('B', 86, "left"), new Note('C', 87, "left"),**
**                new Note('D', 88, "left"), new Note('E', 89, "left"),**
**                new Note('F', 90, "left"), new Note('G', 91, "left"),**
**                new Note('A', 92, "left"), new Note('B', 93, "left"),**
**                new Note('C', 94, "left")**
**        ]**
**    });**

**    WinJS.Namespace.define("Notes", {**
**        rightHand: [**
**                new Note('C', 82, "right"), new Note('D', 83, "right"),**
**                new Note('E', 84, "right"), new Note('F', 85, "right"),**
**                new Note('G', 86, "right"), new Note('A', 87, "right"),**
**                new Note('B', 88, "right"), new Note('C', 89, "right"),**
**                new Note('D', 90, "right"), new Note('E', 91, "right"),**
**                new Note('F', 92, "right"), new Note('G', 93, "right"),**
**                new Note('A', 94, "right"),**
**        ],**
**    });**

})();`

结果是一个包含leftHandrightHand注释的名称空间。当然,您可以自己执行这些检查,但是使用WinJS.Namespace.define方法更方便。对于许多 WinJS 功能来说都是如此——您可以自己编写这些功能的实现,但是 Microsoft API 通常更方便。

添加闪存卡页面

NoteFlash应用中缺失的部分是测试用户音符知识的页面。构建应用的下一步是添加这个页面,所以我使用Page Control项模板在pages文件夹中创建一个名为flashCardsPage.html的新页面。这是我在上一章用来创建selectorPage.html页面的同一个模板,当你使用这个模板时,Visual Studio 会向项目添加 HTML、CSS 和 JavaScript 文件。

为了解释我如何实现这个页面,我需要在这个页面的 HTML、CSS 和 JavaScript 之间切换。最简单的方法是列出组成页面的文件内容,然后解释各个部分是如何组合在一起的。在这个过程中,我将介绍一些重要的 WinJS 特性。

首先,你可以在清单 4-4 的中看到我添加到flashCardsPage.html文件的内容。在接下来的几节中,我将分解这个文件的每个部分是做什么的。

清单 4-4 。flashCardsPage.html 文件的内容

`

<html> <head>     <meta charset="utf-8" />     <title>flashCardsPage</title>


         
    


**    **
    

**    
** **        
** **            ** **        
**

**        

Title Will Go Here

**
**        

**
**            1 of**
**            1**
**            (**
**                
Correct/**
**             Wrong)                    **
**        

**

**        

**
**            
**
**                '&=!**
**            
**
**            
**
**                '¯=!**
**            
**
**        
**

**        

**
**            **
**            **
**            **
**            **
**            **
**            **
**            **
**            **
**            **
**        
**
**    
**

`

本文档中的head元素包含您所期望的内容:WinJS API 文件的script元素、flashCardsPage.jsnotes.js文件以及flashCardsPage.css文件的link元素。

添加页面特定的 CSS

元素中的标记为应用的测试部分提供了布局。flashCardsPage.css文件包含页面特定的 CSS,它布局了flashCardsPage.html中的元素,您可以在清单 4-5 中看到 CSS 文件的内容。

清单 4-5 。flashCardsPage.css 文件

`header {
    margin: 20px;
}

h1.subhead {
    font-size: 30pt;
    margin: 10px;
}

backButtonContainer {

width: 100%;
    padding: 20px;
}

backButton {

margin-left: 20px;
}

flashContainer {

**    display: -ms-flexbox;**
**     -ms-flex-direction: column;**
**     -ms-flex-align: center;**
**     -ms-flex-pack: justify;  **
    height: 100%;
}

noteButtons {

margin-bottom: 50px;
}

noteButtons button {

font-size: 30pt;
    margin: 5px;
}

button.correct {
    background-color: #4cff00;
}

noteButtons button[id] {

width: 200px;
}`

我强调了 CSS 文件最重要的部分:使用flexbox布局,这是我在本书中经常使用的另一个 CSS3 布局(第一个是我在第三章中展示的grid布局)。我在flashCardsPage.css中使用的其他属性是常用的,但是flexbox布局是新的,还没有被广泛采用——部分是因为规范仍在开发中,这就是为什么我必须使用特定于供应商的属性名。

了解柔性盒布局

柔性盒布局,通常被称为柔性盒,它提供了一种流体布局,当屏幕尺寸改变时,这种布局能够很好地响应。这在 Windows 应用中很重要,因为用户可以重新定位设备或改变分配给应用的屏幕大小(我会讨论这两个功能,并在第六章的中告诉你如何适应它们)。

images 提示当我需要精确划分屏幕空间时,我倾向于使用网格布局,当我更关心流动性和居中元素时,我倾向于使用 flexbox 布局。

您可以通过将display属性设置为–ms-flexbox来启用 flexbox 布局,就像我在清单中所做的那样。最重要的属性是–ms-flex-direction,它指定了子元素的布局方式。我已经在表 4-1 中列出了该属性支持的值。

images

在清单中,我指定了列值,这意味着我的元素将按照它们在 HTML 中定义的顺序从上到下排列。

–ms-flex-pack属性指定元素如何沿–ms-flex-direction属性指定的轴对齐(沿垂直轴为column值,沿水平轴为row值)。我已经在表 4-2 中列出了该房产的价值。

images

在清单中,我使用了justify属性,这意味着flashContainer元素中的元素将被隔开,这样它们就占据了元素的整个高度。

–ms-flex-align属性指定元素沿轴的对齐方式,该轴未被–ms-flex-direction属性使用——也就是说,与元素布局所沿的轴成 90 度,称为正交轴。该属性值如表 4-3 所示。

images

使用这三个属性,你可以在布局中构建很多流动性,但是还有一些我在本书中没有用到的属性(并且经常发现没有用)。你可以在[msdn.microsoft.com/en-us/library/windows/apps/hh453474.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/hh453474.aspx)看到完整的列表。

images只需将–ms-flex-align属性设置为justify并将–ms-flex-pack属性设置为center,子元素将被放置在元素的中心。

在清单中,我指定了center值,这意味着我的元素将与flashContainer元素的中心对齐。结合我指定的其他值,flashContainer元素的子元素将垂直布局,分布在元素的整个高度,并位于元素的中心。您可以在图 4-2 中看到页面的布局。

images

图 4-2。闪存卡页面的布局

我还没有定义 JavaScript 代码来控制这些元素,但是你已经可以看到该功能的主要组成部分:用户正确和错误回答的详细信息的占位符,显示左手和右手笔记所需的五线谱,以及用户点击或触摸来识别笔记的一组按钮。

定义闪存卡页面的代码

清单 4-6 显示了flashCardsPage.js文件的内容。没有大量的代码,但是有一些有趣的东西在进行,我将在下面的部分中解释。

清单 4-6 。flashCardsPage.js 文件

`(function () {
    "use strict";

var appState = WinJS.Binding.as({
        title: "", mode: null, leftNote: "=",
        rightNote: "=", currentIndex: 0, noteCount: 0,
        notes: [], currentNote: null,
        results: {
            numberCorrect: 0,
            numberWrong: 0
        },
    });

WinJS.UI.Pages.define("/pages/flashCardsPage.html", {
        ready: function (element, options) {

WinJS.Binding.processAll(document.body, appState);

backButton.addEventListener("click", function (e) {
                showPage("/pages/selectorPage.html");
            });
            $("#noteButtons button").listen("click", handleButtonClick);

setState(options);
            selectAndDisplayNote();
        }
    });

function setState(mode) {

appState.mode = mode;
        switch (mode) {
            case "leftHand":
                appState.title = "Left Hand Notes";
                break;
            case "rightHand":
                appState.title = "Right Hand Notes";
                break; case "bothHands":
                appState.title = "All Notes";
                break;
        }

appState.notes = [];
        if (mode == "leftHand" || mode == "bothHands") {
            Notes.leftHand.slice().forEach(function (item) {
                appState.notes.push(item);
            });
        }
        if (mode == "rightHand" || mode == "bothHands") {
            Notes.rightHand.slice().forEach(function (item) {
                appState.notes.push(item);
            });
        }
        appState.currentIndex = 0;
        appState.results.numberCorrect = 0;
        appState.results.numberWrong = 0;
        appState.noteCount = appState.notes.length;
    }

function selectAndDisplayNote() {
        if (appState.notes.length > 0) {
            var index = Math.floor((Math.random() * appState.notes.length));
            var note = appState.notes.splice(index, 1)[0];
            appState.leftNote = (note.hand == "left" ? "&#"
                + note.character + ";" : "=");
            appState.rightNote = (note.hand == "right" ? "&#"
                + note.character + ";" : "=");
            appState.currentNote = note;
            appState.currentIndex++;
        } else {
            \(("#noteButtons button").forEach(function (item) {                 item.style.display = "none";             });             \)("#noteButtons button[id]").setAttribute("style", "");
        }
    }

function handleButtonClick(e) {
        if (this.id == "restart") {
            showPage("/pages/flashCardsPage.html", appState.mode);
        } else if (this.id == "back") {
            showPage("/pages/selectorPage.html");
        } else {
            \(("button[data-note].correct").removeClass("correct");             \)("button[data-note=" + appState.currentNote.note + "]")
                .addClass("correct");

if (this.innerText == appState.currentNote.note) {                 appState.results.numberCorrect++;
            } else {
                appState.results.numberWrong++;
            }
            selectAndDisplayNote();
        }
    }
})();`

使用 WinJS 数据绑定

保持元素的内容与应用的状态同步可能是一个乏味且容易出错的过程。有两个问题:第一个是简单地确保所有的 HTML 元素在用户与应用交互时显示正确的数据。第二个问题是确定哪个元素是特定数据的权威来源——如果用户可以使用一系列不同的元素编辑相同的数据值,这是很困难的。

web 应用和 Windows 应用的解决方案是一样的:数据绑定。数据绑定将数据值保存在 JavaScript 代码中,并提供自动同步数据变化和显示给用户的 HTML 元素内容的机制。

我最喜欢的 web 应用数据绑定包是由我偶尔的合作者和全面的好人史蒂夫·桑德森编写的。我在我的《网络应用 JavaScript》一书中广泛使用了 Knockout,它是一个非常好的工具。您可以在 JavaScript Windows 应用中使用 Knockout 或其任何竞争对手,但 WinJS API 中内置了对数据绑定的支持。

WinJS 数据绑定支持不像一些 web 应用库那样功能丰富,但我还是会使用它——不仅因为它是 Windows API 的一部分,还因为它集成到了我在本书第三部分中描述的一些 UI 控件中。我在这一章给你一个关于绑定的简要概述,这样我就可以介绍这个主题并让NoteFlash应用工作起来。我在第八章中再次深入讨论这个话题。

定义数据

对于数据绑定,我首先需要一些数据,表达这些数据的最简单方式是使用一个对象,就像这样:

... var appState = **WinJS.Binding.as**({     title: "", mode: null, leftNote: "=",     rightNote: "=", currentIndex: 0, noteCount: 0,     notes: [], currentNote: null,     results: {         numberCorrect: 0,         numberWrong: 0     }, }); ...

这个代码片段最重要的部分是对WinJS.Binding.as方法的调用,它将一个对象作为参数。我传递给as方法的对象包含了代表我的应用当前状态的所有数据。通过将这个对象传递给as方法,我可以观察到对象中的数据值

可观察值是动态数据绑定所必需的,其中数据项和 HTML 的内容或属性值总是同步的,这就是我将在NoteFlash应用中使用的数据绑定类型。现在不要太担心数据——当你看到绑定系统的其他部分就位时,它会开始变得更有意义。

声明绑定

数据必须被绑定到某个东西,对于这个应用,绑定关系的另一面出现在 HTML 标记中。下面是来自flashCardsPage.html文件的一个例子:

... <span **data-win-bind="innerText: results.numberCorrect"**></span> ...

这是一个声明性绑定,这意味着我已经在 HTML 代码中包含了绑定的细节。为了创建这个绑定,我向元素添加了data-win-bind属性。

images 提示data-win-*绑定用于表示 WinJS 功能。以data-开头的属性称为数据属性。一段时间以来,它们一直是将自定义属性应用于元素的一种非正式方式。HTML5 规范使数据属性成为 HTML 的正式组成部分。

这个绑定属性的值由两部分组成。第一部分是绑定所应用到的 DOM 中的HTMLElement对象的属性名——在本例中是innerText属性,它控制元素的文本内容。属性名后跟一个冒号(:),然后是将绑定到该属性的数据值的名称。

在这种情况下,我选择了results.numberCorrect作为数据值。该属性的结果是span元素的innerText属性被绑定到results.numberCorrect属性的值。

将数据应用于绑定

剩下的步骤是将数据和声明性绑定放在一起。上一节中我的span元素知道它需要results.numberCorrect属性的值,但是它不知道如何获得该值。WinJS.Binding.as方法创建可观察的数据对象,但是它不知道这些值应该显示在哪里。我需要使用另一个WinJS方法连接数据和声明,如下所示:

`...
WinJS.UI.Pages.define("/pages/flashCardsPage.html", {
    ready: function (element, options) {

**        WinJS.Binding.processAll(document.body, appState);**

backButton.addEventListener("click", function (e) {
            showPage("/pages/selectorPage.html");
        });
        $("#noteButtons button").listen("click", handleButtonClick);

setState(options);
        selectAndDisplayNote();     }
});
...`

WinJS.Binding.processAll方法有两个参数:DOM 中的一个元素和一个数据对象。该方法处理指定元素的所有后代,并将数据对象设置为任何具有data-win-bind属性的元素的数据源。在我的示例应用中,我已经指定了document.body元素——以便处理整个布局——以及我之前创建的可观察的appState对象。

这缩小了数据和声明性绑定之间的差距。您可以看到,这个方法是我在处理函数中为WinJS.UI.Pages.ready事件调用的第一个方法。我喜欢尽快设置我的绑定,但这只是个人偏好。您可以在任何合适的时候调用processAll方法,但是如果您在 HTML 中使用数据绑定,那么调用这个方法是很重要的——否则绑定不会被激活,您的数据值也不会被 HTML 元素显示。

配置按钮和导航

当 Visual Studio 创建一个新页面时,它会在 HTML 标记中包含一个导航Back按钮,如下所示:

... <button id="backButton" class="win-backbutton" aria-label="Back"></button> ...

这是 Windows 应用中常见的布局功能,允许用户在应用中后退一步。当我更新flashCardsPage.html文件的内容时,我保留了这个元素。元素被赋予的 CSS 类win-backbutton,在ui-dark.cssui-light.css文件中定义,我在第二章中介绍过,它们定义了 Windows 应用的所有基本样式。在图 4-3 中,您可以看到按钮是如何显示的。

images

图 4-3。后退按钮

你不必在你的应用布局中有这个button,但是我将保留它,以允许用户返回到选择页面。处理 Windows 应用中的按钮就像处理 web 应用中的按钮一样,您可以在下面的flashCardsPage.js文件中的ready事件处理程序片段中看到这一点:

... backButton.addEventListener("click", function (e) {     showPage("/pages/selectorPage.html"); }); ...

我使用addEventListener方法注册一个函数来处理click事件。Windows 应用有一些微软特有的事件,我在第十八章中描述过,但是标准的click事件工作得非常好,可以响应鼠标和触摸事件。当按钮被点击时,我调用我的全局showPage函数(您还记得,我在default.js文件中定义了它)来显示selectorPage.html文件。

配置应答按钮

页面上的其他按钮允许用户识别显示的每个注释。在图 4-4 中可以看到这些按钮。

images

图 4-4。音符识别按钮

我想以同样的方式处理所有这些按钮的click事件,如下所示:

... $("#noteButtons button").listen("click", handleButtonClick); ...

我使用了我在default.js文件中为WinJS.Utilities.query方法设置的$别名。我可以将listen方法应用于来自query方法的结果,为所有匹配元素的事件注册相同的处理函数——在本例中,为所有按钮的click事件注册handleButtonClick函数。(我将在本章后面描述handleButtonClick函数是如何工作的。)

设置状态

Windows 应用单页模型中的一些奇怪之处意味着每次收到ready事件时重置状态是很重要的。我会在第五章中解释原因。在NoteFlash应用中,我创建了一个名为setState的专用设置功能,如下:

`...
function setState(mode) {

appState.mode = mode;
    switch (mode) {
        case "leftHand":
            appState.title = "Left Hand Notes";
            break;
        case "rightHand":
            appState.title = "Right Hand Notes";
            break;
        case "bothHands":
            appState.title = "All Notes";
            break;
    }

appState.notes = [];     if (mode == "leftHand" || mode == "bothHands") {
        Notes.leftHand.slice().forEach(function (item) {
            appState.notes.push(item);
        });
    }
    if (mode == "rightHand" || mode == "bothHands") {
        Notes.rightHand.slice().forEach(function (item) {
            appState.notes.push(item);
        });
    }
    appState.currentIndex = 0;
    appState.results.numberCorrect = 0;
    appState.results.numberWrong = 0;
    appState.noteCount = appState.notes.length;
}
...`

该函数的参数是由ready事件处理程序接收的值,该值指示用户想要测试哪组音符:左手音符、右手音符,或者两者都测试。我根据这个值在数据对象中设置了appState.title属性的值,这触发了对一个 HTML 数据绑定的更新:

`...

...`

该函数的下一部分清除appState.notes数组,并用来自Notes名称空间(我在本章前面创建的)的值重新填充它。在appState.notes数组中的最后一组音符取决于用户在selectorPage中点击了哪个按钮。

重置其他状态值

该函数的其余部分重置appState对象中的剩余值。所有这些值都在数据绑定中使用:

`...

    1 of     1     (      Correct/      Wrong)                    

...`

重置这些属性的值具有清除应用状态和重置用户界面的双重效果。如果没有数据绑定,我将不得不手动重置元素的内容,确保找到显示每个数据值的所有实例——对于像这样的基本应用来说,这相当简单,但对于更复杂的应用来说,这很快就会变成一个痛苦且容易出错的过程。当我更深入地回顾数据绑定并介绍视图模型时,我将在第八章回到这个主题。

每当我接收到WinJS.UI.Pages.ready事件时,我就调用setState函数,如下所示:

... WinJS.UI.Pages.define("/pages/flashCardsPage.html", {     ready: function (element, options) {         // *...other statements removed for brevity...* **        setState(options);**         selectAndDisplayNote();     } }); ...

这确保我在每次显示页面时重置应用状态并清除数据绑定值。

展示抽认卡

selectAndDisplayNote函数负责从appState.notes数组包含的集合中随机选取一个音符并显示给用户:

... function selectAndDisplayNote() {     if (appState.notes.length > 0) {         var index = Math.floor((Math.random() * appState.notes.length));         var note = appState.notes.splice(index, 1)[0];         appState.leftNote = (note.hand == "left" ? "&#"             + note.character + ";" : "=");         appState.rightNote = (note.hand == "right" ? "&#"             + note.character + ";" : "=");         appState.currentNote = note;         appState.currentIndex++;     } else { **        $("#noteButtons button").forEach(function (item) {** **            item.style.display = "none";** **        });** **        $("#noteButtons button[id]").setAttribute("style", "");**     } } ...

这是使用标准 JavaScript 代码实现的。如果没有留下测试用户的注释,那么我会修改布局——我用粗体标记的代码。这些语句隐藏答案按钮,并显示附加的导航按钮。您可以在图 4-5 中看到替代按钮组。注意,我已经使用了WinJS.Utilities.query方法,之前我将其别名化为$,使用id属性值来定位button元素。

images

图 4-5。附加导航按钮

我将这些按钮定义在常规的回答按钮旁边,但是将 CSS display属性的值设置为none,所以它们最初是不可见的。我这样定义按钮是为了再次强调标准的 HTML 和 CSS 特性在 Windows 应用中是如何可用的:

`...

                                   **    ** **    **
...`

作为一个相关的好处,所有的按钮——回答和导航——都与我使用别名$搜索的元素相匹配,并被配置为当它们被点击时调用handleButtonClick函数。

处理回答和导航按钮事件

应用功能的最后一部分包含在handleButtonClick功能中,当点击任何一个回答按钮或附加导航按钮时,就会执行该功能:

`...
function handleButtonClick(e) {
    if (this.id == "restart") {
        showPage("/pages/flashCardsPage.html", appState.mode);
    } else if (this.id == "back") {
        showPage("/pages/selectorPage.html");
    } else {
**        \(("button[data-note].correct").removeClass("correct");** **        \)("button[data-note=" + appState.currentNote.note + "]")**
**            .addClass("correct");**

**        if (this.innerText == appState.currentNote.note) {**
**            appState.results.numberCorrect++;**
**        } else {**
**            appState.results.numberWrong++;**
**        }**
        selectAndDisplayNote();
    }
}
...`

该函数在很大程度上使用了标准的 JavaScript 技术,但是有几个方面值得您注意,我将在下面的部分中进行描述。请注意我是如何在回答按钮中添加和删除 CSS 类的:

... $("button[data-note]").**removeClass("correct").removeClass("normal")**; $("button[data-note=" + appState.currentNote.note + "]")     **.addClass("correct").addClass("normal").removeClass("correct")**; ...

CSS 类将绿色背景应用到一个按钮上。当用户点击一个按钮时,我从应用它的任何按钮中删除正确的类,然后将它重新应用到当前显示的便笺的正确按钮上。这允许我创建一个简单的视觉提示来指示正确的答案。

依靠数据绑定发布数据更新

我想提醒您注意动态数据绑定的使用方式:

... if (this.innerText == appState.currentNote.note) { **    appState.results.numberCorrect++;** } else { **    appState.results.numberWrong++;** } ...

每当我评估用户提供的答案时,我就增加appState对象中的数据值,记录正确和错误答案的数量。

重要的是我必须做的事情:我不必手动更新 HTML 元素来显示更新的信息。这是自动发生的,因为我更新的值是可观察的,并且布局中的元素通过数据绑定系统显示数据。

这不仅是一种更方便的方法,而且还创建了一个更具可伸缩性和可维护性的应用结构。我可以更改 HTML 元素的布局,并且可以在应用的许多地方显示相同的数据值——但是当我需要进行更新时,我必须只给数据对象分配一个新值,就像在代码片段中一样。这在任何有 UI 的应用中都是一个重要的概念,但在 Windows 应用开发中尤其重要,我将在第八章中回到这个主题。

你可以在图 4-6 中看到完整的应用,显示了正在向用户显示的笔记,以及关于正在测试哪组笔记的信息,用户的进度,以及指示正确答案的颜色提示。

images

图 4-6。完成的 NoteFlash 应用

正如我之前提到的,这不是NoteFlash应用的最终版本,但基本功能已经完成,用户可以测试他的视奏能力。在后面的章节中,我将回到这个应用并添加更多的功能。

更新应用清单

虽然我已经完成了应用的 HTML、CSS 和 JavaScript 组件,但还有一点工作要做。如果你转到Start屏幕并找到NoteFlash应用的磁贴,你会发现应用呈现给用户的方式相当简单,如图图 4-7 所示。

images

图 4-7。note flash 应用的默认磁贴

默认情况下,Visual Studio 会为应用磁贴分配一个默认图标,并使用项目作为应用名称。我们可以通过对清单进行一些简单的更改来改善应用呈现给用户的方式。

双击Solution Explorer窗口中的package.appxmanifest文件,并导航到清单编辑器中的Application UI选项卡。此页面包含应用的基本设置。

设置平铺图像和颜色

我要做的第一个改变是应用磁贴的图像和颜色。Windows 应用出于各种目的使用不同大小的图像。有一个正方形拼贴的图像,如图 3-7 所示,它必须是 150 像素乘 150 像素。

平铺也可以是宽的(右击平铺并选择Larger按钮),这需要一个 310 像素乘 150 像素的图像。有一个小图标,30 x 30 像素,当应用显示为列表的一部分时使用,最后,一个 620 像素 x 300 像素的图像,当应用启动时用作启动屏幕。

对于这个应用,我创建了一系列图像,并将它们放在 Visual Studio 项目的images文件夹中。这些文件显示高音谱号符号,但由于我使用了白色图标和透明背景,我无法在打印页面上显示图像,但当它应用到应用时,您将能够看到它们。(当然,它们在本章的源代码下载中。)

在清单编辑器的Application UI选项卡中,有许多文本字段,这样你就可以为图块指定图像,如图 4-8 中的所示。

images

图 4-8。设置磁贴的图像

我的图像文件的名称以谱号开头,然后详细说明分辨率,例如clef30x30.png。您可以看到我是如何为清单中的各个字段设置图像名称的。图像没有缩放,如果图像大小不合适,您将无法使用它。

还要注意,我已经为Background color选项设置了一个值。这用于设置瓷砖的颜色——对于这个应用,我选择了蓝色的阴影,由十六进制代码#528FC8指定。

images 提示虽然图中没有显示,但是我也设置了闪屏的图像。

设置应用名称

我还想更改显示在磁贴上的名称,该名称取自清单中的Display name字段。我已经把这个改成了Note Flash,如图图 4-9 所示。

images

图 4-9。更改应用的显示名称

我还更改了Description字段,它为用户提供了应用的摘要。更改显示名称和应用图像的结果是如图 4-10 所示的磁贴,为用户呈现一个更加完美的应用磁贴。

images 提示这是一个静态磁贴,只显示图片和应用名称。在第二十七章的中,我将向你展示如何创建动态磁贴,向用户显示有用的信息。

images

图 4-10。note flash 应用的更新磁贴

测试完成的应用

这就是NoteFlash应用的全部内容。这是一个简单的软件,但它展示了 Windows 应用开发的一些基本特征和功能。要提高您的基本音符识别技能,只需启动应用,选择您想要测试的音符,然后给出您的答案。在本书每一部分的结尾,我将回到这个应用,并使用我在前一章描述的 Windows 特性对它进行改进。在我继续之前,我想回顾一下本章和第三章的关键主题。

基于网络技术的 Windows 应用

我怎么强调这一点都不为过:如果你正在使用 HTML 和 JavaScript 开发 Windows 应用,那么你就是在利用你已经掌握的常规 web 应用开发技能。有一些与 HTML 世界相当大的差异,特别是当它与一些高级 Windows 8 功能集成时,但正如 NoteFlash 应用所展示的那样,你可以使用标准的 web 技术和技巧完成很多事情。

Windows 应用不是网络应用

尽管你的 web 应用开发经验非常有用,但如果不使用平台功能和遵循微软惯例,你就无法发挥应用的全部潜力。你已经看到了 Windows 应用不同于 web 应用的关键领域——在核心导航模型中——我将在本书的其余部分向你展示更多。

数据绑定简化了应用开发

我是 web 和 Windows 应用中数据绑定的忠实粉丝,我在第八章中深入讨论了这个主题。Windows 应用可能会变得非常复杂,您应该采用一切可能的技术来保持代码和标记的可管理性,包括数据绑定。如果你愿意,你可以手动设置 HTML 元素的内容,但是你会让自己的日子更难过,尤其是在维护或增强你的应用的时候。

WinJS API 是用 JavaScript 写的

WinJS API 是常规 web 应用和 Windows 应用开发之间的桥梁。最重要的是,您可以通读 WinJS 代码,了解微软如何实现不同的功能,并应用调试器来跟踪困难的问题。对于 Windows API 来说,情况并非如此,但是随着您对 Windows 应用开发的掌握,您会发现您大部分时间都在使用 WinJS 特性。

总结

在这一章中,我向你展示了如何完成 NoteFlash 应用的功能,同时,也介绍了更多的核心 WinJS 功能。特别值得注意的是 WinJS 对类、名称空间和数据绑定的支持。既然您已经看到了如何创建一个基本的 Windows 应用,那么是时候开始深入研究细节了。在本书的第二部分,我展示了 WinJS API 的核心特性,你可以用它来创建一个好的 Windows 应用的结构。

五、单页模型

当我创建NoteFlash应用时,我简要介绍了单页导航模型的思想。在这一章中,我将深入探讨这个话题。单页模型背后的基本思想是,有一个 HTML 页面(通常称为母版页或主页)总是向用户显示,并负责在应用状态改变时将其他内容导入其结构。

这个模型为允许用户浏览你的应用提供了基础。您将内容导入到主页中以响应用户输入和交互,通常会替换以前显示的内容。WinJS API 提供了导入和显示应用内容以及在其中导航所需的工具和功能。在这一章中,我将向您展示如何执行这些操作,并解释处理导航操作的不同模型。这不是最令人兴奋的话题,但单页模型是 Windows 应用的核心,正确地使用它会使应用开发的其他方面变得更简单和容易——这可能有点枯燥,但值得关注。表 5-1 对本章进行了总结。

images

创建单页项目

为了演示支撑单页模型的技术,我使用Blank Application模板创建了一个名为SinglePageNav的示例 Visual Studio 项目(这是我在第二章中为NoteFlash应用使用的同一模板,也是我在本书中使用的模板)。对于我导入其他内容的基础,我将使用 Visual Studio 创建的default.html文件。你可以在清单 5-1 中看到default.html的起点。

清单 5-1 。SinglePageNav 项目中 default.html 文件的初始版本

`

<html> <head>     <meta charset="utf-8">     <title>SinglePageNav</title>


    
    
    


    
    

    
        
            

Content Controls

                                  


            

Top Right


            
This is part of default.html

            

                This is where the content will go
            

        


            

Bottom Right


            
This is part of default.html

            

                This is where the content will go
            

        

    

`

定义该应用布局结构的三个元素是 id 为lefttopRightbottomRightdiv元素。left元素包含一些button元素,我将在本章后面使用它们来导入内容以响应用户输入。topRightbottomRight元素提供了显示导入内容的结构。

定义初始 CSS

我使用 CSS 网格布局来定位元素,使用我在css/default.css文件中定义的样式,你可以在清单 5-2 中看到。

清单 5-2 。default.css 文件

`#gridContainer {
    height: 100%;
**    display: -ms-grid;**
**    -ms-grid-columns: 1fr 1fr;**
**    -ms-grid-rows: 1fr 1fr;**
}

left {

**    -ms-grid-row-span: 2;**
    background-color: black;
    padding: 10px;
}

topRight {

**    -ms-grid-column: 2;**
    background-color: #617666;
}

bottomRight {

**    -ms-grid-column: 2;**
**    -ms-grid-row: 2;**
    background-color: #767676;
}

div.contentBlock {
    border: medium solid white;
    padding: 5px; margin: 2px;    
}

div.message {
    font-size: 30px;
}

button {
    font-size: 30px; margin: 10px;
}

frameTarget {     width: 100%;

height: 100%;
}`

我强调了与网格布局相关的 CSS 属性。这些属性创建一个两行两列的网格。left元素横跨两行,由于我没有明确指定位置,所以它会在第一列和第一行。topRightbottomRight元素在第二列,每行一个。你可以在图 5-1 中看到网格布局的效果。

images

图 5-1。样板工程的初始布局

定义初始 JavaScript

我在js/default.js文件中的初始 JavaScript 代码如清单 5-3 所示。除了为WinJS.Utilities.query方法创建我喜欢的$别名之外,代码还在名为leftdiv元素中找到了button元素,并为click事件注册了一个回调函数。

清单 5-3 。js/default.js 文件的初始内容

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {
**        $('#left button').listen("click", function (e) {**
**            // button handler code will go here**
**        });**
    };     app.start();
})();`

我将在本章的后面添加处理click事件的代码。

以声明方式导入内容

将内容引入单页应用的最简单方法是以声明的方式进行,这仅仅意味着将一种机制应用于default.html文件中的 HTML 元素。清单 5-4 展示了如何将声明性技术应用到示例应用布局中的一个容器元素中。

清单 5-4 。以声明方式导入内容

`...

    

Top Right

    
This is part of default.html
    
        This is where the content will go     
...`

WinJS API 的主要部分是一组 UI 控件。这些是应用于标准 HTML 元素的增强,以提供特定于应用的功能。为了以声明方式将内容导入到我的应用中,我使用了一个非常基本的控件HtmlControl

我将HtmlControl应用到div元素中我想要插入导入内容的地方。应用控件就是将data-win-control属性设置为 WinJS API 中控件对象的全名,也就是WinJS.UI.HtmlControl

我必须配置控件,以指定我想要导入的文件的名称。格式是包含一个 JSON 片段,其中的uri属性定义了文件名,在本例中是contentBasic.html。我不喜欢像这样将 JSON 嵌入到 HTML 中,但是声明式导入需要它。

images 提示在这一章中,我不会深入探究 WinJS UI 控件的机制。我在本书的第三部分中详细介绍了它们。

最后一步是激活控件,这需要一个 JavaScript 方法调用。这在一定程度上破坏了这个示例的声明性,但是这是一个激活您添加到 HTML 中的任何控件的调用。你可以在清单 5-5 的中看到所需的方法调用,我已经将它添加到了default.js中。

清单 5-5 。激活 HTML 标记中的控件

(function () {     "use strict"; `var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {
        $('#left button').listen("click", function (e) {
            // button handler code will go here
        });
**        WinJS.UI.processAll();**
    };

app.start();
})();`

Windows 运行时不会自动搜索 DOM 来查找带有data-win-control属性的元素。我必须显式地请求这个搜索,这就是WinJS.UI.processAll方法所做的。最后一步是定义我想要导入的内容。清单 5-6 显示了contentBasic.html文件的内容。

images 提示我创建了 contentBasic.html 文件,方法是在解决方案浏览器窗口中右键单击项目条目,从弹出菜单中选择Add New Item,并使用HTML Page项模板。这个模板创建一个 HTML 页面。这不同于我之前使用的Page Control项目模板,它创建了 HTML 文件、CSS 文件和 JavaScript 文件(并在 HTML 文件中添加了linkscript元素)。Page Control模板只是为了方便,并没有赋予它创建的文件任何特殊属性。事实上,我更喜欢在需要时单独创建文件,很少在自己的项目中使用Page Control模板。

清单 5-6 。contentBasic.html 档案

`

             Basic Content                            
            Hello from the contentBasic.html file         
     `

这是一个简单的 HTML 文件,包含一个内嵌的style元素和一些基本的 HTML。如果您启动示例应用,您将看到声明式导入的效果,如图图 5-2 所示。我在图中突出显示了导入的内容。

注意,已经在div元素中的内容仍然存在。导入内容不会替换任何现有的子元素——如果您只想要导入的内容,就必须显式删除目标元素的内容(我很快就会这么做)。

images

图 5-2。以声明方式导入内容

了解导入机制

如果你运行这个例子或者查看图 5-2 ,你会注意到右栏中的所有文本现在都对齐到了父元素的右边,而之前它是对齐到左边的(如图图 5-1 所示)。发生这种情况是因为内容导入应用时的处理方式。

这个例子使用了HtmlControl,但是我在本章后面描述的 Pages 特性也发生了同样的事情。为了理解发生了什么,考虑示例应用中的 CSS。default.css文件包含了message类的样式,如下所示:

... div.message {     font-size: 30px; } ...

contentBasic.html文件包含一个style元素,message类定义了一个样式:

... div.message {     font-family: serif;     text-align: right; } ...

导入文件时,它包含的script元素被添加到主 HTML 文档的head元素中。Windows 应用中的 CSS 遵循与 web 应用相同的优先级规则。这意味着应用于message类中元素的样式是来自两个文件的组合属性集,并且由于script元素被添加到主文档的head中,这个组合属性集将影响所有的message元素,而不仅仅是那些已经导入的元素。

images 提示 CSS 优先考虑属性定义的顺序。这意味着导入内容中的属性会覆盖主文件中定义的属性。

确保您的样式只影响一个文档中的元素的最简单方法是确保您缩小 CSS 样式的范围,使它们只影响导入的元素。你可以在清单 5-7 中看到我是如何为contentBasic.html做这些的。

清单 5-7 。缩小对导入内容中 CSS 样式的关注

`

             Basic Content                    **        
**             
                Hello from the contentBasic.html file             
**        
**      `

我添加了一个div元素,作为将要导入到文档中的元素的父元素。div不会改变导入内容的外观或结构,但它允许我缩小样式的关注范围,这样它们就不会泄露到布局的其他部分。你可以在图 5-3 的中看到这种变化的效果。注意来自default.html文件的内容不再受右对齐的影响。

images

图 5-3。缩小导入内容中 CSS 样式焦点的效果

以编程方式导入内容

声明性的HtmlControl是导入内容的最简单的方式,但是这种简单性带来了一些限制。第一个问题是一旦内容被加载,就没有办法改变它——使用 JavaScript 来改变data-win-options属性的值不会加载新的内容。第二个限制是,如果您导入包含script元素的内容,您几乎肯定会遇到问题——HtmlControl只能可靠地处理简单的静态内容,就像我在前面的例子中使用的那种内容。

JavaScript 的问题取决于代码本身。导入内容时,script元素的处理方式与style元素相同,并被添加到主文档的head元素中。一旦script元素的内容被插入到head元素中,就会被执行,这发生在 HTML 元素被导入之前。由于您想要操作的元素尚不存在,代码将会失败。您不能依赖 DOM 事件或类似 jQuery ready方法的技巧,因为底层事件是在 Internet Explorer 加载主控文档时触发的。当内容被导入时,浏览器已经触发了它的就绪事件并继续前进。

只对主文档中已经存在的元素进行操作的 JavaScript 代码可以工作,但是将这种代码放入要导入的文件中是违反直觉的。这样做在主布局和导入的代码之间创建了一个紧密耦合——我将在下一节更详细地讨论这一点,但这通常不是一个好主意。

这并不意味着声明性地使用HtmlControl没有用,但这是非常基本的。如果你想把你的应用布局分成可管理的部分,并在运行时加载它们,那么声明式的HtmlControl是完美、简单和可靠的。如果您想要更复杂的东西,那么您应该看看可编程的替代方案,我将在下一节对此进行描述。

以编程方式使用 HtmlControl

灵活性的下一步是以编程方式使用HtmlControl。这就改变了对HtmlControl的使用,使其全部发生在 JavaScript 代码中,而不再嵌入到 HTML 中。现在,我必须说我在我的项目中没有以这种方式使用HtmlControl——它缺乏声明式方法的简单性,也没有我稍后描述的页面特性的灵活性。我向您展示这项技术的主要原因是为了演示一个常见的陷阱:我提到的紧耦合概念。让我从展示编程用法开始。首先,我需要重置我在default.html文件中的 HTML 元素来移除声明性属性,如清单 5-8 所示。

清单 5-8 。删除声明性 HtmlControl 属性

`...

    

Top Right

    
This is part of default.html
    
        This is where the content will go     
...`

我现在可以在我的 JavaScript 代码中添加语句到default.js文件中,以编程方式使用HtmlControl,我在清单 5-9 中就是这么做的。

清单 5-9 。以编程方式使用 HtmlControl】

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {
        $('#left button').listen("click", function (e) { **            var targetElem;**
**            if (this.id == "button1") {**
**                targetElem = $('#topRight div.contentTarget')[0];**
**            } else {**
**                targetElem = $('#bottomRight div.contentTarget')[0];**
**            }**
**            WinJS.Utilities.empty(targetElem);**
**            new WinJS.UI.HtmlControl(targetElem, {uri: 'contentBasic.html'});**
        });
        WinJS.UI.processAll();
    };
    app.start();
})();`

我已经为button控件的click处理函数添加了新代码。每个按钮在文档中定位一个不同的目标元素,并将它赋给targetElem变量。

images 注意这是内容导航的基本形式,因为我为用户提供了一种改变布局组成的方式。在 Windows 应用中使用常规的 HTML 元素和事件进行导航是完全允许的,但是也有一些特定于应用的 UI 控件和 API 专用于导航,我在第七章的中对此进行了描述。

我使用WinJS.Utilities.empty方法删除目标元素中的现有内容,然后创建一个新的HtmlControl对象来导入contentBasic.html文件。一个HtmlControl对象的两个构造函数参数是目标 HTML 元素和一个包含配置信息的对象,配置信息的格式与我在上一节中声明的格式相同。您可以使用标准的 DOM API 方法(比如document.getElementById),使用将id属性值视为变量的 IE 特性,或者像我在本例中所做的那样,使用WinJS.Utilities.query方法来获得目标元素。这是我别名为$的方法,它返回一个匹配 CSS 查询字符串的元素数组,即使只有一个匹配元素。这就是为什么我必须通过将[0]附加到方法调用来提取我想要的元素。

images 提示以编程方式创建的HtmlControl的配置信息是一个对象,而不是一个 JSON 字符串。你必须记住不要把论点用引号括起来。

这些更改的结果是,在单击左侧面板中的某个按钮之前,不会导入任何内容。两个按钮导入相同的内容——contextBasic.html文件——但是内容导入到的元素不同。这些动作相互独立工作,也就是说如果你同时点击两个按钮,contentBasic.html文件的内容将被导入到文档中的两个位置,如图图 5-4 所示。

images

图 5-4。以编程方式将相同的内容导入两个位置

使用HtmlControl以编程方式解决了声明性使用的一些不足。首先,我获得了对何时导入内容的控制权——在本例中,我在响应按钮点击时导入内容。其次,我可以通过删除目标元素中存在的任何内容并创建另一个HtmlControl对象来更改显示的内容。这是一个很大的进步,它让我可以将我的应用分解成可管理的块,我可以按需组合并显示给用户,根据应用的变化状态重用部分布局来导入和显示内容。

进口互动内容的风险

即使以编程方式使用,HtmlControl也不能以有效的方式处理script元素——它们仍然被添加到主文档的head元素中,并在导入常规 HTML 元素之前执行。然而,HtmlControl对象构造函数接受一个回调函数的可选参数,该函数将在内容导入后执行。这提供了我以前没有的定时信号,允许我创建可以对新添加的元素进行操作的代码。至少,最初看起来是这样——但这是一个陷阱,需要谨慎。我将介绍回调函数的使用,并向您展示它所产生的问题。首先,我需要一些需要 JavaScript 才能有用的内容。清单 5-10 显示了我添加到根项目文件夹中的一个新文件的内容,名为contentButton.html

清单 5-10 。contentButton.html 档案

`

             Button Content                   
` `            
                             
        
     `

这是一个包含一个button元素的普通 HTML 文件。这给我带来了一个问题,因为我需要设置一个处理函数,这样我就可以响应button的事件。在清单 5-11 中,你可以看到我是如何使用default.js文件中的第三个HtmlControl参数来提供一个找到按钮并绑定它的函数的。

清单 5-11 。使用 HtmlControl 回调函数处理导入的元素

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

function loadLowerContent() {
        var targetElem = $('#bottomRight div.contentTarget')[0];
        WinJS.Utilities.empty(targetElem);
        new WinJS.UI.HtmlControl(targetElem, { uri: "contentBasic.html" });
    }

app.onactivated = function (eventObject) {
        $('#left button').listen("click", function (e) {
            var targetElem = $('#topRight div.contentTarget')[0];
            WinJS.Utilities.empty(targetElem);

**            new WinJS.UI.HtmlControl(targetElem, { uri: "contentButton.html" },**
**                function () {**
**                    contentButton.addEventListener("click", loadLowerContent);**
**                });**
        });
        WinJS.UI.processAll();
    };
    app.start();
})();`

我通过id属性值定位button元素,并使用addEventListener方法设置一个事件处理程序。

这一切都如你所料:如果按下应用布局左侧的按钮,就会加载contentButton.html文件,其中包含的元素就会添加到布局中。然后调用我的回调函数,并配置我的按钮。要查看效果,启动应用并单击左侧面板中的Button One。您将看到contentButton.html文件的内容被导入到右上角的面板中,包括button元素。点击新导入的buttoncontentBasic.html文件的内容被导入到右下面板。

这种方法有两个问题。一个是可见的,并且相当容易分类。另一个是无形的,但更重要,需要时间和注意力来理解和解决。

了解 CSS 问题

可见的问题是我前面描述的 CSS 范围问题的变体。我想重温一下,因为它强调了在导入内容时缩小 CSS 选择器范围的重要性。

如果您运行示例应用并单击Button One,则会加载contentButton.html文件。出现的Press Me按钮与其父容器的左边缘对齐。如果点击Press Me按钮,contentBasic.html文件被导入到布局中,Press Me按钮的对齐被移动到父项的右侧。你可以在图 5-5 中看到效果,图中显示了点击button前后的 app。

images

图 5-5。由进口 CSS 驱动的按钮延时动作

这种变化的原因是在contentBasic.html文件中为message样式定义的附加属性。contentButton.html文件中的button包含在message类的div元素中,两个文件共享相同的元素结构和命名模式。当导入contentBasic.html文件时,我定义的 CSS 样式比我预期的应用得更广泛。

事实上,问题是由一系列交互触发的,这使得在开发和测试过程中很难发现问题(这种性质的大多数问题比按钮突然移动位置更加微妙)。

理解紧耦合问题

更严重的问题是,我在default.jscontentButton.html文件之间创建了一个紧密耦合。紧密耦合简单地说就是对一个应用中一个组件的更改将要求我在其他地方做一个或多个更改——也就是说,default.js文件中的代码依赖于contentButton.html文件的内容。因此,例如,如果我更改了contentButton.html文件中button元素的id或者用不同种类的元素替换它,我必须更新default.js文件,以便HtmlControl回调函数也反映这些更改。

这是一个比听起来严重得多的问题。对于一个简单的示例应用,管理紧密耦合的组件之间的依赖关系所带来的额外工作并不多。但是对于一个真正的应用,有真正的用户,真正的时间表和真正的测试计划,这就成了一个严重的负担。每次更改都需要跟踪应用中所有受影响的地方,正确应用更新,然后测试整个应用。这是一个非常痛苦的过程,并且对软件质量和开发人员的生产力有着非常不利的影响,因此在可能的情况下避免紧密耦合是非常重要的。

对于我的例子,我需要从default.js中删除关于contentButton.html文件的内容和结构的知识。这意味着使contentButton.html成为一个独立的单元,default.js可以像黑盒一样对待它,这意味着修复我前面解释的导入的script元素执行问题,以便导入的内容可以包含代码。接下来,我将解释 WinJS Pages 特性如何提供我正在寻找的解决方案。

使用 WinJS 页面功能

WinJS Pages 特性提供了解决内容导入问题所需的工具,而不会产生耦合问题。因为 Pages 特性解决了 JavaScript 计时问题,所以它适用于导入所有类型的内容。这对于将应用分成可管理的静态内容块以及创建对导入文档中的元素进行操作的 JavaScript 代码非常有用。这种灵活性是以复杂性为代价的——没有对 Pages 特性的声明性支持,导入内容至少需要两步。正如您将看到的,第一步需要添加代码来导入内容。这发生在default.js文件中。第二步是将代码添加到正在导入的文件中,遵循特定的模式,这样当导入的元素被添加到 DOM 中时,我会得到通知。

Pages 特性的功能包含在WinJS.UI.Pages名称空间中。在接下来的小节中,我将向您展示如何使用这个名称空间来实现导入过程(以及一个可选的步骤,它可以使以特定的顺序导入内容变得更加容易)。

导入内容

当然,第一步是通过WinJS.UI.Pages.render方法导入内容。对此没有声明性支持,因此任何内容导入都依赖于render方法。你可以在清单 5-12 中看到对default.js文件的修改,以使用渲染方法。

清单 5-12 。使用 WinJS。导入内容的 UI.Pages.render 方法

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {
        $('#left button').listen("click", function (e) {
            var targetElem = $('#topRight div.contentTarget')[0];
            WinJS.Utilities.empty(targetElem);

**            var buttonTargetElem = $('#bottomRight div.contentTarget')[0]**

**            WinJS.UI.Pages.render("contentButton.html", targetElem,**
**                { content: "contentBasic.html", target: buttonTargetElem });**
        });
        WinJS.UI.processAll();
    };
    app.start(); })();`

render方法的前两个参数指定要导入的内容和内容将插入的目标元素。第三个参数是可选的,更有趣。此参数允许您指定任意数据对象,该对象可用于导入内容中的代码。这是一个很好的特性,因为它允许您创建复杂的功能块,您可以通过传入不同的数据值以不同的方式重用这些功能块。数据对象没有要求的格式,您可以将任何内容从简单的字符串传递到复杂的对象。在这个例子中,我使用了一个对象,它的属性指定了contentButton.html文件中的按钮所需的信息——单击按钮时应该导入的内容和应该插入内容的元素。

注册回拨

Pages 功能不会改变内容的加载方式。导入文件中的任何script元素仍然被添加到head元素中并立即执行。Pages 特性添加了一个回调机制,当我的内容被插入到文档中时它会通知我,这意味着我可以推迟 JavaScript 代码的执行,直到我想要操作的元素被添加到 DOM 中。回调的处理程序是使用WinJS.UI.Pages.define方法设置的,你可以在清单 5-13 中的contentButton.html文件的script元素中看到我是如何使用这个方法的。

清单 5-13 。使用 WinJS。注册回调函数的方法

`

             Button Content                            
            
                             
        
     `

define方法有两个参数。第一个是您希望得到通知的文件的名称——在这个场景中,这总是当前文件的名称,因为我希望在 HTML 元素导入后得到通知。

第二个参数更复杂—它是一个对象,其属性指定回调函数,这些函数将在响应内容生命周期的不同部分时执行。属性集由WinJS.UI.Pages.IPageControlMembers接口定义,涵盖了生命周期中的各个阶段。

images 注意 JavaScript 不支持接口,但其他 Windows app 编程语言支持。不必拘泥于细节,对于 JavaScript Windows 应用,接口定义了一组方法和属性,对象必须定义这些方法和属性才能在特定情况下使用,或者在本例中,定义了一组受支持的值或属性名。接口的概念不太适合 JavaScript,但它是让这种语言被视为一等 Windows 应用公民并访问 Windows API 的成本的一部分。

支持的属性名集合有errorinitloadprocessedready,它们可以用于在导入特定文件时对其进行监控。

当您在导入的文件中使用define方法时,最有用的属性是ready,当内容已经加载并插入到主布局中时,您分配给该属性的函数将被执行。ready属性是我支持 JavaScript 代码所需的定时信号,该代码对导入文件的 HTML 元素进行操作。

执行分配给 ready 属性的函数时,会传递两个参数。第一个参数是内容将要插入的元素。第二个参数是传递给render方法的数据对象。

在这个例子中,我使用数据对象的targetcontent属性来配置来自Press Me按钮的click事件的处理方式。通过以这种方式传递信息,我确保了contentButton.html中的代码不依赖于default.html文件中的元素结构。

images 提示这种技术并不完美,因为导入的文件需要知道自己在 Visual Studio 项目中的路径——这意味着移动或重命名文件需要更改代码。我还没有找到解决这个问题的方法,但是结果仍然比我开始时的那种深度依赖要好。

确保内容顺序

WinJS 和 Windows APIs 中的许多方法都是异步工作的。这意味着当您调用一个方法时,它需要做的工作被安排在以后执行。方法立即将控制权返回给代码,以便可以执行脚本中的下一条语句。如果这个方法有一个结果,它通常是通过一个回调函数来处理的,当你调用这个函数时,你把它传递给这个方法。

您可以在 API 文档中找到异步方法,因为它们返回一个WinJS.Promise对象。一个Promise代表了在未来某个时候执行一些工作的承诺,并定义了当Promise完成时(即工作已经完成)设置回调函数所需的所有功能。我在第九章中解释了Promise对象的工作原理,但是我现在需要介绍一些基本用法来告诉你如何处理一些内容导入问题。

微软如此广泛使用异步方法的原因是为了迫使开发人员创建响应性应用。特别是,微软希望避免困扰 Windows 以前版本的一个问题,即应用的 UI 冻结,因为它正在前台执行一些长期活动,如等待连接到服务器或保存大量数据。通过在整个 API 中驱动 Promise对象,微软确保 Windows 应用 ui 很少落入这个陷阱,尽管代价是让开发人员的生活稍微复杂一些。

我说稍微复杂一点,是因为您可能已经熟悉了 web 应用开发中异步编程的概念。Ajax 请求一个在后台执行的操作的完美示例,其结果使用回调函数发出信号。如果你是 jQuery 的粉丝,你会很高兴地得知 jQuery 延迟对象特性与 Windows Promise对象基于相同的CommonJS Promises/A规范(你可以在[wiki.commonjs.org/wiki/Promises/A](http://wiki.commonjs.org/wiki/Promises/A)了解更多)。

承诺在本章中很重要的原因是WinJS.UI.Pages.render方法是异步的。当您调用此方法时,您指定的内容不会立即加载,而是计划在以后加载。如果你用一段内容填充你的目标元素,就像我在这一章中一直做的那样,这没问题,但是当你把几个条目导入到同一个元素中时,这可能会引起问题。

问题是不能保证后台任务按照它们被调度的顺序执行。Windows 运行时可以按照它喜欢的任何顺序自由地履行承诺(异步编程中的一种常见方法),并且内容一旦可用就被插入(这意味着需要较长处理时间的内容可以比较简单的内容晚插入,即使较长内容的处理先开始)。

在清单 5-14 中,我已经展示了将多个内容项导入单个元素的两种方法中的第一种。当点击idbutton1的按钮时,我调用render方法三次,忽略方法调用返回的Promise对象。当您不关心它们的插入顺序时,您会采用这种方法。清单显示了default.js文件的内容。

清单 5-14 。使用 render 方法返回的承诺来订购内容

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {
        $('#left button').listen("click", function (e) {
            var targetElem = $('#topRight div.contentTarget')[0];
            WinJS.Utilities.empty(targetElem);

if (this.id == "button1") {
**                WinJS.UI.Pages.render("contentButton.html", targetElem)**
**                WinJS.UI.Pages.render("contentBasic.html", targetElem);**
**                WinJS.UI.Pages.render("contentButton.html", targetElem);**
            }
        });
        WinJS.UI.processAll();
    };
    app.start();
})();`

我称为render方法的顺序建议内容顺序为contentButton.htmlcontentBasic.html,然后再一次为contentButton.html。但是由于我已经忽略了WinJS.Promise对象,我实际上是让顺序在运行时确定。如果运行该示例并单击Button One,您将看到如图图 5-6 所示的结果。

images

图 5-6。不受管理的内容顺序

如图所示,内容导入的顺序与我调用render方法的顺序不匹配。发生这种情况的原因有很多,但是对排序最重要的影响来自于render方法缓存内容这一事实。加载contentButton.html文件的第二次调用完成得非常快,因为第一次调用的结果被缓存了。

images 提示尽管缓存在这个例子中起了很大的作用,但是当你丢弃由render方法返回的Promise对象时,你仍然不能依赖特定的顺序。因为有很多因素会影响后台工作的执行顺序,所以即使第二次调用相同的方法,也不能指望得到相同的结果——每次都可能不同。

用承诺强迫内容订单

Promise对象定义了一个名为then的方法,用于指定当Promise完成时(即后台工作完成时)要执行的功能。then方法允许您将函数链接在一起,从而将一个后台任务的调度推迟到另一个任务完成之后。清单 5-15 展示了当点击标记为Button Two的按钮时,我如何使用Promise.then来强制输入内容的顺序。

清单 5-15 。使用 render 方法返回的承诺来订购内容

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {
        $('#left button').listen("click", function (e) {
            var targetElem = $('#topRight div.contentTarget')[0];
            WinJS.Utilities.empty(targetElem);

if (this.id == "button1") {                 WinJS.UI.Pages.render("contentButton.html", targetElem)
                WinJS.UI.Pages.render("contentBasic.html", targetElem);
                WinJS.UI.Pages.render("contentButton.html", targetElem);
            } else {
**                WinJS.UI.Pages.render("contentButton.html", targetElem)**
**                .then(function () {**
**                    return WinJS.UI.Pages.render("contentBasic.html", targetElem);**
**                }).then(function () {**
**                    return WinJS.UI.Pages.render("contentButton.html", targetElem);**
**                });**
            }
        });
        WinJS.UI.processAll();
    };
    app.start();
})();`

注意,我返回了从render方法得到的Promise对象,作为我传递给then方法的函数的结果。这确保了在Promise完成之前不会执行后续功能。我将在第九章中向你展示不同的编排Promise的方法,但你可以通过启动应用并点击Button Two来查看这种编排的效果。图 5-7 显示了结果——如你所料,内容已经按照我调用render方法的顺序导入。

images

图 5-7。强制输入内容的顺序

这种方法的好处是内容按照我想要的顺序排列。缺点是我拒绝了运行时一次加载和处理多个项目的机会,这是异步编程的主要好处之一。当涉及到对布局中的内容进行排序时,为了得到您需要的结果,牺牲性能通常是值得的。

使用导航 API

在我的示例应用中,我仍然有一个问题:用户可以导航到哪里的细节必须包含在每个页面中。当您希望为用户提供不同的路线来导航到相同的内容时,这可能会有所限制。例如,想象一个以特定方式显示数据并可以通过另外两个页面访问的页面——您想为用户提供一种返回到他来自的页面的方法,但是没有办法知道那是哪个页面。

您可以使用包含在WinJS.Navigation名称空间中的 WinJS 导航 API 来解决这个问题。导航 API 帮助您跟踪您在应用中所做的导航更改,并使用这些数据来创建更灵活的布局。在接下来的小节中,我将向您展示如何使用这个 API。导航 API 实际上不做任何导航,它只是在需要导航服务的应用部分和实现您的首选导航策略的代码之间充当代理。随着导航请求的创建和完成,导航 API 会维护一个导航历史,您可以使用它在内容中创建更灵活的导航。

处理导航事件

使用导航 API 时,您必须做的第一件事是为至少一个导航事件注册回调函数。这些事件是在发出导航请求时触发的(我将在下一节演示)。对于每个导航请求,三个导航事件依次发生——我在表 5-2 中总结了这些事件。这些事件没有内置的处理程序,所以你可以在你的应用中把它们解释成最有意义的。我倾向于处理navigating事件,而忽略其他事件。

images

您可以用WinJS.Navigate.addEventListener方法或我在表中显示的便利属性为每个事件注册一个处理函数。导航 API 是一个自我组装的导航系统,所以你在应用中解释这些事件的方式完全取决于你。对于我的示例应用,我将为navigating事件注册一个处理程序,并通过使用 Pages API 将内容导入到应用布局中来进行响应。您必须安排注册您的事件处理函数,作为应用初始设置的一部分,以便您可以响应所有导航请求。在清单 5-16 中,你可以看到我已经添加到default.js文件中的事件处理程序:将代码放在这个文件中意味着当应用加载我的 HTML 主文件时,我的事件处理程序将被注册。

清单 5-16 。处理导航事件

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {

**        WinJS.Navigation.addEventListener("navigating", function (e) {**
**            var targetElem = $('#topRight div.contentTarget')[0];**
**            WinJS.Utilities.empty(targetElem);**

**            var content = e.detail.location == "basic"**
**                ? "contentBasic.html" : "contentButton.html";             WinJS.UI.Pages.render(content, targetElem);**
**        });**
    };
    app.start();
})();`

在这个清单中,我为navigating事件注册了一个处理函数。当我收到这个事件时,我在布局中定位并empty一个目标元素,然后使用render方法导入内容,就像我在前面的例子中所做的一样。(我只导入了一个项目,所以忽略了 render 方法返回的Promise对象。)

我的处理函数接收一个Event对象作为参数。detail.location属性为我提供了所请求的导航位置。使用导航 API 的优点之一是,您的内容不必知道它想要导航到的文件的名称,它可以使用您创建的任何命名机制请求任何内容。在我的例子中,导航到basic将导入contentBasic.html文件,导航到button将导入contentButton.html文件。

提示这似乎是一个小功能,但却是一个有用的想法。如果项目的结构发生变化,嵌入到内容中的每个显式文件名都必须更新。通过将请求位置的名称与导入文件的名称分开,可以将需要与项目结构保持同步的代码放在一个地方。当项目结构发生变化时,您只需更新导航事件处理程序。

调用导航方法

navigate方法是导航 API 的核心。当您想要导航到应用的另一部分时,可以调用此方法。导航 API 触发我在上一节中描述的事件,这些事件反过来执行您在事件处理函数中设置的导航代码。

navigate方法的参数是您请求的位置和一个可选的状态对象,您可以用它将信息传递到您导航到的内容。清单 5-17 展示了我如何在default.js文件中使用navigate方法。

清单 5-17 。使用 default.js 文件中的 navigate 方法

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

app.onactivated = function (eventObject) {

WinJS.Navigation.addEventListener("navigating", function (e) {
            var targetElem = $('#topRight div.contentTarget')[0];
            WinJS.Utilities.empty(targetElem);

var content = e.detail.location == "basic"
                ? "contentBasic.html" : "contentButton.html";             WinJS.UI.Pages.render(content, targetElem);
        });

**        $('#left button').listen("click", function (e) {**
**            if (this.id == "button1") {**
**                WinJS.Navigation.navigate("basic", "Hello from default.js");**
**            } else {**
**                WinJS.Navigation.navigate("button");**
**            }**
**        });**
        WinJS.UI.processAll();
    };
    app.start();
})();`

在定义导航事件处理函数的同一个代码块中调用navigate方法看起来有点奇怪,但是随着我在示例应用的其余部分继续采用导航 API,这种模式开始变得更有意义。清单 5-18 显示了对contentButton.html文件的修改,这样点击按钮元素就会调用navigate方法。

清单 5-18 。使用 contentButton.html 文件中的导航方法

`

             Button Content                            
            
                             
        
     `

现在我有了一些内容,它们可以请求导航到basic内容,而不需要知道内容文件的名称或者内容将被插入到布局中的什么位置。导航事件处理函数负责处理导航,它为应用的其余部分提供服务。注意在navigate方法的调用者和事件处理函数之间没有直接的关系。当您调用navigate方法时,您依赖导航 API 来发送事件,并期望有一个处理函数愿意并能够代表您执行导航。(这就是为什么确保在应用首次启动时注册您的处理函数非常重要。)

使用导航历史

此时,我有了一个工作的导航系统,它允许我以两种方式访问contentBasic.html文件。我可以点击Button One,直接导航到内容,也可以点击Button Two,带我到contentButton.html,然后点击Press Me。现在,我已经建立了到相同内容的两条路径,我可以使用导航 API 的其他特性来确保用户获得一致的导航体验。对于我的简单示例应用,这意味着我可以设置Back按钮,以便它返回到用户来自的页面。我已经修改了contentBasic.html文件来使用导航 API,如清单 5-19 所示。

清单 5-19 。使用 contentBasic.html 文件中的导航 API

`

             Basic Content                  
    
    
        
            

                No Message
            

**            **
        

    

`

本文档中的重要元素是button。当我的ready处理程序被执行时,我通过id找到了button元素,并使用导航 API 来配置它。导航 API 维护已经导航到的位置的历史。如果有可能返回到先前的位置,那么WinJS.Navigation.canGoBack属性返回true

在我的清单中,如果不能返回,我禁用了button,如果可以,我为click事件设置了一个处理函数。如果您在按钮被启用时单击它,我的处理程序将调用WinJS.Navigation.back方法。这导致导航 API 使用最后访问的位置发出导航事件,为我的内容提供了一种简洁的方式来展开导航序列。导航 API 中有用于向前移动和确定当前位置的等效成员,就像在 web 应用中使用浏览器的历史 API 一样。

我在清单中包含的导航 API 的另一部分是 state 对象。在default.js文件中,我用一个简单的字符串作为状态对象参数调用了navigate方法。这个对象可以通过WinJS.Navigation.state属性获得。如果一个状态对象可用,我用它来设置导入内容中的div元素的内容。

images 提示通过Event对象的detail.state属性,导航事件处理函数可以使用状态对象。

在图 5-8 中点击Button One可以看到导航到contentBasic.html的效果。如果你在应用刚刚启动的时候以这种方式导航,没有导航历史,因此canGoBack属性返回false,这意味着Back按钮将被禁用。虽然没有历史记录,但是有一个可用的状态对象,您可以在布局中看到显示的消息。

images

图 5-8。点击按钮一导航至 contentBasic.html

作为对比,你可以看到在图 5-9 中点击Button Two然后点击Press Me按钮导航到contentBasic.html的效果。有可用的导航历史,所以Back按钮被激活,点击它将导航回contentButton.html(当然,这里的要点是在contentBasic.html文件的代码或标记中没有对contentButton.html的引用)。在这个导航序列中,我没有向navigate方法传递状态对象参数,这在消息中有所反映。

images

图 5-9。点击按钮一导航到 contentBasic.html,然后按我

避开浏览器导航功能

既然你已经看到了导航 API 是如何工作的,你可能想知道为什么不直接使用浏览器内置的historylocation对象。问题是这些对象不允许你根据应用的单页内容模型来定制导航。一个 JavaScript Windows 应用可以从主页面导航到一个新的顶级页面,但是当这种情况发生时,应用中的所有状态和内容都会丢失(因为它是使用简单的 JavaScript 对象维护的)。这在使用视图模型时尤其成问题,我在第八章中介绍了这一点。

你需要小心不要使用浏览器内置的导航功能——如果你这样做,你将打破单页模式。这样做不会杀死应用,但会丢失任何基于常规全局 JavaScript 对象和变量的数据,例如导航历史和自定义名称空间。一旦您导航到另一个顶级页面,这些数据就会丢失,并且导航回您的母版页不会恢复这些数据。

在我的项目中,疏忽的顶级导航最常见的原因是当我使用a元素进行导航时,忘记覆盖默认的click行为。如果您在项目中使用a元素,您必须确保处理click事件,调用事件上的preventDefault方法,并使用 WinJS 导航 API 请求导航更改。

总结

在这一章中,我已经向你展示了 Windows 应用的一个基本构件:单页布局。这种布局在 web 应用中越来越流行,但是要让它在应用中正常工作需要使用一些 WinJS 功能。我向您展示了如何使用HtmlControl导入简单的静态内容,如何使用WinJS.UI.Pages API 导入和管理更复杂的内容,最后,如何使用WinJS.Navigation API 在您的应用中创建灵活的导航。

Windows 应用处理单页布局模型的方法很复杂,需要考虑很多东西。但是不要担心——它很快就会成为你的第二天性,你很快就会熟悉我所描述的特性和技术。在下一章,我将向你展示如何让你的应用适应和响应不同的布局和方向,如果你想给你的用户提供一流的 Windows 体验,这是很重要的。

六、创建自适应布局

在第三十章,我向你展示了如何使用 Metro 对单页内容模型的支持来为你的应用创建布局。在这一章中,我将向您展示如何使布局适应不同的视图和方向。大多数 Windows 8 设备上都有视图,允许用户选择不同的方式与应用交互,包括并排运行两个 Metro 应用。方位出现在可以保持在不同位置或容易旋转的设备上,并且这些设备配备有传感器来报告它们的状态。你需要仔细考虑如何在应用中容纳不同的视角和方向,以创造一流的地铁体验。

我还将向您展示如何处理高像素密度显示器。这些显示器在平板电脑和手机平台上越来越常见,为用户提供了比传统硬件更清晰的显示器。在大多数情况下,Windows 8 会为您处理像素密度,但有一个关键的例外需要注意:位图图像。我解释了 Windows 8 如何接近像素密度,并向您展示 Metro 功能,这些功能可以帮助您为正在使用的硬件呈现正确的分辨率位图。表 6-1 对本章进行了总结。

images

images

创建示例项目

我已经创建了一个名为AppViews的示例项目,这样我可以在本章中演示不同的特性。我再次使用了 Visual Studio Blank App项目模板。你可以在清单 6-1 的中看到我对default.html所做的添加,我将把它用作我的 HTML 母版页。

清单 6-1 。AppViews 项目中 default.html 文件的内容

`

<html> <head>     <meta charset="utf-8">     <title>AppViews</title>


    
    
    


    
    
    

    
        
Top Left
        
Top Right
        
Bottom Left
        
Bottom Right
    
`

我使用 CSS grid特性创建了一个 2 乘 2 网格的主布局。网格将包含在 id 为gridContainerdiv元素中,每个子元素包含一个标签来指示其位置。你可以在清单 6-2 中看到我用来创建布局的 CSS 属性,它显示了css/default.css文件。还有第二个链接到default.html的 CSS 文件——这个文件叫做views.css。它目前是空的,我将在本章后面回到它。

清单 6-2 。default.css 文件

`#gridContainer {
    display: -ms-grid;
    -ms-grid-rows: 1fr 1fr;
    -ms-grid-columns: 1fr 1fr;
    height: 100%;
    font-size: 40pt;
}

gridContainer > div {

border: medium solid white;
    padding: 10px; margin: 1px;
}

topLeft {

-ms-grid-row: 1; -ms-grid-column: 1;
    background-color: #317f42;
}

topRight {

-ms-grid-row: 1; -ms-grid-column: 2;
    background-color: #5A8463;
}

bottomLeft {

-ms-grid-row: 2; -ms-grid-column: 1;
    background-color: #4ecc69;    
}

bottomRight {

-ms-grid-row: 2; -ms-grid-column: 2;
    background-color: #46B75E;
}

span, button, img {
    font-size: 25pt;
    margin: 5px;
    display: block;
}

testImg {

width: 100px;
    height: 100px;
}`

images 提示我保留了 Visual Studio 创建的js/default.js文件。我将回到这个文件,并在本章的后面向您展示它的内容。

这些文件产生了一个简单的应用,它有四个彩色象限,如图 6-1 所示。在本章的剩余部分,我将使用这个应用来解释应用可以显示的不同视图,以及如何适应它们。

images

图 6-1。默认视图中显示的示例应用

了解地铁景观

Metro 应用可以以四种视图之一显示。图 6-1 显示了全屏横向视图中的应用,其中整个显示屏专用于示例应用,屏幕的最长边位于设备的顶部和底部。还有另外三个视图需要你处理:全屏人像抓拍填充

全屏纵向视图中,你的应用占据了整个屏幕,但是最长的边在设备的左右两边。在截图中,应用以 320 像素宽的条状显示在屏幕的左边缘或右边缘。在填充视图中,除了被抓拍的应用占据的 320 像素条之外,应用显示在整个屏幕上。仅当显示器的水平分辨率为 1366 像素或更高时,才支持对齐和填充视图。对于当今的大多数设备来说,这意味着只有当设备处于横向方向时,填充和对齐视图才可用,但这不是必需的,并且在屏幕足够大的情况下,设备将能够在横向和纵向方向上对齐和填充。你可以在图 6-2 中的截图和填充视图中看到示例应用。

images 提示在填充视图和捕捉视图之间切换的最简单方法是按下Win + .(句点键)。

images

图 6-2。截图和填充视图中的示例应用

如您所见,Metro 应用的默认行为只是适应任何可用的空间。在我的示例应用中,这意味着可用空间在我的网格中的列间平均分配。当你的应用在填充视图中时,这通常不是那么糟糕,因为 320 像素并不是屏幕空间的巨大损失。当你的应用在快照视图中时,它会有更大的影响,因为 320 像素根本不是很大的空间。如图所示,我的示例被压缩到可用空间中,没有完全显示文本。

images 注意图中另一个 app 报告其当前视图。我在这本书的源代码下载中包含了这个应用,以防你会觉得它有用——这个应用叫做PlaceHolder,在本章的文件夹中。它使用的特性和功能与我在本章中描述的相同,这也是我没有列出代码的原因。

用户决定他想要哪个视图以及何时想要。你不能创建一个只在特定视图下工作的应用,所以你需要花时间让你的应用布局以一种有意义的方式适应每个视图。有不同的方法来适应这些视图,我将在接下来的小节中带您了解它们。

images 注意理解这一章的最好方法是跟随并像我一样构建应用。这将让你看到应用响应视图变化的方式,这是静态截图无法正确捕捉的。

使用 CSS 适应视图

适应不同视图的第一种方法是使用 CSS。微软已经定义了一些特定于 Metro 的 CSS media规则,当应用从一个视图移动到另一个视图时会应用这些规则。你可以在清单 6-3 中看到这四个规则,它显示了我前面提到的css/views.css文件。

清单 6-3 。响应 views.css 文件中的视图更改

`@media screen and (-ms-view-state: fullscreen-landscape) {
}

@media screen and (-ms-view-state: fullscreen-portrait) { }

@media screen and (-ms-view-state: filled) {
    #topLeft {
        -ms-grid-column-span: 2;
    }

#topRight {
        -ms-grid-row:  2;
    }

#bottomRight {
        display: none;
    }
}

@media screen and (-ms-view-state: snapped) {
    #gridContainer {
        -ms-grid-columns: 1fr;
    }
}`

至少在我看来,这是 Metro 和支撑它的标准 web 技术之间最好的接触点之一。CSS media规则简单而优雅,通过定义少量特定于 Metro 的属性,微软使得响应不同的视图变得非常容易。

我经常与微软斗争,我认为它倾向于忽视或扭曲公认的标准,但我不得不称赞该公司对 Metro 采取了更温和的态度。我已经为两个media规则定义了属性,我将在下面的章节中解释。

当 Visual Studio 创建一个 CSS 文件作为新项目的一部分时,它会添加四个对应于四个视图的media规则。这通常在default.css文件中,但是对于这个项目来说,将它们移到views.css更适合我。仅当您的应用显示在相应视图中时,您在每个规则中定义的样式才有效。通常的 CSS 优先规则适用,这意味着规则通常被定义为项目的 CSS 文件中的最后一项。如果您使用一个单独的文件来定义规则,就像我对示例项目所做的那样,那么您需要确保导入 CSS 的link元素出现在最后,就像我在default.html文件中所做的那样:

`...


...`

适应填充视图

大多数应用可以容忍屏幕丢失 320 像素,没有太多问题。如果你创建了一个布局不能自动适应的应用,你可以在–ms-view-state属性值为filled时应用的media规则中定义样式。为了演示如何适应填充视图,我重新定义了一些 CSS 网格属性,这些属性应用于填充默认横向视图中每个象限的div元素:

`...
@media screen and (-ms-view-state: filled) {
    #topLeft {
        -ms-grid-column-span: 2;
    }

#topRight {
        -ms-grid-row:  2;
    }

#bottomRight {
        display: none;
    }
}
...`

CSS 网格布局和媒体规则的结合使您在适应特定视图时可以轻松地应用全面的更改。对于这个视图,我改变了布局,使四个div元素中的三个可见,扩展了一个div元素,使其跨越两列,并将第三个元素重新定位到网格中的不同位置。你可以在图 6-3 中看到效果。

images

图 6-3。使用 CSS 网格调整布局以适应填充的视图

适应快照视图

快照视图通常需要更多的思考。你需要在那个 320 像素的长条中放一些有用的东西,但是整个应用的布局通常放不下。我的首选方法是在应用处于快照视图时切换到仅显示信息的视图,并在用户与我的应用交互后立即脱离该视图。在本章的后面,我将向你展示如何改变你的应用的视图。

不管你用什么方法,你都必须面对这样一个事实:与整个屏幕相比,你的空间相对较小。在我的示例应用中,我通过改变我的 CSS 网格来做出响应,这样它只有一列——这具有在网格的其余部分隐藏内容的效果,使用了清单 6-4 中所示的属性。

清单 6-4 。适应快照视图

... @media screen and (-ms-view-state: snapped) { **    #gridContainer {** **        -ms-grid-columns: 1fr;** **    }** } ...

你可以在图 6-4 中看到结果。

images

图 6-4。使用 CSS 网格来适应抓取的视图

使用 JavaScript 适应视图

我喜欢 CSS 适应视图的方法,但是它只能让我到此为止——例如,我不能用它来改变元素的内容。对于更广泛的变化,您可以用一些 JavaScript 代码来补充您的 CSS media规则。视图相关的功能包含在Windows.UI.ViewManagement名称空间中。这是我在本书中第一次使用 Windows API 的功能,而不是 WinJS API。Windows API 在 HTML/JavaScript Metro 应用和用 Microsoft 编写的应用之间共享。NET 技术,如 XAML/C#。因此,一些方法和事件的命名可能会有点笨拙。在接下来的小节中,我将向您展示如何检测当前视图并在视图改变时接收通知。

检测当前视图

您可以通过读取Windows.UI.ViewManagement的值来找出应用当前显示在哪个视图中。ApplicationView.value属性(正如我说过的,Windows API 中的一些命名有点奇怪)。该属性返回一个与Windows.UI.ViewManagement中的值相对应的整数。ApplicationViewState枚举,如表 6-2 所示。

images 提示 Metro 枚举有点像名称空间和接口。它们在 JavaScript 中实际上没有多大意义,但是它们使得像 C#等其他 Metro 语言一样使用 Windows API 中的对象成为可能。在 JavaScript 中,它们被表示为对象,这些对象的属性定义了一组预期或支持的值。

images

我已经在default.js文件中添加了一些代码,这样其中一个网格元素就会显示当前的方向,如清单 6-5 所示。

images 提示我不想在我的代码中一直输入Windows.UI.ViewManagement,所以我创建了一个名为 view 的变量作为名称空间的别名——您可以在清单中看到强调这一点的语句。

清单 6-5 。在 JavaScript 中获取并显示当前方向

`(function () {
    "use strict";

var app = WinJS.Application;
**    var view = Windows.UI.ViewManagement;**

app.onactivated = function (eventObject) {
**        topRight.innerText = getMessageFromView(view.ApplicationView.value);**
    };

**    function getMessageFromView(currentView) {**
**        var displayMsg;**
**        switch (currentView) {**
**            case view.ApplicationViewState.filled:                 displayMsg = "Filled View";**
**                break;**
**            case view.ApplicationViewState.snapped:**
**                displayMsg = "Snapped View";**
**                break;**
**            case view.ApplicationViewState.fullScreenLandscape:**
**                displayMsg = "Full - Landscape";**
**                break;**
**            case view.ApplicationViewState.fullScreenPortrait:**
**                displayMsg = "Full - Portrait";**
**                break;**
**        }**
**        return displayMsg;**
**    }**

app.start();
})();`

在这个清单中,我获取当前视图,并使用ApplicationViewState枚举从数字字符串映射到可以显示给用户的消息。然后,我使用这个消息来设置表示 DOM 中的topRight元素的对象的innerText属性。

接收视图变化事件

前面清单中的代码在应用启动时获取视图,但是当用户切换到不同的视图时,它不会保持 UI 最新。为了创建一个适应不同视图的应用,我需要监听视图变化事件,这是通过 DOM window 对象的resize事件发出的信号。您可以在清单 6-6 中的default.js文件中看到我是如何处理这些事件的。

清单 6-6 。处理视图变化事件

`(function () {
    "use strict";

var app = WinJS.Application;
    var view = Windows.UI.ViewManagement;

app.onactivated = function (eventObject) {

topRight.innerText = getMessageFromView(view.ApplicationView.value);
**        window.addEventListener("resize", function () {**
**            topRight.innerText = getMessageFromView(view.ApplicationView.value);**
**        });**
    }

function getMessageFromView(currentView) {
        var displayMsg;
        switch (currentView) {
            case view.ApplicationViewState.filled:
                displayMsg = "Filled View";
                break;             case view.ApplicationViewState.snapped:
                displayMsg = "Snapped View";
                break;
            case view.ApplicationViewState.fullScreenLandscape:
                displayMsg = "Full - Landscape";
                break;
            case view.ApplicationViewState.fullScreenPortrait:
                displayMsg = "Full - Portrait";
                break;
        }
        return displayMsg;
    }

app.start();
})();`

resize事件表示视图发生了变化,但是为了弄清楚用户选择了哪个视图,我必须再次读取ApplicationView.value属性。然后,我将该值传递给getMessageFromView函数,以创建一个可以显示在应用布局右上面板中的字符串。

你可以在图 6-5 的中看到我添加示例应用的结果。示例应用响应视图变化,使用 CSS media规则控制布局,使用 JavaScript 修改内容(尽管在两种情况下都做了简单的修改)。当然,您可以用 JavaScript 做任何事情,但是我发现这种方法变得非常笨拙,很难测试。CSS 布局和 JavaScript 内容的良好结合对我来说是最好的。

images

图 6-5。通过使用 JavaScript 改变元素内容来适应视图

适应导入内容的视图变化

没有特殊的机制将视图信息传播到您导入到布局中的内容,但是您可以使用 CSS media规则并响应resize事件,就像处理母版页一样。清单 6-7 显示了我添加到项目content.html中的一体化页面的简单例子。(一体化页面将脚本和样式包含在与标记相同的文件中——我在整本书中使用它们来为示例应用添加自包含的演示。)

清单 6-7 。响应导入内容的视图更改

`

                      
        
    
    
        

            
            
        

    

`

我已经使用default.js文件中的WinJS.UI.Pages.render方法导入了这些内容,如清单 6-8 所示。有关该方法的更多详细信息,请参见第 XXX 章。内容被导入到具有bottomRightid的元素中,占据了网格布局的右下部分。

清单 6-8 。将 content.html 文件导入到右下角的元素

... app.onactivated = function (eventObject) {     topRight.innerText = getMessageFromView(view.ApplicationView.value);     window.addEventListener("resize", function () {         topRight.innerText = getMessageFromView(view.ApplicationView.value);     }); **    WinJS.UI.Pages.render("/content.html", document.getElementById("bottomRight"));** } ...

content.html文档包含两个button元素。当应用显示在快照视图中时,我将其中一个button隐藏起来,并更改另一个的内容和风格。请注意,除了响应更改事件之外,我还检查当前视图——这很重要,因为您无法假设应用在加载内容时显示在哪个视图中。在图 6-6 的中可以看到按钮元件的两种状态。

images

图 6-6。在导入的内容中使用视图更改事件和 CSS 媒体规则

这与我用于主内容的技术完全相同,但我想强调的是,你需要在整个应用中应用它们,包括你导入的任何内容。如果你不严格地应用这些技术,当某些应用状态和视图组合在一起时,你最终会得到一个对用户来说很奇怪的应用。

images 提示您可能会看到对一个updateLayout属性的引用,该属性与用于响应视图变化的WinJS.UI.Pages.define方法一起使用。Visual Studio 导航应用项目模板使用它来将视图事件与页面功能结合起来。它不是 WinJS 或 Windows APIs 的一部分,它依赖于各种各样的东西,坦率地说,我不喜欢或不推荐这些东西。我建议您处理变更事件,并在您的内容中使用 CSS media规则,如我在本节中所示。

脱离快照视图

根据您对捕捉视图采取的方法,您可能希望将该选项分解成一个更大的布局。我之前提到过,我倾向于在快照视图中使用仅显示信息的布局,因此,例如,当用户与我的应用交互时,我希望切换到更大的视图,这样他就可以看到用于创建和编辑数据的控件。您可以通过调用ApplicationView.tryUnsnap方法来请求取消应用的快照。清单 6-9 展示了这个方法在content.html文件中的使用。

清单 6-9 。从快照视图中取消应用快照

`...

...`

tryUnsnap方法仅在应用处于快照视图中且处于前台(即,向用户显示)时有效。如果取消捕捉有效,该方法返回true,如果无效,则返回false。取消应用的快照会触发视图更改事件,并应用 CSS media规则,就像用户已经更改了视图一样,因此您不必使用tryUnsnap方法的结果来直接重新配置应用。

适应设备方向

许多 Windows 8 设备都是便携式的,并且配备了方位传感器。Windows 8 将自动改变其方向,以匹配设备的握持方式。有四种方向:横向、纵向、横向翻转和纵向翻转。方向和视图密切相关,例如,当设备处于横向时,您的应用可以以全屏、快照和填充视图显示。翻转方向是通过将设备从相应的常规方向旋转 180 度来实现的,实质上是将设备倒置。

设备有两个方向。如你所料,设备的当前方向是当前的方向,也是我列出的四个方向之一。设备还具有自然方向,即方向传感器处于零度的位置。自然方向只能是横向或纵向,并且通常是设备的硬件按钮与显示器的方向相匹配的地方。

不是所有的设备都会改变方向,有些设备很少会改变方向。桌面设备就是一个很好的例子,在这种设备中,显示器通常是固定位置的,重新调整它们的方向需要明确的配置更改。在接下来的章节中,我将向您展示如何处理设备方向来创建一个灵活且适应性强的 Metro 应用。

确定和监控设备方位

Windows.Graphics.Display名称空间提供了确定当前方向并在方向改变时接收通知的方法。我已经在default.html文件中添加了元素,如清单 6-10 所示,这样我就可以很容易地显示视图和方向。

清单 6-10 。向 default.html 添加元素以显示视图和方向

`...

    
Top Left
    
**        ** **        ** **        **     
    
Bottom Left
    
Bottom Right
...`

清单 6-11 展示了我如何使用这些元素,并演示了如何获取方向值并监听default.js文件中的变化。

清单 6-11 。确定和监控设备方向

`(function () {
    "use strict";

var app = WinJS.Application;
    var view = Windows.UI.ViewManagement;
**    var display = Windows.Graphics.Display;**

app.onactivated = function (eventObject) {

view.innerText = getMessageFromView(view.ApplicationView.value);

window.addEventListener("view", function () {
            topRight.innerText = getMessageFromView(view.ApplicationView.value);
        });

**        displayOrientation();**

**        display.DisplayProperties.addEventListener("orientationchanged", function (e) {**
**            displayOrientation();**
**        });** WinJS.UI.Pages.render("/content.html", document.getElementById("bottomRight"));
    };

**    function displayOrientation() {**
**        var msg = getStringFromValue(display.DisplayProperties.currentOrientation);**
**        currentOrientation.innerText = "Current: " + msg;**

**        msg = getStringFromValue(display.DisplayProperties.nativeOrientation);**
**        nativeOrientation.innerText = "Native: " + msg;**
**    }**

**    function getStringFromValue(value) {**
**        var result;**
**        switch (value) {**
**            case display.DisplayOrientations.landscape:**
**                result = "Landscape";**
**                break;**
**            case display.DisplayOrientations.landscapeFlipped:**
**                result = "Landscape Flipped";**
**                break;**
**            case display.DisplayOrientations.portrait:**
**                result = "Portrait";**
**                break;**
**            case display.DisplayOrientations.portraitFlipped:**
**                result = "Portrait Flipped";**
**                break;**
**        }**
**        return result;**
**    }**

function getMessageFromView(currentView) {
        var displayMsg;
        switch (currentView) {
            case view.ApplicationViewState.filled:
                displayMsg = "Filled View";
                break;
            case view.ApplicationViewState.snapped:
                displayMsg = "Snapped View";
                break;
            case view.ApplicationViewState.fullScreenLandscape:
                displayMsg = "Full - Landscape";
                break;
            case view.ApplicationViewState.fullScreenPortrait:
                displayMsg = "Full - Portrait";
                break;
        }
**        return "View: " + displayMsg;**
    }

app.start(); })();`

Windows.Graphics.Display.DisplayProperties对象提供对器件方向的访问。currentOrientationnativeOrientation值返回对应于DisplayOrientations枚举值的整数。我已经在表 6-3 中列出了这些值。

images

nativeOrientation属性将只返回landscapeportrait值,并设置基线与其他值是相对的。在清单中,我读取了currentOrientationnativeOrientation属性的值,并将它们显示在布局中。我还为由DisplayProperties对象发出的orientationchanged事件创建了一个处理函数。当当前方向或自然方向改变时,触发此事件。传递给 handler 函数的Event对象不包含关于哪个值已更改的信息,这意味着您必须读取属性值,并确定您需要在应用的上下文中做什么。图 6-7 显示了方向信息是如何在示例应用中显示的(当然,您看到的值将取决于设备方向)。我留下了显示当前视图的代码,以强调您需要管理方向和视图的组合,以创建一个具有完全响应布局的应用。

images

图 6-7。显示当前和本地方向

您可以使用 Visual Studio 模拟器来测试方向,并且可以使用图中突出显示的两个按钮来更改方向。模拟器的自然方向是横向,在图中,我将模拟器旋转了 180 度(您可能会看到小的 Microsoft 徽标位于模拟器窗口的顶部)。

使用 CSS 适应设备方向

您还可以使用 CSS media规则来响应设备方向,但仅限于设备是横向还是纵向——标准视图和翻转视图之间的差异无法通过 CSS 来表达。关键规则属性为orientation,支持的值为landscapeportrait。清单 6-12 展示了我如何在css/views.css文件中添加一个媒体规则,以使应用布局适应方向的变化。

清单 6-12 。使用 CSS 媒体规则来适应设备方向

`@media screen and (-ms-view-state: fullscreen-landscape) {
}

@media screen and (-ms-view-state: fullscreen-portrait) {
}

@media screen and (-ms-view-state: filled) {
    #topLeft {
        -ms-grid-column-span: 2;
    }

#topRight {
        -ms-grid-row:  2;
    }

#bottomRight {
        display: none;
    }
}

@media screen and (-ms-view-state: snapped) {
    #gridContainer {
        -ms-grid-columns: 1fr;
    }
}

@media screen and (orientation: portrait) {
**    #topLeft {**
**        background-color: #eca7a7;**
**    }**
}`

当设备处于纵向方向时,我的添加会更改布局中某个div元素的背景颜色。当设备处于纵向或纵向翻转方向时,应用此样式。你可以在图 6-8 中看到效果(如果你正在阅读这本书的印刷版本,有黑白图像,你将需要运行这个例子)。

images

图 6-8。根据方向变化改变元素的颜色

看起来这种orientation媒体规则属性重复了我在本章前面向您展示的–ms-view-state的功能,但实际上它们可以很好地协同工作。当我想在设备处于横向时应用样式时,我发现orientation属性很有用,例如,不管它是在全屏、快照还是填充视图。

表达您的设备方向偏好

并非所有应用都能够在所有方向上提供完整的用户体验,因此当设备在不同方向之间移动时,您可以要求应用不要旋转。你可以使用应用清单来声明你的长期偏好。当我向NoteFlash应用添加磁贴图标时,您在第 XXX 章看到了清单,它包含了运行时执行您的应用所需的配置信息。要打开清单,双击Solution Explorer窗口中的package.appxmanifest文件。默认情况下,Visual Studio 为清单打开一个漂亮的编辑器,但是清单只是一个 XML 文件,如果愿意,您可以直接编辑文本。我很喜欢这个编辑器,因为它用一个漂亮的 UI 覆盖了所有的配置选项。如图 6-9 所示,要更改方向设置,点击Application UI选项卡并在Supported Rotations部分检查您想要支持的方向。

images

图 6-9。使用清单编辑器为应用选择支持的方向

当您设置您想要支持的方向时,您只是表达了一种偏好,仅此而已。在图中,我已经说明了我的示例应用在纵向和纵向翻转方向上效果最好,如果可能的话,Windows 应该只在这些方向上显示我的应用。

images 提示不勾选任何一个选项表明你的应用很乐意在所有选项中显示。

不选中某个选项并不会阻止您的应用以该方向显示。Windows 将尝试满足您的愿望,但前提是它在运行您的应用的设备上有意义。作为一个例子,我的只有纵向的偏好在只有横向的桌面设备上没有意义,所以 Windows 会以横向显示我的应用,因为否则对用户来说毫无用处。

您不能使用方向首选项来避免实现对捕捉和填充视图的处理。如果你的应用真的不能在特定的方向和视图组合中工作,那么你需要通过监听视图和方向变化事件来处理这个问题,并向用户显示一条消息来解释这个问题,并鼓励他将你的应用切换到首选的排列。

对于首选项有意义的设备,Windows 将尊重您的首选方向。对于我的示例应用,这意味着如果你以横向方向启动应用,应用将以纵向模式启动,即使这意味着布局将呈 90 度——这将鼓励用户重新调整设备以适应你的应用。它工作得很好,对于平板设备来说,这是一个自然和无缝的反应。

images 注意截图并不能很好地说明效果,因为它只是显示了与常规方向成 90 度的布局。你真的需要对这个例子进行实验来理解它的效果。不幸的是,您将需要一个带有加速度计的设备来进行测试,因为 Visual Studio 模拟器忽略了方向首选项。我使用戴尔 Inspiron Duo 进行这种测试-它有点动力不足,但价格合理,而且它有方向变化所需的硬件。

覆盖清单方向首选项

您可以在应用运行时更改应用的方向首选项,这将临时覆盖清单中的设置。我说暂时,因为下一次你的应用启动时,清单首选项将再次生效。如果你的应用有独特的模式,其中一些模式在某些方向上无法有意义地表达,那么能够覆盖方向偏好是有用的。当用户在应用内容和布局中导航时,你可以让窗口知道你在任何给定时刻想要支持哪些方向。

images 注意你必须向用户提供一个视觉提示,来解释你的应用的当前状态和你当前支持的一组方向之间的关系。如果你不提供这个提示,你将会迷惑用户,创建一个应用,它将进入一个方向,然后,没有明显的原因,陷入其中。用户不会将应用的状态已经改变联系起来。我建议您不要动态更改方向首选项,而是支持所有方向,并调整您的布局,以解释为什么某些应用状态在某些方向上不起作用。

可以通过Windows.Graphics.Display.DisplayProperties对象的autoRotationPreferences属性动态更改方向首选项。您使用 JavaScript 按位 OR 操作符(使用|字符表示)来组合来自DisplayOrientations枚举的值,以指定您想要支持的方向。为了演示这个特性,我在default.html文件中添加了一个标记为Lockbutton元素,如清单 6-13 所示。该按钮覆盖orientation首选项,防止方向改变。

清单 6-13 。给 default.html 文件添加一个按钮

`...

    
Top Left **        **     
    
                               
    
Bottom Left
    
Bottom Right
...`

清单 6-14 展示了我如何在default.js文件中改变我的应用的首选方向来响应被点击的button元素。

清单 6-14 。动态更改方向偏好

`(function () {
    "use strict";

var app = WinJS.Application;
    var view = Windows.UI.ViewManagement;
    var display = Windows.Graphics.Display;

app.onactivated = function (eventObject) {

view.innerText = getMessageFromView(view.ApplicationView.value);

window.addEventListener("view", function() {
            topRight.innerText = getMessageFromView(view.ApplicationView.value);
        });

displayOrientation();

display.DisplayProperties.addEventListener("orientationchanged", function (e) {
            displayOrientation();
        });

**        lock.addEventListener("click", function (e) {**
**            if (this.innerText == "Lock") {**
**                display.DisplayProperties.autoRotationPreferences =**
**                    display.DisplayOrientations.landscape |**
**                    display.DisplayOrientations.landscapeFlipped;**
**                this.innerText = "Unlock";**
**            } else {**
**                display.DisplayProperties.autoRotationPreferences = 0;**
**                this.innerText = "Lock";             }**
**        });**

WinJS.UI.Pages.render("/content.html", document.getElementById("bottomRight"));
    };

// ... code removed for brevity

app.start();
})();`

这是另一个不能在模拟器中测试的例子——你需要使用一个带有方位传感器的设备。当点击button时,我将我的偏好限制在横向和横向翻转方向。当再次单击按钮时,我将autoRotationPreferences属性设置为零,表示我没有方向偏好。

images 注意如果你更改了首选项,Windows 会立即旋转你的应用,使当前方向不是你的首选选项之一。您应该小心使用这种行为,除非是为了明确响应明确描述的用户交互,否则不要强制改变方向。在没有用户指导的情况下触发方向改变是令人讨厌和困惑的,它会破坏用户对你的应用的信心。

适应像素密度

有一种趋势是显示器每英寸具有更大数量的像素。这最初是由苹果及其“视网膜”显示器流行起来的,但这种硬件已经变得更加普遍,也用于 Windows 8 设备。更高的像素密度的效果是打破显示器中的像素数量和显示器尺寸之间的联系,创建物理上更小的高分辨率显示器。

传统上,当显示器的分辨率增加时,屏幕的尺寸也会增加。目标是能够在屏幕上显示更多内容——更多 UI 控件、更多窗口、更多表格行等等。

对于高像素密度显示器,目标是显示与相同尺寸的低像素密度显示器相同的数量,并使用额外的像素使图像更清晰和锐利。为了实现这一点,Windows 扩展了 Metro 应用——如果不这样做,你最终会得到太小而无法阅读的文本和太小而无法触摸或点击的 UI 控件。表 6-4 显示了 Windows 8 基于显示屏像素密度应用于 Metro 应用的三个缩放级别。

images

Windows 会自动缩放 Metro 应用,并将始终缩放到表中的某个级别。你不必担心放大你的布局,即使它们包含绝对尺寸——例如,Windows 会将你指定的 CSS 像素数量转换成场景背后放大的显示像素数量。当您在 DOM 中查询元素的大小时,您将得到 CSS 像素而不是缩放值。所有这些使得创建在高和低像素密度下看起来都不错的 Metro 应用变得非常容易。

在这种排列中,有一点不太好,那就是位图图像,随着像素密度的增加,显示质量会下降。高密度显示的效果是位图图像看起来模糊不清,边缘参差不齐,如图图 6-10 所示。左边的图像显示了放大图像时会发生什么,右边的图像显示了您应该瞄准的清晰边缘。

images

图 6-10。位图图像在高像素密度显示器上放大时产生的模糊效果

解决这个问题的最好方法是使用矢量图像格式,比如 SVG。这说起来容易做起来难,因为许多设计包缺乏良好的 SVG 支持。更实用的解决方案是为每个 Windows 8 缩放级别创建一个位图,并确保使用最合适的图像来匹配显示器的像素密度。为了演示这种方法,我创建了三个图像文件。你可以在图 6-11 中看到它们。

images

图 6-11。展示 Metro 支持高像素密度显示器的图像

在实际项目中,您将创建同一图像的三个版本。因为我想弄清楚显示的是哪个图像,所以我创建了三个单独的图像。每个图像都显示了我想要使用的缩放因子,并且图像大小与该缩放比例匹配:第一个图像是 100 x 100 像素,第二个是 140 x 140 像素,最后一个是 180 x 180 像素。在接下来的章节中,我将向您展示如何在 Metro 应用中使用这些图像。

使用自动资源加载

最简单的方法是遵循命名约定,让 Metro 运行时为正在使用的缩放因子加载正确的文件。为了演示这一点,我使用以下文件名将测试图像的副本添加到示例项目中的images文件夹中:

  • img.scale-100.png
  • img.scale-140.png
  • img.scale-180.png

通过在文件后缀前插入scale-XXX,其中XXX是缩放百分比,您告诉 Metro 运行时这些文件是用于不同密度的图像的变体。当您在 HTML 中使用这些文件时,您排除了缩放信息,正如您在清单 6-15 中看到的,它显示了我添加到default.html文件中的一个img元素。

images 注意仅仅将图像文件复制到磁盘上的图像文件夹是不够的。您还需要在 Visual Studio Solution Explorer窗口中右键单击 images 文件夹,并从弹出菜单中选择Add Existing项。选择图像文件并点击Add按钮。

清单 6-15 。使用一组缩放文件中的图像

`...

    
Top Left              
    
                               
    
Bottom Left **        **     
    
Bottom Right
...`
测试图像选择

如果没有一系列配备不同像素密度显示屏的设备,测试这项功能是很棘手的。Visual Studio simulator 支持在不同的屏幕分辨率和密度之间切换,但它不能正确处理图像,而是以原始大小显示图像,这就违背了模拟的目的。然而,模拟器确实加载了正确的图像来反映模拟的密度,所以我可以通过限制img元素的大小来得到我想要的效果,我已经使用了我在default.css文件中定义的样式之一,如清单 6-16 所示。对于真实的设备和真实的项目,这不是你需要做的事情。

清单 6-16 。固定图像元素的大小以说明像素密度支持

`...

testImg {

width: 100px;
    height: 100px;
}
...`

模拟器窗口边缘的一个按钮改变屏幕尺寸和密度,如图图 6-12 所示。最有用的设置是 10.6 英寸显示屏,可以用四种不同的分辨率进行模拟,覆盖 Windows 8 支持的不同比例级别。

images

图 6-12。在 Visual Studio 模拟器中更改屏幕特性

为了测试这种技术,启动应用并在可用的像素密度之间切换。您将看到自动显示针对像素密度的图像。

images 提示每次更改后,你都必须在调试器中重新加载应用才能看到正确的图像——请求的图像名称(img.png)和缩放版本(img.scaled-XXX.png)之间的映射似乎被缓存了。

使用 JavaScript 适应像素密度

您可以通过Windows.Graphics.Display.DisplayProperties对象获得像素密度和缩放工厂的详细信息。为了演示这是如何工作的,我使用名称img100.pngimg140.pngimg180.png将图像文件的副本添加到项目images文件夹中。我已经创建了这些副本,因此它们不受我在上一节中描述的缩放命名方案的约束。此外,我已经从default.html文件的img元素中移除了src属性,如清单 6-17 所示。

清单 6-17 。从 default.html 文件的 img 元素中删除 src 属性

`...

    
Top Left              
    
                          ` `    
    
Bottom Left **        **     
    
Bottom Right
...`

我想要的信息可以通过DisplayProperties.resolutionScale属性获得,该属性返回一个对应于Windows.Graphics.Display.ResolutionScale枚举的值。这个枚举中的值是scale100Percentscale140Percentscale180Percent。在清单 6-18 中,您可以看到我是如何基于来自default.js文件中resolutionScale属性的值为img元素设置src属性的值的。

清单 6-18 。基于显示比例因子明确选择图像

`(function () {
    "use strict";

var app = WinJS.Application;
    var view = Windows.UI.ViewManagement;
    var display = Windows.Graphics.Display;

app.onactivated = function (eventObject) {

**        switch (display.DisplayProperties.resolutionScale) {**
**            case display.ResolutionScale.scale100Percent:**
**                testImg.src = "img/img100.png";**
**                break;**
**            case display.ResolutionScale.scale140Percent:**
**                testImg.src = "img/img140.png";**
**                break;**
**            case display.ResolutionScale.scale180Percent:**
**                testImg.src = "img/img180.png";**
**                break;**
**        };**

// ...code removed for brevity...
    };

app.start();
})();`

images 注意你也可以使用 CSS media规则来适应像素密度,但是没有方便的缩放贴图,你必须直接处理像素密度值。这是 Visual Studio 模拟器的问题,因为它以错误的方式舍入了像素密度数字,所以模拟的分辨率不属于正确的类别。此外,由于您只需要使位图图像适应像素密度,而使用 CSS 无法做到这一点,因此这种技术没有什么价值。如果你发现自己在调整布局的任何其他部分以适应像素密度,那你的方法就有问题了。请记住:Windows 将为您缩放应用中的所有其他内容。

总结

在这一章中,我向你展示了让你的应用布局适应设备的不同方法——适应视图、适应方向和适应像素密度。确保您的应用以有意义的方式适应,对于创建流畅、高质量的用户体验非常重要,这种体验可以与设备和操作系统功能完美融合。如果你不仔细考虑你的适应方法,或者更糟的是,完全跳过它们,你将创建一个行为怪异的应用,并且不能真正与 Metro 体验相融合。我的建议是花时间考虑你的应用能做出什么样的最佳反应,特别是在快照视图和纵向视图方向上。

在下一章中,我将介绍一些 UI 控件,它们是 Metro 的独特部分,您可以使用它们来提供一致且简单的应用功能导航。

七、命令和导航

在第五章中,我向你展示了如何使用单页布局来创建一个 Windows 应用的基础。当我这样做的时候,我使用了button和锚(a)元素来导航内容,就像我在普通的 web 应用中所做的一样。在这一章中,我将向你展示应用特有的控件,这些控件专用于提供在应用中导航的命令,并操纵它所呈现的数据或对象:应用工具栏和导航工具栏。这些控件提供了 Windows 应用独特的视觉风格和交互模型的很大一部分,并且存在于大多数应用中(游戏似乎是个例外,在游戏中非标准界面很常见)。在这一章中,我将向你展示如何创建和应用这些控件,并且在这样做的时候,提供更多关于 WinJS 控件模型的细节,我在第五章中使用HtmlControl导入内容的时候提到过。表 7-1 对本章进行了总结。**

*images

创建示例项目

我使用Blank App模板创建的本章示例项目名为AppBars,它遵循单页面导航模型。这一章是关于导航的,所以我已经创建了一个项目,它有一个母版页和两个内容文件,稍后我将向您展示。当应用启动时,我导入其中一个内容文件,然后,在本章的后面,我将添加一些 Windows 应用 UI 控件,以便在其他内容之间导航。default.html文件将作为母版页,如清单 7-1 中的所示。

清单 7-1 。AppBars 项目的 default.html 主页

`

<html> <head>     <meta charset="utf-8">     <title>AppBars</title>


    
    
    


    
    

    
`

这将是我的母版页,我将把内容导入到属性为contentTargetdiv元素中。在所有其他方面,我保留了这个文件,就像我使用Blank Application项目模板时由 Visual Studio 创建的一样。

定义和加载内容

我从这个项目中的一个内容文件开始,我将其命名为page1.html(我已经将它添加到根项目文件夹中,与default.html放在一起)。正如你在清单 7-2 中看到的,这个文件包含一个h1元素,它清楚地表明哪个文件已经被加载,还有一些span元素(我将在本章后面解释命令时用到它们)。

清单 7-2 。page1.html 文件的内容

`

                                
            

This is Page 1

` `            Command: None         
     `

定义 JavaScript

我已经使用了WinJS.Navigation API 来处理请求和加载js/default.js文件中的内容,你可以在清单 7-3 中看到。我的导航事件处理程序清除母版页中的目标元素,并使用WinJS.UI.Pages.render方法加载指定的内容。我将直接使用文件的名称来请求此应用中的内容。应用的开始位置是加载page1.html,我将使用导航控件切换到本章后面的其他内容。

清单 7-3 。加载 default.js 文件中的初始内容

`(function () {
    "use strict";

var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {
        WinJS.Utilities.empty(contentTarget);
        WinJS.UI.Pages.render(e.detail.location, contentTarget);
    });

app.onactivated = function (eventObject) {
        WinJS.Navigation.navigate("page1.html");
    };

app.start();
})();`

定义 CSS

这个项目中的最后一个文件是css/default.css文件,我用它来定义母版页和单独内容文件的样式。你可以在清单 7-4 中看到 CSS 样式。

清单 7-4 。default.css 文件的内容

`#contentTarget, div.container {
    width: 100%;
    height: 100%;
    text-align: center;
    display: -ms-flexbox;
    -ms-flex-direction: column;
    -ms-flex-align: center;
    -ms-flex-pack: center;
}

page1Container {     background-color: #317f42;

}

page2Container {

background-color: #5A8463;    
}

div.container span {
    font-size: 30pt;
}`

我已经使用了 flexbox 布局来排列元素,正如你在图 7-1 中看到的,它显示了示例应用的初始外观。

images

图 7-1。app bars 示例 app 的初始外观

创建应用命令

应用栏出现在屏幕底部,为用户提供对命令的访问。命令对当前范围内的数据或对象执行一些功能,这通常意味着您的命令应该与您当前呈现给用户的内容相关。

AppBar 是 Windows 应用用户体验的重要组成部分。它为关键交互提供了一致和熟悉的锚点,并允许您将 UI 控件从应用的主布局中移出,以便您使用屏幕空间为用户提供更多数据。在这一节中,我将详细介绍 AppBar,向您展示如何创建、填充和管理 UI 控件以及如何响应用户命令,并告诉您在应用中充分利用 app bar 所需要知道的一切。很自然,可以从创建 AppBar 开始,这是通过WinJS.UI.AppBar对象完成的。创建 AppBar 最简单的方法是通过向标准 HTML 元素添加数据属性,以声明的方式完成。应用栏通常被添加到母版页,这样你就不必在每次导入新内容时重新设置它们。考虑到这一点,清单 7-5 展示了在default.html文件中添加一个 AppBar。

images 注意WinJS.UI.AppBar对象是 WinJS UI 控件的一个例子。我在这本书的这一部分中提到的都与应用的基本布局和结构有关。在本书的第三部分的中,我将介绍更通用的 UI 控件对象,并向你展示如何应用它们。

清单 7-5 。给 default.html 文件添加一个 app bar

`

<html> <head>     <meta charset="utf-8">     <title>AppBars</title>


    
    
    


    
    

    

**    

**

**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'cmdBold', label:'Bold', icon:'bold',**
**                section:'selection', tooltip:'Bold', type: 'toggle'}">**
**                    **

**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'cmdFont', label:'Font', icon:'font',**
**                section:'selection', tooltip:'Change Font'}">**
**                    **

**        <hr data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{type:'separator', section:'selection'}" />**

**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'cmdCut', label:'Cut', icon:'cut',**
**                section:'selection', tooltip:'Cut'}">**
**                    **

**        <button data-win-control="WinJS.UI.AppBarCommand"             data-win-options="{id:'cmdCut', label:'Paste', icon:'paste',**
**                section:'selection', tooltip:'Paste'}">**
**                    **

**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'cmdRemove',label:'Remove', icon:'remove',**
**                section:'global', tooltip:'Remove item'}">**
**        **

**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'cmdAdd', label:'Add', icon:'add',**
**                section:'global', tooltip:'Add item'}">**
**        **
**    

**

`

这些元素中有很多东西,我将一步一步地分解它们。图 7-2 显示了这些元素创建的 AppBar,当我解释各个部分是如何组合在一起的时候,它给了你一些上下文。我已经把图中 AppBar 的中间部分切掉了,这样更容易看到各个按钮。

images 提示如果你现在运行这个例子,AppBar 不会出现。您需要首先应用清单 7-6 中的所示的更改。

images

图 7-2。清单 7-5 中的元素创建的 app bar

声明 AppBar 控件

AppBar 的起点是创建一个WinJS.UI.AppBar对象,我使用div元素上的data-win-control属性来完成,如下所示:

`...

    // ...elements removed for brevity
...`

这是我在前面章节中使用的 UI 控制模式,也是 WinJS 中使用的模式。您获取一个常规的 HTML 元素,在 AppBars 中是一个div元素,并使用data-win-control属性来指示您想要创建哪个用户控件。正如我之前提到的, Windows 运行时不会自动搜索这些属性,这就是为什么我在default.js文件中添加了对WinJS.UI.processAll方法的调用,如清单 7-6 所示。如果不调用processAll方法,AppBar控件将不会应用于div元素,当用户右击或滑动鼠标时,AppBar 也不会弹出。

清单 7-6 。调用 processAll 方法应用 WinJS UI 控件

`(function () {
    "use strict";

var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {
        WinJS.Utilities.empty(contentTarget);
        WinJS.UI.Pages.render(e.detail.location, contentTarget);
    });

app.onactivated = function (eventObject) {        
**        WinJS.UI.processAll().then(function() {**
**            WinJS.Navigation.navigate("page1.html");**
**        });**
    };
    app.start();
})();`

processAll方法在后台处理元素,并返回一个WinJS.Promise对象,当处理完成且所有 UI 控件都已创建时,该对象被实现。我在第五章的中介绍了Promise对象,并简要介绍了使用then方法来推迟操作直到Promise完成。我在第九章的中深入讨论了Promise对象。

images 提示注意,我不必亲自处理滑动或右键单击来显示 AppBar 控件。这在应用中是默认连接的,因此应用栏(和导航条,我将在本章后面介绍)会自动出现。

我已经把对WinJS.Navigation.navigate方法的调用放在一个函数中,我把它传递给由processAll方法返回的Promisethen方法。这很重要。虽然我在母版页中设置了 AppBar,但我想从内容页中管理和控制它(我将在本章的后面解释为什么并向您展示如何做到这一点)。我无法通知导入的内容母版页中的 HTML 元素已经被处理并转换为 WinJS UI 控件,这一点很重要,因为我不希望该内容中的代码在准备好之前就开始访问控件功能(这将导致引发异常)。解决方案是确保processAll方法在导入内容之前已经完成了它的工作,这意味着使用 WinJS Promise,如代码所示。

images 提示我说 WinJS 控件是从 HTML 元素创建的,或者应用于 HTML 元素,但实际发生的是 WinJS 用一些 CSS 格式化 HTML 元素,并且在某些情况下,添加一些新元素。该技术非常类似于基于 JavaScript 的 UI 库,如 jQuery UI 和 jQuery Mobile。我不想给你留下正在进行某种魔术的印象——在大多数情况下,WinJS 控件是标准的 HTML 和 CSS。在某些情况下,JavaScript 代码会使用一些 Windows API 调用,但这种情况非常少见,而且这些也是您可以在代码中使用的 Windows API(这也是我在本书中描述的)。

向 AppBar 添加按钮

通过向 AppBar 添加具有data-win-controlWinJS.UI.AppBarCommandbutton元素,向用户显示命令,如下所示:

... <button **data-win-control="WinJS.UI.AppBarCommand"**     data-win-options="{id:'cmdBold', label:'Bold', icon:'bold', section:'selection', tooltip:'Bold', type: 'toggle'}"> </button>             ...

使用包含配置信息的 JSON 字符串,通过data-win-options属性为 AppBar 按钮提供配置。JSON 字符串中的属性对应于WinJS.UI.AppBarCommand对象中的属性。我在表 7-2 中总结了这些配置属性,并在下面的章节中逐一描述。理解这些选项是充分利用 AppBars 的关键。

images

设置命令 ID

id属性标识与按钮相关的命令。没有预定义的命令,因此您可以自由分配在应用环境中有意义的id值。该属性有两种用途。首先,在响应用户交互时,使用id属性来确定请求了哪个命令。其次,您使用id告诉 AppBar 在导入内容时显示哪些命令。我将在本章后面的响应命令部分演示这两种用法。

配置命令按钮的外观

iconlabeltooltip属性定义了按钮的外观。icon属性指定按钮中使用的字形。字形是通过显示一个来自Segoe UI Symbol字体的字符产生的,这是 Windows 8 的一部分。您可以使用 Windows 8 中的Character Map工具查看字体中的图标范围。

icon属性的值通常是来自WinJS.UI.AppBarIcon枚举的值,这为字符代码提供了一个方便的映射方案。分配给我之前展示的按钮的bold值等同于WinJS.UI.AppBarIcon.bold属性,该属性在 Visual Studio 添加到应用的base.js文件中定义为:

... bold:               "\uE19B" ...

(在Solution Explorer窗口中展开References可以看到base.js的内容。)您不需要为您想要的字符指定名称空间或对象名——只需bold即可。如果您想要使用一个在枚举中没有值的字符,您可以简单地使用字体字符代码而不是名称(例如,\uE19B而不是bold)。

images 提示如果找不到适合自己需要的图标,可以将icon属性设置为包含自定义图标的 PNG 文件的名称。

labeltooltip属性指定显示在按钮图标下的字符串,以及当鼠标悬停在按钮上或者当用户在按钮上滑动手指而没有松开时显示的字符串。这些属性的值不是根据icon值自动推断出来的,你可以在你的应用中自由使用任何有意义的文本。然而,重要的是不要给众所周知的图标赋予新的含义,因为你赋予labeltooltip属性的值不会总是显示给用户。在我向您展示的代码片段中的button中,我将icon设置为bold并将labeltooltip都设置为Bold,这产生了如图图 7-3 所示的按钮。

images

图 7-3。配置应用栏中命令按钮的外观

图的左侧显示了全屏横向视图中显示的 AppBar 的一部分,其中显示了label文本(我只显示了 AppBar 的一侧,其他按钮都在旁边)。图的右侧显示了快照视图中的 AppBar。在这个视图中没有显示label文本,因为 AppBar 已经通过省略label和将图标打包堆叠在一起来适应缩小的屏幕空间。你也不能依靠tooltip的值来帮助用户理解一个按钮会做什么,因为它们不会显示在只支持触摸的设备上。你可以依靠图标的清晰度来传达按钮的含义——这使得做出适当的选择并尊重与众所周知的图标相关的惯例变得很重要。

images 提示如果你发现自己很难通过图标传达命令,你可能想停下来想想你的应用的设计。Windows 应用的用户体验是关于即时和明显的交互,这可能是因为你试图在一个命令中包含太多的含义。通过将动作分解成一系列命令或者使用上下文菜单(我在本书的第三部分中描述了这一点),你可以为你的用户创造更好的体验。

分组和分隔按钮

AppBar 中有两个部分:选择部分位于 AppBar 的左侧,包含应用于用户选择的数据或对象的命令。全局部分位于应用栏的右侧,包含始终可用且不受单个选项影响的命令。section属性决定了一个按钮被分配到哪个部分,如你所料,支持的值是selectionglobal

当您创建 AppBar 时,您用您的应用在所有情况下支持的所有命令填充每个部分。然后,您可以更改命令集和单个命令的状态,以匹配应用的状态——稍后我将向您展示如何做到这一点。

选择全局命令的位置

微软已经为一些全局命令在应用栏上的位置定义了一些规则。如果你的应用有一个NewAdd命令,那么它应该被放在最右边的全局命令,并且应该用add图标显示(这个图标不应该用于任何其他命令)。

放置在NewAdd左侧的命令应为对应命令。如果你的应用处理的数据或对象具有应用之外的生命(比如照片,它们驻留在设备存储上,可以被其他应用访问),那么你必须使用一个Delete命令(一个Deletelabel值和一个deleteicon值)。如果你的应用只处理自己的数据,那么你应该使用一个Remove命令(一个label值为Remove,一个图标值为remove)。如果该操作将删除多个项目,那么您应该使用一个Clear命令(一个标签值为Clear,一个图标值为clear)。

设置命令类型

有四种类型的命令可以添加到 AppBar,由type属性指定——这些类型对应于值buttontoggleseparatorflyout。如果您没有为type属性显式设置值,那么将使用button值。

buttontoggle类型创建常规按钮,当它们被点击时触发事件。它们之间的区别在于toggle类型创建了一个具有开和关状态的切换按钮。我之前关注的Bold按钮是一个toggle命令,你可以在图 7-4 中看到关闭和打开状态是如何显示的。我将很快向您展示如何以编程方式检查和更改切换状态。

images

图 7-4。用分隔符显示的切换命令的不同状态

该图还显示了可以添加到 AppBar 的第三种命令:separator类型。(这在页面上可能很难辨认——分隔符是一条细长的竖线,通过运行示例可以清楚地看到。)其他三种命令类型是从button元素创建的,但是您可以从hr元素创建一个分隔符,如下所示:

... <**hr** data-win-control="WinJS.UI.AppBarCommand"     data-win-options="{type: **'separator'**, section:'selection'}" /> ...

命令按照它们在 HTML 中定义的顺序被添加到它们在 AppBar 中的部分,因此您可以简单地在代表其他类型命令的button元素之间添加分隔符。最后一个命令类型flyout用于将命令与弹出菜单相关联。我将在本章后面的“使用弹出型按钮”部分向您展示如何使用这种命令。

响应命令

通过在 AppBar HTML 元素上为click事件注册一个处理函数来响应来自 AppBar 的命令。处理 click 事件的最佳位置是您导入到母版页的内容中——这允许您针对不同的内容适当地响应命令,而无需诉诸紧耦合。清单 7-7 展示了在page1.html文件中添加一个script元素来响应 AppBar 中的命令。

清单 7-7 。响应 page1.html 文件中的命令

`

              **        **                   
            

This is Page 1

            Command: None         
     `

我使用addEventListener方法为应用了 AppBar 控件的div元素注册一个click事件的处理函数。这依赖于 DOM 事件通过文档中元素的层次结构向上传播的方式,这意味着我可以避免找到单个 command button元素并直接处理它们。清单中的script元素的重要声明是这样的:

... document.getElementById("command").innerText = **e.target.winControl.label**; ...

该语句获取被单击命令的label属性的值,并使用它来设置标记中span元素的innerText属性。当WinJS.UI.processAll方法处理一个具有data-win-control属性的元素时,它将一个winControl属性附加到表示 DOM 中元素的对象上。属性返回一个对象,允许你使用 UI 控件的特性和功能。

images 提示注意,我已经使用了e.target来定位点击事件所源自的元素。该事件来自命令,而不是 AppBar,因此您需要确保处理正确的元素。

winControl属性返回的对象是由data-win-control属性指定的对象。对于 AppBar 命令,winControl是一个WinJS.UI.AppBarCommand对象,我可以访问这个对象定义的所有属性和方法。在本例中,我已经读取了label属性来获取显示在命令按钮下的文本字符串。

AppBarCommand对象非常简单,它定义的大多数成员对应于我在本章前面描述的配置选项(idiconsectiontype等等)。还有一些额外的属性,我已经在表 7-3 中描述了它们,尽管我在本章后面才详细解释其中的几个。

images

使 AppBar 适应特定内容

在声明 AppBar 时,添加应用中需要的所有命令,然后指定在将内容导入母版页时向用户显示哪些命令。这允许您为应用栏提供一组一致的元素,并且仍然适应应用的状态,以便导入内容的功能在应用栏的命令中得到反映。

区分禁用隐藏的命令非常重要。禁用的命令仍然出现在应用栏上,但是命令按钮是灰色的,表示该命令现在不适用,但是以后可能会适用—例如,可能在用户选择了对象或数据项时。

隐藏的命令会从应用栏中完全删除—当命令不适用于当前内容时,您会隐藏命令。您可以使用应用了WinJS.UI.AppBar控件的元素的winControl对象来隐藏和禁用命令。在清单 7-8 的中,我已经添加了page1.html文件来配置 AppBar 命令。

清单 7-8 。定制 AppBar 命令以匹配导入内容的功能

`

             ` `        
    
    
        

            

This is Page 1


            Command: None
        

    

`

使用winControl属性,我能够访问WinJS.UI.AppBar控件的方法和属性。在这个清单中,我使用了两种可用的方法:showOnlyCommands方法接受命令id值的数组,并隐藏数组中没有指定的所有命令。我用这个方法来隐藏除了cmdBoldcmdFontcmdAdd命令之外的所有命令。

getCommandById接受一个id值并返回相应的AppBarCommand对象。在清单中,我为cmdBold命令找到了AppBarCommand对象,并将disabled属性设置为true(正如我在上一节中所描述的,它将按钮保留在 AppBar 上,但阻止它被使用)。你可以在图 7-5 中看到这些方法在 AppBar 上的效果。

images

图 7-5。为导入的内容定制 app bar

我喜欢使用showOnlyCommands方法,因为这意味着我提供了我想要显示的命令的明确列表,但是在AppBar对象中还有其他方法可以用来为您的内容准备 AppBar。在表 7-4 中,我描述了你可以用来显示、隐藏和定位命令的一整套方法。

使用弹出按钮

基本命令可以用buttontoggle命令类型处理,但是对于复杂的命令,你需要使用flyout类型,它将命令与一个被称为弹出按钮的弹出窗口链接起来。使用WinJS.UI.Flyout UI 控件创建弹出按钮,在这一节中,我将向您展示如何创建和使用弹出按钮,以及如何将它们与您的 AppBar 命令相关联。

images 提示在这一节中,我将解释如何使用 AppBars 的Flyout控件,但是您也可以使用Flyout来创建通用的弹出窗口。更多细节和示例见第十二章。

声明弹出型按钮

声明弹出按钮的最佳位置是在母版页中,这样整个应用中都可以使用相同的元素集。通过将data-win-control属性设置为WinJS.UI.Flyout,弹出按钮被应用于div元素。您可以设置div元素的内容,以适应您的应用的需求——FlyoutUI 控件的作用是处理弹出窗口,对您向用户呈现的内容没有任何限制。清单 7-9 显示了我添加到default.html文件中的一个简单的弹出按钮。

清单 7-9 。向 default.html 文件添加弹出按钮

`

<html> <head>     <meta charset="utf-8">     <title>AppBars</title>


    
    
    


    
    

    
` `    
        

**    

**
**        

Select a Font

**
**        **
**    
**

`

在这个清单中,我定义了一个弹出按钮,它包含一个简单的标题和一个带有三个option元素的select元素。为了将弹出按钮与命令相关联,我将命令的type属性设置为flyout,将flyout属性设置为已经应用了WinJS.UI.Flyout控件的div元素的id

形成弹出按钮的元素是隐藏的,直到用户单击或触摸相关联的命令按钮。此时,弹出按钮显示在按钮上方,如图图 7-6 所示。飞出式按钮被轻轻关闭,这意味着如果用户点击飞出式按钮占据的屏幕区域之外,它们将再次隐藏。这意味着您不必添加任何种类的取消按钮来删除弹出按钮。

images

图 7-6。使用带有 AppBar 命令的弹出菜单

样式弹出按钮

弹出型按钮的默认样式非常简单。如果你想让你的弹出按钮与应用的其他部分的视觉主题相适应,你可以覆盖 CSS win-flyout类。清单 7-10 展示了我如何在default.css文件中覆盖这个样式来改变背景颜色和应用边框。如果你遵循这个例子,你需要将这些样式添加到default.css文件中。

清单 7-10 。通过 win-flyout 类设计弹出菜单

`...
div.win-flyout {
    background-color: #4FCB6A;
    border: thick solid black;
}

div.win-flyout select {
    border: medium solid black;
}
...`

响应弹出交互

声明和显示弹出型按钮只是该过程的一部分。您还需要响应用户与您添加到弹出按钮的元素的交互。处理弹出控件交互的最佳位置是在母版页的 JavaScript 代码中——这允许您为弹出控件创建一致的处理方式,不管显示的内容是什么。

然而,您需要使用一个视图模型数据绑定来使这种方法工作,而不会在母版页和导入的内容之间产生紧耦合问题。在第八章之前,我不会描述视图模型和数据绑定,所以我将接受母版页需要有内容的详细知识,这样我就可以演示如何处理弹出按钮,并在本书的后面向您展示如何解决紧耦合问题。清单 7-11 显示了对default.js文件的添加,以响应弹出按钮。

清单 7-11 。响应弹出按钮

`(function () {
    "use strict";

var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {
        WinJS.Utilities.empty(contentTarget);
        WinJS.UI.Pages.render(e.detail.location, contentTarget);
    });

app.onactivated = function (eventObject) {

WinJS.UI.processAll().then(function() {
            WinJS.Navigation.navigate("page1.html");

**            fontSelect.addEventListener("change", function (e) {                 command.innerText = this.value;**
**                fontFlyout.winControl.hide();**
**            });**
        });
    };
    app.start();
})();`

我已经处理了来自select元素的change事件,就像我在常规 web 应用中一样,并且我将在page1.html中定义的span元素的innerText属性设置为用户选择的值。这是紧密耦合的部分——default.js文件中的代码不应该知道 page1.html 中内容的结构和性质(但是正如我所说的,您可以使用视图模型和数据绑定来解决这个问题,我在第八章中对此进行了描述)。

这个例子的关键部分是这样的陈述:

... fontFlyout.winControl.hide(); ...

一旦我处理了用户交互,我就定位到应用了Flyout控件的元素,并通过winControl属性调用hide方法。仅当用户在弹出窗口之外单击时,弹出窗口灯光消除功能才适用,而当用户使用弹出窗口包含的控件执行操作时,该功能不适用。这意味着在成功交互完成时,您必须明确地从显示屏上移除弹出窗口(使用hide方法)。

对于这个简单的弹出按钮,我可以在用户使用select元素做出选择时立即做出响应,但是对于更复杂的交互,您可以依靠OK按钮或其他类型的明确信号来表明用户已经完成了。

创建导航命令

导航栏从屏幕顶部向下滑动,充当应用栏的对应物。AppBar 提供了对当前内容中的数据和对象进行操作的命令,而 NavBar 提供了在应用的不同区域中移动的方法。NavBars 是使用与 AppBars 相同的WinJS.UI.AppBar UI 控件创建的,但是在向用户呈现命令的方式上,您拥有更大的灵活性。在这一部分,我将向你展示如何在你的应用中添加和管理导航条。为了演示导航控件,我在项目中添加了两个新文件,名为page2.htmlpage3.html。你可以在清单 7-12 中看到page2.html

清单 7-12。【page2.html 档案】??

`

                                
            

This is Page 2

        
     `

这些文件最初将用于演示导航,但是在本章的后面我将使用page2.html文件来演示如何创建一组自定义的导航控件。你可以在清单 7-13 的中看到page3.html的内容。

清单 7-13。【page3.html 档案】??

`

                                
            

This is Page 3

        
     `

使用标准导航条

虽然我发现在母版页中定义 AppBar 更容易,但在 NavBar 中我倾向于采用不同的方法。我创建了一个包含 NavBar 元素的单独文件,并使用WinJS.UI.Pages名称空间将它导入到每个内容文件中,采用了我在第五章的中描述的技术。在这一节中,我将向您展示一个标准的 NavBar,它遵循与我在 AppBar 中使用的相同的基于命令的方法。清单 7-14 显示了我使用 Visual Studio HTML Page项目模板添加到示例项目中的standardNavBar.html文件的内容。(在本章的后面,我将向你展示一个不同的导航条设计。)

清单 7-14 。standardNavbar.html 文件的内容

`

                                         
    

`

NavBar 遵循我为 AppBar 使用的命令模式,有两个按钮导航到page2.htmlpage3.html文件。当您使用一个WinJS.UI.AppBar控件作为导航栏时,您必须使用data-win-options属性将placement属性设置为top,就像我在清单中所做的那样。

placement属性是区分用作 AppBars 的WinJS.UI.AppBar控件和用作 NavBars 的控件的方式。当用户点击或触摸其中一个命令按钮时,我接收到click事件,并使用命令的id属性计算出用户想要导航到的页面,并将其传递给WinJS.Navigation.navigate方法。

应用标准导航条

我想在page1.html文件中使用标准的导航条,你可以在清单 7-15 中看到我是如何做的。

清单 7-15 。使用 page1.html 文件中的标准导航条

`

                      
    
    
        

            

This is Page 1


            Command: None
        

    

`

这是render方法的标准用法,除了我正在导入内容的元素没有在page1.html文件中定义。我将在这个示例应用中使用多种样式的 NavBar,这需要对元素进行一些调整,首先是在母版页中有一个公共元素,内容可以在其中加载所需的 NavBar。你可以看到我是如何将这个元素添加到清单 7-16 中的default.html文件中的。(让我再次强调,当我在第八章中介绍视图模型时,我会演示一些更好的技术来处理这种情况。)

清单 7-16 。向 default.html 添加一个通用元素,导航条将被导入其中

`...

<body>     <div id="contentTarget"></div> **    <div id="navBarContainer"></div>**     <div id="appbar" data-win-control="WinJS.UI.AppBar">


        

Select a Font


        
    

...`

page1.html文件被导入时,它将导入standardNavBar.html文件的内容,并将它们插入到default.html文件的公共元素中。你可以在图 7-7 中看到效果。NavBar 与 AppBar 同时显示,并响应相同的交互。同样,我不需要处理任何事件来显示 NavBar——这是在应用控件时自动连接的,当用户滑动或右键单击时,NavBar 和 AppBar 都会显示。

images

图 7-7。向示例应用添加导航栏

图中的细节可能很难辨认,但是你可以看到我在图 7-8 的中特写的两个命令按钮。数字没有预定义的图标,所以我将命令的icon值设置为来自Segoe UI Symbol字体的字符代码(\u0032\u0033)。点击命令将导航到相应的页面。

images

图 7-8。标准导航条按钮上的命令

使用自定义导航条

在导航条上使用一系列按钮来表示命令并不总是有意义的。在这些情况下,您可以为WinJS.UI.AppBar控件定义一个自定义布局。清单 7-17 显示了customNavBar.html文件的内容,这是我使用 Visual Studio HTML Page项模板添加到项目中的。

清单 7-17 。customNavBar.html 文件的内容

`

                                               `

这是另一个自包含的 NavBar,HTML 元素和响应它们的代码定义在同一个文件中。不同的是,我在data-win-options 属性中将layout属性设置为customcustom值告诉WinJS.UI.AppBar控件,你不想要标准的基于命令的布局,你将自己提供和管理导航条中的元素。

images 注意可以在应用栏和导航条上使用自定义布局——但这通常不是个好主意。应用的导航结构可能会有所不同,用户希望导航条能够反映出你的应用的独特特征。另一方面,命令应该是一致的,并使用我前面描述的约定来应用。用户将期待在应用栏中的命令,它们的布局和响应方式与所有其他 Windows 应用一致。如果你必须使用一个非标准的 AppBar 来表达你的应用的独特性,那么你的设计一定有问题。

如清单所示,我已经用一个button和一个h1元素填充了导航栏。我使用了一个叫做win-backbutton的方便的 Windows CSS 类,它创建了一个圆形边框中带有箭头的按钮。导航栏中的元素包含在一个div元素中。我采用了这种结构,这样我可以很容易地设计元素的样式——你可以在清单 7-18 的中看到我添加到default.css文件中的样式。

清单 7-18 。自定义导航栏布局的 default.css 文件中的样式

`...

navBarContent {

display: -ms-flexbox;
    -ms-flex-direction: row;
    -ms-flex-align: start;
    width: 100%;
    height: 80px;
}

navBarBack {

margin-top: 18px;
    margin-left: 20px;
}

navBarTitle {

padding-top: 3px;
    margin-left: 20px;
}
...`

我使用 CSS flexbox 布局来定位元素。正如你已经意识到的,我经常使用这种布局,因为它非常适合 Windows 应用的一般风格,其中控件沿着公共轴流动——当我在第十六章中描述语义缩放概念时,你会更清楚地看到这种视觉主题。

应用自定义导航栏

应用带有自定义布局的导航栏的过程与应用常规导航栏的过程是一样的,只是您必须确保布局中的元素得到了正确的配置和准备。对于我的自定义 NavBar 布局,我需要设置h1元素的内容,这是通过用render 方法传递一个数据对象来完成的,这样它就可以被在customNavBar.html文件中定义的ready函数使用。清单 7-19 显示了使用自定义导航条的page2.html文件。

清单 7-19 。page2.html 文件的内容

`

              **        **                   
            

This is Page 2

        
     `

我不需要监听导航栏中按钮的click事件,因为它是使用WinJS.Navigation API 在customNavPage.html文件中处理的。

images 提示我也在项目中添加了page3.html,但它本质上与page2.html相同,所以我不会浪费篇幅列出内容。

调整导航条和应用条的行为

WinJS API 期望一个应用中只有一个 NavBar。当WinJS.UI.processAll方法处理一个已经应用了WinJS.UI.AppBar控件的元素时,它会将该元素移出它在 DOM 中的原始位置,这样它就不会受到导入内容变化的影响。对于我的例子来说,这是一个问题,因为我想在导航条之间自由切换,以反映我正在显示的内容。

我还有第二个问题要解决。当用户导航到应用的另一部分时,应用栏和导航栏不会自动隐藏。我将删除 NavBar,这样我就不用担心隐藏它了——但是我需要确保 AppBar 是隐藏的;否则,它只会在屏幕上徘徊,遮住我新导入的内容。

为了解决这两个问题,我在default.js文件中添加了一些代码来响应导航请求。您可以在清单 7-20 中看到这些变化。

清单 7-20 。移除导航条并隐藏应用条以响应导航事件

(function () {     "use strict"; `var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {

**        if (window.navbar) {**
**            window.navbar.parentNode.removeChild(navbar);**
**        }**

**        if (window.appbar) {**
**            window.appbar.winControl.hide();**
**        }**

WinJS.Utilities.empty(contentTarget);
        WinJS.UI.Pages.render(e.detail.location, contentTarget);
    });

app.onactivated = function (eventObject) {
        WinJS.UI.processAll().then(function () {
            WinJS.Navigation.navigate("page1.html");

fontSelect.addEventListener("change", function (e) {
                command.innerText = this.value;
                fontFlyout.winControl.hide();
            });
        });
    };
    app.start();
})();`

在导航到请求的内容之前,我移除了idnavbar的元素,并通过winControl隐藏了appbar元素。这给了我所需的应用状态,以便新内容可以加载自己的导航栏,并且不会被应用栏遮挡。你可以在图 7-9 中看到导航条产生的效果。

images

图 7-9。在一个应用中使用两种风格的导航条

制作导航过渡动画

在这一章中,我要做的最后一件事是制作从一页内容到另一页内容的动画。WinJS.UI.Animation名称空间定义了许多动画,您可以在应用状态的关键转换时将这些动画应用于元素。我将向您展示这些动画,因为我描述了它们相关的功能——在这一章中,当一个内容页面离开显示屏以及当另一个内容页面到达时会应用相关的动画。我会在第十八章中详细解释这些动画,但我只想在这里提供一个快速的概述。

应用中的内容转换可能发生得如此之快,以至于人眼并不总是能察觉到它们——特别是如果涉及的两个内容页面共享共同的视觉线索,例如颜色和字体。您希望您的用户知道,由于单击或触摸导航命令,内容已经发生了变化。如果不明显,你将打破好的应用所拥有的流畅的交互模式,并导致用户花一秒钟来检查他期望的动作已经被执行了。

你让用户知道视觉信号发生了变化。我在示例中为内容页面使用了不同的背景颜色,这是一个非常有用和强大的信号,尤其是如果在整个应用中使用一致的颜色来指示不同的功能区域。另一个强大的视觉信号是动画。清单 7-21 展示了我是如何将动画添加到default.js文件中的导航事件处理函数中的——在这个函数中应用动画意味着它们将被一致地应用到整个应用中。

清单 7-21 。将导航动画应用于示例应用

`(function () {
    "use strict";

var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {

if (window.navbar) {
            window.navbar.parentNode.removeChild(navbar);
        }

if (window.appbar) {
            window.appbar.winControl.hide();
        }

// These statements are commented out
        //WinJS.Utilities.empty(contentTarget);
        //WinJS.UI.Pages.render(e.detail.location, contentTarget);

**        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {**
**            WinJS.Utilities.empty(contentTarget);**
**            WinJS.UI.Pages.render(e.detail.location, contentTarget).then(function () {**
**                return WinJS.UI.Animation.enterPage(contentTarget.children)**
**            });**
**        });**
    });

app.onactivated = function (eventObject) {
        WinJS.UI.processAll().then(function () {
            WinJS.Navigation.navigate("page1.html");
        });
    };

app.start();
})();`

我在这个清单中使用的两个动画方法是exitPageenterPage。名称解释了每个动画的用途,两种方法的参数都是一个或多个需要动画的元素。为了简单起见,我传递了母版页中承载导入内容的元素的children属性的结果。

请注意,没有为这些动画配置样式或持续时间的选项。通过使用这些方法,您可以应用标准的页面过渡动画,这对于所有 Windows 应用都是一样的。这是一个很好的方法,因为很多非常明智和理性的开发人员在得到一个动画库时会变得有点疯狂,并且往往会忘记动画是为了帮助用户。

我不能给你看截图中的动画,所以你应该运行示例应用来看看效果。由exitPageenterPage方法触发的动画简洁、简单且有效。它们足够简单和快速,当用户第 100 次看到动画时不会感到厌烦,但它们足够丰富,足以引发用户意识到应用中的内容已经发生了变化。

总结

在这一章中,我已经向你展示了如何使用WinJS.UI.AppBar控件来创建应用栏和导航条。这些是 Windows 用户体验中的基本元素,它们为用户与你的应用的交互带来了一致性和清晰性。这并不是说使用 AppBars 和 NavBars 应该受到限制——即使有微软制定的惯例,也有许多不同的方式来组织您呈现给用户的命令,并且您可以选择使用自定义 NavBar 布局来定制您提供的导航体验。

在下一章,我将介绍视图模型数据绑定的主题。现在我们已经了解了 Windows 应用布局和导航的基本结构,是时候考虑如何采用同样的方法来组织和显示应用的数据了。*

八、查看模型和数据绑定

在本章中,我将向您介绍视图模型数据绑定。这是两个基本的技术,可以让您创建可伸缩性好、易于开发和维护、能够流畅地响应数据变化的应用。

您可能已经从设计模式中熟悉了模型和视图模型,如模型-视图-控制器(MVC),模型-视图-视图模型(MVVM 和模型-视图-视图控制器(MVVC))。我不打算在本书中详细讨论这些模式。有很多关于 MVC、MVVM 和 MVVC 的好信息,从维基百科开始,它有一些非常平衡和深刻的描述。

我发现使用视图模型的好处是巨大的,除了最简单的应用项目,其他项目都值得考虑,我建议你认真考虑遵循同样的道路。我不是一个模式狂热者,我坚信应该采用解决实际问题的部分模式和技术,并将它们应用到具体的项目中。最后,你会发现我对如何使用视图模型持相当开放的观点。

我在本章中描述的 WinJS 特性支撑了 Windows 应用支持的一些基本交互模型。为了确保我为更高级的特性打下坚实的基础,我慢慢地开始这一章,并逐步介绍关键概念。理解这些特性是充分利用高级 UI 控件和概念的前提,比如语义缩放,我在第十六章中对此进行了描述。表 8-1 对本章进行了总结。

images

images

重温示例应用

在这一章中,我继续构建我在前一章中创建的AppBars项目。提醒一下,这个应用引入了 NavBars 和 AppBars,并包含了一些简单的内容页面。我将在此基础上展示新的应用特性。

分离应用组件

我将首先应用一个视图模型来修复第七章中的示例应用的一些缺点。在此过程中,我将向您展示我首选的视图模型对象结构,并演示视图模型可以有多简单,同时还能使开发人员的工作更加轻松。

images 注意如我之前所说,我对视图模型的构成持非常开放的态度,它包括不直接呈现给用户的数据。

定义视图模型

视图模型最重要的特征是全局可用性和一致性。在 Windows 应用中,创建基本视图模型最简单的方法是使用WinJS.Namespace特性(我在第三章和第四章中介绍过)来创建视图模型对象并将其导出到全局名称空间。清单 8-1 显示了viewmodel.js文件的内容,我将它添加到了AppBars示例项目的js文件夹中。

清单 8-1 。viewmodel.js 文件的内容

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    }); WinJS.Namespace.define("ViewModel.UserData", {

});
})();`

我喜欢创建一个名为ViewModel的顶级名称空间,它包含嵌套的名称空间,代表我想要处理的每一大类数据。在这个例子中,我定义了两个名称空间。ViewModel.State是我定义关于应用状态的数据的地方,我将它粗略地定义为应用的一部分为了与应用的其他部分顺利工作而需要知道的数据。这个名称空间包含三个属性,我将使用它们来解决我在第七章的中引入到示例应用中的挥之不去的紧耦合问题。

我在ViewModel.UserData中定义的第二个名称空间。我使用这个名称空间来存储用户关心的数据。这因应用而异,但它包括用户输入的任何值和我从这些值中导出的任何数据。这是前台数据,而不是我放在ViewModel.State名称空间中的后台数据。最初这个名称空间中没有属性,但是我会在本章的后面添加一些。

images 提示 JavaScript 是一种动态语言,这意味着在给属性赋值之前,我不需要在视图模型中定义属性。我还是这样做了,因为我希望我的视图模型定义成为它包含的数据的规范引用;在我看来,这意味着定义属性并将null分配给它们,而不是在我第一次使用它们时在应用的其他地方创建属性。

我的 Windows 应用通常有一个包含这两个名称空间的视图模型。我根据我的应用支持的契约添加其他契约。我在本书的第四部分解释了契约,但这两个是我最常用的基本契约。我将状态数据从用户数据中分离出来,因为我发现这样更容易确定哪些数据应该持久存储;我会在第二十章中进一步讨论这个问题。

导入并填充视图模型

视图模式是在一个自动执行的 JavaScript 函数中定义的,所以在示例应用中使用它所要做的就是导入带有script元素的代码,并为视图模型包含的属性设置值。我希望视图模型在应用启动时就可用,并且无论导入和显示了哪些内容都可用,这意味着需要将script元素作为示例应用的母版页放在default.html文件中。清单 8-2 显示了添加的script元素。

清单 8-2 。使用脚本元素加载视图模型代码

`...

<head>     <meta charset="utf-8">     <title>AppBars</title>


    
    
    
    
**    **
    

...`

我在之前添加了viewmodel.js文件的脚本元素,这让我有机会在初始化应用时引用视图模型。在示例应用中,我想为在default.js文件中包含 AppBar 和 NavBar 控件的元素设置值,如清单 8-3 中的所示。

清单 8-3 。填充视图模型

`(function () {
    "use strict";

var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {

**        var navbar = ViewModel.State.navBarControlElement;**
**        if (navbar) {**
**            navbar.parentNode.removeChild(navbar);**
**        }**

**        if (ViewModel.State.appBarElement) {**
**            ViewModel.State.appBarElement.winControl.hide();**
**        }**

//WinJS.Utilities.empty(contentTarget);
        //WinJS.UI.Pages.render(e.detail.location, contentTarget);

WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {
            WinJS.Utilities.empty(contentTarget);
            WinJS.UI.Pages.render(e.detail.location, contentTarget).then(function () {
                return WinJS.UI.Animation.enterPage(contentTarget.children)
            });
        });
    });

app.onactivated = function (eventObject) {
        WinJS.UI.processAll().then(function () {

**            ViewModel.State.appBarElement = appbar;**
**            ViewModel.State.navBarContainerElement = navBarContainer;**

WinJS.Navigation.navigate("page1.html");

fontSelect.addEventListener("change", function (e) {
                command.innerText = this.value;                 fontFlyout.winControl.hide();
            });
        });
    };
    app.start();
})();`

消费视图模型

我在视图模型中设置了值,这样,default.html文件中元素结构的细节就不会在整个应用中扩散。如果我更改了default.html文件,我只需在default.js文件中反映这些更改,而不必搜寻并找到所有使用了appbarnavBarContainer值的实例。在包含在内容文件中的代码中,我可以将我想要的导航条导入到视图模型中列出的元素中,如清单 8-4 所示的,它显示了page2.html文件的内容。

清单 8-4 。使用视图模型定位导航条加载到的元素

`

              
        

This is Page 2

    
`

default.js文件中只设置了ViewModel.State名称空间中的两个属性。第三个属性navBarControlElement,由设置应用使用的每种 NavBar 样式的代码设置,如清单 8-5 所示,它显示了来自customNavBar.html文件的script元素——这允许我拥有一个全局可用的属性,该属性被设置为反映当前导入的内容。

清单 8-5 。在 NavBar 代码中设置视图模型属性值

`...

...`

该属性用于default.js,中的导航事件处理函数,这意味着导航条控件不必应用于idnavbar的元素。最后,我对standardNavBar.html文件进行了同样的修改,如清单 8-6 所示。

清单 8-6 。在 NavBar 代码中设置视图模型属性值

`...

...`

这些变化的结果是,视图模型充当了关于应用的状态和结构的信息库,允许各种组件协同工作,而无需事先了解彼此。当我在标记或代码中进行更改时,我只需要确保视图模型属性反映了更改,而不是搜寻所有的依赖项并手动进行更改。

从布局中分离数据

视图模型的最大价值来自于将应用中的数据从 HTML 中分离出来,并呈现给用户。通过将视图模型与数据绑定相结合,您可以使您的应用更容易开发、测试和维护。我将在本节的后面解释数据绑定是如何工作的,但是首先我将向您展示我正在着手解决的问题。在开始之前,我需要为我将在本章中添加的元素添加一些新的 CSS 样式。为此,我创建了一个名为/css/extrastyles.css的新文件,其内容你可以在清单 8-7 中看到。在这个 CSS 中没有新的技术,我列出了新的样式,所以你可以看到这个项目的各个方面。

清单 8-7 。extrastyles.css 文件的内容

`#page2BoxContainer {
    display: -ms-flexbox;
    -ms-flex-direction: row;
    -ms-flex-align: stretch;
    -ms-flex-pack: justify;
    margin-top: 20px;
}

div.page2box {
    width: 325px;
    padding: 10px;
    margin: 5px;
    border: medium solid white;
    background-color: gray;
    display: -ms-flexbox;
    -ms-flex-direction: column;
    -ms-flex-pack: center;
}

div.page2box * {
    display: block;
    margin: 4px;
    font-size: 18pt;
}`

这些样式中引用的类和id属性值是针对我稍后将添加的元素的。为了将文件的内容纳入项目范围,我向default.html文件添加了一个link元素,如下所示:

`...

<head>     <meta charset="utf-8">     <title>AppBars</title>


    
    
    


    
**    **
    
    

...`

论证问题

没有视图模型,数据项的权威来源是 HTML 元素;也就是说,当您想要一个数据值时,您必须在 DOM 中找到包含它的元素,并从适当的HTMLElement对象中读取该值。作为示范,我对page2.html文件做了一些修改,如清单 8-8 所示。

清单 8-8 。使用布局中的元素作为数据值的权威来源

`

                      
    
    
        

            

This is Page 2

**    

**
**                
**
**                    **
**                    **
**                
**

**                

**
**                    The word is:**
**                    ????**
**                
**

**                

**
**                    The length is:**
**                    ????**
**                
**
**            
**
        

    

`

这是一个简单的例子。我在布局中添加了三个div元素。我使用 CSS flexbox 布局对它们进行了定位,并应用了我在extrastyles.css文件中定义的类。

您可以在图 1 中看到新标记和代码的效果。您在左侧面板的input元素中输入一个单词,然后点击OK按钮。您输入的单词显示在中间面板中,单词的长度显示在右侧面板中。在图中,我已经输入了单词press

images

图 8-1。向 page2.html 布局添加三个面板

本例中的input元素是用户输入的数据的权威来源。如果我想让用户输入单词,我需要读取代表 DOM 中的input元素的HTMLElement对象的value属性。这种方法的好处是简单,但是它引入了一些深刻的问题。

一个问题是input元素并不是真正的权威。当点击button时,您可以合理地确信input元素包含在精确时刻的用户数据,但是在所有其他时间,用户可能正在改变值。如果你在除了点击button之外的任何时候读取value属性,你不能确定你有有用的数据。

更严重的问题是,在单页内容模型中使用时,数据值不是持久的。当用户导航到新页面时,Windows 应用运行时会丢弃input元素及其内容。当用户导航回该页面时,会生成新元素,并且用户在导航之前输入的任何数据都会丢失。

应用视图模型

当然,解决方案是使用视图模型。清单 8-9 显示了在viewmodel.js文件中添加的两个新属性,代表我向用户显示的两个数据项。因为我正在处理用户输入的数据,所以我在名称空间ViewModel.UserData中定义了这些属性。

清单 8-9 。在视图模型中为用户数据定义属性

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    });

WinJS.Namespace.define("ViewModel.UserData", {
**        word: null,**
**        wordLength: {             get: function() {**
**                return this.word ? this.word.length : null;**
**            }**
**        }**
    });
})();`

word属性将包含用户输入的值。这个数据项没有默认值,所以我将null赋给了属性。

wordLength属性被称为派生值计算值,这意味着它返回的值基于视图模型中的某个其他值。为了创建这个属性,我使用了一个相对较新的 JavaScript 特性,称为 getter 。Getters 及其对应的setter,允许您使用函数来创建复杂的属性。在这个例子中,我只定义了一个 getter,这意味着该值可以被读取,但不能被修改。

images 提示在视图模型中使用计算值的好处是生成值的逻辑保存在一个地方,而不是嵌入到需要显示值的每个script元素中。如果我以后需要更改wordLength属性,也许是为了只计算元音,那么我只需要更改视图模型。这是向应用添加结构的另一个方面,这样负责配置 HTML 元素的代码就不用负责生成这些值。

视图模型的消费者看不到 getters 和 setters 的使用,他们通过读取ViewModel.UserData.wordLength获得wordLength值,就像它是一个常规属性一样。定义完这些属性后,我需要对page2.html做两组修改,如清单 8-10 所示。

清单 8-10 。使用视图模型存储和检索数据

`...

...`

我通过更新视图模型中的属性来响应被点击的button,这和其他 JavaScript 赋值一样。此时,视图模型对象没有特殊的能力或特性,除了我已经使它成为我的数据的权威来源:

... **ViewModel.UserData.word** = wordinput.value; ...

另一个变化是当ViewModel.UserData.word属性的值改变时,使用视图模型更新中间和右边的面板。我已经将更新面板的语句转移到一个名为updateDataDisplay的函数中,该函数使用视图模型来获取wordwordLength数据值。应用的功能是相同的,但现在视图模型充当数据存储库以及更新属性的代码和响应这些更新的代码之间的中介。

使用数据绑定

在这一点上,我已经达到了我的目标,但不是以一种有益的方式。例如,尽管我已经将更新代码分离到它自己的函数中,并使用视图模型数据进行更新,但我仍然必须直接从与button元素相关联的click事件处理函数中触发更新。

构建应用的下一步是断开更新函数和click事件处理程序之间的链接。我将使用数据绑定来实现这一点,这是两个数据项链接在一起并保持同步的地方。

对于 Windows 应用,数据绑定是通过WinJS.Binding名称空间提供的。设置数据绑定的过程需要两个步骤:使一个数据项可观察,然后观察该数据项,这样当该数据项的值改变时,您就会收到通知。在接下来的小节中,我将向您展示这两个步骤。

使物体可见

一个可观察对象每当它的一个属性值改变时就会发出一个事件。此事件允许相关方监控属性值并做出相应的响应。你通过使用WinJS.Binding.as方法创建一个可观察的对象,你可以看到我是如何将这个方法应用到清单 8-11 中的viewmodel.js文件的。

清单 8-11 。使部分视图模型可见

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    }); **    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({**
**        UserData: {**
**            word: null,**
**        }**
**    }));**

**    WinJS.Namespace.define("ViewModel.UserData", {**
**        wordLength: {**
**            get: function () {**
**                return this.word ? this.word.length : null;**
**            }**
**        }**
**    });**
})();`

创建一个可观察的视图模型并不完全简单,您可以从我在视图模型中所做的结构更改中看到这一点。需要这些更改来解决 WinJS API 中的一系列冲突和限制。我将向您详细介绍每一个问题,以便您理解发生了什么,并且如果您在自己的代码中遇到它们,可以识别它们。

解决命名空间与绑定的冲突

WinJS.Binding.as方法的工作原理是用 getters 和 setters 替换对象中的属性。这允许您继续使用属性,而不必担心它们是如何实现的,并允许可观察对象在设置新值时无缝地发出事件。结果是一个可见的对象,但它仍然与使用它的代码兼容。

为了支持这种方法,WinJS.Binding.as方法使用在成员名前加下划线字符(_)的 JavaScript 约定来创建一些私有成员。

但是当WinJS.Namespace.define方法将一个对象导出到全局名称空间时,它会隐藏任何名称以下划线开头的对象成员。我认为,这个想法是为了加强一些严格性,防止名称空间的消费者访问私有变量和方法。

当您使用WinJS.Binding.as方法创建一个想要使用WinJS.Namespace.define方法导出的可观察对象时,问题就来了——as方法添加的私有成员被define方法隐藏,破坏了对象。结果是,您不能简单地将as方法应用于您想要导出到名称空间的对象,就像这样:

... WinJS.Namespace.define("ViewModel.UserData",     WinJS.Binding.as({         word: null     }) ); ...

define方法只对它所传递的对象的顶层隐藏私有成员,这意味着你可以通过构造不同的名称空间来解决这个问题,就像这样:

... WinJS.Namespace.define("ViewModel", WinJS.Binding.as({ **    UserData: {** **        word: null,** **    }** })); ...

名称空间的UserData部分被传递给as方法,这解决了问题。WinJS API 的不同部分在一些地方不能很好地协同工作,这是新的 Windows 应用开发人员最常遇到的问题。

解决 Getter 冲突

WinJS.Binding.as方法转换对象中的简单值属性,使它们变得可观察。它不对函数进行操作,但是它会导致使用 getters 和 setters 的值出现问题。正是因为这个原因,我将wordLength属性的定义移到了对WinJS.Namespace.define方法的单独调用中:

... WinJS.Namespace.define("ViewModel.UserData", {    wordLength: {        get: function () {            return this.word ? this.word.length : null;        }     } }); ...

define方法是附加的,这意味着我可以定义属性或函数,它们将被添加到我已经用相同名称定义的任何现有名称空间对象中(而不是替换它们)。在这种情况下,我的wordLength属性被添加到ViewModel.UserData名称空间,但没有通过WinJS.Binding.as方法传递,这意味着我的计算视图模型属性工作正常,并返回从同一名称空间中的word属性派生的值。

消费可观察的对象事件

这个有点复杂的设置过程的结果是我的ViewModel.UserData名称空间中的word属性是可见的。当我更改word属性的值时,ViewModel.UserData对象将向任何感兴趣的方发送一个事件。

您使用bind方法在可观察对象上注册对属性的兴趣。在清单 8-12 的中,你可以看到我是如何在page2.html文件的script元素中使用这个方法的。

清单 8-12 。使用 WinJS。Binding.bind 方法接收属性值更改事件

`...

...`

当我调用WinJS.Binding.as方法时,bind方法被添加到可观察对象中。bind的参数是一个包含您想要观察的属性名称的字符串,以及一个当属性值改变时处理事件的函数。在我的例子中,我已经在ViewModel.UserData对象上调用了bind方法,指定我想要观察word属性,并且变更事件将由updateDataDisplay函数处理。

事件处理函数传递了两个参数——被观察属性的新值和旧值(mu 示例只有一个参数,因为我不关心前一个值)。在这个清单中,我修改了updateDataDisplay函数,以便使用新的 value 参数设置布局中的中间面板,并更新右侧面板以反映单词长度。

使用bind方法创建一个编程绑定(即绑定是在 JavaScript 代码中定义的)。我能够使用编程绑定来简化我的代码,并确保我的应用的不同部分自动保持彼此同步,使用视图模型作为代理或中介。

重构可观察的属性

我的数据绑定还有一个问题需要解决,那就是wordLength属性是不可见的。这样做的效果是,视图模型中值的消费者必须知道wordLength属性是从word属性计算出来的,并使用后者属性中的事件作为触发器来检查前者的变化。

修复这个问题相当简单,只需要稍微修改一下viewmodel.js文件,如清单 8-13 所示。

清单 8-13 。手动发送更新事件使属性可见

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    });

WinJS.Namespace.define("ViewModel", WinJS.Binding.as({
        UserData: {
            word: null,
**            wordLength: null**
        }
    }));

**    ViewModel.UserData.bind("word", function (newVal) {         if (newVal) {**
**            ViewModel.UserData.wordLength = newVal ? newVal.length : null;**

**        }**
**    });**
})();`

在这个清单中,我使用了bind方法让视图模型观察自己。这允许我将为wordLength属性计算值的逻辑移出ViewModel.UserData对象,这意味着wordLength属性本身就可以被观察到。

这并不是一种完美的技术,因为视图模型的消费者有可能接收到word属性的变更事件,并在更新之前读取wordLength属性。然而,对于大多数项目来说,这是一种完全合理的方法,并且只需要少量的工作就可以避免 WinJS 数据绑定支持中的一些问题;我将很快向您展示一种替代方法,用于那些无法接受时间问题的应用。

通过使这两个属性都是可观察的,我使视图模型的消费者能够在其中一个发生变化时收到通知。清单 8-14 显示了来自page2.htmlscript元素,它显示了正在为wordwordLength属性处理的变更事件。

清单 8-14 。绑定到多视图模型属性

`...

...`

事件的处理函数没有传递哪个属性已经改变的细节,这意味着处理改变事件的最简单的方法是使用一个专用于单个属性的函数。您可以在清单中看到这一点,我将updateDataDisplay函数分成了两个独立的函数,每个函数对应一个可观察的属性。结果是,wordLength属性的观察者不需要知道该属性是从word属性派生出来的。

彻底解决问题

在我继续之前,我想向您展示一种使视图模型可观察的不同方法,这种方法没有上一节中潜在的时间问题。如果您使用计算属性,您只需要使用这种方法。在发送任何变更事件之前,更新所有相关的属性值是必要的。对于所有其他情况,你应该使用我在清单 8-14 中展示的方法,这更简单,也更容易操作。清单 8-15 显示了完全解决更新问题所需的更高级的方法。

清单 8-15 。解决 viewmodel.js 文件中的更新排序问题

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    });

**    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({**
**        UserData: {**
**            // no properties defined here**
**        }**
**    }));**

**    WinJS.Namespace.define("ViewModel.UserData", {**
**        _wordValue: null,**
**        wordLength: null,**
**        word: {**
**            get: function() {**
**                return this._wordValue;**
**            },**
**            set: function (newVal) {**
**                var oldWordVal = this._wordValue;**
**                var oldLengthVal = this.wordLength;**
**                this._wordValue = newVal;**
**                this.wordLength = newVal ? newVal.length : null;**
**                this.notify("word", newVal, oldWordVal);**
**                if (this.wordLength != oldLengthVal) {**
**                    this.notify("wordLength", this.wordLength, oldLengthVal);**
**                }**
**            }**
**        }**
**    }); })();**`

以这种方式设置视图模型需要两个步骤。第一种是使用WinJS.Binding.as方法创建一个不包含属性的可观察对象。这在对象中设置了可观察的功能,但是没有用 getters 和 setters 替换任何属性。

第二步是仅使用define方法手工定义需要同步更新的属性,以便将它们添加到您在第一步中作为名称空间导出的对象中。

您可以使用 getters 和 setters 来定义其他值的派生属性。在 setter 中,您执行派生属性的计算,然后使用notify方法发布 changes 属性的更新,如下所示:

... this.notify("word", newVal, oldWordVal); if (this.wordLength != oldLengthVal) {     this.notify("wordLength", this.wordLength, oldLengthVal); } ...

当我调用WinJS.Bind.as方法时,notify方法被添加到了ViewModel.UserData对象中,观察者需要使用bind方法来注册事件。notify方法的参数是发生变化的属性的名称、新值和旧值。notify方法与可观察对象内部使用的机制相同,它允许您完全控制视图模型发出变更事件的方式。在清单中,通过确保在更新完所有属性值之前不调用notify方法,我能够确保在更新完这两个值之前不发送wordwordLength属性的变更事件。

让我重申一下,你只需要在非常特殊的情况下走这么远。我已经向您展示了这种技术,因为这些情况经常令人惊讶地出现,并且因为它允许您获得关于 WinJS 数据绑定机制如何配合的更多细节。

使用声明绑定

到目前为止,在我向您展示的示例中,视图模型中的值是使用编程绑定来消费的,这意味着我在我的 JavaScript 代码中接收属性值更改的通知,并通过更新应用的 HTML 中的一个或多个元素的内容来响应。我可以通过使用声明性绑定来优化这个过程,其中我将数据绑定的细节作为 HTML 元素声明的一部分,并允许 WinJS 为我管理更新过程。

处理文档

使用声明性绑定的前提是调用WinJS.Binding.processAll方法,该方法在 HTML 中定位绑定并激活它们。使用单页内容模型时,将对此方法的调用放在导航事件处理程序函数中很重要,以便在导入内容时处理页面中的声明性绑定;否则,您的绑定将不会被激活。在我的示例应用中,导航事件处理程序在default.js文件中,你可以在清单 8-16 中看到processAll方法调用。

清单 8-16 。打电话给温家。导航事件处理函数中的 Binding.processAll 方法

(function () {     "use strict"; `**    WinJS.Binding.optimizeBindingReferences = true;**

var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {

var navbar = ViewModel.State.navBarControlElement;
        if (navbar) {
            navbar.parentNode.removeChild(navbar);
        }

if (ViewModel.State.appBarElement) {
            ViewModel.State.appBarElement.winControl.hide();
        }

WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {
            WinJS.Utilities.empty(contentTarget);
            WinJS.UI.Pages.render(e.detail.location, contentTarget)
                .then(function () {
**                    return WinJS.Binding.processAll(document.body, ViewModel);**
                }).then(function() {
                    return WinJS.UI.Animation.enterPage(contentTarget.children)
                });
        });
    });

app.onactivated = function (eventObject) {
       WinJS.UI.processAll().then(function () {

ViewModel.State.appBarElement = appbar;
            ViewModel.State.navBarContainerElement = navBarContainer;

WinJS.Navigation.navigate("page1.html");

fontSelect.addEventListener("change", function (e) {
                command.innerText = this.value;
                fontFlyout.winControl.hide();
            });
        });    
    };
    app.start();
})();`

images 注意当你使用声明式绑定时,你应该总是将WinJS.Binding.optimizeBindingReferences属性的值设置为true,就像我在例子中所做的那样。虽然我没有遇到任何问题,但微软警告说,省略这一步会产生绑定操作导致内存泄漏的风险。我没有在本书的一些例子中应用这个属性,因为我想让他们尽可能专注于手头的功能,但是对于真正的项目,你应该采纳微软的建议。

您需要将对WinJS.Binding.processAll方法的调用放入到Promise.then调用链中,以便在用WinJS.UI.Pages.render方法导入内容之后、向用户显示之前处理绑定。processAll方法返回一个Promise对象,所以很容易得到正确的顺序,尽管嵌套的then调用链可能会变得相当深。

images 提示我在第九章中深入覆盖了WinJS.Promise对象。

WinJS.Binding.processAll方法的参数是处理的开始元素和数据值的来源。我已经将开始元素设置为document.body,这确保了整个布局被处理。对于数据值的来源,我已经指定了ViewModel对象。

声明绑定

一旦安排好用processAll方法处理绑定,就可以继续在 HTML 中声明绑定了。为了演示声明性绑定,我更新了page2.html文件中的面板,如清单 8-17 所示。

清单 8-17 。使用 page2.html 文件中的十进制装订

`

                      
    
    
        

            

This is Page 2


                

                    
                    
                


                    The word is:
                    <span id="wordspan" **                        data-win-bind="innerText: UserData.word"**>????
                


                    The length is:
                    <span id="lengthspan"
**                        data-win-bind="innerText: UserData.wordLength"**>????
                

            

        

    

`

首先,请注意我已经从script元素中移除了编程绑定——我不再调用bind方法或者在代码中更新span元素的内容。相反,我给span元素添加了data-win-bind属性。该属性是声明性绑定特性的核心,它允许您指定如何设置一个HTMLElement对象的一个或多个属性值。

WinJS 声明性绑定在表示 DOM 中元素的HTMLElement对象上操作。这意味着如果你想设置一个元素的文本内容,你可以指定将innerText属性设置为你传递给processAll方法的对象的UserData.word属性,就像这样:

... <span id="wordspan" **data-win-bind="innerText: UserData.word"**></span> ...

冒号(:)将属性名称与数据属性名称分开。如果与声明性绑定相关的数据项是可观察的,则声明性绑定会自动保持最新。清单中的两个声明性绑定都与可观察的数据项相关,因此我的布局与视图模型保持同步。当您只需要向用户显示值时,这是使用编程绑定的一个很好的替代方法。

images 提示通过用分号(;)分隔绑定,您可以在单个**data-win-bind**属性中绑定多个属性。

创建可观察数组

正如我前面提到的,WinJS.Binding.as方法只能观察简单的值属性。它将忽略对象、函数、日期和数组(尽管在对象的情况下,它将寻找嵌套的简单值属性并使它们可被观察到)。在这一节中,我将向您展示如何创建可观察数组。可观察数组不仅有用,而且是我在本书第三部分中描述的一些 UI 控件的重要基础。

创建可观察数组

通过创建新的WinJS.Binding.List对象来创建可观察数组。你可以看到我是如何在清单 8-18 中的文件中添加这样一个对象的。我将使用这个List来保存用户输入的单词的简单历史。

清单 8-18 。向视图模型添加一个可观察数组

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    });

WinJS.Namespace.define("ViewModel", WinJS.Binding.as({
        UserData: {
            word: null,
            wordLength: null,
        }
    }));

**    ViewModel.UserData.wordList = new WinJS.Binding.List();**

ViewModel.UserData.bind("word", function (newVal) {
        if (newVal) {
            ViewModel.UserData.wordLength = newVal ? newVal.length : null;
**            ViewModel.UserData.wordList.push(newVal);**
        }
    });
})();`

WinJS.Binding.List对象受WinJS.Namespace.define方法删除名称以下划线开头的属性和函数的影响——解决方案是手动将List对象添加到名称空间,如清单所示。

images 提示注意,我已经返回到清单中视图模型的更简单形式。我在本章其余部分描述的技术可以应用于任何一种方法,但是这个更简单的视图模型使我更容易说明我所做的更改。

List对象不是 JavaScript 数组的直接替代。它实现了数组中的许多方法和属性,包括我在清单中使用的在word属性更新时向List添加一个项目的push方法。主要的区别是没有数组索引器(像myArray[0]),你必须使用getAtsetAt方法(myArray.getAt(0))来代替。您很快就会习惯它,从积极的方面来看,List对象支持一些很好的特性来排序和过滤它的内容。表 8-2 总结了由List对象定义的最重要的方法。

images

images

这些只是基本的方法,我建议您花些时间看看 API 文档,更详细地探索一下List对象的功能。

images 提示List对象支持的许多功能是它作为一些 WinJS UI 控件的数据源所必需的。你可以在第三部分中了解更多关于数据源和使用它们的控件的信息。

在清单 8-18 中,我创建了一个空的List对象,但是如果你将一个 JavaScript 数组传递给构造函数,你可以预先填充一个List。你也可以传递一个可选的配置对象,它支持表 8-3 所示的属性。

images

您可以按如下方式应用这些选项:

... var myArray = ["Apple", "Orange", "Cherry"]; var myList = new WinJS.Binding.List(myArray, **{** **    proxy: true,** **    binding: false** **}**); ...

我还没有在实际项目中使用过这些选项。proxy属性是危险的,并且binding选项需要在由List发出的事件的处理函数中非常小心地编码,以防止观察到不再是集合的一部分的对象(并且在有用的情况下,我更喜欢自己注意使对象可被观察到)。

观察列表

正如您所料,List对象会在集合内容发生变化时发出事件。有几个事件,最有用的是iteminserteditemchangeditemremoveditemmoved。每个事件的重要性从它的名字就可以看出,通过查看传递给事件处理函数的事件的detail属性,您可以获得插入、更改、删除或移动内容的详细信息。

images更改列表中对象的属性值不会触发事件。

为了演示如何观察一个List对象,我向由page2.html定义的布局添加了一个新面板,并向script元素添加了一些新代码。您可以在清单 8-19 中看到这些变化。

清单 8-19 。观察一个 WinJS。Binding.List 对象

`

         

    
        

This is Page 2


            

                
                
            


                The word is:
                ????
            


                The length is:
                ????
            

        

**        

**
**            Word List:**
**            
**
**        
**
    

`

您通过调用addEventListener方法来观察一个List对象,指定您感兴趣的事件类型和将处理变更事件的回调函数。在这个清单中,我通过创建一个新的div元素来响应iteminserted事件,将它分配给 CSS word类,并将其添加到新的布局面板中。我将传递给处理函数的Event对象的innerText属性设置为detail.value属性。Event.detail对象还为新添加的项目定义了index属性,这样您就可以知道项目被添加到了List中的什么位置。你可以在图 8-2 中看到结果:每次用户输入一个单词并点击OK按钮时都会添加一个新元素,创建一个简单的历史。

images

图 8-2。观察列表对象以创建简单的历史显示

您会注意到,我只展示了List对象的编程绑定。有一些可用的声明性绑定,但是它们被包装在一些 WinJS UI 控件中。我将在本书的第三部分中介绍这些控件并解释它们如何与List对象一起使用。

使用模板

在前面的例子中,我最终创建了一系列的div元素来表示用户输入单词的历史。我使用了 DOM API,它可以工作,但是使用起来很笨拙,而且容易出错。幸运的是,WinJS 提供了一些与数据绑定特性相关的基本模板支持,这使得基于视图模型中的数据创建元素变得更加容易。清单 8-20 显示了我在前面的例子中使用 JavaScript 创建的元素——这些将作为我的模板的(简单)目标。

清单 8-20 。单词历史列表中的代码生成元素

`...

    Word List:     
**        
HTML
** **        
CSS
** **        
JavaScript
**     
...`

定义和使用模板

通过将data-win-control属性设置为WinJS.Binding.Template来表示将成为模板的元素。当文档由WinJS.UI.processAll方法处理时,元素从它在 DOM 中的位置被移除,并准备用作模板。因为这是一个常规的 WinJS 控件,所以在代表 DOM 中主机元素的HTMLElement对象中添加了一个winControl属性。清单 8-21 显示了向page2.html文件添加一个模板以及使用它的 JavaScript 代码。

清单 8-21 。声明和使用模板

`

         

**    
** **        
** **    
**


        

This is Page 2


            

                
                
            


                The word is:
                ????
            


                The length is:
                ????
            

        
        

            Word List:
            

        

    

`

您将WinJS.Binding.Template控件应用于包含您的模板的元素。您需要为模板元素定义一个id属性,以便以后可以定位它。在容器元素中,使用data-win-bind属性定义模板内容,以引用要在模板中应用的数据值。

您将为绑定提供值的数据对象直接传递给render方法模板控件,这意味着数据项不必是视图模型的一部分。在清单中,我的模板由单个div元素组成,匹配我在代码中生成元素时使用的模式。我使用了data-win-bind属性来指定将使用传递给模板的数据对象的wordValue属性来设置innerText属性:

`...

    
....`

要使用模板,您需要在 DOM 中找到模板容器元素,并使用winControl属性来访问WinJS.Binding.Template对象。模板对象定义了接受两个参数的render方法——应用于模板的数据和从模板生成的内容将要插入的元素。您需要使数据值与模板期望的一致,所以我创建了一个数据对象,将添加到WinJS.Binding.List对象的新值映射到wordValue属性,然后我将它传递给 render 方法:

... wordTemplate.winControl.render(**{ wordValue: e.detail.value }**, wordList); ...

结果是我的元素是从标记中生成的,而不是纯粹用代码。我喜欢模板方法,我在我的 web 应用中广泛使用模板。在 Windows 应用中,一些更高级的 UI 控件依赖于模板来显示数据,我将在第三部分中向您展示它们是如何操作的(以及它们扮演的角色)。

使用价值转换器

我想回去稍微整理一下。通过使用数据绑定到一个可观察的视图模型值,我引入了一个轻微的修饰问题,你可以在图 8-3 中看到。

images

图 8-3。布局的初始状态

我将视图模型中的wordwordLength属性的值设置为null,这样属性就被定义了,并表明还没有设置任何值。问题是null值在布局中显示给用户,这并不美观。

为了解决这个问题,我将使用一个 WinJS 绑定转换器,它是一个 JavaScript 函数,充当视图模型值和数据绑定之间的中介。你可以在清单 8-22 中看到我是如何创建我的转换器的,它显示了对viewmodel.js文件的一些添加。您不必将转换器放在视图模型文件中,但是我采用了厨房水槽的方法,将所有东西放在一起。

清单 8-22 。向 viewmodel.js 文件添加绑定转换器

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    });

WinJS.Namespace.define("ViewModel", WinJS.Binding.as({
        UserData: {
            word: null,
            wordLength: null,
        }
    }));

ViewModel.UserData.wordList = new WinJS.Binding.List();

ViewModel.UserData.bind("word", function (newVal) {
        if (newVal) {
            ViewModel.UserData.wordLength = newVal ? newVal.length : null;
            ViewModel.UserData.wordList.push(newVal);
        }
    });

**    WinJS.Namespace.define("ViewModel.Converters", {**
**        defaultStringIfNull: WinJS.Binding.converter(function (val) {**
**            return val ? val : "";**
**        })**
**    });**
})();`

我创建了一个名为ViewModel.Converters的新名称空间,并定义了一个名为defaultStringIfNull的转换器。转换器是一个常规的 JavaScript 函数,它接受单个参数并返回转换后的值。这个函数被传递给WinJS.Binding.converter方法,该方法返回一个可以在数据绑定中使用的对象。在这个例子中,我的转换器返回它所传递的值,除非它是null,在这种情况下,返回<No Word>

在声明性绑定中,将转换器的名称放在视图模型属性之后。清单 8-23 展示了我如何更新了page2.html文件中的绑定以使用defaultStringIfNull转换器。

清单 8-23 。应用数值转换器

`...

    The word is:     ????
    The length is:     ????
...`

您必须指定转换器的完整路径,该路径必须通过全局命名空间可用。对于我的示例应用,这意味着我必须将转换器称为ViewModel.Converters.defaultStringIfNull。你可以在图 8-4 中看到结果,当wordwordLength属性为null时,一个更有意义的值呈现给用户。

images

图 8-4。使用绑定转换器避免向用户显示空值

使用开放值转换器

我在上一节中使用WinJS.Binding.converter方法创建的值转换器不知道它正在处理的值来自哪里,也不知道转换后的值将应用于哪个元素。这是一个闭值转换器,因为对于给定的输入值,它总是产生相同的转换结果。你可以在图 8-4 中看到这样的效果——我使用转换器的两个地方都显示了<No Word>,我没有办法修改结果来更好地匹配目标元素。我可以获得不同转换值的唯一方法是创建多个转换器,这并不理想,因为大多数转换器代码都非常相似,会导致不必要的重复。

一个更高级的选择是一个开放转换器,它可以看到绑定请求的全部细节。这种转换器更难创建,但这意味着您可以根据所请求的数据值或转换后的值将应用到的元素来定制转换器产生的结果。在清单 8-24 中,我已经用一个开放的转换器替换了前一个例子中的viewmodel.js文件中的转换器,该转换器根据被转换的数据属性调整显示在标记中的值。

小心这是一项先进的技术,在大多数情况下,封闭式捆绑已经足够了。只有在您需要在多个绑定中复制大量代码以生成略微不同的结果的情况下,才使用开放绑定。

清单 8-24 。应用开放式数据转换器

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel.State", {
        appBarElement: null,
        navBarContainerElement: null,
        navBarControlElement: null
    });

WinJS.Namespace.define("ViewModel", WinJS.Binding.as({
        UserData: {
            word: null,
            wordLength: null,
        }
    }));

ViewModel.UserData.wordList = new WinJS.Binding.List();

ViewModel.UserData.bind("word", function (newVal) {
        if (newVal) {
            ViewModel.UserData.wordLength = newVal ? newVal.length : null;
            ViewModel.UserData.wordList.push(newVal);
        }
    });

**    WinJS.Namespace.define("ViewModel.Converters", {**
**        defaultStringIfNull: function (src, srcprop, dest, destprop) {**
**            var srcObject= src;**
**            var targetObject = dest;**

**            srcprop.slice(0, srcprop.length -1).forEach(function (prop) {**
**                srcObject = srcObject[prop] != undefined ? srcObject[prop] : null;**
**            });**

**            destprop.slice(0, destprop.length -1).forEach(function (prop) {                 targetObject = targetObject[prop] == undefined ? null:**
**                    targetObject[prop];**
**            });**

**            srcObject.bind(srcprop[srcprop.length - 1], function (val) {**
**                var value = val != null ? val 😗*
**                    srcprop[srcprop.length - 1] == "wordLength" ? "-" : "";**
**                targetObject[destprop[destprop.length - 1]] = value;**
**            });**
**        }**
**    });**

**    ViewModel.Converters.defaultStringIfNull.supportedForProcessing = true;**

})();`

您不需要调用任何WinJS.Binding方法来创建一个开放的转换器。相反,您只需创建一个带有四个参数的函数。您的函数将被调用来转换数据值,并且您可以使用参数来确定需要什么以及它将被应用到哪里。为了解释这些论点,我将基于示例应用中的以下声明性绑定进行描述:

... <span id="lengthspan" data-win-bind="innerText: UserData.wordLength       ViewModel.Converters.defaultStringIfNull">????</span> ...

第一个参数是传递给WinJS.Binding.processAll方法的数据对象——例如,这将是ViewModel对象。第二个参数是被请求的数据值,表示为名称数组。在示例绑定中,我想要将在第二个参数中显示为["UserData", "wordLength"]UserData.wordLength属性。

第三个参数是绑定的目标。对于我的例子,这将是跨度元素,它的idlengthspan。最后一个参数是转换后的值将应用到的属性——这是另一个名称数组,因此我的函数将接收数组["innerText"]作为示例绑定。

这个转换器中的大部分代码处理名称数组,这样我就可以获得数据值并将它们应用到目标对象。

你不只是从一个开放的转换器返回一个值。相反,您必须设置一个编程绑定,以便随着数据值的变化,目标保持最新。正如我前面所描述的,我使用了bind方法。在该示例中,根据所请求的属性,我从转换中返回不同的值,如下所示:

... srcObject.bind(srcprop[srcprop.length - 1], function (val) {     var value = val != null ? val : srcprop[srcprop.length - 1] == **        "wordLength" ? "N/A" : "<No Word>";**     targetObject[destprop[destprop.length - 1]] = value; }); ...

这种技术是用于支持声明性绑定的编程绑定的奇怪组合,它比任何一种技术本身都要多做很多工作。但是灵活性有时是值得努力的。

images 提示请注意,我没有根据所使用的目标元素做出任何决定——这样做是实现紧密耦合的途径,因为标记结构的知识将嵌入到转换器中。如果您必须根据应用的位置来定制转换后的结果,那么就限制自己根据元素类型或它所属的类来做出决定。

默认情况下,除非将supportedForProcessing属性显式设置为true,否则不能从标记中调用函数,如下所示:

... ViewModel.Converters.defaultStringIfNull.supportedForProcessing = true; ...

当您创建封闭转换器时,这个属性是自动为您设置的,但是由于开放转换器需要更多的手动操作,您需要自己负责设置它。如果您忘记了,应用将在执行转换器功能时终止。

总结

在本章中,我向您展示了如何将视图模型引入到您的 Windows 应用中,如何使其可观察,以及如何使用编程和声明性数据绑定来响应更新的数据值。我还向您展示了如何创建可观察数组,以及如何用它们来驱动简单的模板。

通过应用视图模型,您可以将数据从标记中分离出来,确保您的数据在整个应用中都可用,并使长期开发和维护变得更加简单和容易。通过将数据绑定应用到视图模型,您可以创建一个能够流畅地响应数据变化的应用。在下一章,我将深入研究WinJS.Promise对象的细节,并向您展示如何使用它来充分利用 WinJS 和 Windows APIs。

九、利用承诺

Windows 应用中异步编程的基本前提很简单。您调用一个方法,它执行的工作被安排在以后执行。在未来的某个时刻,工作被执行,并通过回调函数通知您结果。

异步编程在 JavaScript 中已经很成熟了。当您在 web 应用中发出 Ajax 请求时,您可能已经遇到过异步编程。你想从服务器加载一些内容,但不想阻止用户与应用进行交互。因此,您使用XMLHttpRequest对象(或 jQuery 之类的包装器库,使XMLHttpRequest对象更容易使用)进行了一次方法调用,并提供了一个在服务器内容到达时执行的函数。如果您没有使用 Ajax,web 应用在数据从服务器返回之前不会对用户交互做出响应,从而造成应用停滞不前的现象。

Windows 应用中的异步编程以相同的方式工作,并用于相同的目的-允许用户在执行其他操作时与应用进行交互。Windows 应用更广泛地使用异步编程,而不仅仅是 Ajax 请求,这就是为什么有一个通用对象来表示异步操作:WinJS.Promise对象。

术语 promise 表示在未来某个时间执行任务并返回结果的承诺。当这种情况发生时,这个承诺就被说成是兑现了。表 9-1 对本章进行了总结。

images 提示WinJS.Promise对象是CommonJS Promises/A规范的一个实现,你可以在[commonjs.org](http://commonjs.org)读到。这正在成为 JavaScript 异步编程的标准,并在 jQuery 库采用它作为其延迟对象特性的基础时得到了极大的普及。

images

images

创建示例项目

开始异步编程的最佳方式是直接投入进去。为了构建一些熟悉的东西,我将使用用WinJS.Promise包装XMLHttpRequest对象的WinJS.xhr函数。为了演示这个特性,我使用 Visual Studio Blank App模板创建了一个名为Promises的新项目。清单 9-1 显示了default.html文件的内容。

清单 9-1 。default.html 文件的内容

`

<html> <head>     <meta charset="utf-8" />     <title>Promises</title>


    
    
    
    
    
    

    
        
            
                                              
            
                                              
            
                                              
            
        
        
Content will go here
             


        

    


        
        
        
        
        
        
        
        
        
        
    

`

这个应用执行邮政编码的网络搜索。布局分成三个面板,你可以在图 9-1 中看到。最左边的面板包含一对input元素,允许你输入邮政编码,旁边是GoCancel按钮。还有一个区域,我将在其中显示关于我发出的 Ajax 请求的消息。

images

图一。诺言 app 的初步布局

中间和右侧面板是显示搜索结果的地方。正如您在清单中看到的,我已经定义了一个显示搜索结果的模板,使用了我在第八章中描述的技术。

你可以在清单 9-2 中看到我用来创建这个布局的 CSS,它显示了css/default.css

清单 9-2 。default.css 的内容

`body {
    display: -ms-flexbox;
    -ms-flex-direction: column;
    -ms-flex-align: center; -ms-flex-pack: center;
    background-color: #5A8463; color: white;      
}

div.container {
    display: -ms-flexbox; -ms-flex-direction: row;
    -ms-flex-align: center; -ms-flex-pack: center;
}

div.panel {
    width: 25%; border: thick solid white;
    margin: 10px; padding: 10px; font-size: 14pt;    
    height: 500px; width: 350px;
    display: -ms-flexbox; -ms-flex-direction: column;
    -ms-flex-align: center; -ms-flex-pack: center;
}

left > div

left input

div.panel label {display: inline-block; width: 100px; text-align: right;}
div.panel span {display: inline-block; width: 200px;} #messages {width: 80%; height: 250px; padding: 10px; border: thin solid white;}

middle, #right

middle label, #right label {color: darkgray; margin-left: 10px;}`

如您所料,我已经为这个应用定义了一个简单的视图模型。清单 9-3 显示了视图模型的内容,我在一个名为js/viewmodel.js的文件中创建了这个视图模型。

清单 9-3 。承诺应用的视图模型

`(function () {
    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({
        State: {
            zip1: "10036", zip2: "20500",
        }
    }));

ViewModel.State.messages = new WinJS.Binding.List();
})();`

最后,清单 9-4 显示了js/default.js文件的内容。这个文件包含为布局中的buttoninput元素定位和设置事件处理函数的代码,但是它不包含实际向 web 服务发出请求的代码——我将在本章后面添加这个代码。

清单 9-4 。default.js 文件

`(function () {
    "use strict";

var app = WinJS.Application;
    var $ = WinJS.Utilities.query;

**    function requestData(zip, targetElem) {**
**        ViewModel.State.messages.push("Started for " + zip);**
**        // ...code will go here...**
**    }**

app.onactivated = function (args) {

$('input').listen("change", function (e) {
            ViewModel.State[this.id] = this.value;
        });

$('button').listen("click", function (e) {
            if (this.id == "go") {
                var p1 = requestData(ViewModel.State.zip1, middle);
                var p2 = requestData(ViewModel.State.zip2, right);
            };
        });

ViewModel.State.messages.addEventListener("iteminserted", function (e) {
                messageTemplate.winControl.render({ message: e.detail.value }, messages);
            });         WinJS.UI.processAll().then(function () {
            return WinJS.Binding.processAll(document.body, ViewModel);
        });
    };

app.start();
})();`

至此,应用的基本结构已经完成。你可以在输入元素中输入邮政编码,你可以点击按钮——我所缺少的是做实际工作的代码,这也是我在本章剩余部分要关注的。

这个示例应用通过网络连接向远程服务器请求数据。这需要在应用清单中启用一个功能,让 Windows 和用户知道你的应用能够发出这样的请求。这允许 Windows 实施安全策略(不具备该功能的应用将不被允许发起请求),并且允许用户在考虑从 Windows 应用商店购买应用时对您的应用存在的风险进行评估(尽管很明显用户实际上并不太关注此类信息)。

当您创建新的应用开发项目时,Visual Studio 会自动为您启用这一特定功能。要查看功能,双击Solution Explorer中的package.appxmanifest文件并导航到Capabilities选项卡。你会看到Internet (Client)被选中,如图图 9-2 所示,告诉 Windows 你的 app 能够发起出站网络连接。

images

图 9-2。启用呼出网络连接的能力

处理基本异步编程流程

您可以判断何时处理异步操作,因为您调用的方法将返回一个WinJS.Promise对象。返回一个Promise的方法将把它们的工作安排在未来的某个时间发生,工作的结果将通过Promise发出信号。

Scheduled 在这个上下文中不是最有用的词,因为它暗示了未来某个固定的时间。事实上,你所知道的是工作将会完成——你对何时完成没有任何影响,也不知道离任务开始还有多长时间。延迟可能是一个更好的词,也是 jQuery 团队采用的词,但是调度是使用最广泛的术语。

使用异步方法是一种权衡。好处是你的应用可以执行后台任务,同时保持对用户的响应。缺点是你失去了对任务执行的直接控制,你不知道你的任务什么时候会被执行。

然而,在很大程度上,你没有选择。Windows 在整个 WinJS 和 Windows API 中使用异步编程,如果不采用Promise对象和它所代表的编程方法,你就无法创建一流的应用。

使用异步回调

Promise对象使用回调函数为您提供关于异步任务结果的信息。您使用then方法注册这些函数。then方法最多接受三个回调函数作为参数。如果任务成功完成,则调用第一个函数(成功回调),如果任务遇到错误,则调用第二个函数(错误回调),并且在任务执行期间调用第三个函数来通知进度信息(进度回调)。

为了演示Promise对象,我使用了WinJS.xhr方法,它是标准XMLHttpRequest对象的包装器,您可以在 web 应用中使用它来发出 Ajax 请求。WinJS.xhr方法接受一个对象,该对象包含与XMLHttpRequest定义的属性相对应的属性,包括urltypeuserpassworddata,所有这些属性都不加修改地传递给XMLHttpRequest对象。

images 提示您可能使用了 jQuery 等库中的便利包装器来管理您的请求,而没有直接使用XMLHttpRequest对象。你不需要理解XMLHttpRequest的工作原理来理解本章,但是如果你想要更多的信息,那么 W3C 规范是一个很好的起点:[www.w3.org/TR/XMLHttpRequest](http://www.w3.org/TR/XMLHttpRequest)

在清单 9-5 的中,你可以看到我是如何在它返回的Promise上使用WinJS.xhr方法和then函数的,这展示了我是如何在default.js文件中实现requestData函数的。如清单所示,我使用 success 回调函数显示来自服务器的数据,使用 error 回调函数显示请求中任何问题的细节。

清单 9-5 。使用 WinJS。约定方法

`...
function requestData(zip, targetElem) {

ViewModel.State.messages.push("Started for " + zip);

var promise = WinJS.xhr({
        url: "http://gomashup.com/json.php?fds=geo/usa/zipcode/" + zip
    }).then(function (xhr) {
        ViewModel.State.messages.push(zip + " Complete");
        var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;
        WinJS.Utilities.empty(targetElem);
        zipTemplate.winControl.render(dataObject[0], targetElem);
    }, function (xhr) {         WinJS.Utilities.empty(targetElem);
        targetElem.innerText = "Error: " + xhr.statusText;
    });
    return promise;
}
...`

我传递给WinJS.xhr方法的对象有一个url属性,它指定了我想要请求的 URL。WinJS.xhr方法返回一个Promise对象,我使用then方法为成功和错误回调注册函数。

images 提示你没有要用then的方法。如果您不关心异步任务的结果,只需丢弃或忽略该方法返回的Promise来创建一个“一劳永逸”的任务。

WinJS.xhr方法立即返回,Ajax 请求将在未来某个未指定的时间执行。我无法控制请求何时开始,只有当我的一个回调函数被执行时,我才知道请求何时结束。

如果我的成功回调函数被执行,我知道我有一个来自服务器的响应,我处理并显示在布局中。如果我的错误回调函数被执行,那么我就知道出错了,并显示错误的详细信息。你可以在图 9-3 中看到结果,该图展示了点击Go按钮并完成创建的Promise对象后应用的布局。

images

图 9-3。使用回调处理程序响应已履行的承诺

WinJS 和 Windows APIs 中的大多数异步方法往往比WinJS.xhr更细粒度,将某种结果对象传递给成功回调函数,将描述性字符串消息传递给错误函数(进度回调函数并不经常使用,尽管在本章后面我向您展示如何创建自己的Promise时,您可以看到演示)。

使用 GOMASHUP 邮政编码服务

我在这个例子中使用的 web 服务来自GoMashup.com,他提供了许多有用的数据服务。我选择 GoMashup 是因为他们的服务快速、可靠,并且不需要在请求中包含任何开发者密钥,这使得他们非常适合用于演示。例如,如果我想要关于邮政编码10036的信息,我使用以下 URL 进行查询:

[gomashup.com/json.php?fds=geo/usa/zipcode/10036](http://gomashup.com/json.php?fds=geo/usa/zipcode/10036)

我得到这样一个字符串:

({"result":[{     "Longitude" : "-073.989847",     "Zipcode" : "10036",     "ZipClass" : "STANDARD",     "County" : "NEW YORK",     "City" : "NEW YORK",     "State" : "NY",     "Latitude" : "+40.759530" }]})

GoMashup 服务旨在与 JSONP 一起使用,其中调用一个函数将数据插入到应用中。这意味着我需要去掉字符串的第一个和最后一个字符来获得一个有效的 JSON 字符串,我可以解析这个字符串来创建一个 JavaScript 对象。

创建链

then方法的一个有趣的方面是它返回一个Promise,当回调函数被执行时,这个 ?? 被实现。

这意味着当 Ajax 请求完成时,我从requestData函数返回的Promise并没有完成。相反,它是一个Promise,当 Ajax 请求已经完成并且成功或错误回调也已经执行时,它就完成了。

使用then方法创建动作序列被称为链接,它允许你控制任务执行的顺序。作为一个例子,我可以改变requestData函数的结构,使它更有用。目前,如果我的请求成功,我只向ViewModel.State.messages对象添加一条消息,但是使用then方法,我可以区分 Ajax 请求的实现和初始回调集的实现,如清单 9-6 所示。

清单 9-6 。使用 then 方法将动作链接在一起

`...
function requestData(zip, targetElem) {

ViewModel.State.messages.push("Started for " + zip); var promise = WinJS.xhr({
        url: "http://gomashup.com/json.php?fds=geo/usa/zipcode/" + zip
    }).then(function (xhr) {
**        ViewModel.State.messages.push(zip + " Successful");**
        var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;
        WinJS.Utilities.empty(targetElem);
        zipTemplate.winControl.render(dataObject[0], targetElem);
    }, function (xhr) {
**        ViewModel.State.messages.push(zip + " Failed");**
        WinJS.Utilities.empty(targetElem);
        targetElem.innerText = "Error: " + xhr.statusText;
    });

**    return promise.then(function () {**
**        ViewModel.State.messages.push(zip + " Complete");**
**    });**

}
...`

你可以在图 9-4 中看到这些信息的顺序,图中显示了点击Go按钮的结果。

images

图 9-4。使用 then 方法控制异步任务的顺序

Promise对象和then方法的一个缺点是,你最终会得到难以阅读的代码。更具体地说,很难确定任务链的执行顺序。为了帮助在清单 9-6 中弄清楚这一点,将Promise赋给一个名为promise的变量,然后分别使用then方法创建一个链。然而,通常情况下,then方法会更直接地应用,如清单 9-7 所示。

清单 9-7 。直接在承诺上使用 then 方法创建链

`function requestData(zip, targetElem) {

ViewModel.State.messages.push("Started for " + zip);     var promise = WinJS.xhr({
        url: "http://gomashup.com/json.php?fds=geo/usa/zipcode/" + zip
    }).then(function (xhr) {
        ViewModel.State.messages.push(zip + " Successful");
        var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;
        WinJS.Utilities.empty(targetElem);
        zipTemplate.winControl.render(dataObject[0], targetElem);
    }, function (xhr) {
        ViewModel.State.messages.push(zip + " Failed");
        WinJS.Utilities.empty(targetElem);
        targetElem.innerText = "Error: " + xhr.statusText;
**    }).then(function () {**
**        ViewModel.State.messages.push(zip + " Complete");**
**    });**

return promise;
}`

因为then方法返回一个Promise对象,所以来自requestData方法的结果是一个Promise,当 Ajax 请求已经完成并且一个回调函数已经执行并且Complete消息的函数已经执行时,这个结果被实现。链是组合Promise的一种简单方式,但是它们可能很难阅读。

images 提示Promise.done方法是对then方法的补充。done方法必须作为一个链中的最后一个方法,因为它不返回结果。链中任何未处理的异常在到达done方法时被抛出(然而它们只是通过then方法反映在Promise的状态中)。像这样抛出一个异常并不是特别有用,因为在调用done方法时,应用代码的执行已经开始了。更好的方法是确保在链中使用错误回调函数来处理任何异常。

连锁承诺

注意,图 9-4 中的所示的信息是混合在一起的。这是因为 IE10 能够同时执行多个请求,并且每个请求独立地通过其生命周期。请求之间没有协调,这就是为什么消息是交错的,也是为什么您在运行示例时可能会看到不同的结果。当您看到我是如何在Go按钮的事件处理程序中调用requestData函数时,这是有意义的,如下所示:

... var p1 = **requestData**(ViewModel.State.zip1, middle); var p2 = **requestData**(ViewModel.State.zip2, right); ...

我接收requestData函数返回的Promise对象,但是我不对它们做任何事情,所以请求是独立调度的。这对于我的示例应用来说很好,因为每个请求更新布局的不同部分,我不需要协调结果。

但是,在许多情况下,您会希望将一项任务推迟到另一项任务完成之后。你可以使用then方法创建一个链来完成这个任务,但是你必须注意从第二个请求中返回Promise对象作为你的回调函数的结果,如清单 9-8 所示,它展示了我对default.js文件所做的更改,以确保请求按顺序执行。

清单 9-8。连锁承诺

... $('button').listen("click", function (e) {     if (this.id == "go") { **        requestData(ViewModel.State.zip1, middle).then(function () {** **            return requestData(ViewModel.State.zip2, right);** **        });**     }; }); ...

这真的很重要。如果从回调函数中返回Promise对象,那么链中的任何后续动作都不会被调度,直到Promise被完成。这意味着清单 8 中的代码会产生以下效果:

  1. Schedule the first request
  2. Wait until Promise from the first request
  3. Schedule the second request
  4. Wait until Promise from the second request
  5. Add a message to the ViewModel.State.messages object

这通常是所需要的效果——在前一个活动完成之前,不要安排下一个活动。然而,如果您省略了return关键字,您会得到一个非常不同的效果:

  1. Schedule the first request
  2. Wait until the first request Promise is satisfied
  3. Schedule the second request
  4. To the ViewModel.State.messages object

添加消息

如果你没有从回调函数中返回一个Promise,那么后续的活动将会在回调函数执行完成后立即被调度——当你调用一个异步方法时,是在任务被调度后,而不是在任务完成后。

在我的示例应用中,这种差异很容易发现,因为我在请求的整个生命周期中都在编写消息。图 9-5 显示了两种情况下的消息顺序——左图显示了使用 return 关键字的效果,右图显示了没有 return 关键字的效果。指示器是All Requests Complete消息在事件序列中出现的地方。

省略关键字return并不总是错误的。如果您想将某个任务推迟到其前任完成之后,但不关心该任务的结果,那么省略return关键字是完全合适的。只要确保你知道你的目标是什么效果,并根据需要包括或省略return

images

图 9-5。在回调函数中省略 return 关键字的影响

取消承诺

您可以通过调用Cancel方法来请求取消Promise。这并不像听起来那么有用,因为不要求Promise支持取消,如果支持,取消是一个请求,并且Promise几乎肯定会在检查取消之前完成当前的工作(当我在本章后面向您展示如何创建自己的Promise时,您可以看到这是如何工作的)。

WinJS.Xhr函数返回的Promise确实支持取消,这也是我在本章一直使用它的原因之一。没有办法在你的应用中发现未实现的Promise对象,所以你需要保存对Promise的引用,就像你想要再次引用任何变量一样。你可以看到我是如何连接Cancel按钮并保存对我在清单 9-9 中创建的Promise对象的引用的。

清单 9-9 。取消承诺

`...
var p1;
var p2;

$('input').listen("change", function (e) {
    ViewModel.State[this.id] = this.value;
});

$('button').listen("click", function (e) {
    if (this.id == "go") {
**        p1 = requestData(ViewModel.State.zip1, middle);**
**        p1.then(function () {             p2 = requestData(ViewModel.State.zip2, right);**
**            return p2;**
**        }).then(function () {**
**            ViewModel.State.messages.push("All Requests Complete");**
**        });**
    } else {
**        p1.cancel();**
**        p2.cancel();**
**        ViewModel.State.messages.push("All Requests Canceled");**
    }
});
...`

当按下Cancel按钮时,我在每个我在按下Go按钮时创建的Promise对象上调用cancel方法。这向Promise对象发出信号,我想终止对服务器的请求。

当您取消Promise时,会调用错误回调。传递给函数的对象有三个属性(namemessagedescription),它们都被设置为字符串Canceled。你可以在清单 9-10 中的回调函数中看到我是如何处理这种情况的。如果有值的话,我显示statusText的值,否则显示message属性的值。

清单 9-10 。在错误回调中处理取消

`...
function requestData(zip, targetElem) {

ViewModel.State.messages.push("Started for " + zip);

var promise = WinJS.xhr({
        url: "http://gomashup.com/json.php?fds=geo/usa/zipcode/" + zip
    }).then(function (xhr) {
        ViewModel.State.messages.push(zip + " Successful");
        var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;
        WinJS.Utilities.empty(targetElem);
        zipTemplate.winControl.render(dataObject[0], targetElem);
    }, function (xhr) {
        ViewModel.State.messages.push(zip + " Failed");
        WinJS.Utilities.empty(targetElem);
        targetElem.innerText = "Error: "
            + (xhr.statusText != null ? xhr.statusText : xhr.message);
    }).then(function () {
        ViewModel.State.messages.push(zip + " Complete");
    });

return promise;
}
...`

测试该功能最简单的方法是重启(而不是刷新应用),点击Go按钮,然后立即点击Cancel按钮。重新启动很重要,因为这意味着请求的任何方面都不会被缓存,只给你足够的时间来执行取消。你可以在图 9-6 中看到效果。

images

图 9-6。取消请求的影响

从承诺中传递结果

你可以在图 9-6 的中看到,当发出取消请求的Promise任务被取消时,为每个请求写Complete消息和整个All Requests Complete消息的链式任务仍然被执行。

有时候,这正是你想要的:不管前面的Promise中发生了什么都要执行的任务,但是你经常会想要在面对错误时有选择地进行。在清单 9-11 的中,您可以看到我对default.js文件中的requestData函数所做的修改,以便在请求被取消时不显示Complete消息,这是通过从我的Promise函数返回结果来实现的。

清单 9-11 。从承诺传递结果

`...
function requestData(zip, targetElem) {

ViewModel.State.messages.push("Started for " + zip);

var promise = WinJS.xhr({
        url: "http://gomashup.com/json.php?fds=geo/usa/zipcode/" + zip
    }).then(function (xhr) {
        ViewModel.State.messages.push(zip + " Successful");
        var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;
        WinJS.Utilities.empty(targetElem);
        zipTemplate.winControl.render(dataObject[0], targetElem);
**        return true;**
    }, function (xhr) {
        ViewModel.State.messages.push(zip + " Failed");
        WinJS.Utilities.empty(targetElem);
        targetElem.innerText = "Error: "
            + (xhr.statusText != null ? xhr.statusText : xhr.message);
**        return false;     }).then(function (allok) {
**        if (allok) {

**            ViewModel.State.messages.push(zip + " Complete");**
**        }**
        return allok;
    });

return promise;
}
...`

当由WinJS.xhr函数返回的Promise被满足时,我的成功或错误处理函数将被执行。我已经修改了成功处理程序,使它返回true,表明请求已经完成。我将错误处理程序改为返回 false,表示出现了错误或请求被取消。

来自被执行的处理函数的truefalse值被作为参数传递给链中的下一个then函数。在这个例子中,我使用这个值来判断是否应该显示请求的Complete消息。

您可以通过这种方式沿着Promise对象链传递任何对象,每个then函数可以返回不同的结果,甚至是不同种类的结果。在清单中,当我在default.js文件的其他地方使用时,我从作为参数接收的then函数返回相同的值,如清单 9-12 所示。

清单 9-12 。将结果沿着链传递得更远

... $('button').listen("click", function (e) {     if (this.id == "go") {         p1 = requestData(ViewModel.State.zip1, middle);         p1.then(function () {             p2 = requestData(ViewModel.State.zip2, right);             return p2;         }).then(function (**allok**) { **            if (allok) {**                 ViewModel.State.messages.push("All Requests Complete"); **            }**         });     } else {         p1.cancel();         p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");     } }); ...

记住,requestData函数返回的Promise对象是最后一个then函数返回的对象,所以我从那个函数返回的结果将作为参数传递给链中的下一个函数——如清单所示。我使用true / false值来决定是否应该向用户显示All Requests Complete消息。这是一个简单的数据流经一系列Promise的例子,但是它清楚地展示了这种技术的灵活性。当点击图 9-7 中的Cancel按钮时,您可以看到向用户显示的一组精简的消息。

images

图 9-7。通过一个链传递来自承诺对象的结果的效果

协调承诺

then函数并不是协调异步任务的唯一方法。Promise对象支持其他几种方法,这些方法可以用来创建特定的效果,或者使处理多个Promise对象变得更容易。我在表 9-2 中总结了这些方法,并在下面演示了它们的用法。

images

使用任意方法

any方法接受一组Promise并返回一个Promise作为结果。当自变量数组中的Promise对象中的任意一个被满足时,由any方法返回的Promise被满足。你可以在清单 9-13 中看到正在使用的any方法。

清单 9-13 。使用任意方法

... var p1, p2; `$('button').listen("click", function (e) {
    if (this.id == "go") {
        p1 = requestData(ViewModel.State.zip1, middle);
        p2 = requestData(ViewModel.State.zip2, right);

**        WinJS.Promise.any([p1, p2]).then(function (complete) {**
**            complete.value.then(function (result) {**
**                if (result) {**
**                    ViewModel.State.messages.push("Request " + complete.key**
**                        + " Completed First");**
**                } else {**
**                    ViewModel.State.messages.push("Request " + complete.key**
**                        + " Canceled or Error");**
**                }**
**            });**
**        });**

} else {
        p1.cancel();
        p2.cancel();
        ViewModel.State.messages.push("All Requests Canceled");
    }
});
...`

如果您使用then方法在由any方法返回的Promise上设置一个回调,您的函数将被传递一个具有两个属性的对象。key属性返回参数数组中被满足的Promise的索引(结果导致 any Promise被满足)。value属性返回一个Promise,当它被满足时,传递来自任务链的结果。您可以看到我是如何使用这两个值向布局中写入消息的,该消息报告了哪个请求首先完成及其结果。您可以在图 9-8 中看到该示例生成的输出。

images

图 9-8。用任意方式报告哪个承诺先兑现

images 提示any方法返回的Promise在底层的Promise之一满足后立即满足。any Promise不会等到所有的Promise都实现了才告诉你哪一个是第一个。当任何一个Promise完成时,其他Promise对象可能仍未完成。

使用 join 方法

join方法类似于any方法,但是它返回的Promise直到参数数组中所有Promise对象的都被实现后才被实现。你可以在清单 9-14 中看到正在使用的join方法。传递给then回调函数的参数是一个数组,包含所有原始Promise对象的结果,按照原始数组的顺序排列。

清单 9-14 。使用连接方法

`...
var p1, p2;

$('button').listen("click", function (e) {
    if (this.id == "go") {
        p1 = requestData(ViewModel.State.zip1, middle);
        p2 = requestData(ViewModel.State.zip2, right);

WinJS.Promise.any([p1, p2]).then(function (complete) {
            complete.value.then(function (result) {
                if (result) {
                    ViewModel.State.messages.push("Request " + complete.key
                        + " Completed First");
                } else {
                    ViewModel.State.messages.push("Request " + complete.key
                        + " Canceled or Error");
                }
            });
        });

**        WinJS.Promise.join([p1, p2]).then(function (results) {**
**            ViewModel.State.messages.push(results.length + " Requests Complete");**
**            results.forEach(function (result, index) {**
**                ViewModel.State.messages.push("Request: " + index + ": " + result);**
**            });**
**        });**

} else {
        p1.cancel();
        p2.cancel();
        ViewModel.State.messages.push("All Requests Canceled");
    }
});
...`

注意,我可以在同一套Promise对象上使用anyjoin方法。一个Promise能够支持对then方法的多次调用,并将正确执行多组回调。你可以在图 9-9 中看到使用anythen方法的效果。

images

图 9-9。any 和 join 方法显示的消息

使用超时

方法有两种用途,做完全不同的事情。最简单形式的timeout方法采用一个数字参数,并返回一个在指定时间段后实现的Promise。这可能看起来有点奇怪,但是当你想推迟一系列Promise的调度时,它会很有用。你可以在清单 9-15 中看到它是如何工作的。

清单 9-15 。使用超时方法推迟承诺

`...
var p1, p2;

$('button').listen("click", function (e) {
    if (this.id == "go") {

**        WinJS.Promise.timeout(3000).then(function () {**
            p1 = requestData(ViewModel.State.zip1, middle);
            p2 = requestData(ViewModel.State.zip2, right);

WinJS.Promise.any([p1, p2]).then(function (complete) {
                complete.value.then(function (result) {
                    if (result) {
                        ViewModel.State.messages.push("Request "
                            + complete.key + " Completed First");
                    } else {
                        ViewModel.State.messages.push("Request "
                            + complete.key + " Canceled or Error");
                    }
                });
            });             WinJS.Promise.join([p1, p2]).then(function (results) {
                ViewModel.State.messages.push(results.length + " Requests Complete");
                results.forEach(function (result, index) {
                    ViewModel.State.messages.push("Request: " + index + ": " + result);
                });
            });
**        });**
    } else {
        p1.cancel();
        p2.cancel();
        ViewModel.State.messages.push("All Requests Canceled");
    }
});
...`

在这个清单中,我创建了三秒钟的延迟。一旦这段时间过去,由timeout方法返回的Promise将自动完成,我用then方法设置的回调函数被调用——在这种情况下,我对服务器的邮政编码数据的请求直到单击Go按钮三秒后才开始。

为承诺设置超时

timeout方法的另一个用途是设置Promise的到期时间。要使用这个版本的方法,您需要传入一个超时值和您想要应用它的Promise。你可以看到这种形式的timeout方法是如何用在清单 9-16 中的。

清单 9-16 。使用超时方法自动取消承诺

`...
var p1, p2;

$('button').listen("click", function (e) {
    if (this.id == "go") {

WinJS.Promise.timeout(250, p1 = requestData(ViewModel.State.zip1, middle));
        WinJS.Promise.timeout(2000, p2 = requestData(ViewModel.State.zip2, right));

WinJS.Promise.any([p1, p2]).then(function (complete) {
            complete.value.then(function (result) {
                if (result) {
                    ViewModel.State.messages.push("Request "
                        + complete.key + " Completed First");
                } else {
                    ViewModel.State.messages.push("Request "
                        + complete.key + " Canceled or Error");
                }
            });
        });        
    } else {
        p1.cancel();
        p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");
    }
});
...`

在这个清单中,我使用timeout方法为一个请求设置 250 毫秒的最大持续时间,为另一个请求设置 2 秒。如果请求在这些时间内完成,那么没有什么特别的事情发生。然而,如果在周期结束时没有实现Promise对象,它们将被自动cancelled(这是通过调用我在本章前面演示的cancel方法来执行的)。为了让这个有用,你需要确保你正在使用的Promise对象支持取消。

对多个承诺应用相同的回调函数

theneach方法是将同一组回调函数应用于一组Promise对象的一种便捷方式。这个方法不会改变Promise的调度顺序,但是它会返回一个Promise,这相当于为回调函数返回的所有Promise调用join方法。清单 9-17 显示了正在使用的theneach方法。

清单 9-17 。使用新方法

`...
var p1, p2;

$('button').listen("click", function (e) {
    if (this.id == "go") {

p1 = requestData(ViewModel.State.zip1, middle);
        p2 = requestData(ViewModel.State.zip2, right);

**        WinJS.Promise.thenEach([p1, p2], function (data) {**
**            ViewModel.State.messages.push("A Request is Complete");**
**        }).then(function (results) {**
**            ViewModel.State.messages.push(results.length + " Requests Complete");**
        });

} else {
        p1.cancel();
        p2.cancel();
        ViewModel.State.messages.push("All Requests Canceled");
    }
});
...`

我只指定了一个成功函数,但是您也可以指定错误和进度回调。没有传递给回调的上下文信息来指示哪个Promise正在被处理,这使得theneach方法没有它本来应该有的用处。

创建自定义承诺

创建异步方法有两种方式。第一种,您已经看到了,是建立在现有的异步方法上,操作它们返回的Promise对象并返回结果。本章第一个示例应用中的requestData函数就是这样创建的异步方法的一个很好的例子。

另一种方法是实现您自己的Promise,并创建一个在未来某个时间执行的定制任务。当您想从头开始创建异步方法时,可以采用这种方法。在这一节中,我将向您展示如何创建您自己的Promise对象。我创建了一个名为CustomPromise的 Visual Studio 项目,其中所有的标记、代码和 CSS 都包含在一个文件中。您可以在清单 9-18 中看到这个文件default.html的内容。

images 注意这是一个高级主题,大多数应用都不需要。也就是说,即使您不需要立即使用这种技术,快速浏览这一部分也会帮助您理解由 WinJS 和 Windows 名称空间中的方法返回的Promise对象是如何工作的。

清单 9-18 。default.html 档案

`

                                  

         
        Output will appear here     
`

这个简单的应用非常适合演示如何创建定制的Promise。你可以在图 9-10 中看到布局。

images

图 9-10。custom promise app 的布局

当点击Go按钮时,我调用calculateSum函数,该函数生成前 10,000,000 个整数的和。这个任务需要几秒钟才能完成,在此期间,应用没有响应。当应用没有响应时,用户界面不会响应用户交互。对于这个简单的例子,您可以看出有问题,因为在点击了button元素之后,直到求和计算完成,它才返回到未按下的状态。这是因为click事件是在 CSS 更改被应用之前被触发和处理的,这意味着计算会阻止任何 UI 更新,直到它完成。这就是异步方法要解决的问题。

images 提示10,000,000 这个值对我的电脑来说很好,但是如果你有一个更快的系统,你可能需要增加它,或者为一个更慢的系统减少它。为了获得问题的本质(和解决方案),您希望任务花费大约 5-10 秒。

实现承诺

第一步是创建Promise对象,通过向Promise对象构造函数传递一个函数来完成。你可以在清单 9-19 中看到我是如何做到的。

清单 9-19 。创建承诺对象

`...

...`

传递给Promise构造函数的函数有三个参数,每个参数都是一个函数。第一个参数是您在完成任务并希望返回结果时调用的参数。如果要报告错误,可以调用第二个参数。当您想要制作进度报告时,会调用最后一个参数。

您可以看到,我在这个清单中添加了对报告错误的支持。如果calculateSum函数的 count 参数小于 5000,我调用fError函数来指出问题。对于其他值,我计算总和并通过fDone函数返回结果。(如果您愿意,可以忽略目标是固定的这一事实——calculateSum函数不知道这一点)。

当您创建一个异步方法时,您返回Promise对象,以便调用者可以使用then方法来接收任务的结果或创建一个链。您可以在示例中看到我是如何这样做的,以便从由calculateSum方法返回的承诺中获得结果。

延期执行

我已经实现了一个Promise,但是我仍然有一个问题:当我点击Go按钮时,应用仍然没有响应。创建一个Promise不会自动推迟任务的执行,这是一个常见的陷阱。要创建一个真正的异步方法,我必须采取额外的步骤,显式地调度工作。我已经用setImmediate函数完成了,如清单 9-20 所示。

清单 9-20 。推迟任务的执行

`...

...`

创建一个好的异步方法有两个基本规则。第一条规则是将任务分成小的子任务,这些子任务只需要很短的时间就能完成。第二个规则是一次只安排一个子任务。如果你偏离了其中任何一条规则,那么你最终会得到一个没有响应的应用创建和管理一个Promise所涉及的费用。

创建子任务的最佳方式会因所做工作的种类而异。对于我的例子,我只需要在较小的块中执行计算,每个块都通过调用清单中的内联calcBlock函数来处理。

我已经使用setImmediate方法安排了我的子任务,这被定义为 IE10 对 JavaScript 支持的一部分。这是一种相对较新的方法,旨在补充常用的setTimeout。当您将一个函数传递给setImmediate时,您要求它在所有未决事件和 UI 更新完成后立即执行。

您需要使用子任务的原因是,一旦 JavaScript 运行时开始执行您的函数,任何新的事件和 UI 更新都会建立起来。通过将工作分解成子任务,并仅在每个任务完成时调用setImmediate,您给了 JavaScript 运行时一个清除事件和更新积压的机会。在完成执行一个子任务和开始下一个子任务之间,运行时能够响应用户输入并保持应用响应。

因为我已经将工作分解成子任务,所以我利用这个机会在每组计算结束时调用fProgress函数来向任何感兴趣的听众报告进度。您可以看到,我在我的then调用中添加了第三个函数来接收和显示这些信息。

WINDOWS 应用中的并行处理

如果你密切关注了这一章,你会注意到我没有使用平行这个词。JavaScript 在单线程运行时中执行,这就是为什么没有关键字来确保原子更新或创建关键部分,就像在 C#和 Java 等语言中一样。当你创建一个异步方法并实现后台任务时,你并没有创建一个新线程;相反,您只是简单地推迟任务,直到主(也是唯一的)线程能够并且愿意执行它。

然而,有可能用 JavaScript 创建真正的并行应用,不同的任务由不同的线程同时执行。一种方法是构建用本机代码编写的异步功能。当我使用一个XMLHttpRequest对象发出 Ajax 请求时,您已经看到了这样一个例子。XMLHttpRequest对象是浏览器的一部分,能够创建和管理多个并发请求。这种并行性对 JavaScript 代码是隐藏的,回调通知被封送到主 JavaScript 线程进行简单处理。Windows API 也是用本机代码编写的,您的调用通常会导致多线程的创建和执行,即使作为 JavaScript 程序员,您并不知道这种复杂性。

如果你想用 JavaScript 创建一个真正的并行应用,那么你应该看看 Web Workers 规范。这是与 HTML5 相关的规范之一,它受 IE10 支持。创建和维护 Web workers 的成本相对较高,这意味着它们只适合于长期任务,应该谨慎使用。我在本书中没有深入讨论 Web Workers 规范,因为它不是一个特定于应用的特性,但是你可以在[msdn.microsoft.com/en-us/library/windows/apps/hh767434.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/hh767434.aspx)阅读更多关于 IE10 支持的内容。

实施取消

您不必在自己的定制中实现对取消的支持,但是这样做是一个好主意,尤其是对于长期任务或资源密集型任务。

这是因为调用cancel方法将触发错误回调来通知取消,即使您的Promise不支持取消。这意味着你的Promise将继续调度工作(并消耗资源),即使回调函数已经被调用,应用已经继续运行。作为最后的侮辱,你的任务结果将被悄悄地丢弃。

要实现取消,您需要向Promise构造函数传递第二个函数。如果在Promise对象上调用了cancel方法,那么你的函数将被执行。你可以看到我是如何在清单 9-21 中的例子中添加取消支持的。

清单 9-21 。在自定义承诺中增加取消支持

`

                                  

     **    **     
        Output will appear here     
`

我在 HTML 的default.html中添加了一个Cancel按钮,点击这个按钮会调用在点击Go按钮时创建的Promise的取消方法。注意,我很小心地取消了由calculateSum函数返回的Promise,而不是由then方法返回的Promise——重要的是取消正在工作的Promise,而不是随后将被启动的Promise

images 提示注意,你不必经常检查取消。我发现在安排下一个子任务之前执行检查是一种合理的方法,在响应性和复杂性之间取得了良好的平衡。

创造合成承诺

您会发现,WinJS.Promise对象在 Windows APIs 中被广泛使用,有时您需要创建一个Promise,作为您已经拥有的数据值的包装器。在这种情况下,WinJS.Promise对象定义了一些有用的方法。我已经在表 9-3 中描述了这些方法,并在下面的章节中演示了两个最有用的方法。

images

我已经更新了CustomPromise应用,这样就有一个函数接受一个Promise并使用then方法来设置将向用户显示信息的回调。你可以在清单 9-22 的中看到这个例子的script元素(HTML 和 CSS 没有改变)。

清单 9-22 。创建一个接受承诺的函数

`...

...`

在这种安排下,如果我想显示不在Promise中的数据,我会受到限制。当然,我可以重写函数使之更加灵活,但是这并不总是可能的,尤其是在使用别人的代码时。解决方案是创建一个Promise,它的唯一目的是返回一个值或错误。这样的Promise没有异步方面,这就是为什么它们被称为合成 Promise的原因

写下你自己的承诺

理解这是如何工作的最好方法是从编写你自己的合成Promise开始。对于这个例子,我想在我的代码中处理两种新的情况。如果用户在点击Go按钮之前点击Cancel按钮,我想显示一个错误,我想优化应用,这样如果我已经知道结果,我就不会执行计算(即,如果用户按顺序点击Go按钮两次)。你可以在清单 9-23 的中看到我需要添加的script元素。

清单 9-23 。创建自定义合成承诺对象

`...

...`

我在这个清单中创建的Promise只返回一个结果——没有任务,也没有对setImmediate的调用。在每种情况下,我只是调用其中一个函数来指示我的Promise完成了或者遇到了错误。

使用包装方法

我在上一节中创建的合成Promise对象的问题是,WinJS API 不知道我只是将它们用作适配器,所以我可以使用displayResults函数。当一个Promise被创建时,有许多管道要设置,我承担了设置所有东西的成本,只是为了稍后丢弃它。为了解决这个问题,Promise对象定义了wrapwrapError方法,它们创建轻量级的Promise对象。这意味着它们被明确地用作适配器,并且创建起来不那么复杂和昂贵。当您想要创建一个满足预定结果的Promise时,您可以使用wrap方法;当您想要打包一个错误消息时,您可以使用wrapError方法。清单 9-24 展示了这些方法在示例应用中的应用。

清单 9-24 。使用 wrap 和 wrapError 方法

`...

...`

你应该优先使用wrapwrapError方法来创建你自己的合成Promise物体。它们不仅更便宜,而且代码更简单,因此更容易阅读和维护。

总结

在这一章中,我已经向你展示了WinJS.Promise对象背后的细节。WinJS 和 Windows APIs 中有许多异步方法,对它们的机制有很好的理解对于编写复杂的应用是必不可少的。重要的是要记住,Windows 应用中的 JavaScript 代码是在单线程上执行的,同时还有事件处理程序和 UI 更新。当您创建自定义异步方法时,您管理的是单线程上的工作调度,而不是创建多个并行线程。如果您在应用中遇到异步操作的问题,通常是因为您将代码视为多线程。在本书的第三部分中,我向你展示了 WinJS UI 控件,你可以用它来增强你的应用,并创建一个与其他 Windows 应用一致的外观。

十、创建 UI 控件示例框架

在本书这一部分的章节中,我向您展示了 WinJS UI 控件。这些是 WinJS UI 的重要组成部分,也是我在示例应用中一直使用的标准 HTML 元素的有益补充。这些控件不仅为用户提供了更丰富的体验,还提供了 Metro 应用的部分独特外观。

有各种各样的控制,我把它们都展示给你。每个控件都有许多改变其外观和行为的配置选项,为了尽可能容易地理解这些功能,我希望能够通过一个实例向您展示它们对控件的影响,而不仅仅是描述它们。

演示每个控件所需的标记和代码量很大,单独处理每个控件将需要为每一章列出无尽的页面,这对于我来说没有吸引力,对于您来说也没有吸引力。

相反,我已经构建了一个框架,可以用来简单明了地生成每个 UI 控件所需的示例,我将在接下来的章节中使用这个框架。在这一章中,我将介绍这个框架,并向您展示它是如何运作的,这样您就会理解我提供的较小的列表意味着什么。这种方法的一个好处是,我已经使用了我在前面章节中描述的相同的 WinJS 特性和技术来创建这个示例,因此您可以看到如何将它们应用到更重要的应用中。

images 注意你不需要详细阅读本章来理解后面的章节和它们包含的 WinJS UI 控件的描述。我已经包括了这一章,所以你可以看到我是如何创建这些例子的。这一章有很多代码和标记,可能会很难,所以你可能想浏览一下这些材料,并在你构建了最初的几个 Metro 应用后返回来更深入地阅读。

了解最终应用

如果你能看到我想要的结果,这将有助于理解应用。我的目标是向您展示一个简单的初始布局,带有一个包含每个 UI 控件命令的导航栏。你可以在图 10-1 中看到这个初始布局,它显示了导航条命令。

images

图 10-1。应用和导航条的初始布局只需一个命令

每个 NavBar 命令都有一个按钮,点击它会产生一个包含两个面板的页面。左侧面板将包含我正在演示的 UI 控件。右面板将包含其他 UI 控件,您可以使用这些控件来更改左面板中控件的设置。你可以在图 10-2 的中看到一个例子,它展示了我如何演示FlipView控件(我在第十四章的中描述了它)。

images

图 10-2。flip view 控件的显示

右侧面板中的每个控件都允许您查看或更改左侧面板中控件的属性。在创建示例框架时,我的目标是能够尽可能简洁地生成这种标记和驱动它的代码。在图 10-2 中,你可以看到我需要生成的不同类型的控件:

  • The select element allows the user to choose from a fixed range of values.
  • The input element allows the user to enter unconstrained values.
  • The span element displays a read-only value to the user.
  • A set of button elements allows users to perform operations.
  • The ToggleSwitch control lets the user select the boolean value.

ToggleSwitch控件是 WinJS UI 控件之一,我在第十一章中描述了它。你可能想读完第十一章然后回到这里,但是我在这一章的重点是生成我需要的标记,我不会深入控件的细节。

images 注意虽然总体结果比冗长重复的清单更简洁,但框架本身相当复杂,这一章包含了大量代码,其中大部分与处理模板有关,我在第八章的中描述过。

创建基本项目结构

首先,我将创建应用的基本结构,以便向用户显示初始消息,并且导航条已经就位。我还将添加导航机制,并将用于视图模型的代码文件放在适当的位置,并保存我需要的每组配置控件的详细信息列表。我首先使用Blank App模板创建一个新的 Visual Studio 项目调用UIControls,并对 default.html 文件做一些基本的添加,如清单 10-1 所示。

清单 10-1。来自 UIControls 项目的初始 default.html 文件

`

<html> <head>     <meta charset="utf-8">     <title>UIControls</title>


    
    
    


    
**    **
**    **
**    **
    

<body> **    <div id="navBarCommandTemplate" data-win-control="WinJS.Binding.Template">** **        <button data-win-control="WinJS.UI.AppBarCommand"** **            data-win-options="{section:'selection'}"** **            data-win-bind="winControl.label: name; winControl.icon: icon">** **        </button>  ** **    </div>**

**    

**
**        

Select a Control from the NavBar

**
**    
**

**    <div id="navbar" data-win-control="WinJS.UI.AppBar"**
**        data-win-options="{placement:'top'}">

**

`

我添加的script元素指的是我稍后将添加的代码文件。

templates.js文件将包含使用 WinJS 模板生成元素所需的代码。这些是我在第八章中介绍的同类模板,我将在default.html文件中定义它们。事实上,您已经可以看到清单中的第一个模板——其idnavBarCommandTemplate的元素将用于为 NavBar 生成命令,允许用户导航到应用中的内容页面,每个页面将展示一个 WinJS UI 控件。

controls.js文件将包含我需要生成的配置控件的细节,以便演示每个 WinJS UI 控件。viewmodel.js文件将包含我需要的其他部分,比如绑定值转换器和 WinJS UI 控件的数据集,这些控件是由数据驱动的。

添加模板生成代码

我的下一步是创建js/templates.js文件,并用生成 NavBar 命令所需的代码填充它。你可以在清单 10-2 的中看到这个文件的初始版本,我将在整个章节中添加这个文件,为创建我需要的不同种类的配置控件添加支持。

清单 10-2 。templates.js 文件的初始版本

`(function () {

var navBarCommands = [
          { name: "AppTest", icon: "target" },
          { name: "ToggleSwitch", icon: "\u0031" },
          { name: "Rating", icon: "\u0032" },
          { name: "Tooltip", icon: "\u0033" },
          { name: "TimePicker", icon: "\u0034" },
          { name: "DatePicker", icon: "\u0035" },
          { name: "Flyout", icon: "\u0036" },
          { name: "Menu", icon: "\u0037" },
          { name: "MessageDialog", icon: "\u0038" },
          { name: "FlipView", icon: "pictures" },
          { name: "ListView", icon: "list" },
          { name: "SemanticZoom", icon: "zoom" },           { name: "ListContext", icon: "list" },
    ];

WinJS.Namespace.define("Templates", {

generateNavBarCommands: function (navBarElement, templateElement) {
            navBarCommands.forEach(function (commandItem) {
                templateElement.winControl.render(commandItem, navBarElement);
            });
        },

});
})();`

navBarCommands数组包含我想要创建的每个命令的细节。数组中的每一项都有nameicon属性,我在default.html文件中定义的navBarCommandTemplate模板中使用了这些属性。

当我创建完框架后,我将删除其中的第一项,即 name 属性为AppTest的项。我添加它只是为了在我创建示例框架时演示它,在本书这一部分的其余章节中不会用到它。

我使用了WinJS.Namespace.define方法来创建一个名为Templates的新名称空间。这个名称空间将包含我需要从我在default.html文件中定义的模板生成元素的函数。开始只有一个函数,它对应于我已经定义的单个模板。generateNavBarCommands函数有两个参数:第一个参数是 NavBar 元素,生成的命令元素将插入其中,第二个参数是用来生成这些元素的模板。该函数枚举navBarCommands数组中的元素,并使用WinJS.Binding.Template.render方法从数组项中生成元素,并将它们插入到 NavBar 元素中。

了解 WINCONTROL 属性

当我向你展示如何使用模板时,我在第八章中介绍了winControl属性,但是它是 WinJS 的一个更普遍的特征,并且当涉及到 UI 控件时特别重要。您很快就会看到,WinJS UI 控件被应用于常规的 HTML 元素,最典型的是div元素。WinJS 通过向底层元素添加子元素、CSS 样式和事件侦听器来创建控件,这是一种由 jQuery UI 等 web 应用 UI 库共享的技术。

当 WinJS 创建控件时,它会将winControl属性添加到代表底层元素的HTMLElement对象中,并将该属性的值设置为来自WinJS.UI命名空间的对象。从winControl属性中获取的对象让您可以访问由WinJS.UI对象定义的属性、方法和事件,您可以使用它们来配置控件或响应用户交互。在本章和后面的章节中,你会看到我经常使用winControl属性来设置和管理我创建的 WinJS 控件。

WinJS.Binding.Template对象遵循相同的模式。我通过将data-win-control attribute设置为我想要创建的控件的名称来创建一个模板,在本例中是WinJS.Binding.Template。模板没有可视组件,但是 WinJS 仍然创建控件并设置winControl属性。WinJS.Binding.Template控件定义了 render 方法,因此为了访问这个方法,我找到了具有 data -win-control属性的元素,并对由winControl属性返回的对象调用了render方法。这是您将在所有 WinJS 控件中看到的模式。

添加导航码

/js/default.js文件中,我添加了处理WinJS.Navigation.navigating事件的代码,并注册了一个来自导航条的click事件的监听器函数。我还调用了ViewModel.Templates.generateNavBarCommands方法来使用命令填充 NavBar,这些命令将应用导航到各个 UI 控件的内容页面(尽管我还没有创建这些文件,因此单击 NavBar 命令会导致错误)。你可以在清单 10-3 中的文件中看到代码。

清单 10-3 。/js/default.js 文件的内容

`(function () {
    "use strict";

var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;
    WinJS.Binding.optimizeBindingReferences = true;

WinJS.Navigation.addEventListener("navigating", function (e) {
        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {
            WinJS.Utilities.empty(contentTarget);
            WinJS.UI.Pages.render(e.detail.location, contentTarget)
                .then(function () {
                    return WinJS.Binding.processAll(contentTarget, ViewModel.State)
                        .then(function () {
                            return WinJS.UI.Animation.enterPage(contentTarget.children)
                        });
                });
        });
    });

app.onactivated = function (eventObject) {
        WinJS.UI.processAll().then(function () {

Templates.generateNavBarCommands(navbar, navBarCommandTemplate);

navbar.addEventListener("click", function (e) {
                var navTarget = "pages/" + e.target.winControl.label + ".html";
                WinJS.Navigation.navigate(navTarget);
                navbar.winControl.hide();
            });
        })

//.then(function() {
        //    return WinJS.Navigation.navigate("pages/AppTest.html");
        //})
    };

app.start();
})();`

navigating事件的处理程序使用WinJS.UI.Animation名称空间来制作从一个内容页面到另一个内容页面的动画,我在第十八章的中描述了这个名称空间。我之所以在这里使用它,是因为内容之间的转换可能太快而无法注意到,而且动画有助于将用户的注意力吸引到内容已经改变的事实上。

NavBar 的click事件处理程序从所使用的命令按钮(通过winControl属性)获取label属性的值,并使用该值导航到pages目录中相应的 HTML 文件(我将很快创建该文件)。这遵循了我在第五章中介绍的相同的导航和内容管理模式,从那以后我已经在几个示例应用中使用过了。

default.html文件中的 JavaScript 非常简单,因为繁重的工作将由我添加到templates.jsviewmodel.js文件中的代码来完成。对于这个应用,default.html 文件只负责设置导航和导航条。

images 提示您会注意到清单中有一些语句被注释掉了。这些是在应用首次加载时自动显示给定内容页面的有用快捷方式,这在您测试和调试示例应用中的页面时非常有用(否则您必须使用 NavBar,如果您像我一样使用短暂的代码然后测试周期,这很快就会变得令人厌倦)。

添加其他 JavaScript 文件

在开始添加内容页面之前,我想让应用的基本结构就位,所以我现在将添加viewmodel.js文件和controls.js文件,尽管它们将只包含创建名称空间的代码。清单 10-4 显示了js/controls.js文件的内容,它创建了一个名为App.Controls的名称空间。我将使用这个名称空间来包含我需要为每个内容页面生成的控件的细节。

清单 10-4。controls . js 文件的内容

`(function () {

WinJS.Namespace.define("App.Controls", {
        // ...details of configuration controls will go here
    });

})();`

初始版本的js/viewmodel.js文件如清单 10-5 所示,目前它只是创建了一个名为ViewModel的名称空间。

清单 10-5 。js/viewmodel.js 文件的初始内容

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel", {
        // ...code for view model will go here
    });

})();`

在本章中我不会用到viewmodel.js文件,但是当我遇到一些更复杂的 WinJS UI 控件,比如FlipViewListView时,我会用到它(我在第十四章和第十五章中描述过)。

添加 CSS

css/default.css文件中,我已经定义了我将用来显示不同 UI 控件的通用样式。有些控件需要额外的 CSS,但是我会在向你展示控件如何工作的时候处理这个问题。您可以在清单 10-6 中看到default.css文件的内容。为了完整起见,我展示了这个文件,但是这些样式中没有新的技术,并且为了简单起见,我没有添加对响应应用视图或方向变化的支持。

清单 10-6 。css/default.css 文件的内容

`body {
    background-color: #5A8463; display: -ms-flexbox; -ms-flex-direction: column;
    -ms-flex-align: center; -ms-flex-pack: center; }

.inputContainer, .selectContainer, .spanContainer {
    width: 100%;}

h2.controlTitle {
    margin: 10px 0px; color: white; font-size: 20px; display: inline-block;
    padding: 10px; font-weight: bolder; width: 140px;}

.controlPanel .win-toggleswitch { width: 90%; margin: 15px; padding-left: 20px;}
.win-toggleswitch .win-title { color: white; font-size: 20px;}

div.flexContainer { display: -ms-flexbox;  -ms-flex-direction: row;
    -ms-flex-align: stretch; -ms-flex-pack: center; }

.controlPanel {
    display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: center;
    -ms-flex-pack: center;    border: medium white solid;
    margin: 10px; padding: 20px; min-width: 300px;}

[data-win-control="WinJS.UI.ToggleSwitch"] {
    border: thin solid white; margin: 5px; padding: 10px;width: 250px;}

input.controlInput, select.controlSelect, span.controlSpan  {
    width: 150px;display: inline-block; margin-left: 20px;}

span.controlSpan {
    white-space: nowrap; text-overflow: ellipsis; overflow: hidden; font-size: 14pt;}

div.buttonContainer button {margin: 5px; font-size: 16pt;}

.textPara { display: inline-block; width: 200px; font-size: 18pt;}
.win-tooltip { background-color: #8ED09C; color: white;
    border: medium solid white; font-size: 16pt;}`

如果你此时运行应用,你会看到类似于图 10-3 的东西。导航栏在图中显示为两行,因为标准 Visual Studio 模拟器分辨率的按钮太多了——如果模拟器设置为更大的分辨率(我在第六章的中描述了这一点),你就不会有这个问题。在这一章的最后,我将把AppTest按钮从导航条上移除,这将把命令放在一行上。AppTest按钮将导航到pages/Apptest.html文件,我只使用它来开发和演示示例框架应用。

images

图 10-3。示例 app 的初始状态

创建测试内容页面

应用的基本结构已经就绪,这意味着我可以将注意力转向创建测试内容页面和填充它所需的代码。同样,向您展示完成的内容,然后再向您展示我是如何创建的,会更容易。在图 10-4 中可以看到点击导航栏上AppTest按钮的结果。

images

图 10-4。点击导航栏上的 AppTest 按钮时显示的完成内容

在接下来的几节中,我将带您了解我用来创建这些内容的过程以及生成大量内容的代码。

创建内容页面

单击其中一个导航栏按钮会从项目pages文件夹中加载一页内容,所以我的第一步是使用解决方案浏览器实际创建pages文件夹。然后我可以添加AppTest.html文件,这个文件将在点击导航栏上的AppTest按钮时被加载。你可以在清单 10-7 中看到这个文件的初始内容。

清单 10-7AppTest.html的初始内容

`

              


            
        


    

`

这个文件显示了标准模式,我将遵循这个模式来演示每个 WinJS UI 控件。文件中的 HTML 标记包含两个div元素。第一个包含我正在演示的 UI 控件。为了保持本章的简单,我将使用常规的 HTML input元素,而不是 WinJS UI 控件之一——这将允许我专注于我正在构建的框架,而不必深入 WinJS 控件的细节。对于这个例子,我给input元素分配了一个inputElemid值,如下所示:

`...

    
...`

另一个div元素将包含允许用户配置正在演示的控件的元素。这是本章的主要焦点——描述我需要的元素并从模板中生成它们的过程,这样我就不必在的后续章节中重复大量的标记和代码。目标div元素的idrightPanel,因为它是布局中最右边的面板而得名:

`...

...`

内容页面最重要的部分是包含在script元素中的 JavaScript。我使用第五章的中的 WinJS Pages 特性来注册一个ready函数,该函数将在加载内容页面时执行。关键语句是对Templates.createControls方法的调用,如下所示:

... Templates.createControls(rightPanel, inputElem, "apptest"); ...

这是生成我需要的配置元素的方法。此方法的参数是:

  • Element, which will contain the created configuration control.
  • UI control element being demonstrated
  • A key that identifies the set of controls to be generated.

对于AppTest.html文件,目标元素是idrightPanel的元素。input元素是正在演示的元素,我使用的键是apptest。在下一节中,您将看到如何使用这些值。

创建模板系统

现在我已经有了一个要处理的测试内容页面,我可以开始构建代码来生成我需要的元素,以演示input元素的一些特性,正如您所记得的,它是一个真正的 WinJS UI 控件的简单替代。在接下来的小节中,我将向您展示我如何描述所需的配置元素集,以及我创建它们的方法。

我将从创建一个配置元素开始。它将是一个select元素,可用于禁用或启用input元素。你可以在图 10-5 中看到这个选择元素创建后的样子。它非常简单,是我开始描述代码不同部分的好地方。

images

图 10-5。生成选择元素来配置输入元素

描述一个选择元素

我必须从描述我想要生成的select元素开始。我在/js/controls.js文件中这样做,你可以看到我在清单 10-8 中添加的内容。select 元素有两个选项——NoYes,,当选择No值时,它们将把input元素的disabled属性的值设置为空字符串(""),当选择Yes值时,它们将设置为disabled

images 注意我花了这么多时间来生成一个简单的可以用四行 HTML 标记编写的select元素,这可能有点奇怪。我试图解决的问题是,我有许多这些select元素要生成,我不想在本书这一部分的其他章节中一遍又一遍地列出本质上相同的标记。此外,您很快就会看到,我不仅仅是生成元素,我还创建了允许配置元素在我演示的 UI 控件上操作的事件处理程序,这是另一个非常重复的代码块,我不必在每章中列出。总的来说,在本章中致力于创建框架可以让我在接下来的章节中花更多的时间关注 WinJS UI 控件和它们的特性。

清单 10-8 。选择元素的初始定义

`(function () {

WinJS.Namespace.define("App.Controls", {

apptest: [{
            type: "select",
            id: "disabled",
            title: "Disabled",
            values: ["", "disabled"],
            labels: ["No", "Yes"]
        }]
    });

})();`

你可以看到我已经在App.Controls名称空间中创建了一个名为apptest的数组——这是从AppTest.html文件传递给Templates.createControls方法的键。这个数组将包含一系列的定义对象,它们描述了我需要创建的每个配置元素。目前只有一个对象,它描述了你在图 10-4 中看到的select元素。

images 提示在后面的章节中,我将更简洁地列出定义对象。在这个例子中,我使用了在自己的行上显示每个属性,以便于理解这些对象是如何工作的。

我将很快解释对象中的每一个属性,但是首先我将把我用来生成select元素的模板添加到default.html文件中,如清单 10-9 所示。在本章中,我将为我在这个框架中支持的每种配置元素添加一个模板。

清单 10-9 。向 default.html 文件添加用于生成选择元素的模板

`...

<body>` `<div id="navBarCommandTemplate" data-win-control="WinJS.Binding.Template">         <button data-win-control="WinJS.UI.AppBarCommand"             data-win-options="{section:'selection'}"             data-win-bind="winControl.label: name; winControl.icon: icon">         </button>       </div>

**    

**
**        
**
**            

**
**            **
**        
**
**    
**


        

Select a Control from the NavBar


    

...`

我将使用的模板结构将帮助我解释清单 10-8 中定义对象的属性的用途,我已经在表 10-1 中列出了这些属性。

images 提示此表解释了选择元素的定义对象的属性的含义。定义对象的每个类型都有一些额外的或不同的属性,我将在本章后面生成这类元素时解释这些属性。

images

如果你回头看清单 10-8 ,你可以看到我分配给属性的值是如何与图 10-4 中显示的结果相对应的。type 属性设置为select,表示我想要一个select元素;id 属性设置为 disabled,对应的是我要管理的input元素属性;title 属性被设置为Disabled,这样用户就知道改变配置元素会对正在演示的 UI 控件产生什么影响。

最后,我使用了values labels属性,因为input.disabled属性允许的值不适合显示给用户。通过为这两个属性提供值,我在呈现给用户的内容和分配给 UI 控件属性的值之间创建了一个映射。

添加元素生成代码

我有了select元素的定义,也有了生成select元素的模板。现在我所需要的是将这些结合在一起的代码——我已经将这样做的结构添加到了/js/templates.js文件中,如清单 10-10 所示。

清单 10-10 。使用 templates.js 文件中的定义和模板创建元素的代码

`(function () {

var navBarCommands = [
        { name: "AppTest", icon: "target" },
];

WinJS.Namespace.define("Templates", {

generateNavBarCommands: function (navBarElement, templateElement) {
        navBarCommands.forEach(function (commandItem) {
            templateElement.winControl.render(commandItem, navBarElement);
        });
    },

**    createControls: function (container, uiControl, key) {**
**        var promises = [];**

**        App.Controls[key].forEach(function (definition) {**
**            var targetObject = uiControl.winControl ? uiControl.winControl : uiControl;**
**            promises.push(Templates"create" + definition.type);**
**        });**

**        return WinJS.Promise.join(promises).then(function () {**
**            $("*[data-win-bind]", container).forEach(function (childElem) {**
**                childElem.removeAttribute("data-win-bind");**
**            });**
**        });**
**    },**

**    createtoggle: function (containerElem, definition, uiControl) {**
**        // ...code to create ToggleSwitch element will go here**
**    },**

**    createinput: function (containerElem, definition, uiControl) {         // ...code to create input element will go here**
**    },**

**    createselect: function (containerElem, definition, uiControl) {**
**        // ...code to create select element will go here**

**    },**

**    createspan: function (containerElem, definition, uiControl) {**
**        // ...code to create span element will go here**
**    },**

**    createbuttons: function (containerElem, definition, uiControl) {**
**        // ...code to create button elements will go here**
**    }**
});

})();`

我对这个文件所做的添加是以createControls方法为中心的,我从AppTest.html文件中调用这个方法来创建我想要的配置控件。createControls方法比看起来要简单,但是它是我构建这个框架所采用的方法的核心,所以我将向您详细介绍它,并将代码的工作方式与我在上一节中描述的示例select元素联系起来。

该方法的参数有:创建配置控件时应该放入的容器,将要由新创建的控件配置的 UI 控件,以及引用controls.js文件中定义对象集的键。对于select元素,这些参数将是来自AppTest.html文件中标记的rightPanel元素和input元素以及值apptest,它对应于包含我在清单 10-8 中展示的select元素定义的数组。

处理定义对象

我使用我收到的键从App.Controls名称空间获得关联的定义对象的数组。目前只有一个键(apptest),我得到的数组只包含一个定义对象(对于我的select元素),但是我将在本章后面添加更多的定义(在后续章节中添加更多的键)。

我使用forEach方法来枚举数组中的项目。我做的第一件事是建立我正在工作的目标对象。在后面的章节中,我将使用 WinJS 控件,它们都定义了winControl属性(详见理解 winControl 属性侧栏),但是对于这一章,我将使用没有winControl属性的 input 元素。我是这样弄清楚我在做什么的:

... var targetObject = uiControl.winControl ? uiControl.winControl : uiControl; ...

这很重要,因为我创建了事件处理程序,当我创建配置元素时,它将应用更改目标对象的属性值。当我使用 WinJS 控件时,我想操作控件,而不是应用它的底层 HTML 元素。在这一章中,没有 WinJS 控件,HTML 元素是我必须使用的全部。

确定了我的目标之后,我基于当前定义对象的 type 属性值,调用了Templates名称空间中的其他方法之一,如下所示:

... promises.push(Templates"create" + definition.type); ...

目前我只有一个定义对象,它的 type 属性的值是 select,这意味着将调用Templates.createSelect方法。名称空间中的其他方法负责创建一种元素,并被传递给应该插入元素的容器、当前定义对象和目标对象。我将很快实现其中的第一个方法来演示它们是如何工作的。

元素创建方法返回一个Promise,我将它添加到一个名为 promises 的数组中。我在promises数组上使用了Promise.join方法(我在第九章中描述过)来创建一个Promise,当所有的单个元素都被创建、添加到容器元素并配置好之后,这个方法就完成了。

清理结果

createObjects方法中的最后一步是使用then方法指定一个函数,当所有的单个Promise对象都被满足时,这个函数将被执行。在这个函数中,我从所有拥有属性的元素中移除了属性data-win-bind。当我介绍FlipView元素时,我将详细解释为什么这是一件有用的事情第十四章,但简短的版本是,如果在生成的elements添加到文档后调用WinJS.Binding.processAll方法,将data-win-bind属性留在已生成的元素上会导致问题。通过移除属性,我确保这种情况不会发生。

生成一个选择元素

现在一切就绪,我可以生成我的 select 元素了,我将通过在templates.js文件中实现createselect方法来实现它。你可以在清单 10-11 中看到我是如何做到这一点的。

清单 10-11 。生成选择元素

`...
createselect: function (containerElem, definition, uiControl) {
    return selectTemplate.winControl.render(definition).then(function (newElem) {

var selectElem = WinJS.Utilities.query("select", newElem)[0];
        selectElem.id = definition.id;
        definition.values.forEach(function (value, index) {
            var option = selectElem.appendChild(document.createElement("option"));
            option.innerText = definition.labels ? definition.labels[index] : value;
            option.value = value;
        });

selectElem.addEventListener("change", function (e) {
            setImmediate(function () {
                uiControl[definition.id] =
                   selectElem.options[selectElem.selectedIndex].value;             });
        });
        containerElem.appendChild(newElem.removeChild(newElem.children[0]));
        uiControl[definition.id] = selectElem.options[0].value;
    });
},
...`

由于这是我生成的第一种类型的元素,我将逐步遍历代码,并解释我如何创建结果。

渲染模板

我首先使用添加到default.html文件中的模板来生成一组新的元素,如下所示:

... createselect: function (containerElem, definition, uiControl) {     return selectTemplate.winControl.render(definition).then(function (newElem) {        // *...code removed for brevity...* }, ...

当你在一个只有一个参数的WinJS.Binding.Template对象上调用render方法时,你得到的Promise产生了当它被实现时已经被创建的元素,并且这些元素没有被插入到应用布局中。在这个清单中,我将定义对象从controls.js文件传递给render方法,这允许定义中包含的细节用于填充模板。提醒一下,下面是我正在使用的模板(我已经使用id属性值在代码中找到了它):

`...

    
        

             
...`

WinJS.Binding.Template对象完成元素的渲染后,我就有了一些从模板生成的分离元素。分离的元素还不是应用内容的一部分,这些元素如下所示:


`


    

        

Disabled


        
    

`

配置&填充选择元素

此时,我已经拥有了我需要的元素,但是它们只是部分完成。我的下一步是完成 select 元素并添加代表用户可以做出的选择的option元素,如下所示:

`...
createselect: function (containerElem, definition, uiControl) {
    return selectTemplate.winControl.render(definition).then(function (newElem) {

**        var selectElem = WinJS.Utilities.query("select", newElem)[0];**
**        selectElem.id = definition.id;**
**        definition.values.forEach(function (value, index) {**
**            var option = selectElem.appendChild(document.createElement("option"));**
**            option.innerText = definition.labels ? definition.labels[index] : value;**
**            option.value = value;**
**        });**

// ...code removed for brevity...
    });
},
...`

我使用WinJS.Utilities.query方法定位从render方法传递给我的函数的元素集中的select元素。

我的第一个动作是将id属性设置为由定义对象指定的值。然后,我使用定义对象中的valueslabels数组来创建一系列的option元素,我将它们作为子元素添加到 select 元素中。这给了我下面的 HTML:


`


    

        

Disabled


        <select class="controlSelect" id="disabled">
**            **
**            **
        
    

`

创建事件处理程序

当用户在select元素中选择option的 on 选项时,我想更新目标对象的属性值。为了确保这一点,我使用addEventListener方法为change事件注册一个处理函数,如下所示:

`...
createselect: function (containerElem, definition, uiControl) {
    return selectTemplate.winControl.render(definition).then(function (newElem) {

var selectElem = WinJS.Utilities.query("select", newElem)[0];

// ...code removed for brevity... **        selectElem.addEventListener("change", function (e) {**
**            setImmediate(function () {**
**                uiControl[definition.id] =**
**                    selectElem.options[selectElem.selectedIndex].value;**
**            });**
**        });**

// ...code removed for brevity...
    });
},
...`

change事件被触发时,我更新目标对象的属性以匹配从select元素中选取的值。请注意,我使用了setImmediate方法来延迟属性更改——这允许选择元素在 UI 控件的属性更改之前完成向新选择的值的转换。如果不调用setImmediate,应用会暂时没有响应,因为控件的属性更改会在select元素完成响应用户选择值之前执行。

整理完毕

当我从模板中生成元素时,我最终得到了一个我不想添加到应用布局中的外部div元素。为此,在我设置了事件处理程序之后,我选择了第一个子元素,并使用传递给该方法的container参数将其添加到应用布局中,如下所示:

`...
createselect: function (containerElem, definition, uiControl) {
    return selectTemplate.winControl.render(definition).then(function (newElem) {

// ...code removed for brevity...

**        containerElem.appendChild(newElem.removeChild(newElem.children[0]));**
**        uiControl[definition.id] = selectElem.options[0].value;**
    });
},
...`

最后一步是设置 UI 控件的属性,以匹配选择菜单的初始值,这确保了选择控件和所演示的 UI 控件的状态是同步的。

您可以通过启动应用并从导航栏中选择AppTest命令来测试这些附加功能。您将能够通过从右侧面板的select元素中选取值来启用和禁用左侧面板中的input元素。

使用代理对象

不是所有我想在后面章节中展示的特性都可以简单地通过设置属性值来演示。在这些情况下,我需要使用一个代理对象,这样我就可以以一种有用的方式响应对配置元素所做的更改。在这一节中,我将向您展示我是如何将这个特性添加到示例框架中的。

添加定义对象

首先,我将向controls.js文件添加一个新的定义对象,这将提供一个配置选项,它不能被转换成简单的属性更改。你可以在清单 10-12 中看到这个新定义。

清单 10-12 。向 controls.js 文件添加新的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

apptest: [
        { type: "select", id: "disabled", title: "Disabled",
            values: ["", "disabled"], labels: ["No", "Yes"] },
**        { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],**
**              useProxy: true },**
    ]
});

})();`

这个定义指定了另一个select元素,带有SmallBig选项。重要的添加是useProxy属性,我已经将它设置为true。这将表明,当用户从select元素中选择一个option时,新值应该应用于代理,而不是直接应用于正在演示的 UI 控件。

创建代理对象

我在内容页面的script元素中创建代理对象,如清单 10-13 中的所示,它展示了我对AppTest.html文件所做的添加。我创建了一个名为proxyObject的可观察对象,并将其作为参数传递给createControls方法。我使用WinJS.Binding.as方法创建可观察对象,我在第八章的中描述过。

清单 10-13 。向 AppTest.html 文件添加代理对象

`

         

    


            
        


    

`

检测和使用代理对象

接下来,我需要更新templates.js文件中的createControls方法,以便它可以接收代理对象,并在定义对象需要时使用它。你可以在清单 10-14 中看到我为此所做的修改。

清单 10-14 。在 createControls 方法中添加对代理对象的支持

`...
createControls: function (container, uiControl, key, proxy) {
    var promises = [];

App.Controls[key].forEach(function (definition) {
**        var targetObject = definition.useProxy ? proxy : uiControl.winControl ?**
**            uiControl.winControl : uiControl;**
        promises.push(Templates"create" + definition.type);
    });

return WinJS.Promise.join(promises).then(function () {
        $("*[data-win-bind]", container).forEach(function (childElem) {
            childElem.removeAttribute("data-win-bind");
        });
    });
},
...`

更改相对简单——我只需扩展为配置控制更改选择目标的语句,以考虑代理对象。

这种添加的效果是,当定义对象指定应该使用代理对象时,从模板生成的select元素的事件处理程序将更改代理上指定属性的值,而不是winControl属性或 HTML 元素本身。

响应代理对象属性变化

最后一步是返回到AppTest.html文件中的 script 元素,并添加一些代码来监控可观察代理对象的变化。你可以在清单 10-15 中看到我是如何做到的。

清单 10-15 。观察代理对象的变化

`...

...`

当用户使用新的select元素选取一个值时,代理对象中的theme属性将会改变。我使用bind方法观察主题属性,我在第八章的中描述过,并改变两个 CSS 属性来创建不同的视觉效果。这是一个简单的演示,说明了我如何使用我的示例框架将配置元素与更复杂的 UI 控件特性绑定在一起——这是我将在接下来的章节中经常用到的。你可以在图 10-6 中看到选择BigSmall值的结果。

images

图 10-6。使用代理对象支持更复杂的配置

生成其他元素类型

现在,您已经看到了示例框架中的所有复杂性。剩下的工作就是为不同的元素类型添加剩余的模板,并在controls.js文件中实现使用它们的方法。在接下来的小节中,我将结束这个框架,并创建一个新的定义对象来演示其余类型的配置元素。本章的剩余部分没有新的技术,所以我将列出每种元素类型所需的模板和代码,并展示一个生成每种元素类型的定义对象的例子。

生成输入元素

我使用input元素来允许用户输入不受约束的值。您可以在清单 10-16 的中看到输入配置元素的定义对象。

清单 10-16。输入配置元素的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

apptest: [
        { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],
            labels: ["No", "Yes"] },
        { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],
            useProxy: true },
**        { type: "input", id: "value", title: "Value", value: "Hello" },**
    ]
});

})();`

你可以在表 10-2 中看到该定义对象中属性的含义。

images

您可以从我用来生成清单 10-17 中的input元素的default.html文件中看到模板。

清单 10-17 。来自 default.html 文件的输入元素模板

`...

    
` `        

             
...`

您可以在清单 10-18 中的controls.js文件中看到createinput方法的实现。

清单 10-18。controls . js 文件中 createinput 方法的实现

... createinput: function (containerElem, definition, uiControl) {     return inputTemplate.winControl.render(definition).then(function (newElem) {         WinJS.Utilities.query("input", newElem).forEach(function (elem) {             elem.id = definition.id;             elem.addEventListener("change", function (e) {                 setImmediate(function () {                     uiControl[elem.id] = elem.value;                 });             });             uiControl[definition.id] = elem.value;                         });         containerElem.appendChild(newElem.removeChild(newElem.children[0]));     }); }, ...

您输入到我在本节中创建的input配置元素中的值更新了布局左侧面板中的input元素的值,这是 WinJS UI 控件将在后面章节中出现的位置。请注意,这种关系只是单向的——也就是说,在左侧面板的 input 元素中输入文本不会更新右侧面板中 input 元素的内容。

生成一个跨度元素

我使用span元素来显示 UI 控件的一些只读特性,通常是为了支持演示一些其他特性。我在生成span元素时没有创建事件监听器,而是从内容页面的script元素中更新内容。清单 10-19 显示了向controls.js文件添加一个span定义对象。

清单 10-19 。向 controls.js 文件添加 span 定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {
    apptest: [
        { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],
            labels: ["No", "Yes"] },
        { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],
            useProxy: true },
        { type: "input", id: "value", title: "Value", value: "Hello" },
**        { type: "span", id: "value", value: "", title: "Value" },**     ]
});

})();`

你可以在表 10-3 中看到该定义对象中属性的含义。

images

您可以从 default.html 文件中看到我用来生成清单 10-20 中的元素的模板。

清单 10-20 。来自 default.html 文件的跨度模板

`...

    
        

             
...`

您可以在清单 10-21 中的templates.js文件中看到createinput方法的实现。

清单 10-21 。templates.js 文件中 createspan 方法的实现

... createspan: function (containerElem, definition, uiControl) {     return spanTemplate.winControl.render(definition).then(function (newElem) {         WinJS.Utilities.query("span", newElem).forEach(function (elem) {             elem.id = definition.id;         });         containerElem.appendChild(newElem.removeChild(newElem.children[0]));     }); }, ...

生成按钮元素

我使用button配置元素让用户触发某种动作——通常是从数据源中添加或删除项目,我会在第十四章的中向您介绍。您可以在清单 10-22 的中看到按钮元素的定义对象。

清单 10-22 。按钮元素的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

apptest: [
        { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],
            labels: ["No", "Yes"] },
        { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],
           useProxy: true },
        { type: "input", id: "value", title: "Value", value: "Hello" },
        { type: "span", id: "value", value: "", title: "Value" },
**        { type: "buttons", title: "Buttons", labels: ["Add Item", "Delete Item"] },**
    ]
});

})();`

你可以在表 10-4 中看到该定义对象中属性的含义。

images

我不使用模板来生成按钮元素,我将事件处理程序留给内容页面,这意味着在templates.js文件中实现createbuttons方法,如清单 10-23 所示,特别简单。

清单 10-23 。templates.js 文件中 createbuttons 方法的实现

... createbuttons: function (containerElem, definition, uiControl) {     var newDiv = containerElem.appendChild(document.createElement("div"));     WinJS.Utilities.addClass(newDiv, "buttonContainer");     if (definition.title) {         var titleElem = newDiv.appendChild(document.createElement("h2"))         titleElem.innerText = definition.title;         WinJS.Utilities.addClass(titleElem, "controlTitle");     }     definition.labels.forEach(function (label) {         var button = newDiv.appendChild(document.createElement("button"));         button.innerText = label;     }); } ...

生成 ToggleSwitch 控件

WinJS.UI.ToggleSwitch控件让用户选择真/假值。我将在下一章详细演示这个控件,所以我不想在这一章讨论任何细节。我将按原样呈现定义对象、模板和代码,在您阅读完第十一章后,它们将变得有意义。你可以在清单 10-24 中看到一个ToggleSwitch控件的定义对象。

清单 10-24 。ToggleSwitch 控件的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

apptest: [
        { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],
            labels: ["No", "Yes"] },
        { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],
             useProxy: true },
        { type: "input", id: "value", title: "Value", value: "Hello" },
        { type: "span", id: "value", value: "", title: "Value" },
        { type: "buttons", title: "Buttons", labels: ["Add Item", "Delete Item"] },
**        { type: "toggle", id: "disabled", title: "Disabled", value: false},**
    ]
});

})();`

你可以在表 10-5 中看到该定义对象中属性的含义。

images

你可以在清单 10-25 中看到我用来创建ToggleSwitch控件的default.html文件中的模板。

清单 10-25 。生成 ToggleSwitch 控件的模板

`...

    
    
...`

最后,您可以在清单 10-26 中看到createtoggle方法的实现。我将在第十一章中解释我通过winControl属性设置的属性。

清单 10-26 。templates.js 文件中 createtoggle 方法的实现

... createtoggle: function (containerElem, definition, uiControl) {     return toggleSwitchTemplate.winControl.render(definition).then(function (newElem) {         var toggle = newElem.children[0];         toggle.id = definition.id;         if (definition.labelOn != undefined) {             toggle.winControl.labelOn = definition.labelOn;             toggle.winControl.labelOff = definition.labelOff;         }         toggle.addEventListener("change", function (e) {             setImmediate(function () {                 uiControl[definition.id] = toggle.winControl.checked;             });         });         containerElem.appendChild(newElem.removeChild(toggle));         uiControl[definition.id] = toggle.winControl.checked;     }); }, ...

如果您运行带有所有这些定义对象、模板和方法实现的示例应用,您将会看到如图图 10-4 所示的布局。

打扫卫生

现在剩下的就是从导航条上移除AppTest命令按钮,这样我在下一章就有了一个干净的项目。你可以在清单 10-27 中看到我对templates.js文件所做的最后修改。

清单 10-27 。从导航条上取下测试按钮

... var navBarCommands = [         **//**{ name: "AppTest", icon: "target" },         // *...other commands omitted for brevity...* ]; ...

总结

在这一章中,我已经解释了我是如何创建框架的,我将用它来演示本书这一部分的剩余章节中的 WinJS UI 控件。尽管这个框架本身解释起来相当冗长,但它让我避免了在每章的前十页列出大部分相同的代码和标记。这个项目还有一个额外的好处,就是演示了一些核心的 WinJS 特性如何以更复杂的方式组合起来,以创建更丰富的效果。在接下来的章节中,我将带您浏览一下您可以在 Metro 应用中使用的 UI 控件,以提供更丰富的体验,并为您的用户提供与其他 Metro 应用一致的外观。

十一、使用ToggleSwitchRatingTooltip控件

在这一章,我开始详细描述 WinJS UI 控件,使用我在第十章中创建的框架应用。这些 UI 控件是创建与更广泛的 Windows 视觉主题一致的应用的重要构件,值得详细研究。

WinJS UI 控件的机制类似于您可能使用过的其他 JavaScript UI 工具包,如 jQuery UI。将控件应用于元素,并应用其他元素、样式和事件处理程序来创建丰富的视觉效果。

您可以轻松地在 Windows 应用中使用 jQuery UI 或类似的库,但最终会得到奇怪的结果,并且会错过一些控件与其他 WinJS 功能(如数据绑定)的紧密集成。在本章和接下来的章节中,我将依次描述每个控件。我告诉你如何应用和配置控件,何时应该使用控件,以及如何观察用户与控件的交互。

在这一章中,我从三个相对简单的控件开始:ToggleSwitchRatingTooltip控件。这些是最基本的控件,虽然它们很有用,但是它们并不有趣,或者与您可能遇到的其他 JavaScript UI 工具包没有什么不同。表 11-1 对本章进行了总结。

images

images

使用切换开关控制

我将从WinJS.UI.ToggleSwitch控件开始。这是一个很好的开始控件,因为它很简单,而且因为我在第十章中创建的框架应用中使用了这个控件来演示本章和后续章节中其他控件的功能。

顾名思义,ToggleSwitch控件让用户在onoff状态之间切换。鼠标用户可以点击ToggleSwitch的空白部分来改变状态,触摸用户可以向左或向右滑动开关。ToggleSwitch控件在从一种状态转换到另一种状态时会执行一个简短的动画。你可以在图 11-1 中看到ToggleSwitch是如何出现的。稍后,我将向您展示我为创建这个布局而添加到示例框架中的内容。

images

图 11-1。示例应用中的 ToggleSwitch 控件

何时使用拨动开关控制

当你需要向用户展示一个二元决策时,你应该使用ToggleSwitch控件。改变值的滑动动作非常友好,比普通的 HTML 复选框或带有YesNo选项的select元素更容易使用。我经常使用这个控件让用户配置应用设置(我在第二十章中描述过)。

images 提示如果你在一列中显示几个ToggleSwitch控件,确保所有的true / on值都在同一侧——如果一些开关需要放在右边来启用某个功能,而其他开关放在左边,只会让用户感到困惑。

演示 ToggleSwitch 控件

为了演示ToggleSwitch控件,我在第十章的UIControls项目的pages文件夹中添加了一个新的 HTML 文件。您可以在清单 11-1 的中看到这个名为ToggleSwitch.html的文件的内容。

清单 11-1 。/pages/ToggleSwitch.html 文件的内容

`

              


**            <div id="mainToggle" data-win-control="WinJS.UI.ToggleSwitch"**
**                data-win-options="{title:'This is a ToggleSwitch:'}">
**
        


        

    

`

您可以从清单中看到,我使用了键toggleSwitch来定位controls.js文件中的定义对象。您可以在清单 11-2 中看到那些定义对象。

清单 11-2 。ToggleSwitch 控件的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

toggleSwitch: [
        { type: "toggle", id: "checked", title: "Value", value: true },
        { type: "toggle", id: "disabled", title: "Disabled", value: false }],
    });

})();`

最后,我通过对清单 11-3 中的文件进行添加,将命令添加到了导航栏中。

清单 11-3 。将 ToggleSwitch.html 文件添加到导航栏

... var navBarCommands = [         //{ name: "AppTest", icon: "target" }, **          { name: "ToggleSwitch", icon: "\u0031" },** ]; ...

应用和配置 ToggleSwitch 控件

通过将data-win-control属性设置为WinJS.UI.ToggleSwitch,将ToggleSwitch控件应用于div元素。正如我在第十章中所解释的,当底层 HTML 元素被WinJS.UI.processAll方法处理时,它被赋予一个winControl属性(要么是因为你已经显式调用了该方法,要么是因为内容已经使用WinJS.UI.Pages.render方法加载,该方法为你调用了processAll)。

属性返回由属性指定的控件对象,这允许你在代码中调用方法和设置控件定义的属性。为了方便起见,您可以使用data-win-options属性在 HTML 中以声明方式设置属性值,指定一个包含名称/值对的 JSON 片段。

对于左边的ToggleSwitch,我已经为title属性设置了一个值,该值用于显示在控件上方的文本,如清单 11-4 所示。

清单 11-4 。使用 data-win-options 属性设置标题属性

`...

    
...`

表 11-2 描述了由ToggleSwitch定义的配置属性,所有这些属性都可以使用data-win-options属性或在您的 JavaScript 代码中设置。

images

在演示ToggleSwitch控件的代码中,我创建了改变checkeddisabled属性的配置控件。通过查看templates.js文件中的createtoggle方法,你可以看到我是如何使用 JavaScript 来设置其他属性的,我在第十章的最后给你展示了这个文件。我使用来自定义对象的值来设置labelOnlabelOfftitle属性。

样式化 ToggleSwitch 控件

WinJS UI 控件可以使用一组 CSS 类来设置样式。这是使用 WinJS 控件的好处之一,也是我还没有感觉到需要在我的任何 Windows 应用项目中使用 jQuery UI 的原因之一(这说明了一些问题,因为我喜欢 jQuery 和 jQuery UI)。我已经在表 11-3 中列出并描述了ToggleSwitch控件支持的一组类。

images

因为我使用了ToggleSwitch控件来帮助演示许多其他 WinJS UI 控件,所以我在/css/default.css文件中添加了一些 CSS 样式,这些样式使用了表 11-3 中的类。你可以在清单 11-5 中看到这些风格。

images 小心不要忘乎所以的设计 UI 控件。你想让你的应用和一般的 Windows 外观保持一致。为此,您应该只设置控件的样式,以便它们易于阅读,并且与布局中使用的配色方案相对应。

清单 11-5 。将样式应用于 ToggleSwitch 控件

`...
.controlPanel .win-toggleswitch {
    width: 90%;
    margin: 15px;
    padding-left: 20px;
}

.win-toggleswitch .win-title {
    color: white;
    font-size: 20px;
}
...`

每当你覆盖一个ToggleSwitch控件的样式时,你必须使用win-toggleswitch类,甚至当你覆盖一个子样式时,比如win-title。如果你不这样做,那么由 Visual Studio 项目中的默认 CSS 定义的样式(在ui-light.cssui-dark.css文件中,我在第二章中描述过)将具有更大的特异性,而你的自定义值将不会有任何影响。

对于default.css文件中的样式,我将一个border和一些marginpadding应用于我用作配置控件的ToggleSwitch控件,并更改了所有ToggleSwitch控件标题的文本大小和颜色。

images 提示很难弄清楚定制风格到底有什么效果。通过在调试器中运行您的应用,切换到 Visual Studio DOM Explorer窗口,单击Select Element按钮并单击布局中的控件,您可以看到 WinJS UI 控件的结构。您将能够看到围绕应用了data-win-control属性的元素创建的 HTML 元素,并看到底层组件的样式。

处理 ToggleSwitch 控件事件

当用户更改开关的位置时,ToggleSwitch控件发出change事件(当以编程方式更改属性值时,不会发出该事件)。我将依次列出每个控件的事件,以便在您需要和翻阅本章时更容易找到这些信息。表 11-4 描述了change事件,尽管只有一个而且是非常基本的事件。

images

我在ToggleSwitch.html文件中添加了一个处理程序,它通过更新右侧面板中一个配置控件的checked值来响应change事件,使得这些控件之间的关系是双向的。您可以在清单 11-6 的中看到事件处理程序。

清单 11-6 。添加 ToggleSwitch 更改事件的处理程序

`

              


            <div id="mainToggle" data-win-control="WinJS.UI.ToggleSwitch"                 data-win-options="{title:'This is a ToggleSwitch:'}">

        


        

    

`

我使用then方法确保配置控件已经由Templates.createControls方法创建,并使用addEventListener方法设置处理程序。注意,我直接在应用了ToggleSwitch控件的div元素上设置了事件监听器,但是我通过读取winControl.checked值来获取控件的状态。这是所有 WinJS UI 控件的模式——事件是从底层 HTML 元素发出的,但是控件的状态是通过winControl属性访问的。

要测试事件处理程序,请启动示例应用,单击导航栏上的ToggleSwitch按钮,并切换左侧布局面板中的ToggleSwitch控件。您将看到右侧面板中的Value配置控件的状态同步变化。这种关系反过来也一样,但那是因为我在Templates.createtoggle方法中为change事件设置了一个监听器,我在第十章中描述过。

images 提示ToggleSwitch被禁用时,用户不能移动开关位置,但如果通过编程修改winControl.checked属性,开关将正确反映变化。

使用评级控制

WinJS.UI.Rating控件允许用户通过提供星级来表达意见。在图 11-2 中,你可以看到Rating控件是如何出现在左侧面板中的。用户可以通过点击或触摸星星来指定评级,并通过在星星阵列上上下拖动鼠标或手指来更改评级。稍后,我将向您展示我在示例项目中添加的内容,以创建这个布局。

images

图 11-2。一个 WinJS。UI .评级控制

何时使用评级控制

用星星的数量来表达观点或评价的想法是如此根深蒂固,以至于你不应该将这个控件用于任何其他目的。如果您想从用户那里获得一个数字,那么使用一个常规的input元素,将type属性设置为number

如果你确实想要用户的意见,那么Rating控件是理想的。尝试使用常见的星级数(3、5 和 10 是常用的),在整个应用中坚持这个数字,并确保你利用这个机会表达积极和消极的观点。每当使用Rating控制时,确保每个星级具有相同的含义。

演示评级控制

为了演示Rating控件,我在UIControls项目的pages文件夹中添加了一个新的 HTML 文件。您可以在清单 11-7 中看到这个名为Rating.html的文件的内容。

清单 11-7。/pages/rating . html 文件的内容

`

              

    


**            
**
        
        

    

`

这个文件包含了一些特定于Rating控件的 CSS 样式(我会简单描述一下)。清单 11-8 显示了我添加到controls.js文件中的定义对象,以在图 11-2 所示的右侧面板中创建配置元素。

清单 11-8。评级控件的定义对象

... **rating**: [     { type: "toggle", id: "enableClear", title: "Enable Clear", value: true },     { type: "toggle", id: "disabled", title: "Disabled", value: false },     { type: "input", id: "userRating", title: "User Rating", value: 0 },     { type: "input", id: "maxRating", title: "Max Rating", value: 6 },     { type: "input", id: "averageRating", title: "Ave. Rating", value: 2.6 }], ...

为了让用户能够导航到Rating.html页面,我对templates.js文件进行了添加,如清单 11-9 所示。

清单 11-9 。将 Rating.html 文件添加到导航栏

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" }, **    { name: "Rating", icon: "\u0032" },** ]; ...

应用和配置评级控制

通过将data-win-control属性设置为WinJS.UI.Rating,将Rating控件应用于div元素。我用配置控件管理的属性在表 11-5 中描述,并可通过winControl属性访问。

images

我为Rating控件定义的配置控件允许您尝试更改除了tooltipStrings之外的所有属性,我在Rating.html页面的script元素中设置了tooltipStrings(我将在下面解释)。

管理评级

averageRatingmaxRatinguserRating属性之间有一个特定的交互,你需要在其中工作以从Rating控件获得正确的效果。

maxRating属性指定了Rating显示的星的数量,并有效地设置了可以使用该控件选择的评级的上限。显示的星星数是一个整数。

属性可以用来显示其他地方的评级。平均值的常见用途是显示其他用户的评级或当前用户在以前提供的评级。您可以将averageRating值指定为实数(如4.2),部分星星将会显示出来。userRating属性表示用户选择的值。这是另一个整数值。

只有当userRating属性为零时,才会显示averageRating值。一旦用户提供了一个等级(或者在外部设置了userRating属性),就会隐藏averageRating值并显示userRating值。我在Rating控件的例子中添加了控件,让你指定这些属性的值,你可以在图 11-3 中看到从averageRatinguserRating值的转换。

images

图 11-3。用用户评级值替换平均评级

如果enableClear属性是true,那么用户可以拖动或滑动到Rating控件的左边,清除userRating值。发生这种情况时,再次显示averageRating值。如果enableClear属性为false,则控制不能被清除。请谨慎使用此设置,因为它会诱使用户做出不可逆的评级。如果您确实将enableClear设置为false,那么您应该提供一个附近且明显的控件,将userRating属性设置回零,并让用户重新开始。一般来说,你应该让用户放弃他们提供的任何值——当这不可能的时候,用户会感到困惑和烦恼。

设置工具提示

如果用户在一颗星星上悬停或滑动,Rating控件将显示一个工具提示。默认情况下,这些是数字,反映评级代表的星级数。如果您将tooltipStrings设置为一个字符串数组,那么数组值将被用作工具提示内容。我在pages/Rating.html文件的脚本元素中显式设置了tooltipStrings属性。为了方便起见,下面是我使用的值:

... rating.winControl.tooltipStrings = ["Terrible", "Pretty Bad", "Not So Good",     "Reasonable", "Good", "Excellent"]; ...

你可以在图 11-4 中看到一个工具提示值是如何呈现给用户的。

images

图 11-4。显示工具提示的评级控件

images 提示如果你设置了一个比maxRating属性的值多一项的数组,那么当用户向左拖动或滑动以清除评级时(假设enableClear属性为true),将显示最后一个字符串值。

设置评级控件的样式

Rating控件支持多种样式,您可以覆盖这些样式来定制控件的外观,每种样式都在表 11-6 中进行了描述。

images

images

这些类必须组合成非常特殊的排列来设计一个Rating控件。清单 11-10 显示了来自Rating.html文件中style元素的 CSS,我用它来改变示例中Rating控件的默认样式。

清单 11-10 。Rating.htmlfile 文件中的 CSS

... .win-rating .win-star.win-user.win-full {     color: yellow; } .win-rating .win-star.win-average.win-full {     color:  #000; } .win-rating .win-star.win-average.win-full, .win-rating .win-star.win-tentative.win-full {     color:  white; }   ...

应用表格中的样式时必须遵循的顺序是:

.win-rating **.win-star.win-[rating value].win-[star state]**

我用粗体标记的类应用于同一个元素,这意味着不用空格来分隔它们。如果您想要为userRating值设置显示的完整星号的样式,您可以覆盖这个序列:

.win-rating .win-star.win-**user**.win-**full**

您可以覆盖多组星形的样式,但需要小心。最安全的方法是列出多个完整的类组合,如下所示:

... .win-rating .win-star.win-average.win-full, .win-rating .win-star.win-user.win-full, .win-rating .win-star.win-tentative.win-full {     color:  white; } ...

您可以省略一些类来用选择器撒一个更大的网,但是您仍然必须确保选择器比微软定义的那些更具体,后者使用完整的类序列。最直接的方法是使用已经应用了Rating控件的元素的id,就像这样:

`...

rating .win-star.win-full {

color:  green;
}
...`

处理评级控制事件

Rating控件支持三个事件,我已经在表 11-7 中描述过。

images

清单 11-11 展示了我如何在/pages/Ratings.html文件中添加脚本元素来处理change事件。我使用这个事件来更新布局右侧面板中的配置控件,这会影响到userRating属性。

清单 11-11 。处理评级变更事件

`...

...`

我对由Template.createControls方法返回的Promise对象使用了then方法,以确保在创建配置元素并将其添加到应用布局之前,我不会创建事件处理程序。

使用工具提示控件

当鼠标或手指停留在一个元素上或当键盘获得焦点时,Tooltip控件弹出一个包含有用信息的窗口。这是一个简单的控件,但是在如何配置和应用一个Tooltip方面仍然有很大的灵活性。你可以在图 11-5 的中看到一个工具提示的例子。HTML 中的其他控件用于配置Tooltip的不同方面,我将在下面的章节中解释。

images

图 11-5。一个工具提示控件

何时使用工具提示控件

当你想为用户提供指导或一些补充信息时,可以使用Tooltip控件,也许是为了帮助他们在选择应用中的特定选项时做出明智的选择,或者只是关于他们正在查看的内容的一些附加信息。

images 提示Tooltip是一个瞬态控件,会自动消失(我在下面解释发生这种情况的情况)。您不能对任何类型的交互式内容使用瞬变控件,如buttonRating控件。为此,您需要使用Flyout控件,我在第七章的中介绍了它,并在第十二章的中再次详细描述了它。

演示工具提示控件

您可以看到我添加到controls.js文件中的定义对象,以演示清单 11-12 中的控件。将使用tooltip键访问这些对象。

清单 11-12 。工具提示控件的定义对象

... tooltip: [     { type: "toggle", id: "infotip", title: "Infotip", value: false,         labelOn: "Yes", labelOff: "No" },     { type: "select", id: "placement", title: "Placement",         values: ["top", "bottom", "left", "right"],         labels: ["Top", "Bottom", "Left", "Right"]},     { type: "buttons", labels: ["Open", "Close"] }], ...

我在添加到UIControls项目的pages文件夹中的新 HTML 文件中使用了定义对象。您可以在清单 11-13 的中看到这个名为Tooltip.html的文件的内容。

清单 11-13 。/pages/Tooltip.html 文件的内容

`

              

    
        
            
I like                                      Apples                 , but I don't like Pears.


                
            

        


    


        

            <img srcimg/apple.png" />
            Apples grow on small, deciduous trees.                 Apples have been grown for thousands of years in Asia
                and Europe, and were brought to North America by European
                colonists.
            

        

    

`

该文件遵循与前面示例相同的基本模式。该文件比前面的例子要长,因为它包含了一些简单的内容,我使用本节中演示的Tooltip控件来显示这些内容。

另一个区别是,我在应用布局的右侧面板中为我创建的button元素添加了一个click事件的处理程序。这个处理程序调用由与被点击的button内容相对应的winControl属性定义的方法,因此点击Open按钮调用winControl.open方法,点击Close按钮调用winControl.close方法。我将很快解释这两种方法。

演示该控件的最后一步是将Tooltip.html文件添加到导航栏,我通过添加到清单 11-14 中所示的templates.js文件来完成。

清单 11-14 。将 Tooltip.html 文件添加到导航栏

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" }, **    { name: "Tooltip", icon: "\u0033" },** ]; ...

images 注意Tooltip.html文件中的标记包含一个img元素,其src属性引用了images文件夹中一个名为apple.png的文件。你可以获得我作为本书源代码下载的一部分使用的图像,或者,如果你喜欢,只需重命名你必须交给apple.png的图像,并将其复制到 Visual Studio 项目的images文件夹中。

应用和配置工具提示控件

基本前提是将与Tooltip相关的 HTML 元素包装在一个已经应用了WinJS.UI.Tooltip控件的div元素中,如清单 11-15 所示。

清单 11-15 。将工具提示控件应用于块元素

... **<div id="blockTooltip" data-win-control="WinJS.UI.Tooltip"** **    data-win-options="{contentElement: tooltipContent, infotip: true}">**         <button>Press Me</button> **</div>** ...

在这个取自/pages/Tooltip.html文件的片段中,我将一个button元素包装在一个已经应用了Tooltipdiv元素中。当用户将鼠标指针悬停或者将手指放在button元素上时,将显示由contentElement属性指定的元素的内容。在示例中,我已经将contentElement属性设置为tooltipContent,它引用了清单 11-16 中的元素。

清单 11-16 。用于工具提示控件的内容

`...

    
                 Apples grow on small, deciduous trees.             Apples have been grown for thousands of years in Asia             and Europe, and were brought to North America by European             colonists.              
...`

这个元素的内容是由imgspan元素组成的标准 HTML 标记。请注意,我已经将内容元素放在了一个div元素中,该元素的 CSS display属性被设置为none——这防止用户看到内容,直到它被Tooltip控件显示出来。

images 提示这是我使用了在上一节中添加到项目中 img/apple.png`文件的地方。你可以在图 11-6 中看到这个图像。

当鼠标指针悬停在图 11-6 中的Press Me按钮上时,可以看到结果。

images

图 11-6。设置工具提示控件的内容

contentElement是可以在Tooltip控件上设置的属性之一,可以通过data-win-options属性设置,也可以通过winControl属性以编程方式设置。你可以在表 11-8 中看到完整的属性集,我将在接下来的章节中演示它们。

创建内联工具提示&使用 innerHTML 属性

您还可以将Tooltip控件应用到span元素,如果您使用的是内嵌显示的内容,并且div元素会扰乱布局,这将非常有用。我在Tooltip.html文件中定义的另一个工具提示已经以这种方式应用,如清单 11-17 所示。

清单 11-17 。将工具提示控件应用于 span 元素

`...

I like                 Apples     , but I don't like Pears.
...`

Tooltip将应用的元素仍然围绕着它所涉及的内容,在这个例子中是单词 apple。当鼠标指针悬停在文本块中的单词 Apples 上时,Tooltip将显示给用户。

作为使用contentElement属性的替代方法,我已经使用innerHTML属性设置了Tooltip控件的内容。这个属性让你将Tooltip的内容设置为一个字符串值,这是我在图 11-5 中展示的工具提示。

设置显示持续时间

用户可以通过多种方式触发Tooltip。如果用户触摸或按住元素上的鼠标按钮,那么将显示Tooltip弹出窗口,直到手指从显示屏上移开或释放鼠标按钮。

如果用户通过键盘操作(比如在元素间跳转)或者将鼠标悬停在目标控件上来触发Tooltip,那么弹出窗口会显示 5 秒钟,之后会自动关闭。

通过将infotip属性设置为true,可以将这个时间延长到 15 秒。该值旨在用于包含大量信息且用户不太可能在较短时间内解析的Tooltip

该示例右侧面板中的ToggleSwitch为左侧面板中的内嵌Tooltip设置了infotip属性。通过使用这个开关,您可以看到不同时间跨度的效果。

images 提示如果你需要让你所有的Tooltip显示 15 秒,你可能要重新考虑你向用户呈现内容的方式。对于引导用户浏览你的应用的小信息片段来说,是完美的。考虑使用一个Flyout作为通用弹出窗口,正如我在第十二章中所描述的。

设置工具提示位置

placement属性控制Tooltip弹出窗口相对于目标元素出现的位置。默认值是top,意味着弹出窗口显示在元素的稍上方,但是您也可以指定leftrightbottom。示例中右侧面板中的select元素设置左侧面板中内联Tooltipplacement属性。你可以在图 11-7 中看到交替放置的效果。

images

图 11-7。左侧、底部和右侧工具提示位置

你不必担心调整placement值来确保Tooltip弹出窗口适合屏幕。如果由placement属性指定的值意味着弹出窗口不合适,WinJS 将自动调整弹出窗口的位置。表 11-9 显示了当弹出窗口不能显示指定值时,对于placement属性的每个可能值将尝试的一系列回退布局。

images 提示对于为左撇子用户配置的设备,回退序列中左右位置的顺序将会颠倒。

images

弹出窗口的确切位置是相对于鼠标或触摸事件的位置,并受到一个偏移量的影响,该偏移量取决于触发Tooltip的事件类型。键盘事件的偏移量是 15 像素(例如,当用户切换到目标元素时),鼠标事件的偏移量是 20 像素,触摸事件的偏移量是 45 像素。较大的偏移量旨在允许用户阅读Tooltip内容,而不会被光标、手写笔或手指遮挡。

以编程方式管理工具提示控件

通过使用openclose方法,你可以直接控制Tooltip何时显示和隐藏——尽管如果你使用这些方法,你应该暂停一会儿,考虑你使用Tooltip的方式是否与其他 Windows 应用一致。(如果你想要一个通用的弹出窗口,你应该使用一个Flyout,正如我在第十二章中描述的那样)。

为了快速参考,我在表 11-10 中描述了这些方法。这看起来可能是多余的,但是 WinJS UI 控件不使用一致的方法命名方案,当你希望能够快速发现Tooltip控件是否定义了showopenclosehide时,就会出现这种情况。

images

open方法采用可选参数,用于模拟不同种类的触发事件,支持的值有touchmouseovermousedownkeyboard。如果不提供参数,则使用default值。每个值都有略微不同的效果,包括用于定位弹出窗口的偏移量(如前一节所述)、弹出窗口显示前的延迟以及弹出窗口关闭前的显示时间。

您需要小心,因为touchmousedowndefault参数不会自动关闭弹出窗口。在touchmousedown模式下,这是因为弹出窗口会一直显示,直到手指从屏幕上移开或鼠标按钮松开。如果您对open菜单使用这些参数,那么您必须使用close方法显式关闭Tooltip弹出菜单。示例右侧面板中的OpenClose按钮调用了openclose方法(调用open方法时没有参数,因此直到使用Close按钮才会关闭弹出窗口)。

images 提示我发现不带参数调用open方法在开发过程中非常有用,因为它让我看到了Tooltip的内容是如何显示的,而无需执行触发操作。

设置工具提示控件的样式

Tooltip控件支持一个 CSS 类:win-tooltip。您可以使用该类作为设计显示内容样式的起点。没有其他类,因为在Tooltip中没有固定的元素结构。清单 11-18 显示了我在/css/default.css文件中为Tooltip控件定义的样式。

清单 11-18 。样式化工具提示控件

... .win-tooltip {     background-color: #8ED09C;     color: white;     border: medium solid white;     font-size: 16pt; } ...

处理工具提示事件

Tooltip控件支持我在表 11-11 中描述的四个事件。为了完整起见,我列出了这些事件,但是我还没有发现这些事件在实际项目中的用途,因为Tooltips旨在呈现简单、自包含的内容。

images

就像Tooltip控件的其他一些特性一样,如果你需要这些事件,我建议你停下来考虑一下你的应用设计。这可能是因为你有非常特殊的需求,但更有可能的是你要强迫Tooltip控件做一些打破 Windows 应用交互规则的事情。考虑一下我在第十二章的中描述的Flyout控制是否是一个更合适的选择。

总结

在这一章中,我已经向你介绍了三个最简单的 WinJS UI 控件——尽管,正如你所看到的,要在你的应用中获得正确的效果,还有很多细节需要考虑。在下一章,我将把注意力转向时间和日期选择器以及FlyoutMenu控件。

十二、使用TimePickerDatePickerFlyout

在这一章中,我继续探索 WinJS UI 控件,重点是TimePickerDatePickerFlyoutMenu控件。TimePickerDatePicker控件允许用户指定时间和日期,顾名思义。这些是带有一些设计问题的基本控件,这些设计问题使它们比它们本应具有的功能更难使用(也更没用)。你已经在第七章的中看到了Flyout控件,在那里我将它与 AppBar 一起使用。作为一个通用控件,?? 有着更广泛的存在,我解释了在这种情况下使用它所需要知道的一切。表 12-1 提供了本章的总结。

images

使用时间选择器控件

WinJS.UI.TimePicker控件允许用户选择时间。HTML5 在input元素中增加了支持时间和日期输入的功能,但是在 Internet Explorer 10 中不支持,你必须使用TimePickerDatePicker控件来代替(我将在本章后面描述DatePicker)。你可以在图 12-1 的中看到TimePicker控件是如何显示给用户的。

images

图 12-1。时间选择器控制和支持配置设置

何时使用时间选择器控件

当您需要用户指定一天中的时间时,您应该使用TimePicker控件。TImePicker控件尊重用户机器的语言环境设置,因此使用本地化的时间首选项捕获时间——尽管,正如您将看到的,这并不完全成功。

控件的质量没有我希望的那么好。众所周知,本地化的时间和日期首选项很难得到正确,必须做出一些让步,但即使如此,在TimePicker中反映出的不幸的设计选择使它的用处大大降低。我对这个控件和它的同伴DatePicker的总体印象是,这是一个仓促的工作,很少考虑控件将如何使用。

演示时间选择器控件

按照上一章的模式,我在第十章的中开始的 Visual Studio 项目的pages文件夹中添加了一个名为TimePicker.html的文件。您可以在清单 12-1 中看到该文件的内容。

清单 12-1 。TimePicker.html 文件的内容

`

` `         

    
        

Enter a time:


            <div id="picker" data-win-control="WinJS.UI.TimePicker"
                data-win-options="{minuteIncrement: 10}">
            

        


    

`

这是我需要代理对象特性的第一个控件,我在第十章的中将其添加到示例框架中。正如您将看到的,当我解释由TimePicker控件定义的属性如何工作时,我不容易将我为布局中的右侧面板生成的配置控件中的值映射到我可以与TimePicker属性一起使用的值。

您可以看到我如何在添加到/js/controls.js文件的定义对象中使用代理对象来演示清单 12-2 中的TimePicker控件。

清单 12-2 。将定义对象添加到 TimePicker 控件的 controls.js 文件中

... **timePicker**: [     { type: "toggle", id: "showPeriod", title: "Show Period", value: true,         useProxy: true, labelOn: "Yes", labelOff: "No" },     { type: "toggle", id: "clock", title: "24 Hour Clock", value: false,         useProxy: true, labelOn: "Yes", labelOff: "No" },     { type: "input", id: "hourLength", title: "Hour Length", value: 2,         useProxy: true },     { type: "input", id: "minuteLength", title: "Min Length", value: 2,         useProxy: true },     { type: "span", id: "current", value: "Pick a Time", title: "Value" }], ...

最后,为了让用户能够通过导航条导航到TimePicker.html文件,我添加了templates.js文件,如清单 12-3 所示。

清单 12-3 。增加了通过导航栏导航到 TimePicker.html 文件的支持

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" }, **    { name: "TimePicker", icon: "\u0034" },** ]; ...

应用和配置时间选择器控件

通过将data-win-control属性设置为WinJS.UI.TimePicker,将TimePicker控件应用于div元素。TimePicker控件支持许多配置属性,我已经在表 12-2 中总结了这些属性,并且我已经为这些属性创建了配置控件。我将在接下来的小节中详细解释这些属性。

images

images

设置时钟类型

您可以使用clock属性选择将要显示的时间类型。如果该属性设置为12HourClock,TimePicker显示三个选择元素,允许用户选择小时、分钟和周期(AMPM)。如果clock属性设置为24HourClock,那么只显示两个select元素,但是小时菜单中有 24 个项目,允许指定 24 小时时间。您可以使用标记为24 Hour Clock的控件更改示例中的clock属性,12 小时和 24 小时时钟显示如图图 12-2 所示。

images

图 12-2。设置时间选择器控制的时钟类型

设置分钟增量

minuteIncrement属性指定可以为分钟值选择的最小间隔。例如,将minuteIncrement属性设置为10将允许用户将分钟设置为01020304050分钟。值15将允许0153045分钟。问题是在控件初始化后更改属性没有任何效果——您必须决定您想要的时间间隔,并使用data-win-options属性指定它有一个配置选项。

在这个例子中,我使用data-win-options属性将minuteIncrement属性设置为10,如下所示:

`...

...`

你可以在图 12-3 的中看到这种效果,我点击了TimePicker控件的分钟部分来显示可用的增量。

images

图 12-3。在时间选择器控件上限制分钟增量

指定显示模式

通过hourPatternminutePatternperiodPattern属性,您可以指定时间的各个组成部分的显示方式。实际上,他们只是让你指定使用多少字符,即使这样,你也必须努力工作。

名称空间包含对象,?? 控件用它来格式化和解析时间值。DateTimeFormatter支持一个全面的基于模板的系统来处理时间和日期。这里有一个例子:

{hour.integer}:{minute.integer(2)}:{second.integer(2)} {period.abbreviated}

该模板包含小时、分钟和秒的组件。分和秒用两个字符显示,周期用缩写形式显示(例如AMPM)。

TimePicker控件中的模式属性作用于该模板的片段。您不能更改元素出现的顺序,但可以更改每个元素使用的字符数。因此,举例来说,如果您想确保小时总是使用 12 小时制显示,那么您可以将minutePattern属性设置为{hour.integer(2)}。括号字符({})是值的必需部分,这意味着在使用data-win-options属性设置这些属性的值时必须小心——很容易混淆括号和引号字符的顺序。

考虑到您可以对这些属性进行的唯一更改是设置字符数,我认为让TimePicker控件负责将整数值转换成模板片段会更明智。实际上,您需要对日期/时间格式的底层工作有足够的了解,但这样做不会有任何好处。但是,正如我之前所说的,TimePicker控件并没有经过特别的考虑或实现。

我在示例的右面板中包含了两个input元素,让您可以更改小时和分钟元素使用的字符数。我通过代理对象更新了hourPatternminutePattern属性,如清单 12-4 所示。

清单 12-4 。在整数值和模板片段之间转换

... ["hour", "minute"].forEach(function (item) {     proxyObject.bind(item + "Length", function (val) {         picker.winControl[item + "Pattern"] = "{" + item + ".integer(" + val + ")}";     }); }); ...

你可以在图 12-4 中的Hour LengthMin Length input元素中看到输入3的效果。

images

图 12-4。指定用于显示小时和分钟时间成分的字符数

以编程方式管理时间选择器

TimePicker控件没有定义任何方法。我包含这一部分只是为了保持与其他控件的一致性,这样您就不会认为它被错误地忽略了。

设置时间选择器控件的样式

TimePicker控件支持许多 CSS 类,这些类可以用来设计整个控件或其中一个元素的样式。我已经在表 12-3 中描述了类的集合。

images

在这个例子中,我使用Show Period配置控件通过win-timepicker-period类改变周期组件的可见性,如清单 12-5 所示。

清单 12-5 。使用 CSS 类定位 TimePicker 控件的组件

... proxyObject.bind("showPeriod", function (val) { **    $('.win-timepicker-period').setStyle("display", val ? "block" : "none");** }); ...

clock属性被设置为12HourClock时,改变标记为Show PeriodToggeSwitch的状态将改变周期select元素的可见性。

响应 TimePicker 事件

当用户改变控件显示的时间时,TimePicker发出change事件。你可以看到我对TimePicker.html文件中的script元素所做的添加,以处理清单 12-6 中的change事件,在那里我显示了用户选择的时间,用TimePicker作为应用布局右侧面板中span元素的内容。

清单 12-6 。从 TimePicker 控件处理变更事件

`...

...`

TimePicker.current属性返回一个标准的 JavaScript Date对象,它允许我调用toLocaleTimeString方法来获得一个值,我可以安全地在布局中显示该值,如图图 12-5 所示,在这里我使用了选取器来选择晚上 9:50。

images

图 12-5。响应时间选择器控件的变更事件

使用日期选择器控件

DatePicker控件是对TimePicker的补充,允许用户选择日期。DatePickerTimePicker控件有很多相似之处——可悲的是,包括一些不太有用的特征,比如模板片段。

DatePicker控件在结构和外观上与TimePicker控件非常相似,并为用户提供了三个select元素,用户可以用它们来选择日期。在图 12-6 中,你可以看到DatePicker是如何显示的,以及我生成的用来演示不同日期相关特性的配置控件。

images

图 12-6。日期选择器控件

何时使用 DatePicker 控件

DatePicker控件适合在您希望用户选择日期时使用。DatePicker控件使用设备区域设置来执行它的大部分日期格式化,并且像TimePicker一样,不提供覆盖区域设置的机制(尽管有一个使用不同种类日历的选项)。这意味着您应该只在一个应用中使用DatePicker,该应用已经在将要部署它的每个地区进行了彻底的测试。

演示日期选择器控件

我在第十章中开始的 Visual Studio 项目的pages文件夹中添加了一个名为DatePicker.html的文件。您可以在清单 12-7 中看到该文件的内容。

清单 12-7 。DatePicker.html 文件的内容

`

         

    
        

Select a date:


**            
**
            

        


    

`

这是另一个我需要代理对象特性的控件,我在第十章的中添加到示例框架中。DatePicker控件使用与TimePicker控件相同的模板模式系统,这意味着我需要代理对象来改变显示的日期格式(我将很快演示)。

您可以看到我如何在添加到/js/controls.js文件的定义对象中使用代理对象来演示清单 12-8 中的DatePicker控件。

清单 12-8 。将定义对象添加到 DatePicker 控件的 control.js 文件中

... **datePicker**: [     { type: "input", id: "dateLength", title: "Date Length", value: 2, useProxy: true },     { type: "select", id: "monthPattern", title: "Month Style", values: ["{month.full}",         "{month.abbreviated}"], labels: ["Full", "Abbreviated"]},     { type: "select", id: "yearPattern", title: "Year Style", values: ["{year.full}",         "{year.abbreviated}"], labels: ["Full", "Abbreviated"]},     { type: "select", id: "calendar", title: "Calendar", values: ["GregorianCalendar",         "HebrewCalendar", "ThaiCalendar"], labels: ["Gregorian", "Hebrew", "Thai"]},     { type: "span", id: "current", value: "Pick a Date", title: "Value"}], ...

最后,为了让用户能够通过导航条导航到TimePicker.html文件,我添加了templates.js文件,如清单 12-9 所示。

清单 12-9 。增加了通过导航栏导航到 TimePicker.html 文件的支持

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" }, ]; ...

应用和配置日期选择器控件

通过将data-win-control属性设置为WinJS.UI.DatePicker,将DatePicker控件应用于div元素。默认情况下有三个下拉菜单(但是可以使用 CSS 类隐藏——请参见设计 DatePicker 控件一节的详细信息),允许用户选择日期的日、月和年组成部分。DatePicker控件支持表 12-4 中列出和描述的配置属性。

images

images

使用不同的日历

属性允许你指定一个日历给 ?? 使用。默认值来自设备区域设置。在本例中,我在右侧面板中添加了一个select元素,允许您选择GregorianCalendarHebrewCalendarThaiCalendar值,其效果可以在图 12-7 的中看到。

images

图 12-7。使用不同的日历显示日期

我添加了对这些日历类型的支持,以展示可用的横截面。该属性支持的全套值为:GregorianCalendarHijriCalendarHebrewCalendarJapaneseCalendarKoreanCalendarThaiCalendarTaiwanCalendarUmAlQuraCalendarJulianCalendar

指定显示模式

DatePicker控件使用模板片段的方式类似于TimePicker控件,通过datePatternmonthPatternyearPattern属性公开。

datePattern的格式是{day.integer( n )},其中n是用于显示日期的字符数。注意,虽然属性的名称是***date***Pattern,但是片段是 **day** .integer(即dateday)。对于月份和年份组件,您可以选择完整值和缩写值。表 12-5 显示了这两个属性的一组支持值。

我添加了三个配置控件来管理日期的显示方式。通过Date Length input元素,您可以更改用于显示日部分的字符数,通过Month StyleYear Style select元素,您可以看到完整和简化显示的样子。在图 12-8 中,您可以看到缩写的月份和年份设置是如何显示的。

images

图 12-8。显示月份和年份的缩写值

以编程方式管理日期选择器

DatePicker控件没有定义任何方法。我包含这一部分只是为了保持与其他控件的一致性,这样您就不会认为它被错误地忽略了。

设置 DatePicker 控件的样式

DatePicker控件支持许多 CSS 类,这些类可以用来设计整个控件或其中一个元素的样式。我已经在表 12-6 中描述了类的集合。

images

我没有在例子中使用任何样式,但是我发现它们对于隐藏控件中的单个组件很有用,这样用户可以指定一个不太精确的日期。因此,举例来说,如果我想让用户只选择月份和年份,我会在DatePicker.html文件中添加一个style元素,如清单 12-10 中的所示。

清单 12-10 。使用 CSS 类隐藏 DatePicker 控件的组件

`...

...`

隐藏其中一个组件会导致控件被调整大小,如图 12-9 所示。

images

图 12-9。显示 DatePicker 控件中组件的子集

响应 DatePicker 事件

当用户选择一个新的日期时,DatePicker发出change事件。你可以看到我在DatePicker.html文件中添加了script元素来处理清单 12-11 中的change事件,这里我使用用户用DatePicker选择的日期来更新应用布局右侧面板中span元素的内容。

清单 12-11 。从 DatePicker 控件处理变更事件

`...

...`

DatePicker.current属性返回一个标准的 JavaScript Date对象,这允许我调用toLocaleDateString方法来获得一个我可以显示的值。

重访弹出控件

我在第七章的中向你介绍了WinJS.UI.Flyout控件,当时我向你展示了如何使用Flyout来响应AppBar命令。Flyout控件是一个通用的弹出菜单,可以在任何情况下使用,这就是为什么我要返回到这个控件,这样我就可以向你展示如何在应用栏之外使用它。我在这个部分的例子中使用了两个Flyout控件,其中一个你可以在图 12-10 中看到。右侧面板中的控件允许您配置可见的Flyout控件。

images

图 12-10。使用弹出控件

何时使用弹出控件

Flyout是一个通用控件,可以在任何想要在主布局之外呈现内容的情况下使用。Flyout有一些特殊的用途,比如使用AppBar(参见第七章)或者使用Menu控件(在本章稍后描述),但是除此之外,你可以在你的应用环境中做任何有意义的事情。对于简单的纯信息内容,考虑使用WinJS.UI.Tooltip控件,我在第十一章中描述了它,它需要更少的编程控制。

images 提示请务必阅读本章后面的使用弹出按钮进行用户交互一节,了解如何使用弹出按钮显示需要用户交互的元素,如button元素和Rating控件。在这些情况下,Flyout有一些特点需要特别注意。

演示弹出控件

为了演示Flyout控件,我在 Visual Studio 项目的pages文件夹中添加了一个名为Flyout.html的新文件。你可以在清单 12-12 中看到这个文件的内容。这个文件比我添加到项目中的其他一些文件稍大,因为它包含了Flyout控件和我将在其中显示的内容。

清单 12-12 。Flyout.html 文件的内容

`

              

    
        
                                  
        
` `    

**    

**
        

Apples


        
            Apples grow on small, deciduous trees.
            Apples have been grown for thousands of years in Asia
            and Europe, and were brought to North America by European
            colonists.
        

    

**    

**
        

How much do you like apples?


        

        

            
        

    

`

为了创建您可以在图 12-10 的右侧面板中看到的配置控件,我将定义对象添加到了您可以在清单 12-13 中看到的controls.js文件中。

清单 12-13 。弹出控件的定义对象

... **flyout**: [      { type: "select", id: "placement", title: "Placement", values: ["top", "bottom",              "left", "right"], labels: ["Top", "Bottom", "Left", "Right"]},      { type: "select", id: "alignment", title: "Alignment", values: ["left", "center",          "right"], labels: ["Left", "Center", "Right"]},      { type: "buttons", labels: ["Show", "Hide"] }], ...

最后,为了让用户可以通过导航栏导航到Flyout.html文件,我添加了templates.js文件,您可以在清单 12-14 中看到。

清单 12-14 。将 Flyout.html 文件添加到导航栏

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" }, **    { name: "Flyout", icon: "\u0036" },** ]; ...

应用和配置弹出控件

WinJS.UI.Flyout控件应用于div元素。Flyout没有固定的内部结构,您可以创建任何元素和控件的组合来满足您的需求,包括用户可以与之交互的内容——稍后我将向您详细介绍这项技术。

显示弹出控件

Flyout定义了showhide方法,可以用来控制控件的可见性。表 12-7 总结了这些方法以供快速参考。

images

show方法有一个强制参数,用它指定一个锚元素,它是Flyout所在的附近。show方法支持两个可选参数,允许您覆盖placementalignment属性的值——我将在下一节解释这些属性的用途和值。

Flyout控件支持我在第十二章中提到的 light-dissolve 技术,当用户点击或触摸Flyout弹出窗口之外的显示屏时,它会自动消失。您可以通过调用hide方法显式地解除Flyout,但是这只在您响应Flyout中的交互控件时有用,我将很快演示这一点。

示例中有两个Flyout控件,您可以通过单击右侧面板中的ShowHide按钮来查看如何应用showhide方法。点击Show按钮会出现一个显示一些静态文本的Flyout(这是图 12-10 中的Flyout)。当点击Show按钮时,我使用标记中的img元素作为锚元素,这就是为什么Flyout位于图中图像的正上方。您可以通过点击Hide按钮或点击Flyout之外的应用布局中的任意位置来关闭Flyout

images 提示点击Rate按钮,显示示例中的另一个Flyout。在本节稍后讨论使用Flyout控件向用户呈现交互式内容时,我会回到这个控件。

配置弹出控件

正如您现在所期望的,有许多由Flyout控件定义的属性,您可以使用它们来改变它的行为。表 12-8 总结了这些特性。

images

设置anchor属性没有用,因为该值将被show方法所需的强制参数替换。但是,您可以读取该属性的值来查看Flyout锚定到了哪个元素。

您可以使用placement属性覆盖Flyout的默认位置。这与Tooltip控件的工作方式相同,支持的值为topbottomleftright。这些值相对于传递给show方法的锚元素。

如果placement属性是topbottom,您可以使用alignment属性进一步细化位置,该属性接受值leftrightcenter

我在示例中添加到右侧面板的select元素允许您更改由ShowHide按钮控制的Flyoutplacementalignment属性。选择您需要的数值组合,点击Show按钮,查看Flyout是如何定位的。Flyout将自动重新定位,使其完全适合屏幕——然而,这可能意味着锚定元素可能会被Flyout遮挡。

在图 12-11 的中可以看到alignmentleftright值、placementtop值。

images

图 12-11。放置属性左右值的效果

设计弹出控件的样式

Flyout控件支持一个 CSS 类:win-flyout。您可以使用该类作为设计显示内容样式的起点。没有其他类,因为在Flyout控件中没有固定的结构。我倾向于不使用这个类,更喜欢将样式直接应用于我在Flyout控件中显示的内容。在清单 12-15 中,你可以看到我添加到使用win-flyout类的Flyout.html文件中的style元素。

清单 12-15 。设计弹出控件的样式

`...

...`

这种样式的效果是将所有Flyout控件中的文本居中。你可以在图 12-12 中看到它的效果。

images

图 12-12。对弹出控件应用样式的效果

处理弹出事件

Flyout控件支持我在表 12-9 中描述的四个事件。我在示例中没有使用这些事件,但是在处理包含交互内容的Flyout控件时,beforehideafterhide事件会很有用——更多信息请见下一节。

images

使用弹出按钮进行用户交互

示例应用中的第二个Flyout显示需要用户交互的控件——你可以在图 12-13 中看到这些控件是如何显示的。您可以使用一个Flyout来显示任何内容,但是当您从用户那里收集数据时,有一些特殊的考虑。

images

图 12-13。包含互动内容的弹出按钮

在这个例子中,我的Flyout包含一个Rating控件,允许用户表达他们喜欢苹果的程度。当用户单击示例应用左侧面板中的Rate按钮时,会显示Flyout。你必须小心设计你的Flyout交互来提供一致和流畅的用户体验。在接下来的部分中,我描述了我所遵循的并且推荐您采纳的指导原则。

确保一致的显示和隐藏

我总是小心翼翼地确保用户可以使用与显示相同的交互类型来隐藏Flyout。如果点击button导致Flyout出现,那么我确保在Flyout内容中有一个按钮可以隐藏弹出窗口。这是我在本章的例子中采用的方法:点击Rate按钮会显示Flyout,点击Close按钮会隐藏弹出窗口。我不想干涉灯光消失功能,这使得这个按钮成为隐藏Flyout的其他方式的补充,而不是替代。

保持弹出型按钮简单

我使用Flyout来执行小而简单的用户交互。对于复杂的 HTML 风格的表单,我不使用Flyout s,在这些表单中有大量的数据值需要从用户那里收集,并且这些值之间存在依赖关系。在这种情况下,使用导航到应用的不同部分,并在专用布局中收集数据,让用户清楚这是一个导入交互。

立即响应数据值

一旦用户在Flyout中向我提供信息,我就更新我的视图模型和应用状态。在这个例子中,我会从Rating控件中响应change事件,而不是等到Flyout被解除后才根据用户的意见更新应用。(我两者都没有做过,因为这是一个关于Flyout控件的例子,但是您已经明白了)。

使撤销或重做弹出型交互变得容易

我试图使Flyouts中的交互尽可能无摩擦,并允许用户轻松地更改他们输入的数据或返回到根本没有数据输入的状态。在很大程度上,这意味着我试图让用户非常清楚如何显示特定的Flyout,并且我利用 WinJS 控件特性和数据绑定来允许用户更改或删除数据。在这个例子中,允许用户删除数据需要我将Rating控件的enableClear属性设置为true

我也避免提示用户“您确定吗?”值被更改或删除时的消息。让更改值变得容易,并通过在应用中立即反映新值,使得检查用户的意图变得多余。

清楚地发出破坏性行动的信号

与使数据值更改变得容易相对应的是,当用户将要执行一个不可撤销的破坏性操作时,比如不可逆地删除一个文件,我使它变得非常明显。在这些情况下,我要求用户使用显示在Flyout中的button给我一个明确的确认,并把一个轻解雇作为一个取消。尽管如此,我还是让用户很容易地将破坏性的动作组合在一起,这样我就不会提示用户确认每个单独的项目应该被销毁。

总结

在这一章中,我已经展示了四个 WinJS UI 控件。这些比我在上一章描述的更复杂,但是它们可以更广泛地应用,并且在Flyout控件的情况下,可以形成你的应用结构的关键部分。在下一章,我将展示如何使用Menu控件,并演示一些不属于 WinJS 名称空间的 UI 控件。

十三、使用菜单和对话框

在这一章中,我将向你展示如何创建两种弹出界面控件。第一个是WinJS.UI.Menu控件,用于创建上下文菜单,允许用户直接在应用布局中的元素上执行操作。Menu控件很灵活,有一些有用的特性,但是我发现用户激活上下文菜单的机制不太明显,这意味着使用这个控件时需要仔细考虑。

我在本章中描述的第二个控件与我在本书中描述的其他控件略有不同。MessageDialog控件是Windows名称空间的一部分,它没有我为 WinJS UI 控件描述的共同特征:例如,它不适用于 HTML 元素,也没有winControl属性。任何 Windows 应用开发语言都可以使用MessageDialog控件,这使得它的使用稍微有些不同。但是,它提供了一个用户交互,这是使用任何 WinJS 控件都无法获得的,这使得掌握它的努力是值得的。表 13-1 对本章进行了总结。

images

images

使用菜单控制

Menu控件提供弹出上下文菜单,该菜单被构造为向用户提供一个或多个由MenuCommand控件表示的命令。MenuCommandMenu控件之间的关系类似于AppBarCommandAppBar之间的关系,我在第七章的中描述过。

默认情况下不显示Menu控件,所以本例中左侧面板中的主要元素是一个图像。如果用鼠标右键单击或触摸并按住图像,将会触发contextmenu事件。我通过使Menu控件出现来处理这个事件,如图图 13-1 所示。

images

图 13-1。显示菜单控件

何时使用菜单控制

为用户提供命令的主要机制是使用 AppBar,我在第七章中描述过。Menu控件提供了一种回退机制,当用户想要操作的对象不适合 AppBar 模型时,可以使用这种机制。坦率地说,这是一个非常主观的决定,我在项目中遵循的规则是尽可能地支持应用栏,因为并非所有用户都意识到 Windows 应用中可以使用上下文菜单。

命令应该在你的应用中只出现一次,这意味着你不应该在一个Menu上重复应用栏支持的命令。微软建议在一个Menu上最多使用 5 个命令,尽管 Windows 目前没有强制限制。

演示菜单控制

我在示例应用中添加了一个pages/Menu.html文件来演示MenuMenuCommand控件,你可以在清单 13-1 中看到该文件的内容。这是一个很长的列表,因为在这个例子中有两个Menu控件和许多菜单命令。

清单 13-1 。Menu.html 文件的内容

`

         

    

`

为了在图 13-1 的的右面板中创建配置控件,我对清单 13-2 中的文件进行了添加。

清单 13-2 。菜单控件的定义对象

... **menu**: [{ type: "select", id: "placement", title: "Placement",         values: ["top", "bottom", "left", "right"],         labels: ["Top", "Bottom", "Left", "Right"], useProxy: true},        { type: "select", id: "alignment", title: "Alignment",         values: ["left", "center", "right"],         labels: ["Left", "Center", "Right"], useProxy: true}], ...

为了允许用户导航到Menu.html文件,我添加了templates.js文件,如清单 13-3 所示。正如你在《??》第七章中回忆的那样,这些条目用于在应用的导航条上生成命令,以支持内容页面之间的导航。

清单 13-3 。将 Menu.html 添加到导航栏

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" },     { name: "Flyout", icon: "\u0036" }, **    { name: "Menu", icon: "\u0037" },** ]; ...

应用和配置菜单控件

WinJS.UI.Menu控件应用于div元素,并与AppBarFlyout控件共享一些共同的特性和功能。使用MenuCommand控件在Menu中定义命令,该控件应用于buttonhr元素,就像我在第七章的中描述的AppBarCommand控件一样。Menu控件支持与Flyout控件相同的配置属性集。表 13-2 总结了这些特性。

一个Menu的默认位置就在锚元素的正上方(如果有足够的屏幕空间)。您可以使用示例右侧面板中的select元素来更改菜单的位置。在图 13-2 中,您可以看到placement属性的leftright值的效果。

images

图 13-2。菜单放置属性的左右值的效果

images 注意如果你习惯于 Windows 桌面的上下文菜单,那么Menu弹出窗口的位置看起来会有点奇怪——就好像Menu与用户交互的元素是断开的。我发现这非常烦人,于是我花了一些时间研究如何移动弹出窗口,使它显示在我点击鼠标的地方旁边。在花了一些时间使用 WinJS Menu控件后,我开始意识到默认位置很有意义,因为它允许用户立即看到他们选择的命令的效果。这是我将在本章后面回到的内容,但是我建议您保持Menu的定位不变。

显示菜单

当用户用鼠标右键单击或触摸并按住屏幕时,Windows 会触发contextmenu事件。不幸的是,这是显示 NavBar 和 AppBar 的同一个事件,所以你需要确保在传递给你的处理函数的事件上调用preventDefault方法,如清单 13-4 所示。重要的是,只在那些你将显示Menu控件的元素上处理contextemenu事件,并让应用显示所有其他元素的导航栏和应用栏。

清单 13-4 。显示菜单控件并阻止默认行为

... targetImg.addEventListener("contextmenu", function (e) {     menu.winControl.show(e.target); **    e.preventDefault();** }); ...

定义菜单命令

使用MenuCommand控件定义显示在Menu上的命令。MenuCommand控件支持由AppBarCommand控件定义的属性子集,我在表 13-3 中总结了这些属性。我不打算详述所有这些属性,因为你可以在第七章中看到它们的影响。有几项技术值得指出,我将在接下来的章节中解释这些技术。否则,您可以从清单中看到如何在Menu控件中创建项目。

images 提示你可以在Menu中创建一个分隔符,将相关命令组合在一起。为此,将MenuCommand控件应用于hr元素,并使用data-win-options属性将type属性设置为separator。你可以在清单 13-1 中看到一个分隔符的例子。

images

创建菜单序列

您可以配置一个MenuCommand控件,当它被选中时显示另一个菜单,创建一个菜单链,允许用户在一组复杂的选项中导航。使用typeflyout属性将菜单关联在一起。你可以在清单 13-5 中的示例应用中看到我是如何做到这一点的。

清单 13-5 。使用弹出属性将方法链接在一起

... <button data-win-control="WinJS.UI.MenuCommand" class="border"     data-win-options="{id: "menuCmdBorderColor", label:"Border Color",         **type:"flyout", flyout:"borderMenu"**}"> </button> ...

对于这个标签为Border ColorMenuCommand,我将type属性设置为flyout,将flyout属性设置为另一个应用了Menu控件的元素的id。你可以在清单 13-6 中看到第二个Menu的定义。

清单 13-6 。应用第二个菜单控件

`...

              
...`

结果是当您从第一个Menu中选择Border Color项时,显示第二个Menu。你可以在图 13-3 中看到效果。第二个Menu代替了第一个,而不是出现在它旁边。

images

图 13-3。将菜单链接在一起

images 提示如果你愿意,你可以用一个Flyout控件代替第二个Menu。这对于向用户提供太复杂而不能用一组菜单项来处理的选项是有用的。也就是说,如果你需要一个Flyout,那么你可能要重新考虑你的策略,看看是否有一种更简单的方式向用户展示你的命令。

创建互斥的菜单项集合

我最常用MenuCommand来创建互斥的菜单项。我想在示例应用中创建两个这样的集合:第一个是主菜单中的RedWhiteGreen项目,第二个集合由较小的二级菜单中的RedBlack BorderWhite Border项目组成。

第一组对应于用于img元素的背景颜色,第二组对应于边框颜色。在这两种情况下,我都希望选择代表当前设置的MenuCommand,并在单击其他项目时保持该选择的最新状态。互斥菜单项易于设置,但这是一个手动过程,并且在MenuMenuCommand控件中没有特定的支持。

技术很简单——我需要将MenuCommand控件上的selected属性设置为用户选择的项目的true,并将同一互斥集合中所有其他MenuCommand的属性设置为false——最有效的方法是使用WinJS.Utilities.query方法定位给定集合中的所有元素。为了帮助我做到这一点,我已经确保每组中的MenuCommand控件都有一个我可以轻松识别的共同特征。对于主菜单中的RedWhiteGreen项,我将MenuCommand控件应用到的所有按钮元素分配给了背景类,如下所示:

`...
<button data-win-control="WinJS.UI.MenuCommand" class="background"
    data-win-options="{id: "menuCmdRed", label:"Red"}">

<button data-win-control="WinJS.UI.MenuCommand" class="background"
    data-win-options="{id: "menuCmdWhite", label:"White"}">

<button data-win-control="WinJS.UI.MenuCommand" class="background"
    data-win-options="{id: "menuCmdGreen", label:"Green"}">
...`

对于其他菜单中使用的button元素,我添加了一个自定义的data-*属性,如下所示:

... <button data-win-control="WinJS.UI.MenuCommand" **data-color="red"**     data-win-options="{id: "menuCmdRedBorder", label:"Red Border"}"> </button> <button data-win-control="WinJS.UI.MenuCommand" **data-color="black"**     data-win-options="{id: "menuCmdBlackBorder", label:"Black Border"}"> </button> <button data-win-control="WinJS.UI.MenuCommand" **data-color="white"**     data-win-options="{id: "menuCmdWhiteBorder", label:"White Border"}"> </button> ...

您通常会坚持一个可识别的特征,但是我想向您展示我最常用的两种方法。现在我可以很容易地用 CSS 选择器查询文档来定位集合中的所有MenuCommand,我可以更新Menu.html文件中的script元素来在用户选择菜单项时设置selected属性,如清单 13-7 所示。

清单 13-7 。确保菜单命令控件组上的互斥

... $("#menu, #borderMenu").listen("click", function (e) { `    if (WinJS.Utilities.hasClass(e.target, "background")) {
        targetImg.style.backgroundColor = e.target.winControl.label.toLowerCase();
**        WinJS.Utilities.query("button.background").forEach(function (menuButton) {**
**            menuButton.winControl.selected = (menuButton == e.target);**
**        });**
    } else if (e.target.winControl && e.target.winControl.id == "menuCmdShowBorder") {

var showBorder = e.target.winControl.selected;
        if (!showBorder) {
            targetImg.style.border = "none";
        }
        this.winControl.getCommandById("menuCmdBorderColor").disabled = !showBorder

} else if (e.target.hasAttribute("data-color")) {
        targetImg.style.border = "medium solid " + e.target.getAttribute("data-color");
**        WinJS.Utilities.query("button[data-color]").forEach(function (menuButton) {**
**            menuButton.winControl.selected = menuButton == e.target;**
**        });**
    }
});
...`

你可以在图 13-4 中看到在主菜单上选择一个项目然后选择另一个项目的效果。

images

图 13-4。创建互斥的菜单命令控件组

以编程方式管理菜单

Menu控件支持的方法类似于AppBar控件定义的方法,如表 13-4 所示,我在这里已经描述了这些方法。

我倾向于不使用显示和隐藏菜单命令的方法,因为它们让我觉得我的Menu结构太复杂了。我也更喜欢在Menu上保留相同的命令集,并简单地禁用那些目前不可用的命令。这就是我在示例中所做的。在选中边框命令之前,边框颜色命令是未选中的,如图图 13-5 所示。我宁愿清楚地表明命令确实存在,但并不适用,也不愿给出一组不断变化的命令。

images

图 13-5。禁用菜单命令而不是隐藏它

设计菜单控件的样式

Menu控件支持两个 CSS 类进行样式化,如表 13-5 中所述。

images

要将样式应用于MenuMenuCommand控件,您需要看看微软用来创建控件的 HTML 和 CSS,并设计一个选择器来覆盖默认情况下添加到 Visual Studio 应用项目中的ui-light.cssui-dark.css文件中定义的默认值。在清单 13-8 中,你可以看到我如何应用一个样式来改变被禁用的MenuCommand控件的显示颜色。

清单 13-8 。使用 win-menu CSS 类来设计 MenuCommand 控件的样式

`...

...`

我在示例中使用了一个Menu控件的id属性值,以确保我定义的样式比ui-dark.css文件中的样式更具体。你可以在清单 13-9 中看到默认情况下决定背景颜色的样式(我使用我在第二章中介绍的DOM Explorer窗口来决定)。

清单 13-9 。设置被禁用的 MenuCommand 的背景颜色的默认样式

.win-menu.win-ui-light button:focus, .win-ui-light .win-menu button:focus, .win-menu.win-ui- light button:active, .win-ui-light .win-menu button:active {    background-color: rgb(222, 222, 222); }

您可以看到类.win-ui-light是如何被使用的。这并不理想,但是一旦知道它的存在,解决这个额外的特性就足够简单了。你可以在图 13-6 中看到我定义的样式的效果。

images

图 13-6。改变被禁用的菜单命令控件的颜色

处理菜单事件

在处理菜单控件时,感兴趣的主要事件是click事件,当用户点击它时,该事件由MenuCommand控件触发,表示菜单项已被选择——您可以在清单 13-1 中看到我是如何处理该事件的,在清单 13-7 中也是如此。

Menu控件本身支持我在表 13-6 中描述的四个事件,这也是Flyout控件支持的四个事件。我还没有找到这些事件的令人信服的用途,这就是为什么我没有把它们包括在Menu控件的例子中。

images

使用 MessageDialog 控件

我在本书中描述的所有其他 UI 控件都在WinJS.UI名称空间中,并且是用 JavaScript 编写的。然而,有一个控件在这个名称空间之外,但它在创建遵循 Windows 外观的应用时非常有用。这个控件是Windows.UI.Popups.MessageDialog控件,你用它向用户显示一个对话框。你可以在图 13-7 中看到MessageDialog的例子。

images

图 13-7。消息对话框 UI 控件

images 注意Windows.UI.Popups命名空间也包含了PopupMenu控件。我没有在本书中描述这个控件,因为它与我在上一节中描述的WinJS.UI.Menu控件具有相同的功能。

何时使用 MessageDialog 控件

当你需要让用户注意到一些重要的事情或者当你需要做出一个重要的决定时,MessageDialog是很有用的。关键词是重要,因为当使用MessageDialog控件时,应用布局变暗,对话框显示在整个屏幕上。所有用户交互都被阻止,直到用户关闭对话框(通过按下EscapeEnter键)或点击/触摸其中一个对话框按钮。

我认为MessageDialog是最后的 UI 控件,因为它打断了用户的工作流程——这与 Windows 广泛的设计精神背道而驰。

您可以在由MessageDialog控件显示的对话框窗口中添加多达三个按钮,这意味着您无法为用户提供细微的选择。这意味着你不仅在打断用户,而且在强迫他们做出一个清晰明确的决定,通常是一个yes / no的选择。

你应该谨慎而不情愿地使用MessageDialog。在我自己的应用项目中,我只在用户将要启动一个会导致永久、不可恢复的数据丢失的操作时使用MessageDialog,比如删除文件或其他数据。我建议你使用同样的约束——不仅因为MessageDialog是侵入性的,而且因为如果你太自由地使用它,用户将学会不读它就关闭对话框——增加了他们忽略真正重要的信息的机会。

images 提示你可以使用一个Flyout向用户显示一个对话框,但是它不会自动使布局的其余部分变暗,也不会像 MessageDialog 控件那样阻止用户交互。

演示消息对话框控件

为了演示MessageDialog控件,我在 Visual Studio 项目的pages文件夹中添加了一个名为MessageDialog.html的新文件。你可以在清单 13-10 中看到这个文件的内容。

清单 13-10 。MessageDialog.html 文件的内容

`

    


            
        
        

    

`

为了在布局的右面板中生成我需要演示的配置控件MessageDialog控件,我已经将清单 13-11 中所示的定义对象添加到了/js/controls.js文件中。

清单 13-11 。MessageDialog 控件的定义对象

... **messagedialog**: [     { type: "toggle", id: "delay", title: "Ignore Input", value: false, labelOn: "Yes",         labelOff: "No", useProxy: true },     { type: "toggle", id: "title", title: "Title", value: true, labelOn: "Yes",         labelOff: "No", useProxy: true },     { type: "toggle", id: "addcommands", title: "Add Commands", value: true,         labelOn: "Yes", labelOff: "No", useProxy: true },     { type: "span", id: "commandSpan", value: "<Ready>", title: "Command" }], ...

最后,我向/js/templates.js文件添加了如清单 13-12 所示的内容,这样用户就可以使用应用的导航条导航到MessageDialog.html文件。

清单 13-12 。启用 MessageDialog.html 文件导航

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" },     { name: "Flyout", icon: "\u0036" },     { name: "Menu", icon: "\u0037" }, **    { name: "MessageDialog", icon: "\u0038" },** ]; ...

你可以在图 13-8 中看到这些增加的布局。与本书这一部分中的其他控件一样,您可以使用布局右侧面板中的控件来配置MessageDialog控件,以探索我在接下来的章节中描述的特性。

images

图 13-8。MessageDialog.html 文件的布局

使用 MessageDialog 控件

MessageDialog控件的工作方式与 WinJS UI 控件不同。该控件没有应用于 HTML 元素,也没有winControl属性。相反,您创建一个Windows.UI.Popups.MessageDialog对象并使用它定义的属性来配置对话框并显示给用户。MessageDialog控件支持表 13-7 中显示的属性和方法,我将在下面的章节中描述和演示。

images

创建基本对话框

显示对话框最简单的方法是创建一个新的MessageDialog对象并调用showAsync方法。构造函数MessageDialog有一个强制参数,它是一个字符串,包含应该在对话框中显示的内容。有一个可选参数,用作title属性的值。

您可以使用示例应用看到最基本的对话框。将右侧面板中的所有ToggleSwitch控件设置为No并点击Show MessageDialog按钮。你可以在图 13-9 中看到结果。(我编辑了这一部分的图片,以便更容易看到细节)。

images

图 13-9。用消息对话框控件创建的基本对话框

当您创建一个基本对话框时,MessageDialog控件会为您添加一个Close按钮,当它被点击时会自动关闭对话框。你可以在图 13-10 中看到给对话框添加一个标题的效果——这个效果是我在例子中通过将Title ToggleSwitch设置为Yes实现的。

images

图 13-10。一个带有标题的基本对话框

添加自定义命令

您可以通过放置最多带有三个自定义按钮的Close按钮来自定义对话框。这些按钮使用Windows.UI.Popups.UICommand对象指定,该对象定义了表 13-8 中所示的属性。

images

在示例应用中,您可以通过将标记为Add CommandsToggleSwitch设置为Yes来向对话框添加命令。我通过创建新的UICommand对象并对从MessageDialog对象的commands属性中获取的对象使用append方法来添加命令。我重复了清单 13-13 中的例子中的相关语句。

清单 13-13 。向对话框添加命令

... if (proxyObject.addcommands) { **    ["Yes", "No", "Help"].forEach(function (text) {** **        md.commands.append(new winpop.UICommand(text));** **    });**     md.defaultCommandIndex = 0;     md.cancelCommandIndex = 1; } ...

命令按照添加的顺序显示,你可以在图 13-7 中看到添加这些命令按钮的效果。

确定点击的命令

您可以使用从showAsync方法返回的对象来确定用户单击哪个按钮来关闭对话框。showAsync方法实际上并不返回一个WinJS.Promise对象,但是它返回的对象定义了thencanceldone方法,通常可以像Promise一样使用。没有取回WinJS.Promise对象的原因是MessageDialog控件不是 WinJS 库的一部分,并且不知道 JavaScript 如何在应用中工作。该对象可以像Promise一样使用的原因是因为微软努力确保所有 Windows 应用开发语言的 API 的一致性,并以 JavaScript 可以使用的方式包装异步操作。

当您对从showAsync方法返回的对象使用then方法时,您的函数被传递给代表用户单击的对话框按钮的UICommand。在清单 13-14 中,你可以看到我如何使用这个特性来设置示例布局右侧面板中span元素的内容,这里我复制了示例应用中的相关代码。

清单 13-14 。确定用户点击了哪个对话框按钮

... md.showAsync().then(function (command) {     commandSpan.innerText = command.label; }); ...

showAsync方法中获得的类似于Promise的对象直到用户关闭对话框后才会实现。

设置默认命令

您可以使用defaultCommandIndexcancelCommandIndex来指定当用户点击EnterEscape键时将被传递给then功能的UICommand。这些属性被设置为使用commands属性添加的UICommands对象的索引,你可以在清单 13-15 的例子中看到我是如何设置这些属性的。

清单 13-15 。指定默认命令

... if (proxyObject.addcommands) {     ["Yes", "No", "Help"].forEach(function (text) {         md.commands.append(new winpop.UICommand(text));     });     md.defaultCommandIndex = 0;     md.cancelCommandIndex = 1; } ...

您的then函数将在您指定的索引处被传递给UICommand,即使用户没有明确点击任何对话框按钮。在清单中,我指定的索引值意味着使用Enter键相当于单击Yes按钮,使用Escape键相当于单击No按钮。

images 注意确保你始终如一地使用默认命令功能,这样Escape键总是取消,Enter键总是确认你呈现给用户的动作或决定。

延迟响应用户交互

MessageDialog.options属性允许你使用来自Windows.UI.Popup.MessageDialogOptions对象的值配置控件的行为,在表 13-9 中有描述。出于某种原因,MessageDialogOptions对象只定义了一个特定行为的值,即当对话框被MessageDialog对象显示时,在一小段时间内禁止用户交互。

images

您可以通过将标签为Ignore InputToggleSwitch设置为true来应用示例应用中的acceptUserInputAfterDelay行为。当你点击Show MessageDialog时,对话框上显示的命令按钮将被暂时禁用,防止命令被点击。几秒钟后,按钮被激活,允许用户像往常一样与对话框交互。

总结

在下一章,我将返回到WinJS.UI名称空间并描述FlipView控件。这是第一个也是最简单的 WinJS 数据驱动控件,它提供了由数据对象集合支持的功能。正如你将了解到的,数据驱动控件建立在我在第八章中介绍的数据绑定和模板特性的基础上,以便灵活地向用户显示那些数据项。

十四、使用FlipView控件

在这一章中,我将描述FlipView控件,它是 WinJS 数据驱动的 UI 控件之一。数据驱动控件从数据源获取内容,并监控数据源以确保它们显示最新的可用数据。FlipView控件一次显示数据源中的一个项目,并允许用户通过单击按钮或做出翻转触摸手势在它们之间移动。

在本章中,我将向您展示如何为数据驱动控件准备数据源,并将其与用于向用户呈现每一项的模板相结合。您将看到 WinJS 的许多特性是如何在一个数据驱动控件中结合在一起的,包括可观察数组、数据绑定和模板。我还将向您展示这些特性是如何相互作用导致棘手的问题的。最后,我将介绍一下FlipView控件在从一个数据项移动到下一个数据项时使用的动画,作为在第十八章中全面讨论这个主题的前奏。表 14-1 对本章进行了总结

images

使用 FlipView 控件

数据驱动的 WinJS UI 控件使用模板来呈现数据源中的项。在这一章中,我将使用一个FlipView来执行一个非常普通的任务——一次显示一组照片中的一张图片。你可以在图 14-1 中看到FlipView是如何出现的。如同本书这一部分的其他章节一样,我使用我在第十章中创建的框架创建了如图所示的布局。

images

图 14-1。用来显示一组照片的 FlipView 控件

有三个数据驱动的 WinJS UI 控件,但我选择了FlipView作为第一个描述,因为它相对简单,但基于许多有用和重要的功能。我可以相对快速地处理控件的使用,然后将重点放在一些关键的基础上,这些基础与我稍后描述的更复杂的控件是相同的。

数据源和接口

WinJS 定义了一组指定数据源功能的接口。如果您使用的是标准的数据源对象,您就不需要担心这些接口,这些对象适用于大多数项目。然而,如果您想要创建一个定制的数据源,那么您需要理解这些接口所扮演的角色。

首先要理解的是,接口的概念在 JavaScript 中没有任何意义。这是您可以看到 JavaScript 和其他 Windows 应用编程语言的契合之处之一。在像 C#这样的语言中,定义一个接口意味着列出特定用途所需的抽象功能。实现接口的程序员实际上同意以某种方式实现该功能。编译器检查以确保接口的所有方面都已实现,如果没有实现,则报告一个错误。在用强类型语言创建松散耦合的软件系统时,接口是一种有用的技术。

接口的想法依赖于不适合 JavaScript 的假设,但是微软需要阐明一种可以描述一组预期的属性和方法的方式,所以我们有了这个想法,但是没有接口的实现。当我提到接口时,我指的是一个必需成员的列表,如果你希望它适合特定的用途,比如作为像FlipView这样的 UI 控件的数据源,你必须在你的对象中实现这些成员。

何时使用 FlipView 控件

每当您需要一次显示一项内容时,就可以使用FlipView控件。在这一章中,我展示了这个控件最常见的用法,它允许用户在媒体中翻页——在这个例子中是一些怒目而视者的照片。然而,FlipView控件可以显示任何内容,并且有一个很好的模板系统,可以让你控制每个项目如何呈现给用户。在后面的章节中,你可以看到使用其他类型的数据来驱动 WinJS UI 控件的例子。

创建 FlipView 控件示例

数据驱动控件比WinJS.UI名称空间中的其他控件更复杂,因此我采用了一种稍微不同的方法来创建演示FlipView控件的代码。我将分解示例代码,使 HTML、CSS 和 JavaScript 位于不同的文件中,更重要的是,在本节结束时,示例不会完成。我将在解释数据源和使用它们的 UI 控件的一些核心特征和特性时完成这个例子。

首先,我在 Visual Studio 项目的pages文件夹中创建了一个名为FlipView.html的新文件,其内容可以在清单 14-1 中看到。这个文件包含创建一个基本的FlipView控件的标记,但是它不包含任何对数据源的引用,而数据源是让FlipView显示数据所必需的。

清单 14-1 。FlipView.html 文件的初始内容

`

     **    ** **    **     
        
**            
**         

    

`

您可以在清单中看到我使用data-win-control应用了FlipView控件的元素。您还可以看到我为 CSS 和 JavaScript 文件添加的linkscript元素。这个/css/flipview.css文件包含了一些我需要用来控制FlipView控件布局的样式,以及我将用它来显示的数据。您可以在清单 14-2 中看到 flipview.css 文件的内容。

清单 14-2 。/css/flipview.css 文件的内容

#flip { width: 400px; height: 400px } .flipItem img { height: 400px } .flipTitle { position: absolute; color: black;     bottom: 2px; font-size: 30pt; width: 100%;     padding: 10px; background-color: rgba(255, 255, 255, 0.6)} .renderDiv { border: thick solid white; height: 200px} .renderDiv img { height: 200px; width: 200px } .renderDiv div { text-align: center; font-size: 30pt }

您可以在清单 14-3 中看到 JavaScript 文件的内容。我想将数据驱动控件的代码与 JavaScript 的其余部分分开,所以我创建了/js/pages文件夹,然后在那里添加了flipview.js文件——这就是您可以在清单中看到的文件。

清单 14-3 。flipview.js 文件的内容

`(function() {

WinJS.UI.Pages.define("/pages/FlipView.html", {
        ready: function () {

var proxyObject = WinJS.Binding.as({
                itemTemplate: null,
                customAnimations: false
            });

Templates.createControls(rightPanel, flip, "flipView", proxyObject)
            .then(function () {

});
        }
    });
})();`

目前,flipview.js文件只包含代理对象,我将用它来响应一些配置控件和对将创建它们的Templates.createControls方法的调用。我将在本章中添加代码,解释数据驱动控件的不同特性,特别是FlipView控件。在清单 14-4 中,你可以看到我添加到controls.js文件中的定义对象,以创建图 14-1 右侧面板中显示的配置控件。为了与本书这一部分的其他章节保持一致,我将使用这些控件来演示FlipView控件的关键特性。

清单 14-4 为 FlipView 控件添加到 controls.js 文件中的定义对象

... flipView: [      { type: "select", id: "itemTemplate", title: "Template",          values: ["HTML", "Function"], useProxy: true},      { type: "select", id: "orientation", title: "Orientation",          values: ["horizontal", "vertical"], labels: ["Horizontal", "Vertical"]},      { type: "input", id: "itemSpacing", title: "Item Spacing", value: 10 },      { type: "span", id: "currentPage", value: 0, title: "Current Page" },      { type: "buttons", title: "Move", labels: ["Previous", "Next"] },      { type: "span", id: "itemCount", value: 4, title: "Count" },      { type: "buttons", title: "Change Data", labels: ["Add", "Remove"] },      { type: "toggle", id: "customAnimations", title: "Custom Animations", value: false,         useProxy: true, labelOn: "Yes", labelOff: "No" }], ...

本章中设置基本结构的最后一步是启用从导航栏到FlipView.html页面的导航。你可以在清单 14-5 中看到我是如何做到这一点的,它显示了我对/js/templates.js文件所做的添加。

清单 14-5 。通过导航栏导航至 FlipView.html 页面

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" },     { name: "Flyout", icon: "\u0036" },     { name: "Menu", icon: "\u0037" },     { name: "MessageDialog", icon: "\u0038" }, **    { name: "FlipView", icon: "pictures" },** ]; ...

FlipView控件是我描述的第一个 WinJS 数据驱动控件,顾名思义,我需要一些数据来处理。在本章中,我将使用FlipView来显示一些图像文件,这是该控件的典型用法。你可以通过数据驱动的 WinJS 控件使用任何类型的数据,包括FlipView,但是我现在想保持事情简单。

为了将数据图像与应用图像分开,我创建了一 img/data文件夹,并将我的图像文件复制到那里。这些图像是花的照片,你可以在图 14-2 的解决方案浏览器中看到它们。我在本书附带的源代码下载中包含了这些图片(可从Apress.com`获得)。

images

图 14-2。将数据图像添加到 Visual Studio 项目中

如果此时运行应用,您将会看到如图图 14-3 所示的布局。FlipView控件在布局中,但它还不可见——这是因为我还没有设置我想要显示的图像和FlipView控件之间的关系——我将很快解决这个问题。

images

图 14-3。将基础文件添加到 Visual Studio 项目后 app 的状态

创建和使用数据源

使用数据驱动 UI 控件的关键步骤是创建数据源。方法是由数据源实现的属性,数据源由WinJS.UI.IListDataSource接口定义,如果您想要创建自己的数据源,您需要定义一个对象来实现IListDataSource定义的所有方法和属性,以便公开您的数据。

我不打算详细介绍这个界面,因为几乎在每种情况下都可以使用一些预定义的数据源。WinJS.UI.StorageDataSource可用于从设备文件系统加载数据——我将在本书的第四部分中更广泛地回到文件和数据,并且我将在第二十三章中演示StorageDataSource对象。

在本章中,我将使用另一个预定义的数据源,即WinJS.Binding.List对象。我在第八章中解释了如何为数据绑定创建可观察数组,并一直在例子中使用。List对象有一个dataSource属性,它返回一个实现数据源所需的所有方法和属性的对象。这使得List非常适合在处理内存数据时使用数据驱动的 UI 控件。

因此,首先,我将创建一个包含我想要显示的图像细节的List对象。在清单 14-6 中,你可以看到我对/js/viewmodel.js文件所做的添加。

清单 14-6。在/js/viewmodel.js 文件中定义列表

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel", {
**        data: {**
**            images: new WinJS.Binding.List([**
**                { file:img/aster.jpg", name: "Aster"},**
**                { file:img/carnation.jpg", name: "Carnation"},**
**                { file:img/daffodil.jpg", name: "Daffodil"},**
**                { file:img/lily.jpg", name: "Lilly"},**
**            ]),**
**        }   **
    });

})();`

我创建了一个名为images的新的List对象,并将其分配给了ViewModel.data名称空间。List中的每个对象都有一个包含图像文件路径的file属性和一个包含我将向用户显示的名称的name属性。

images 提示List只包含四个图像文件的对象——我将在本章后面使用其他图像。

应用数据源

要应用数据源,需要设置两个FlipView属性。属性告诉 ?? 在哪里可以找到数据。itemTemplate属性指定了一个模板,该模板将用于显示数据源中的项目——这与我在第八章的中描述的数据绑定模板类型相同。在清单 14-7 中,你可以看到我是如何定义模板并在/pages/FlipView.html文件中设置FlipView属性的值的。

清单 14-7 。设置数据源和模板

`

               **    
** **        
** **            ** **            
** **        
** **    
**


        

            <div id="flip" data-win-control="WinJS.UI.FlipView"
**                data-win-options="{itemTemplate: select('#ItemTemplate'),**
**                   itemDataSource: ViewModel.data.images.dataSource}">**
            

        


    

`

我定义的模板使用数据对象的 file 属性来设置一个img元素的src属性,并显示name属性的值——我用来在模板中布局元素的样式在本章前面列出的/css/flipview.css文件中。

我已经使用data-win-options属性设置了 UI 控件的选项。请注意我是如何为itemTemplate属性指定值的:

itemTemplate: **select('#ItemTemplate')**

select关键字用于定位标记中的元素,它将一个 CSS 选择器作为参数,在本例中,该参数是我应用了WinJS.Binding.Template控件的元素的id属性值。

对于itemDataSource属性,注意我已经指定了我在/js/viewmodel.js文件中创建的WinJS.Binding.List对象的dataSource属性,如下所示:

itemDataSource: ViewModel.data.images.**dataSource**

FlipView控件——实际上是所有数据驱动的 UI 控件——没有关于List对象功能的特殊知识,并期望接收一个定义了在IListDataSource对象中列出的一组方法和属性的对象。这意味着当你告诉一个数据驱动的 UI 控件使用一个List对象作为数据源时,你必须记住使用dataSource属性。

修复第一个图像问题

我已经设置了模板和数据源,但是如果你现在运行这个应用,你会看到一个问题。数据源中的第一项显示不正确。但是,如果您滑动FlipView控件或将鼠标移动到它上面并单击出现的箭头,您将看到第二个和后续项目显示正常。你可以在图 14-4 中看到问题。这是使用FlipView控件时经常遇到的问题,我倾向于把它看作是的第一个图像问题

images

图 14-4。flip view 第一个图像问题

这个问题是由WinJS.UI.Pages API、WinJS.Binding API 和FlipView控件本身之间不幸的交互引起的。当你点击FlipView控件的NavBar命令时,WinJS.UI.Pages.render方法被用来加载FlipView.html文件。

作为这个过程的一部分,render方法自动调用WinJS.UI.processAll方法,以便正确创建导入内容中的任何 WinJS UI 控件。

processAll方法的自动调用初始化了FlipView控件。作为初始化的一部分,FlipView定位我使用itemTemplate属性指定的元素,使用这个模板生成显示第一个项目所需的内容,包括处理数据绑定,为img元素设置src属性。

在这个阶段,FlipView已经从模板中创建了 HTML,如下所示:

`...

         
Aster
...`

您可以从代码片段中看到,数据绑定已经过处理,因此来自第一个数据源项的值反映在 HTML 元素中。

然而,您也可以看到(为了强调,我突出显示了它们),模板中最初的data-win-bind属性已经被复制到新内容中。

那些挥之不去的属性是问题的一部分。尽管为导入的内容自动调用了WinJS.UI.processAll方法,但没有调用WinJS.Binding.processAll方法。为了确保我的绑定被解析,我在default.js文件中的WinJS.Navigation事件的处理程序中直接调用这个方法,如下所示:

... WinJS.Navigation.addEventListener("navigating", function (e) {     WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {         WinJS.Utilities.empty(contentTarget);         WinJS.UI.Pages.render(e.detail.location, contentTarget)             .then(function () {                 return **WinJS.Binding.processAll(contentTarget, ViewModel.State)**                     .then(function () {                         return WinJS.UI.Animation.enterPage(contentTarget.children)                     });             });     }); }); ...

在这种情况下,default.js文件中的代码执行一系列常见的操作:导入内容、处理数据绑定并执行动画。

问题是当WinJS.Binding.processAll方法运行时,它遍历 DOM 寻找定义data-win-bind属性的元素,并找到FlipView控件从模板中创建的元素。尽管这些元素的数据绑定已经被解析,但是processAll方法会再次查看它们并尝试理解它们。

WinJS.Binding.processAll方法将undefined插入到任何引用它无法解析的数据值的数据绑定中。它将无法解析的带有innerText绑定的任何元素的内容保留为空。这不是最有帮助的行为,因为processAll方法试图应用ViewModel.State对象,而绑定旨在处理数据源项,所以processAll方法覆盖了模板元素中的值,如下所示:

`...

         
     ...`

因此,WinJS 功能的各个部分之间的交互意味着第一个数据项不能正确显示,因为img元素上的src属性被设置为 undefined,而我用于图像名称的 div 元素的内容为空。

FlipView控件按需生成显示数据项所需的元素,这意味着在WinJS.Binding.processAll方法完成其工作很久之后,直到您通过单击或滑动前进到下一项,第二个和后续数据项的元素才从模板中生成——这就是为什么只有第一个数据项受到影响。

解决第一个图像问题

有两种方法可以解决这个问题,这两种方法都不需要我修改WinJS.Navigation事件的处理程序。我用来处理导航的功能模式在其他情况下也很好,使得在导入的内容中使用数据绑定变得轻而易举。在接下来的部分中,我将向您展示解决这个问题的解决方案。

以编程方式指定模板或数据源

第一种解决方案是在内容页面的ready函数中的FlipView控件上设置itemTemplate属性或itemDataSource属性。当您更改任一属性的值时,FlipView控件将重新生成其内容,因为有一个重要的更改需要显示给用户。为了演示这个解决方案,我必须将该语句添加到清单 14-8 中的/js/pages/flipview.js文件中。

清单 14-8 。设置 itemTemplate 属性的值

`(function() {

WinJS.UI.Pages.define("/pages/FlipView.html", {
        ready: function () {
            var proxyObject = WinJS.Binding.as({
                itemTemplate: ItemTemplate,
                customAnimations: false
            });

Templates.createControls(rightPanel, flip, "flipView", proxyObject)
            .then(function () {

**                flip.winControl.itemTemplate = ItemTemplate;**
            });
        }
    });
})();`

当你现在启动应用时,你会看到第一项显示正确,如图图 14-5 所示。这个解决方案之所以有效,是因为在调用了WinJS.Binding.processAll方法之后执行了ready函数中的代码,这意味着FlipView从模板中生成的元素不会针对data-win-bind属性被再次处理。

images

图 14-5。通过以编程方式设置 itemTemplate 属性来修复第一个图像问题

当您导航到FlipView.html页面时,您可能会注意到轻微的闪烁,在纠正之前,损坏的图像会显示一秒钟。这是因为当页面第一次加载时,itemTemplate属性仍然以声明方式设置和处理。为了解决这个问题,我需要从应用了FlipView控件的元素的data-win-options属性中删除itemTemplate属性的声明值,如清单 14-9 所示,只留下itemDataSource属性的值。

清单 14-9 。移除 itemTemplate 属性的声明值

`...

    
**        
**         
    

...`
使用函数作为模板

另一个解决方案是去掉声明性模板,使用一个函数来生成元素,供FlipView控件用来显示每个数据项。你可以看到我是如何在清单 14-10 的/js/pages/flipview.js文件中添加这样一个函数的。

清单 14-10 。添加生成显示元素的功能

`(function() {

**    function renderItem(itemPromise) {**
**        return itemPromise.then(function (item) {**
**            var topElem = document.createElement("div");**
**            WinJS.Utilities.addClass(topElem, "renderDiv");**
**            var imgElem = topElem.appendChild(document.createElement("img"));**
**            imgElem.src = item.data.file;**
**            var titleElem = topElem.appendChild(document.createElement("div"));**
**            titleElem.innerText = item.data.name;**
**            return topElem;**
**        });**
**    }**

WinJS.UI.Pages.define("/pages/FlipView.html", {
        ready: function () {

var proxyObject = WinJS.Binding.as({
                itemTemplate:ItemTemplate,
                customAnimations: false
            });

Templates.createControls(rightPanel, flip, "flipView", proxyObject)
            .then(function () {

**                proxyObject.bind("itemTemplate", function (val) {**
**                    flip.winControl.itemTemplate =**
**                        val == "HTML" ? ItemTemplate : renderItem;**
**                });**

});
        }
    });
})();`

当您使用函数生成项目模板时,传递给您的参数是一个IItemPromise对象。这是一个常规的WinJS.Promise,但是当Promise完成时传递给then函数的参数是一个IItem对象。(这两个I是故意的——开头的I表示一个接口)。IItem对象提供了对数据源项的访问,并定义了我在表 14-2 中描述的属性。

要创建一个模板,您需要在IItemPromise对象上调用then方法,指定一个函数,该函数将在IItem对象准备好时接收它(这允许数据源执行某种后台操作——比如查询数据库或从磁盘读取文件——异步完成)。当您获得IItem对象时,您可以使用表中的属性查询它,以生成向用户显示它所需的元素。

images 提示使用函数生成模板时,必须返回单个HTMLElement对象,尽管这个顶级元素可以包含任意多个子元素。这与声明性 HTML 模板的约束相同。

为了帮助演示不同的模板方法,我改变了 JavaScript 文件中设置itemTemplate属性的方式,将它链接到应用布局右侧面板中的select元素。当您从select元素中选取HTML值时,将使用前一节中的声明性模板,当您选取Function值时,将应用清单中的renderItem函数。renderItem功能为数据项产生一种不同的布局样式,以便更容易看出正在使用哪一个,如图 14-6 所示。

images

图 14-6。为 FlipView 控件生成元素的不同技术

这两种方法都解决了第一个图像问题,您可以选择适合您的编程风格的方法。在大多数情况下,我倾向于使用声明性模板,但是使用函数生成元素会更加灵活,尤其是当您希望根据单个数据项来定制元素的内容时。

配置 FlipView 控件

既然我已经解决了第一个图像问题和解决方案,我可以转向FlipView控件支持的其他配置属性,我已经在表 14-3 中总结了这些属性。

images

itemTemplateitemDataSource是最重要的属性,在解释如何解决第一个图像问题时,我已经解释了如何使用它们。在接下来的部分中,我将使用我在应用布局的右侧面板中创建的一些配置控件来演示orientationitemSpacing属性。在本章的后面,当我谈到以编程方式操作FlipView控件时,我将演示currentPage属性。

设置方向

属性允许你改变用户在数据源中前后移动的方向。默认值为horizontal,表示用户在触摸屏上左右滑动或者用鼠标点击控件左右边缘的按钮。另一个支持值是vertical,它要求用户上下滑动,并为鼠标用户改变按钮的位置,使它们出现在控件的顶部和底部。我在右侧面板中添加了一个标记为Orientationselect元素,它允许您更改左侧面板中FlipView控件的orientation属性的值。在图 14-7 中,您可以看到为鼠标用户显示的horizontalvertical值的不同按钮位置。我在图中突出显示了按钮,因为它们的默认样式很难看到(我将在本章的后面向您展示如何更改按钮样式)。

images

图 14-7。水平和垂直方向值按钮的不同位置

设置项目间距

itemSpacing属性设置当用户使用触摸从一个项目翻转到另一个项目时项目之间显示的间隙。在图 14-8 的中,你可以看到这种差距的两个例子,默认值为 10 像素,较大值为 100 像素。

images

图 14-8。设置触摸交互项目之间显示的间距

我在这个属性的例子中包含了一个配置控件——一个标记为Item Spacinginput元素。itemSpacing属性的影响可能看起来很小,但是设置项目间距可以对FlipView控件的外观和感觉产生显著的影响。特别是,我发现当数据源内容只是松散相关时,使用更大的空间会产生更自然的感觉。当然,这纯粹是主观感觉,你应该做对你自己的应用和偏好有意义的事情。

以编程方式管理动画视图

在很大程度上,FlipView控件为用户提供了与正在显示的内容进行交互所需的一切。当鼠标在FlipView上移动时,鼠标用户会看到弹出按钮,触摸用户可以通过滑动动作从一个项目移动到下一个项目。即便如此,有些时候你需要更直接地控制控件如何操作,对于这些情况,FlipView定义了我在表 14-4 中描述的方法。在接下来的小节中,我将向您展示如何使用这些方法。

images

移动浏览项目

您可以通过调用nextprevious方法以编程方式在数据源中的项目间移动。我在示例的右边面板添加了按钮来演示这个特性,你可以从清单 14-11 中的按钮中看到处理click事件的代码。

清单 14-11 。使用下一个和上一个方法在 FlipView 项目中移动

`...
WinJS.UI.Pages.define("/pages/FlipView.html", {
    ready: function () {

var proxyObject = WinJS.Binding.as({
            itemTemplate: ItemTemplate,
            customAnimations: false,
        });

Templates.createControls(rightPanel, flip, "flipView", proxyObject)
        .then(function () {             proxyObject.bind("itemTemplate", function (val) {
                flip.winControl.itemTemplate =
                    val == "HTML" ? ItemTemplate : renderItem;
            });

**            $('#rightPanel button').listen("click", function (e) {**
**                var buttonText = e.target.innerText.toLowerCase();**
**                switch (buttonText) {**
**                    case "previous"😗*
**                    case "next"😗*
**                        flip.winControlbuttonText;**
**                        currentPage.innerText = flip.winControl.currentPage;**
**                        break;**
**                }**
**            });**
        });
    }
});
...`

只有当数据源中有另一个要移动到的项目时,这些方法才会更改显示的项目。这意味着当FlipView显示数据源中的最后一项时,next方法不起作用,当显示第一项时,previous方法不起作用。在我调用上一个或下一个方法之后,我使用currentPage属性的值来更新右侧面板中相应的span元素的值。

images 提示如果您想直接导航到特定的元素,可以设置currentPage属性的值。

操纵数据源

数据驱动 UI 控件最有用的功能之一是,当数据源的内容发生变化时,它们会自动做出响应。这使您可以为应用创建动态且适应性强的布局,从而对基础数据的变化做出即时响应。为了展示这种响应性,我向视图模型添加了一些额外的数据项,如清单 14-12 所示。

清单 14-12。视图模型中定义的附加数据项

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel", {
        data: {
            images: new WinJS.Binding.List([
                { file:img/aster.jpg", name: "Aster"},
                { file:img/carnation.jpg", name: "Carnation"},
                { file:img/daffodil.jpg", name: "Daffodil"},
                { file:img/lily.jpg", name: "Lilly"},
            ]),             extraImages: [{ file:img/orchid.jpg", name: "Orchid"},
                { file:img/peony.jpg", name: "Peony"},
                { file:img/primula.jpg", name: "Primula"},
                { file:img/rose.jpg", name: "Rose"},
                { file:img/snowdrop.jpg", name: "Snowdrop"}]
        }
    });

})();`

当应用启动时,这些数据项不是数据源的一部分,但是当您单击示例右侧面板上的AddRemove按钮时,我会将数据项从List对象移入和移出。我在flipview.js文件中添加了我的click事件处理函数来支持这些按钮,如清单 14-13 中的所示。

清单 14-13 。为点击事件处理程序添加对添加和删除按钮的支持

... $('#rightPanel button').listen("click", function (e) {     var data = ViewModel.data.images;     var extras = ViewModel.data.extraImages;     var buttonText = e.target.innerText.toLowerCase();     switch (buttonText) { **        case "add":** **        case "remove":** **            if (buttonText == "add" && extras.length > 0) {** **                data.push(extras.pop());** **            } else if (buttonText == "remove" && data.length > 1) {** **                extras.push(data.pop());** **            }** **            setImmediate(function () {** **                flip.winControl.count().then(function (countVal) {** **                    itemCount.innerText = countVal;** **                });** **            });** **            break;**         case "previous":         case "next":             flip.winControl[buttonText]();             currentPage.innerText = flip.winControl.currentPage;             break;     } }); ...

您可以通过点击Add按钮向List对象添加新的项目,并通过点击Remove按钮删除它们。查看FlipView控件对数据源变化响应的最好方法是移动到最后一个数据项并点击Remove按钮。FlipView将自动移动到前一项,以反映数据源的变化。

获取数据源中的项目数

您会注意到,我在前面的清单中添加了一些调用count方法的代码。正如你所看到的,这个方法需要一点解释。当您调用count方法时,您会得到一个WinJS.Promise对象,当它被满足时,会将数据源中的项目数作为参数传递给then方法。这是数据源和数据驱动的 UI 控件如何被设计成支持异步操作的另一个例子,即使可以同步获得List对象中的项目数。

我将对 count 方法的调用放在传递给setImmediate函数的函数中。正如我在第九章的中解释的那样,setImmediate函数会推迟一个函数的执行,直到已经传递给setImmediate函数的事件和其他函数得到处理。我这样做的原因是因为数据源使用事件来通知 FlipView 控件它包含的数据已经更改,并且当我立即调用count方法时,该事件没有被处理。为了确保显示最新的值,我使用setImmediate推迟了对count方法的调用。在下一节的稍后部分,我将向您展示使用 FlipView UI 控件支持的事件的另一种方法。

响应 FlipView 事件

FlipView定义了我在表 14-5 中描述的三个事件。

images

只有当数据源中的项目数量改变时,而不是当一个项目被另一个项目替换时,才会触发datasourcecountchanged事件。这意味着当你使用一个WinJS.Binding.List对象作为你的数据源时,使用setAt方法将导致FlipView控件显示你指定的项目,但它不会触发datasourcecountchanged事件。

当显示新页面时,触发pageselected事件。当用户单击导航按钮时,或者当调用nextprevious方法时,或者当使用currentPage属性跳转到特定项目时,该事件被触发。当用户在控件上滑动以选择一项时,不会触发该事件,但当释放触摸时会触发该事件。

每当FlipView改变一个项目的可见性时,就会触发pagevisibilitychanged事件。这是一个很好的想法,但它没有以一种特别有用的方式执行。传递给pagevisibilitychanged的处理函数的Event并不包含事件相关条目的细节,所以您只会收到一连串几乎无用的通知。

我可以使用datasourcecountchanged事件来替换我对setImmediate函数的调用,以推迟使用count方法。您可以在清单 14-14 中看到我对flipview.js文件所做的更改。

清单 14-14 。处理 datasourcecountchanged 事件

`...
WinJS.UI.Pages.define("/pages/FlipView.html", {
    ready: function () {

var proxyObject = WinJS.Binding.as({             itemTemplate: ItemTemplate,
            customAnimations: false,
        });

Templates.createControls(rightPanel, flip, "flipView", proxyObject)
        .then(function () {

proxyObject.bind("itemTemplate", function (val) {
                flip.winControl.itemTemplate =
                    val == "HTML" ? ItemTemplate : renderItem;
            });

**            flip.addEventListener("datasourcecountchanged", function () {**
**                flip.winControl.count().then(function (countVal) {**
**                    itemCount.innerText = countVal;**
**                });**
**            });**

$('#rightPanel button').listen("click", function (e) {
                var data = ViewModel.data.images;
                var extras = ViewModel.data.extraImages;
                var buttonText = e.target.innerText.toLowerCase();
                switch (buttonText) {
                    case "add":
                    case "remove":
                        if (buttonText == "add" && extras.length > 0) {
                            data.push(extras.pop());
                        } else if (buttonText == "remove" && data.length > 1) {
                            extras.push(data.pop());
                        }

break;
                    case "previous":
                    case "next":
                        flip.winControlbuttonText;
                        currentPage.innerText = flip.winControl.currentPage;
                        break;
                }
            });
        });
    }
});
...`

我已经删除了对setImmediate函数的调用,并用一个事件监听器替换它,该监听器调用count方法并更新右面板中span元素的内容。在 FlipView 处理完数据源中的新变化之前,不会触发datasourcecountchanged事件,这意味着我不必再担心推迟方法调用。

样式化 FlipView 控件

FlipView支持使用六个类来设计控件外观的不同方面,如表 14-6 中所总结的。

images

FlipView不会自动调整自己的大小,所以你需要将控件放入一个自动分配大小的布局中,或者指定明确的宽度和高度值。您可以使用win-flipview类来实现这一点,但是我倾向于将id用于已经应用了控件的类,以便同一页面上的多个FlipView控件被单独处理(出于某种原因,我的项目很少需要相同大小的多个FlipView控件)。

我也倾向于不使用win-item类,因为我更喜欢通过项目模板来处理项目的样式。当然,这只是我的偏好,当您使用一个函数来生成不同的元素集以显示项目,并且希望应用一组总体样式时,win-item类会很有用。

我经常使用的类是那些设计当鼠标移动到FlipView控件上时出现的导航按钮的类。当显示浅色图像时,这些按钮可能很难看到,在清单 14-15 中,你可以看到一个我添加到FlipView.html文件中的style元素,它通过应用边框使它们更加明显。

清单 14-15 。导航按钮样式

`...

<head>     <title></title>     <link href="/css/flipview.css" rel="stylesheet" />     <script src="/js/pages/flipview.js"></script> **    <style>** **        #flip .win-navleft, #flip .win-navright {** **            border: medium solid black;** **        }** **    </style>** </head> ...`

你可以在图 14-9 中看到这种风格的效果。如果您自己测试这些附加功能,请记住我只对左右按钮应用了样式,如果将FlipView orientation属性设置为vertical,这将不会产生任何效果。

images

图 14-9。让导航按钮在灯光图像上更容易看到

使用自定义动画

如果您查看布局右侧面板中的控件,您会注意到我添加了一个ToggleSwitch来启用自定义动画。许多 WinJS UI 控件以某种形式使用动画,或者显示从一种状态到另一种状态的转换,或者指示内容以某种方式发生了变化。通常,动画是如此的简短,以至于他们几乎看不见,但是他们仍然足够吸引眼球,向用户传递重要的信号。

我在第十八章中深入解释了动画系统,但是FlipView控件为改变它所使用的动画提供了支持,我想在本章中解释如何做到这一点。这意味着我将解释如何使用一个我还没有正确介绍的功能,所以你可能想去阅读第十八章,然后再回到这一节。

在本节中,你需要知道的关于动画系统的所有事情就是动画是由WinJS.UI.Animation名称空间中的函数来表示的,并且两个这样的函数是fadeInfadeOut。这两个函数都以一个元素作为参数,它们的名字告诉你它们在这个元素上执行什么样的动画。

应用自定义动画

当用户导航到不同的项目时,FlipView控件使用动画。要改变在一种或多种情况下使用的动画,您可以向setCustomAnimations方法传递一个对象,该方法具有名为nextpreviousjump的属性。当用户移动到下一个或前一个数据项时,使用下一个和前一个属性的动画,当用户导航到更远的地方时,使用 jump 属性。

这些属性的值必须是返回一个WinJS.Promise对象的函数,该对象在动画结束时实现。向您定义的函数传递参数,这些参数表示将要删除的元素,以及将替换它作为由FlipView显示的选定项的元素。

在清单 14-16 中,你可以看到我添加到flipview.js文件中的代码,用于为next属性设置自定义动画,当右侧面板中标有Custom AnimationsToggleSwitch控件设置为Yes时,我会应用这些代码。

清单 14-16 。通过 FlipView 控件使用自定义动画

`...

WinJS.UI.Pages.define("/pages/FlipView.html", {
    ready: function () {

var proxyObject = WinJS.Binding.as({
            itemTemplate: ItemTemplate,
            customAnimations: false,
        });

Templates.createControls(rightPanel, flip, "flipView", proxyObject)
        .then(function () {

proxyObject.bind("itemTemplate", function (val) {
                flip.winControl.itemTemplate =
                    val == "HTML" ? ItemTemplate : renderItem;
            });

flip.addEventListener("datasourcecountchanged", function () {
                flip.winControl.count().then(function (countVal) {
                    itemCount.innerText = countVal;
                });
            });

$('#rightPanel button').listen("click", function (e) {
                var data = ViewModel.data.images;
                var extras = ViewModel.data.extraImages;
                var buttonText = e.target.innerText.toLowerCase();
                switch (buttonText) {
                    case "add":
                    case "remove":
                        if (buttonText == "add" && extras.length > 0) {
                            data.push(extras.pop());
                        } else if (buttonText == "remove" && data.length > 1) {
                            extras.push(data.pop());
                        }

break;
                    case "previous":
                    case "next":
                        flip.winControlbuttonText;
                        currentPage.innerText = flip.winControl.currentPage;                         break;
                }
            });

**            proxyObject.bind("customAnimations", function (val) {**
**                if (val) {**
**                    flip.winControl.setCustomAnimations({**
**                        next: function (pageout, pagein) {**
**                            return WinJS.Promise.join([WinJS.UI.Animation.fadeOut(pageout),**
**                                 WinJS.UI.Animation.fadeIn(pagein)]);**
**                        }**
**                    });**
**                } else {**
**                    flip.winControl.setCustomAnimations({**
**                        next: null**
**                    });**
**                }**
**            });**
        });
    }
});
...`

当视图模型属性被设置为true时,我调用setCustomAnimations方法并向FlipView控件提供我希望它使用的动画的细节以及何时应用它们。我传递给setCustomAnimations方法的对象只定义了next属性,它告诉FlipView控件使用默认的动画用于previousjump场景。

我想执行两个动画——我想在输出元素上执行fadeOut动画,在输入元素上执行fadeIn动画。我需要返回一个只有当这两个动画都完成时才完成的Promise对象,这就是为什么我使用了Promise.join方法,我在第九章中描述了这个方法。

ToggleSwitch被设置为No时,我通过向setCustomAnimations方法传递另一个对象来返回默认动画,该方法对于我想要重置的属性具有空值。

您可以通过启动应用并使用控制导航按钮前进到下一个数据项来测试此功能。一个非常简短的动画将创建从右边出现的新项目的效果。当您将ToggleSwitch设置为Yes并再次使用导航按钮时,现有项目将淡出,并被下一个数据项所取代。

images 注意仔细考虑你选择的动画。虽然动画很快,但与其他 Windows 应用不一致的糟糕选择可能意味着与用户正在执行的交互不同的交互类型。尽可能坚持使用默认的动画,如果不可能的话,试着将用户执行的动作和你选择的动画联系起来。这是我在第十八章中更详细解释 WinJS 动画特性时回到的话题。

总结

在这一章中,我描述了 WinJS 数据控件中的第一个,FlipView。我向您展示了如何使用WinJS.Binding.List对象来创建数据源,以及如何将模板与数据相关联来配置向用户显示项目的方式。我还向您展示了第一图像问题,解释了它是如何产生的,并提供了解决该问题的不同方法。最后,我向你展示了如何用FlipView控件来使用自定义动画——一旦你阅读了深入研究 WinJS 动画系统的第十八章,这些信息将会产生更多的共鸣。在下一章,我将向您展示ListView控件,它是对FlipView控件更大更复杂的补充。

十五、使用ListView控件

在这一章中,我描述了ListView控件,这是另一个 WinJS 数据驱动的 UI 控件。与上一章的 FlipView 控件有一个共同的基础,但是ListView显示多个项目,并在如何实现这一点上提供了一些灵活性。在这一章中,我解释了可以使用的不同种类的模板和布局,描述了被调用项和被选择项之间的区别,以及如何处理ListView控件发出的各种事件。我还将向您展示如何使用描述数据源的抽象,这样您就可以编写适用于任何数据源的代码,而不仅仅是那些使用WinJS.Binding.List对象创建的数据源。表 15-1 对本章进行了总结。

images

images

何时使用 ListView 控件

ListView是一个非常灵活的控件,对于如何使用它没有真正的限制。简而言之,如果您想向用户呈现多个项目,那么ListView可能是最好的 WinJS UI 控件。但是,您必须确保用户能够容易地找到特定的项目或项目组。ListView非常适合显示项目,包括大型数据集,但是它很容易被冲昏头脑,呈现给用户一大堆可供选择的项目。对于大型数据集,考虑为用户提供搜索或过滤项目的工具,或者实现基于语义缩放控件的布局,我在第十六章的中对此进行了描述。

添加 ListView 示例

我将从描述ListView控件的基本特性开始,然后在这个例子的基础上演示一些更高级的选项和特性。虽然ListView控件只做一件事(向用户呈现多个项目的列表),但是有很多排列和配置选项。首先,我在/js/viewmodel.js文件中定义了一个数据源,如清单 15-1 所示。

清单 15-1 。ListView 示例的 viewmodel.js 文件中的附加内容

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel", {
        data: {
            images: new WinJS.Binding.List([
                { file:img/aster.jpg", name: "Aster"},
                { file:img/carnation.jpg", name: "Carnation"},
                { file:img/daffodil.jpg", name: "Daffodil"},
                { file:img/lily.jpg", name: "Lilly"},
            ]),

extraImages: [{ file:img/orchid.jpg", name: "Orchid"},
                { file:img/peony.jpg", name: "Peony"},
                { file:img/primula.jpg", name: "Primula"},
                { file:img/rose.jpg", name: "Rose"},
                { file:img/snowdrop.jpg", name: "Snowdrop" }], **            letters: new WinJS.Binding.List(),**
        },
    });

**    var src = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",**
**                "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];**
**    src.forEach(function (item, index) {**
**        ViewModel.data.letters.push({**
**            letter: item,**
**            group: index % 3**
**        });**
**    });**

})();`

我创建了一个新的WinJS.Binding.List对象,包含了字母表中每个字母的对象。每个对象都有一个letter属性,它返回对象对应的字母,还有一个group属性,我用它将对象分配到三个数字组中的一个。在本章的后面,我将在显示项目时使用letter属性的值,在描述将相关项目分组的ListView特性时使用 group 属性。

定义列表视图 HTML

为了演示ListView控件,我在 Visual Studio 项目的pages文件夹中添加了一个名为ListView.html的新文件。你可以在清单 15-2 中看到这个文件的内容。

清单 15-2 。ListView.html 文件的初始内容

`

     **    ** **    **     
        
            
            
        
    


        

**            <div id="list" data-win-control="WinJS.UI.ListView"**
**                data-win-options="{ itemTemplate: ItemTemplate,**
**                    itemDataSource: ViewModel.data.letters.dataSource}">**
**            
**
        
**        
**
**        
**
    

`

通过将data-win-control属性设置为WinJS.UI.ListView,将ListView控件应用于div元素。当使用ListView控件时,使用itemDataSourceitemTemplate属性设置数据源和用于显示数据项的模板,就像使用FlipView控件一样。

ListView控件没有我在第十四章的中为FlipView描述的第一个图像问题,所以你可以使用data-win-options属性安全地声明设置itemDataSourceitemTemplate属性(尽管你也可以编程地设置这些值,并且如果你喜欢的话,使用一个函数来生成你的模板元素——细节和例子参见第十四章)。在我的清单中,我使用添加到viewmodel.js文件中的字母相关对象的List作为数据源,使用在ListView.html文件中定义的WinJS.Binding.Template控件作为模板。

定义 CSS

我已经把这个例子的 CSS 放到一个名为/css/listview.css的文件中,你可以在清单 15-3 中看到它的内容。在这个 CSS 中没有新的技术,所有的风格都是简单和标准的。

清单 15-3 。listview.css 文件的内容

`#list {width:  500px;height: 500px;}
*.listItem {width: 100px;}
*.listData {background-color: black;
    text-align: center; border: solid medium white;font-size: 70pt;}
.listTitle {position: absolute; background-color: rgba(255, 255, 255, 0.6);
    color: black; bottom: 3px;font-size: 20pt; width: 86px;
    padding-left: 10px; padding-top: 20px;font-weight: bold;}
*.invoked {color: red;}
*.invoked .listData {font-weight: bold;}
*.invoked .listTitle {background-color: transparent}

midPanel, #rightPanel

list .win-container {background-color: transparent;}`

定义 JavaScript

我已经把这个例子的 JavaScript 放到了一个名为/js/pages/listview.js的文件中,你可以在清单 15-4 中看到这个文件的内容。

清单 15-4 。listview.js 文件的初始内容

`(function () {

WinJS.UI.Pages.define("/pages/ListView.html", {
        ready: function () {

var proxyObject = WinJS.Binding.as({
                layout: "Grid",
                groups: false,                 groupHeaderPosition: "top",
                maxRows: 3,
                ensureVisible: null,
                searchFor: null,
            });

**            Templates.createControls(midPanel, list, "listView1", proxyObject);**
**            Templates.createControls(rightPanel, list, "listView2", proxyObject);**
        }
    });
})();`

您会注意到,我对清单中的Templates.createControls方法进行了两次调用。当我为这个例子定义 HTML 时,我为配置控件添加了一个额外的容器元素,如下所示:

`...

...`

在这一章中,我需要太多的配置控件来将它们放入模拟器的标准分辨率屏幕上的一个容器中,所以我将元素分成了两个容器,因此,需要对createControls方法进行两次调用。你可以在清单 15-5 中看到我添加到这个例子的controls.js文件中的两组定义控件。

清单 15-5 。ListView 控件的定义对象

`...
listView1: [
    { type: "select", id: "layout", title: "Layout", values: ["Grid", "List"],
         useProxy: true },
    { type: "toggle", id: "groups", title: "Groups", useProxy: true, value: false },
    { type: "select", id: "groupHeaderPosition", title: "Group Position",
        values: ["top", "left"], labels: ["Top", "Left"], useProxy: true },
    { type: "input", id: "maxRows", title: "Max Rows", value: 3, useProxy: true },
    { type: "span", id: "invoked", value: "Invoke an Item", title: "Invoked" },
    { type: "span", id: "selected", value: "Select an Item", title: "Selected" }],

listView2: [
    { type: "select", id: "tapBehavior", title: "tapBehavior",
        values: ["directSelect", "toggleSelect", "invokeOnly", "none"] },
    { type: "select", id: "selectionMode", title: "selectionMode",
        values: ["multi", "single", "none"] },
    { type: "input", id: "ensureVisible", title: "EnsureVisible", value: "",
        useProxy: true },
    { type: "input", id: "searchFor", title: "Search For", value: "", useProxy: true },
    { type: "span", id: "itemCount", value: 26, title: "Count" },
    { type: "buttons", labels: ["Add Item", "Delete Item"] }],
...`

正如我前面提到的,ListView控件非常灵活,这反映在我演示最重要的特性所需的配置控件的数量上。

最后,我需要确保用户可以从导航条导航到ListView.html文件,所以我对templates.js文件进行了添加,如清单 6 所示。

清单 15-6 。确保可以从导航栏访问 ListView.html 文件

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" },     { name: "Flyout", icon: "\u0036" },     { name: "Menu", icon: "\u0037" },     { name: "MessageDialog", icon: "\u0038" },     { name: "FlipView", icon: "pictures" }, **    { name: "Listview", icon: "list" },** ]; ...

使用列表视图控件

如果此时运行 app 并通过导航栏导航到ListView.html文件,你会看到如图图 15-1 所示的布局。在左侧面板中是ListView控件,它显示了我添加到viewmodel.js文件中的字母数据源中的项目。另外两个面板包含配置控件,我将使用它们来演示不同的ListView特性。

images

图 15-1。【ListView.html 文件的布局

在接下来的部分中,我将解释ListView功能的不同方面,并演示使用ListView控件显示数据项的不同方式。

选择布局

ListView可以使用两种不同的布局显示数据项。默认情况下,ListView控件在一个网格中显示来自数据源的项目,如图 1 所示。请注意数据项的显示顺序。网格中的每一列都是从上到下填充的,形成了垂直优先的布局。如果数据源中的项目多于ListView控件占据的屏幕空间,则使用水平滚动。为了使布局对用户更明显,当用户将鼠标移动到ListView控件上或通过触摸交互向左或向右滑动时,会显示一个水平滚动条。

可以使用的另一种布局是垂直列表。您可以使用示例右侧面板中的第一个select元素在网格和列表之间切换,我将它标记为Layout。在清单 15-7 中,你可以看到我添加到/js/pages/listview.js文件中的代码,它将select元素链接到ListView控件。

清单 15-7 。切换 ListView 控件的布局

`(function () {

WinJS.UI.Pages.define("/pages/ListView.html", {
        ready: function () {

var proxyObject = WinJS.Binding.as({
                layout: "Grid",
                groups: false,
                groupHeaderPosition: "top",
                maxRows: 3,
                ensureVisible: null,
                searchFor: null,
            });

Templates.createControls(midPanel, list, "listView1", proxyObject);
            Templates.createControls(rightPanel, list, "listView2", proxyObject);

**            proxyObject.bind("layout", function (val) {**
**                list.winControl.layout = val == "Grid" ?**
**                    new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout();**
**            });**
        }
    });
})();`

ListView控件定义了layout属性,这就是我在清单中所做的更改。您将该属性设置为一个对象——如果您想要网格,则为一个WinJS.UI.GridLayout对象;如果您想要垂直列表,则为一个WinJS.UI.ListLayout对象。

在图 15-2 中可以看到切换到List布局的效果。图中的布局看起来有点奇怪,因为ListView控件的大小适合显示多列。

images

图 15-2。使用列表布局显示元素

这是一个更加传统的垂直列表。我倾向于不像这样单独使用列表布局,但是它在创建语义缩放布局时非常有用,我在第十六章中对此进行了描述。

一个常见的错误是将layout属性设置为字符串形式的对象名称。这是行不通的——您需要使用new关键字来创建一个新对象,并将其赋值,就像我在清单中所做的那样。如果希望以声明方式设置布局,则必须在数据绑定中使用特殊的符号,如下所示:

... data-win-options="{layout: **{type: WinJS.UI.ListLayout}**}" ...

这个符号告诉ListView控件创建一个ListLayout对象的新实例。这是一种笨拙的语法,我倾向于通过在代码中设置布局来避免它。

images 注意原则上,你可以创建自己的布局对象来实现定制的布局策略,但是很难将WinJS.UI.Layout对象中定义的基本功能和ListView控件对布局功能的假设分开。

设置网格的最大行数

GridLayout对象定义了maxRows属性,该属性对用于布局数据源中的项目的行数设置了上限。为maxRows属性设置一个值只会限制行数——例如,它不会强制网格占据您指定的行数。实际行数不同的主要原因是GridLayout永远不会使用垂直滚动。因此,除非有足够的垂直空间来完全容纳从模板生成的元素,否则不会添加新行。

为了演示这个特性,在标签为Max Rows的应用布局的中间面板中有一个input元素。在清单 15-8 中,您可以看到我添加到/js/pages/listview.js文件中的代码,该代码将输入该控件的值链接到maxRows属性。

清单 15-8 。向 listview.js 文件添加对 maxRows 特性的支持

`(function () {

WinJS.UI.Pages.define("/pages/ListView.html", {
        ready: function () {

var proxyObject = WinJS.Binding.as({
                layout: "Grid",
                groups: false,
                groupHeaderPosition: "top",
                maxRows: 3,
                ensureVisible: null,
                searchFor: null,
            });

Templates.createControls(midPanel, list, "listView1", proxyObject);
            Templates.createControls(rightPanel, list, "listView2", proxyObject);

proxyObject.bind("layout", function (val) {
                list.winControl.layout = val == "Grid" ?
                    new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout();
            });

**            proxyObject.bind("maxRows", function (val) {**
**                list.winControl.layout.maxRows = val;**
**            });**
        }
    });
})();`

这是GridLayout对象的一个特性,您创建它并将其设置为ListView控件的layout属性的值——它不是由ListView本身直接定义的一个特性。这意味着该功能仅在网格布局中起作用,并且您必须确保通过从布局属性返回的对象来设置值。如果你给一个ListLayout对象的maxRows属性赋值,不要担心——这不会有不好的影响,但是不会改变布局。您可以在图 3 中的Max Rows input元素中看到输入值2的效果。

images

图 15-3。为 maxRows 属性设置一个值

显示组

ListView控件能够以组的形式显示项目,其中组的细节通过一种特殊的数据源提供,这种数据源很明显叫做组数据源。正是为了准备这个特性,当我在本章前面设置示例应用时,我为我添加到数据列表中的对象定义了group属性。

当您使用WinJS.Binding.List对象时,创建一个组数据源非常简单——您只需调用createGrouped方法。你可以在清单 15-9 的中看到我对/js/viewmodel.js文件所做的添加,以调用这个方法。

清单 15-9 。从列表对象创建分组数据源

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel", {
        data: {
            images: new WinJS.Binding.List([
                { file:img/aster.jpg", name: "Aster"},
                { file:img/carnation.jpg", name: "Carnation"},
                { file:img/daffodil.jpg", name: "Daffodil"},
                { file:img/lily.jpg", name: "Lilly"},
            ]),

extraImages: [{ file:img/orchid.jpg", name: "Orchid"},                 { file:img/peony.jpg", name: "Peony"},
                { file:img/primula.jpg", name: "Primula"},
                { file:img/rose.jpg", name: "Rose"},
                { file:img/snowdrop.jpg", name: "Snowdrop" }],

letters: new WinJS.Binding.List(),
**            groupedLetters: null,**
        },
    });

var src = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
                "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
    src.forEach(function (item, index) {
        ViewModel.data.letters.push({
            letter: item,
            group: index % 3
        });
    });

**    ViewModel.data.groupedLetters = ViewModel.data.letters.createGrouped(**
**        function (item) { return item.group.toString(); },**
**        function (item) { return "Group " + item.group; },**
**        function (g1, g2) { return g1 - g2; }**
**    );**
})();`

我在名称空间ViewModel.data中定义了一个名为groupedLetters的新属性,并将来自List.createGrouped方法的结果赋给它。createGrouped方法返回一个新的List对象,其中的项目按组组织。createGrouped方法有三个函数,用于提供每个项目的分组信息。

第一个函数返回项目所属组的键。数据源中的每个项目都会调用它,并且您必须返回一个字符串。在我的例子中,我能够返回我为每个数据项定义的group属性的字符串值。

第二个函数为每组中的第一项调用一次。结果是要分配给组的文本描述。我对 group 属性的数值进行了简单的修改,因此我返回了名称Group 1Group 2等等。请注意,系统会向您传递一个完整的数据项,因此您可以使用您选择的数据项的任何特征来生成组描述。

第三个函数用于对组进行排序。您被传递了两个组的密钥,要求您返回一个数值。返回值0表示这些组具有相同的等级,返回小于零的值表示第一组应该在第二组之前显示,返回大于零的值表示第二组应该在第一组之前显示。因为我的组合键是数字,所以我可以返回一个减去另一个的结果来得到我想要的效果。

应用组数据源

我将在常规数据源和组数据源之间切换,以响应中间面板中标有GroupsToggleSwitch控件。您可以在清单 10 的中看到我添加到/js/pages/listview.js中的代码,当开关位置改变时,它会做出响应。

清单 15-10 。增加数据源之间切换的支持

`(function () {

WinJS.UI.Pages.define("/pages/ListView.html", {
        ready: function () {

var proxyObject = WinJS.Binding.as({
                layout: "Grid",
                groups: false,
                groupHeaderPosition: "top",
                maxRows: 3,
                ensureVisible: null,
                searchFor: null,
            });

Templates.createControls(midPanel, list, "listView1", proxyObject);
            Templates.createControls(rightPanel, list, "listView2", proxyObject);

proxyObject.bind("layout", function (val) {
                list.winControl.layout = val == "Grid" ?
                    new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout();
            });

proxyObject.bind("maxRows", function (val) {
                list.winControl.layout.maxRows = val;
            });

**            proxyObject.bind("groups", function (val) {**
**                if (val) {**
**                    var groupDataSource = ViewModel.data.groupedLetters;**
**                    list.winControl.itemDataSource = groupDataSource.dataSource;**
**                    list.winControl.groupDataSource = groupDataSource.groups.dataSource;**
**                } else {**
**                    list.winControl.itemDataSource = ViewModel.data.letters.dataSource;**
**                    list.winControl.groupDataSource = null;**
**                }**
**            });**
        }
    });
})();`

为了显示分组数据,我必须更新两个属性:itemDataSourcegroupDataSourceitemDataSource属性用于获取将要显示的项目,而groupDataSource属性用于获取关于项目分组方式的信息。

第一步是设置itemDataSource属性,使其指向分组 List对象的dataSource属性。第二步是将groupDataSource设置为分组 List对象的groups.dataSource属性。

images 注意这导致了很多混乱,需要强调的是:对于itemDataSourcegroupDataSource属性,必须使用分组的数据源。

*按照与常规数据源相同的语法,itemDataSource属性被设置为List.dataSource属性。groupDataSource属性被设置为List.groups.dataSource属性——注意属性路径中添加了groups。设置了这两个属性后,ListView控件将分组显示数据源项,并显示每组的标题,如图 15-4 中的所示。

images

图 15-4。显示分组数据

为了更容易看到这些组,我在导航到ListView.html页面后,通过在JavaScript Console窗口中输入以下语句,临时调整了应用的布局:


rightPanel.style.display = "none"; list.style.width = "800px"


当切换回常规(未分组)数据源时,必须将groupDataSource属性设置为null。如果您不这样做,应用将抛出一个异常,因为数据源缺少将组与单个数据项关联起来所需的结构。

设置割台位置

如图 15-4 中的所示,项目组上方的每组都有一个标题。您可以通过由GroupLayout对象(而不是由ListView控件本身)定义的groupHeaderPosition属性来更改这个位置。中间面板中的select元素可以用来改变组标题的位置,你可以看到我添加到/js/pages/listview.js文件中的代码,当在清单 15-11 中选取一个新的select值时,它会做出响应。

清单 15-11 。增加了改变组头位置的支持

`(function () {

WinJS.UI.Pages.define("/pages/ListView.html", {
        ready: function () {

var proxyObject = WinJS.Binding.as({
                layout: "Grid",
                groups: false,
                groupHeaderPosition: "top",
                maxRows: 3,
                ensureVisible: null,
                searchFor: null,
            });

Templates.createControls(midPanel, list, "listView1", proxyObject);
            Templates.createControls(rightPanel, list, "listView2", proxyObject);

proxyObject.bind("layout", function (val) {
                list.winControl.layout = val == "Grid" ?
                    new WinJS.UI.GridLayout() : new WinJS.UI.ListLayout();
            });

proxyObject.bind("maxRows", function (val) {
                list.winControl.layout.maxRows = val;
            });

**            proxyObject.bind("groupHeaderPosition", function (val) {**
**                list.winControl.layout.groupHeaderPosition = val;**
**            });**

proxyObject.bind("groups", function (val) {
                // ...code removed for brevity...
            });
        }
    });
})();`

groupHeaderPosition属性支持的值是top(默认值)和left。你可以在图 15-5 的中看到left值如何改变布局。在组的顶部显示标题可以减少网格中的行数,所以当那些行更重要时left设置是有用的,即使网格本身会更宽并且需要更多的水平滚动。

images

图 15-5。在群组左侧显示群组标题

使用组页眉模板

默认情况下,ListView只是将组头显示为一个字符串,但是您可以通过提供一个定制模板来定制外观。但是,需要对数据源进行调整,以便组密钥可以与 WinJS 绑定模板一起使用。您可以在清单 15-12 中看到 UI 对/js/viewmodel.js文件中的组数据所做的更改。

清单 15-12 。更改组数据以支持使用标题模板

... ViewModel.data.groupedLetters = ViewModel.data.letters.createGrouped(     function (item) { return item.group.toString(); },     function (item) {         //return "Group " + item.group; **        return {** **            title: "Group " + item.group** **        };**     },     function (g1, g2) { return g1 - g2; } ); ...

问题是 WinJS 数据绑定系统没有一种机制允许你引用传递给Template控件的render方法的数据对象。(有关如何使用数据绑定模板的详细信息,请参见第八章)。为了解决这个问题,我需要更改我传递给createGrouped方法的第二个函数的结果,以便它返回一个具有我可以在数据绑定中引用的属性的对象。

在清单 15-13 中,你可以看到我添加到ListView.html文件中的模板,以及我使用data-win-options属性中的groupHeaderTemplate属性将模板与控件关联起来的方式。我还添加了一个style元素,它适用于从模板创建的元素。

清单 15-13 。向 ListView.html 文件添加并应用组标题模板

`

               **    **     
        
            
            
        
    

**    

**
**        
**
**    
**


        

            

            

        


        

    

`

模板没有什么特别的——它遵循了我在《??》第八章中展示的所有约定,并用于ListViewFlipView控件的项目模板。你可以在图 15-6 中看到结果,我已经在两个位置显示了从模板生成的组标题。

images

图六。使用模板显示群组标题

处理 ListView 事件

ListView控件定义了我在表 15-2 中描述的四个事件。在很大程度上,这些事件与用户交互有关,我将在下面的章节中解释。

images

处理被调用的项目

当用户通过点击或用鼠标点击选择一个项目时,触发iteminvoked事件。传递给事件处理函数的对象是一个IItemPromise,它是一个常规的Promise,当它被满足时返回一个IItem对象(我在第九章中描述过)。在清单 15-14 中,你可以看到我是如何使用IItemPromise来更新中间面板中标有Invoked的 span 元素的。

清单 15-14 。处理 iteminvoked 事件

... list.addEventListener("**iteminvoked**", function (e) {     e.detail.itemPromise.then(function (item) {         invoked.innerText = item.data.letter;         $('.invoked').removeClass("invoked");         WinJS.Utilities.addClass(e.target, "invoked");     }); }); ...

没有视觉提示来显示哪个项目被调用了,所以,如果你想让用户清楚,你必须自己处理。在这个例子中,我将invoked类应用于表示被调用项目的元素(并且,因为一次只能调用一个项目,所以要确保没有其他元素属于这个类)。invoked类对应于我在本章开始时向您展示的/css/listview.css文件中的样式。我已经重复了清单 15-15 中的样式。

清单 15-15 。定义被调用项目的样式

... *.invoked {color: red;} *.invoked .listData { font-weight: bold;} *.invoked .listTitle {background-color: transparent;} ...

你可以在图 15-7 中看到结果,该图显示了同一项目的正常状态和调用状态。但是,您不必指出被调用的项,尤其是当它与项的选择相冲突时,我将在下一节中对此进行描述。

images

图 15-7。为用户标记被调用的项目

设定点击行为

通过改变tapBehavior属性的值,可以改变ListView响应用户点击项目的方式。这个属性可以设置为WinJS.UI.TapBehavior枚举中的一个值,我已经在表 15-3 中描述过了。

这些值定义了调用和选择项目之间的关系。如果您只想让用户调用项目,那么您应该使用的值是invokeOnly。如果您希望用户能够选择,但不能调用项目,那么您应该使用none值。其他值允许同时调用和选择一个项目。我将在下一节解释项目选择。

您可以通过从应用布局的右侧面板中的select元素中选择一个值来更改tapBehavior属性的值,该元素的标签很明显是tapBehavior

处理项目选择

用户可以选择项目并调用它们。这可能会导致一些复杂的交互,因此配置ListView非常重要,这样您就可以获得您想要的效果。您可以通过使用我在上一节中提到的tapBehaviour属性和selectionMode属性来实现这一点,后者是使用来自WinJS.UI.SelectionMode枚举的值来设置的。我已经在表 15-4 中描述了这个枚举中的值。

images 提示您可以通过从标签为selectionMode的应用布局右侧面板的select元素中选取一个值来更改selectionMode属性的值。

images

通过监听selectionchanged事件,您可以在选择发生变化时收到通知。当selectionMode设置为multi时,通过监听selectionchanging事件,您可以在用户向选择中添加项目时接收更新。你可以看到我是如何在清单 15-16 中为selectionchanged定义一个处理程序的。

清单 15-16 。处理 selectionchanged 事件

... list.addEventListener("iteminvoked", function (e) {     e.detail.itemPromise.then(function (item) {         invoked.innerText = item.data.letter;         $('.invoked').removeClass("invoked"); `        WinJS.Utilities.addClass(e.target, "invoked");
    });
});

list.addEventListener("selectionchanged", function (e) {
**    this.winControl.selection.getItems().then(function (items) {**
**        var selectionString = "";**
**        items.forEach(function (item) {**
**            selectionString += item.data.letter + ", ";**
**        });**
**        selected.innerText = selectionString.slice(0, -2);**
**    });**
});
...`

用户选择的项目集可通过 selected 属性获得,该属性返回一个ISelection对象。这个对象定义了一些有用的方法来处理选中的项目,我已经在表 15-5 中描述过了。

images

在清单中,我使用了getItems方法来获取所选项的集合,以便构建一个可以在视图模型中设置的值,从而在示例的右侧面板中显示选择。getItems方法返回一个由IItem对象组成的数组,每个对象对应一个选中的项目。正如您在第十四章中回忆的那样,IItem通过一个Promise使其内容可用,这就是为什么我必须使用 then 方法来获得每一项的细节(参见第九章以获得Promise对象的更多细节)。ListView对选择的项目进行强调,以便用户可以看到整个选择,如图图 15-8 所示。注意,字母D的项目被选中并且被调用。

images

图 15-8。通过 ListView 控件添加到所选项目的强调

设计 ListView 控件的样式

ListView支持许多类,你可以用它们来设计控件外观的不同方面,如表 15-6 中所总结的。

images

并非所有这些 CSS 类都是ListView控件独有的——例如,你会注意到win-item类是与我在第十四章中描述的FlipView控件共享的。当应用这些样式时,您需要确保将焦点缩小到已经应用了ListView控件的元素上。在清单 15-17 中,你可以看到我添加到ListView.html文件中的style元素来演示这些风格。

清单 15-17 。使用 CSS 样式化 ListView 选择

`...

...`

我已经使用了win-selectioncheckmark类来改变选中项目上显示的格子的大小和颜色,你可以在图 15-9 中看到它的效果。

images

图 15-9。使用 CSS 样式化 ListView 控件

以编程方式管理 ListView 控件

您可以使用许多不同的方法和属性来控制ListView的行为。我已经描述了其中的一些,比如通过selection属性可用的addremoveset方法,以及控制数据项如何显示的itemTemplategroupHeaderTemplate属性。表 15-7 显示了从代码中驱动ListView控件行为时有用的一些附加方法和属性。

images

我倾向于不使用indexOfElementelementFromIndex方法,因为我更喜欢让ListView控件处理其内容的外观和布局。然而,其他方法可能非常有用,尤其是当您从外部 UI 控件驱动ListView时。作为一个例子,我在例子的右边面板中包含了一个input元素,这个元素被标记为Ensure Visible。清单 15-18 显示了我添加到/js/pages/listview.js文件中的代码,当input元素的内容改变时,它调用ensureVisible方法。

清单 15-18 。调用 ensureVisible 方法

... proxyObject.bind("ensureVisible", function (val) {     list.winControl.**ensureVisible**(val == null ? 0 : val); }); ...

例如,如果您将 input 元素中的值设置为26,您将看到ListView滚动其内容,以便字母Z可见。

搜索元素

用户通常不会根据索引来考虑数据项,因此作为一个相关的例子,我在右边的面板中定义了另一个标记为Search Forinput元素。在清单 15-19 中,你可以看到我添加到/js/pages/listview.js文件中的代码,当一个值被输入到input元素中时,通过定位相应的项目并选择它来响应。这是一个简单的技巧,但却是一个经常需要的技巧,所以我想把它包含在这一章中,供你将来参考。

清单 15-19 。根据内容定位和选择项目

... proxyObject.bind("searchFor", function (val) {     if (val != null) {         var index = -1;         ViewModel.data.letters.forEach(function (item) {             if (item.letter == val.toUpperCase()) {                 index = ViewModel.data.letters.indexOf(item);             }         });         if (index > -1) {             list.winControl.ensureVisible(index);             list.winControl.selection.set([index]);         }     } }); ...

代码的第一部分定位数据源中与用户在input元素中输入的字母相匹配的项目的索引。如果有匹配,我使用索引来设置ListView选择,并确保选中的项目是可见的。在图 15-10 中,你可以看到在Search For input元素中输入字母J会发生什么。

images

图 15-10。寻找字母 J

使用数据源

在我的例子中,ListView控件的数据源是一个WinJS.Binding.List对象。这是一个方便的安排,因为这意味着我可以通过操作List的内容来改变ListView显示的数据。

虽然这很方便,但不太现实。您可能无法通过这样一个有用的辅助渠道修改数据,甚至不知道您正在处理哪种数据源。在这些情况下,您需要依赖由IListDataSource接口定义的功能。该接口定义了所有数据源都必须实现的方法,并且您可以依赖这些由ListView.itemDataSource属性返回的对象定义的方法。我已经在表 15-8 中描述了IListDataSource定义的方法。

images

images

我在这个例子的右边面板中定义了两个button元素,标记为Add ItemDelete Item。您可以从清单 15-20 中的按钮看到我添加到/js/pages/listview.js文件中处理click事件的代码。在这段代码中,我使用了来自IListDataSource接口的方法来编辑数据源的内容。

清单 15-20 。使用 IListDataSource 方法编辑数据源的内容

... $(".buttonContainer button").listen("click", function (e) {     var ds = list.winControl.itemDataSource;     ds.beginEdits();     var promise;     if (this.innerText == "Add Item") {         promise = ds.insertAtEnd(null, { letter: "A", group: 4 });     } else {         promise = ds.remove("1");     }     promise.then(function () {         ds.endEdits();     }); }); ...

如果点击Add Item按钮,一个新的数据项将被添加到数据源中,并由ListView控件显示——该数据项总是带有字母A,属于组4。如果点击Delete Item按钮,索引1处的元素将被移除。

处理钥匙

如果您仔细查看表中的方法,您会注意到键在标识数据源中的项时起着重要的作用。到目前为止,我还没有担心过键,因为它们是由List对象自动分配的,但是当您直接使用数据源时,它们就会浮出水面。

WinJS.Binding.List对象遵循一个简单的系统,根据条目添加到列表中的顺序生成键。第一项被分配一个键1,第二项被分配一个键2,依此类推。键被表示为字符串值,所以如果你想通过它的索引定位一个键,你必须使用"1"而不是数字值。您可以看到我如何使用键值来响应被点击的Delete Item按钮:

... promise = ds.remove(**"1"**); ...

该方法调用首先删除添加到List中的项目。但是,当数据源的内容改变时,键不会改变,因此再次单击该按钮将会生成错误,因为数据源中没有具有指定键的项。

当使用List对象作为数据源时,这是一个非常常见的错误,因为很容易假设键指的是元素的索引,而不是它被添加的顺序。要删除第一个项目,我需要采取不同的方法,如清单 15-21 所示,它通过位置获取一个项目,然后获取密钥并用它来请求删除。

清单 15-21 。根据项目在数据源中的位置删除项目

... $(".buttonContainer button").listen("click", function (e) {     var ds = list.winControl.itemDataSource;     ds.beginEdits();     var promise;     if (this.innerText == "Add Item") {         promise = ds.insertAtEnd(null, { letter: "A", group: 4 });     } else {         //promise = ds.remove("1");    **     promise = ds.itemFromIndex(0).then(function (item) {** **            return ds.remove(item.key);** **        });**     }     promise.then(function () {         ds.endEdits();     }); }); ...

itemFromIndex方法返回的来自Promise的结果是一个IItem对象,我在第十四章中介绍过。这个对象包含了键,我可以将它传递给remove方法。注意,几乎所有由IListDataSource接口定义的方法都返回一个Promise。你需要使用Promise.then方法将操作数据源内容的动作链接在一起——参见第九章了解Promise对象和如何使用then方法的全部细节。

添加没有关键字的项目

点击Add Item按钮向数据源添加一个新项目。我不想担心为我的数据对象生成唯一的键,所以我将insertAtEnd方法的第一个参数设置为null,如下所示:

... promise = ds.insertAtEnd(**null**, { letter: "A", group: 4 }); ...

这告诉数据源实现对象(在我的例子中是List)应该使用它的标准算法生成一个键。

抑制更新事件

每次项目改变时,数据源都会发出事件,这导致ListView更新其布局,以便与数据保持同步。这是一个很棒的功能,但这意味着如果您进行多次编辑,您需要显式禁用这些事件——如果您不这样做,那么ListView将在每次更改后自我更新,这是一个资源密集型操作,可能会导致用户看到不一致或混乱的数据项视图。

您可以使用beginEdits方法禁用事件。我已经在示例中这样做了,尽管我只编辑了一个项目(我发现总是调用beginEdits是一个好习惯,这样我以后更新编辑代码时就不会有问题了)。一旦您调用了这个方法,您就可以自由地对数据源进行彻底的修改,而不必担心不必要的ListView更新。

完成编辑后,必须确保调用endEdits方法。如果您忘记了,数据源不会发送任何事件,并且ListView也不会更新。你需要确保在调用endEdits之前所有的编辑操作都已经完成,这意味着跟踪IListDataSource方法返回的Promise对象,并在适当的时候使用then方法链接方法调用。你可以在清单 15-22 中看到我是如何做到的。

清单 15-22 。使用 then 方法确保 endEdits 方法具有正确的效果

... $(".buttonContainer button").listen("click", function (e) {     var ds = list.winControl.itemDataSource; **    ds.beginEdits();**     var promise;     if (this.innerText == "Add Item") {         promise = ds.insertAtEnd(null, { letter: "A", group: 4 });     } else {         //promise = ds.remove("1");         promise = ds.itemFromIndex(0).then(function (item) {             return ds.remove(item.key);         });     } **    promise.then(function () {** **        ds.endEdits();** **    });** }); ...

如果您在编辑操作完成之前调用了endEdits方法,那么您将会在ListView中看到您所做的最后几次编辑的每次更改的更新,这就使调用beginEdits失去了意义。

监听数据源的变化

您可以通过使用createListBinding方法接收数据源变化的通知。这个方法的参数是一个实现由IListNotificationHandler接口定义的方法的对象,我已经在表 15-9 中描述过了。

images

你倾听事件的方式有点奇怪,最好用一个例子来解释。清单 15-23 显示了我在例子中定义的处理程序。

清单 15-23 。使用带有数据源的通知处理程序

... var handler = { **    countChanged: function (newCount, oldCount) {** **        itemCount.innerText = newCount;** **    }** }; list.winControl.itemDataSource.**createListBinding(handler)**; ...

第一步是创建一个对象,该对象定义与表中的方法相匹配的方法。您可以在清单中看到,我定义了一个名为countChanged的方法,它有两个参数——这与由IListNotificationHandler接口定义的countChanged方法相匹配。

一旦实现了您感兴趣的方法,您就可以将处理程序对象传递给createListBinding方法。从这一点来看,当数据源发生变化时,将调用处理程序方法。在我的例子中,当数据源中的项数改变时,我的countChanged方法将被执行。您可以通过点击Add ItemDelete Item按钮来实现这一点,并查看示例右侧面板中Count标签旁边显示的结果。

总结

在这一章中,我已经向你展示了ListView控件,这是一个丰富而复杂的 UI 控件。布局和模板系统允许您控制项目的布局,我解释了调用和选择项目以及与它们相关的事件之间的区别。在本章的最后,我向您展示了如何使用数据源,而不是直接操作实现对象。这允许您创建适用于任何数据源的代码,而不仅仅是基于WinJS.Binding.List对象的代码。在下一章,我将向你展示如何使用语义缩放,它结合了不同的 WinJS UI 控件来创建一个关键的窗口交互。*

十六、使用SemanticZoom

在这一章中,我描述了 WinJS UI 控件的最后一个,叫做SemanticZoom。该控件代表 Windows 中的一个关键用户交互,并允许用户在数据集中的两个细节级别之间缩放。控件本身相对简单,依靠一对ListView控件来完成所有的艰苦工作(我在第十五章的ListView控件中描述过)。在这一章中,我将向你展示如何使用SemanticZoom,并演示一种可以同时显示两个细节层次的替代方法。表 16-1 提供了本章的总结。

images

何时使用语义缩放控件

只有当数据可以被有意义地分组时,才应该使用SemanticZoom控件,因为这是提供导航和上下文的基础。我说是有意义的分组,因为以对用户和他们使用数据执行的任务有意义的方式来表示数据是很重要的。将分组应用到数据上很容易,只是为了让使用SemanticZoom变得可行,但是结果会让那些必须处理以无意义的方式组织的数据的用户感到有点困惑。在使用SemanticZoom控件之前,您可能希望阅读整个章节:除了如何使用SemanticZoom的细节,我还演示了一种替代方法,这种方法对于数据分组方式对用户来说不太明显的数据集很有用。

添加 SemanticZoom 示例

SemanticZoom控件相对简单,因为它建立在ListView控件的功能上。对于更大的数据集来说,SemanticZoom控件是最容易演示的,所以我开始对/js/viewmodel.js文件做了一些补充,如清单 16-1 所示。

清单 16-1 。向 viewmodel.js 文件添加数据

`(function () {
    "use strict";

WinJS.Namespace.define("ViewModel", {
        data: {
            images: new WinJS.Binding.List([
                { file:img/aster.jpg", name: "Aster"},
                { file:img/carnation.jpg", name: "Carnation"},
                { file:img/daffodil.jpg", name: "Daffodil"},
                { file:img/lily.jpg", name: "Lilly"},
            ]),

extraImages: [{ file:img/orchid.jpg", name: "Orchid"},
                { file:img/peony.jpg", name: "Peony"},
                { file:img/primula.jpg", name: "Primula"},
                { file:img/rose.jpg", name: "Rose"},
                { file:img/snowdrop.jpg", name: "Snowdrop" }],

letters: new WinJS.Binding.List(),
            groupedLetters: null,
**            names: new WinJS.Binding.List(),**
**            groupedNames: null,**
        },
    });

// ...code for previous chapters removed for brevity...

**    var namesSrcData = ['Aaliyah', 'Aaron', 'Abigail', 'Abraham', 'Adam', 'Addison',**
**    'Adrian', 'Adriana', 'Aidan', 'Aiden', 'Alex', 'Alexa', 'Alexander', 'Alexandra',**
**    'Alexis', 'Allison', 'Alyssa', 'Amelia', 'Andrew', 'Angel', 'Angelina',**
**    'Anna', 'Anthony', 'Ariana', 'Arianna', 'Ashley', 'Aubrey', 'Austin', 'Ava',**
**    'Avery', 'Ayden', 'Bella', 'Benjamin', 'Blake', 'Brandon', 'Brayden', 'Brian',**
**    'Brianna', 'Brooke', 'Bryan', 'Caleb', 'Cameron', 'Camila', 'Carter', 'Charles',**
**    'Charlotte', 'Chase', 'Chaya', 'Chloe', 'Christian', 'Christopher', 'Claire',**
**    'Connor', 'Daniel', 'David', 'Dominic', 'Dylan', 'Eli', 'Elijah', 'Elizabeth',**
**    'Ella', 'Emily', 'Emma', 'Eric', 'Esther', 'Ethan', 'Eva', 'Evan', 'Evelyn',**
**    'Faith', 'Gabriel', 'Gabriella', 'Gabrielle', 'Gavin', 'Genesis', 'Gianna',**
**    'Giovanni', 'Grace', 'Hailey', 'Hannah', 'Henry', 'Hunter', 'Ian', 'Isaac',**
**    'Isabella', 'Isaiah', 'Jack', 'Jackson', 'Jacob', 'Jacqui', 'Jaden', 'Jake',**
**    'James', 'Jasmine', 'Jason', 'Jayden', 'Jeremiah', 'Jeremy', 'Jessica', 'Joel',**
**    'John', 'Jonathan', 'Jordan', 'Jose', 'Joseph', 'Joshua', 'Josiah', 'Julia',**
**    'Julian', 'Juliana', 'Julianna', 'Justin', 'Kaitlyn', 'Katherine', 'Kayla',**
**    'Kaylee', 'Kevin', 'Khloe', 'Kimberly', 'Kyle', 'Kylie', 'Landon', 'Lauren',     'Layla', 'Leah', 'Leo', 'Liam', 'Lillian', 'Lily', 'Logan', 'London', 'Lucas',**
**    'Luis', 'Luke', 'Mackenzie', 'Madeline', 'Madelyn', 'Madison', 'Makayla', 'Maria',**
**    'Mason', 'Matthew', 'Max', 'Maya', 'Melanie', 'Mia', 'Michelle', 'Miriam', 'Molly',**
**    'Morgan', 'Moshe', 'Naomi', 'Natalia', 'Natalie', 'Nathan', 'Nathaniel', 'Nevaeh',**
**    'Nicholas', 'Nicole', 'Noah', 'Oliver', 'Olivia', 'Owen', 'Paige', 'Patrick',**
**    'Peyton', 'Rachel', 'Rebecca', 'Richard', 'Riley', 'Robert', 'Ryan', 'Samantha',**
**    'Samuel', 'Sara', 'Sarah', 'Savannah', 'Scarlett', 'Sean', 'Sebastian', 'Serenity',**
**    'Sofia', 'Sophia', 'Sophie', 'Stella', 'Steven', 'Sydney', 'Taylor', 'Thomas',**
**    'Tristan', 'Tyler', 'Valentina', 'Victoria', 'Vincent', 'William', 'Wyatt',**
**    'Xavier', 'Zachary', 'Zoe', 'Zoey'];**

**    namesSrcData.forEach(function (item, index) {**
**        ViewModel.data.names.push({name: item, firstLetter: item[0]**
**        });**
**    });**

**    ViewModel.data.groupedNames = ViewModel.data.names.createGrouped(**
**        function (item) { return item.firstLetter; },**
**        function (item) { return item; },**
**        function (g1, g2) { return g1 < g2 ? -1 : g1 > g2 ? 1 : 0; }**
**    );**
})();`

我将处理的数据是一个姓名列表。这些是 2011 年纽约州最受欢迎的婴儿名字。

我首先处理名称列表,以填充我分配给ViewModel.data名称空间中的 names 属性的WinJS.Binding.List对象。对于每个名字,我创建一个对象,它有一个name属性和一个firstLetter属性。我将完整的名称分配给 name 属性,并且顾名思义,将名称的第一个字符分配给firstLetter属性(这将使我以后更容易将数据组织成组)。所以,以名称Sophie为例,我在ViewModel.data.names List中创建一个对象,如下所示:

... {     name: "Sophie",     firstLetter: "S" } ...

我使用ViewModel.data.names List中的数据对象创建分组数据,使用的技术与我在第十五章的中展示的技术相同。正如你将看到的,当使用SemanticZoom控件时,群组扮演了一个重要的角色。在这种情况下,我已经按照项目的首字母对它们进行了分组,因此所有以A开头的名称都在同一个组中,所有以B开头的名称也是如此,依此类推。分组数据可通过ViewModel.data.groupedNames属性获得。

定义 HTML 文件

为了演示SemanticZoom控件,我在 Visual Studio 项目的pages文件夹中创建了一个名为SemanticZoom.html的新文件。你可以在清单 16-2 中看到这个文件的内容。

清单 16-2 。SemanticZoom.html 文件的最初内容

`

                        
        
            
        
    


        

            

        

    


        
Letter:

    


        

            

**            
**
        


    

`

这个文件包含了标准的双面板布局,我在本书这一部分的大多数例子中使用了这个布局,还有三个模板,我将用它们来演示SemanticZoom控件的特性。有一点是不包含对SemanticZoom控件本身的任何引用——这是因为通过向您展示底层构建模块并在以后添加控件,我可以更容易地解释该控件的工作原理。

本例的 JavaScript 只包含对Templates.createControl方法的调用,所以我将它包含在 HTML 文件的script元素中。

定义 CSS

SemanticZoom.html文件中有两个link元素。第一个是我在第十五章中创建的/css/listview.css文件,它包含我为用来显示数据的模板定义的样式——出于同样的目的,我在本章中再次使用这些样式。第二个link元素指的是我添加到 Visual Studio 项目中的一个名为/css/semanticzoom.css的新文件,它包含一些我用于SemanticZoom控件的附加样式。您可以在清单 16-3 的中看到semanticzoom.css文件的内容。这个文件中没有新的技术——我只是设置了将应用SemanticZoom控件的元素的大小,并为SemanticZoom所依赖的底层控件之一定义了一些基本样式(稍后我会解释)。

清单 16-3 。semanticzoom.css 文件的内容

`#semanticZoomer {
    width: 500px; height: 500px;
}

*.zoomedInListItem {
    width: 150px;
}

*.zoomedInListData { background-color: black; text-align: center;
    border: solid medium white; font-size: 20pt; padding: 10px;
}`

完成示例

你可以在清单 16-4 中看到我添加到/js/controls.js文件中的定义对象。SemanticZoom控件相对简单,您可以从我定义的少量配置控件中看到这一点。

清单 16-4 。controls.js 文件的定义对象

... **semanticZoom**: [     { type: "toggle", id: "enableButton", title: "EnableButton", value: true },     { type: "toggle", id: "locked", title: "Locked", value: false },     { type: "toggle", id: "zoomedOut", title: "Zoomed Out", value: false },     { type: "input", id: "zoomFactor", title: "Zoom Factor", value: 0.65 }, ], ...

最后一步是确保用户可以导航到SemanticZoom.html文件,我通过在/js/templates.js文件中添加如清单 16-5 所示的内容来完成。

清单 16-5 。增加导航到 SemanticZoom.html 文件的支持

var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" },     { name: "Flyout", icon: "\u0036" },     { name: "Menu", icon: "\u0037" },     { name: "MessageDialog", icon: "\u0038" },     { name: "FlipView", icon: "pictures" },     { name: "Listview", icon: "list" }, **    { name: "SemanticZoom", icon: "zoom" },** ];

如果您在此时运行示例应用并导航到SemanticZoom.html页面,您将看到如图图 16-1 所示的应用布局。面板结构和配置控件已经就位,但是左侧面板中没有控件。在接下来的部分中,我将向您展示如何应用SemanticZoom控件。

images

图 16-1。【SemanticZoom.html 页面的初始布局

了解语义缩放控件

SemanticZoom控件为用户提供了相同数据的两种视图选择——一种是放大的视图,显示分组排列的单个数据项,另一种是缩小的视图,只显示分组,不显示数据项。用户可以使用缩小视图来浏览大型复杂数据集,然后深入了解特定组的详细信息。

向您展示如何应用SemanticZoom的最佳方式是从关注如何创建两个视图开始,这依赖于WinJS.UI.ListView控件的两个实例,我在第十五章中描述过。在接下来的章节中,我将向你展示如何创建底层的ListView控件,然后如何应用SemanticZoom控件来协调它们的行为和外观。

创建放大的列表视图控件

SemanticZoom控件依靠两个ListView控件来表示数据的放大和缩小视图。你可以在清单 16-6 的中看到我是如何定义放大视图的,我已经将它添加到了SemanticZoom.html文件中。

清单 16-6 。定义放大的 ListView 控件

`...

**    
** **    
**
...`

要创建放大视图,将分组数据源的itemDataSource设置为dataSource,将groupDataSource属性设置为groups.dataSource对象。这将设置ListView,使数据项成组显示,并带有组标题。(这与我在第十五章的中描述ListView控件时使用的技术相同。)

您用常规模板设置了itemTemplategroupHeaderTemplate来显示项目和组标题。这些是我在SemanticZoom.html文件中定义的模板,我在清单 16-7 中再次列出了它们。

清单 16-7 。放大的 ListView 控件的项目和显示模板

`...

    
        
    
...
    
Letter:
...`

如果您运行该示例并在此时导航到SemanticZoom.html文件,您将能够看到添加和配置ListView控件的效果,如图图 16-2 所示。我仍然没有SemanticZoom控件,但是数据的两个视图中的一个已经就位。

images

图 16-2。将放大的视图添加到 SemanticZoom.html 文件

创建缩小的 ListView 控件

下一步是添加将显示缩小视图的ListView控件,向用户显示组列表,而不显示这些组中的任何数据项。你可以在清单 16-8 的中看到我对SemanticZoom.html文件添加的ListView控件。

清单 16-8 。为缩小视图添加 ListView 控件

`...

    
    
**    
** **    
**
...`

这个ListView将只显示组,这意味着我必须使用组数据源作为itemDataSource属性的值。这是一个巧妙的技巧,因为它允许ListView控件显示缩小的数据,而不需要理解它正在处理的数据的结构——这是SemanticZoom控件,我将很快添加到示例中,它提供上下文并关联数据的两个视图。

images

图 16-3。两个 ListView 控件都显示在应用布局中

如果你现在运行这个例子,你将会在应用布局中看到两个ListView控件,如图 16-3 中的所示。

根据您的设备分辨率,布局中可能没有足够的空间来显示两个ListView控件,因此其中一个控件可能会溢出屏幕底部。即便如此,这也是一个机会,可以确保在应用SemanticZoom控件并开始管理控件的可见性之前,它们已经被正确配置。

应用语义缩放控件

通过将data-win-control属性设置为WinJS.UI.SemanticZoom,将SemanticZoom控件应用于div元素,其中div元素包含显示数据的两个视图的ListView控件。

当您设置了ListView控件并以您想要的方式显示数据时,您可以添加SemanticZoom控件。你可以在清单 16-9 的中的SemanticZoom.html文件中看到我是如何做的。

images 注意ListView控件在SemanticZoom div元素中声明的顺序很重要——放大视图必须出现在缩小视图之前。

清单 16-9 。应用语义缩放控件

`...

    
        
        
        
        
    
...`

应用SemanticZoom控件的结果是只有一个ListView元素,最初,这是放大的视图。如果你将鼠标移到SemanticZoom控件上,你会看到滚动条上方出现一个小按钮。在图 16-4 中,你可以看到放大的视图,我高亮显示了这个按钮以便更容易看到(直到你知道它在那里,它才那么明显)。

images

图 16-4。放大视图和缩小按钮。

点击该按钮将使SemanticZoom控件显示向缩小视图转换的动画,允许用户浏览组级别的数据,如图 16-5 中的所示。

images

图 16-5。语义缩放控件显示的缩小视图

如果你点击一个组,那么SemanticZoom控件将动画显示转换回放大的视图,显示你选择的组中的项目。

在语义缩放视图之间导航

我向您展示了缩小按钮,因为在没有提示的情况下很难注意到它,但是有几种不同的方式可以在由SemanticZoom控件显示的两个视图之间导航。如果你有带滚轮的鼠标,你可以按住Control键,向上移动鼠标滚轮放大,向下移动缩小。如果你喜欢使用键盘,那么你可以使用Control和加号键(+)放大,使用Control和减号键(-)缩小。

如果您是触控用户,那么您可以使用捏合/缩放触控手势来放大和缩小。我将在第十七章中详细讨论触摸手势,但是如果你选择了我在图 16-6 中突出显示的按钮,Visual Studio 模拟器将允许你执行捏/缩放手势。

images

图 16-6。捏手势模拟按钮并显示

当您选择收缩/缩放手势按钮时,光标将变为图中所示的两个空心圆圈,每个圆圈代表一根手指。按住鼠标按钮,模拟用手指触摸屏幕–光标会发生变化,圆圈被填满,如图的最后一帧所示。使用鼠标滚轮将模拟的手指移近或移远,创建收缩/缩放手势。将手指并拢使SemanticZoom控件缩小,将手指分开使其放大。

配置语义缩放控件

SemanticZoom控件支持我在表 16-2 中描述的配置属性。与其他 WinJS UI 控件相比,这是一个很小的属性集,因为SemanticZoom中的复杂性被委托给它所依赖的ListView控件。

images

我已经在示例的右面板中定义了配置控件来演示所有四个SemanticZoom属性。lockedzoomedOut属性是不言而喻的,但是我将在接下来的章节中解释另外两个属性。

启用和禁用缩小按钮

属性控制我在图 16-4 中展示的缩小按钮的可见性。我在示例的右面板中加入了一个ToggleSwitch,它改变了SemanticZoom控件上的enableButton属性。

该属性的默认值是true,这意味着SemanticZoom控件将显示按钮。将属性设置为false会阻止按钮显示,但是用户仍然可以使用我在上一节中描述的其他技术在视图之间导航。如果您想防止用户在视图之间切换,那么将locked属性设置为true

设置缩放系数

正如您已经注意到的,SemanticZoom控件使用动画在视图之间切换,而zoomFactor属性决定动画的戏剧性。该值可以在0.20.85之间,默认为0.6。我无法在打印页面上演示动画,但是较小的值会产生更生动的缩放效果。我更喜欢一个更微妙的动画,这意味着如果我改变默认值,它通常是一个值0.8,它创建了一个效果,清楚地表明一个过渡,而不是太引人注目。

处理 SemanticZoom 控件事件

SemanticZoom控件定义了一个事件,当控件在缩放级别之间切换时会发出该事件。我在表 16-3 中描述了这一事件。

images

您可以在清单 16-10 中看到我是如何处理这个事件的。当事件被触发时,我更新了视图模型中的一个属性,这使得标记为Zoomed OutToggleSwitch配置控件与SemanticZoom的状态保持一致。

清单 16-10 。处理 SemanticZoom 事件

`...

...`

传递给 handler 函数的Event对象的detail属性在SemanticZoomzoomedOut时设置为true,在放大时设置为false

images 提示你通过直接处理控件的事件来响应用户与放大的ListView的交互。关于项目被调用或选择时ListView用来发送通知的事件的详细信息,请参见第十五章。

设置 SemanticZoom 控件的样式

一个SemanticZoom的大部分样式是通过底层ListView控件使用的模板完成的(我在第十五章中描述过)。然而,有两个类可以用来直接设计这个控件的样式,如表 16-4 所述。

images

我不会在我的项目中使用这些风格。如果我想应用常规样式,那么我的目标是包含SemanticZoom控件的父元素(在我的例子中,这是具有semanticZoomContaineriddiv元素,我通常在我的所有内容中有一个等价的容器元素)。如果我想设计一个更具体的布局部分,那么我会瞄准ListView控件,或者通常依靠项目和组标题模板来获得我想要的效果。

语义缩放控件的替代

在我看来,SemanticZoom控件呈现的交互有一个缺陷,那就是当显示放大视图时,缩小视图呈现的整体上下文丢失了。当可以从数据中推断出上下文时,这不是问题,我的姓名数据就是这种情况。你可以在图 16-7 中看到我的意思。

images

图 16-7。数据中群体的性质非常明显

我只需要向您展示一两个组,就可以清楚地看到数据是按字母顺序分组的,并且根据所显示的组,SemanticZoom显示的数据接近数据源的 50%。在这些情况下,SemanticZoom控件是完美的,因为用户需要知道的一切都显示在布局中,或者可以很容易地从布局中推断出来。

images 以下部分展示了如何将两个ListView控件连接在一起。然而,很难仔细阅读所有对放大和缩小视图的引用。我的建议是在 Visual Studio 中遵循这些部分,以便您可以看到每个更改的效果——它将为描述性文本提供上下文。

有时这种方法不起作用,而您希望同时显示两个视图,以便整体上下文和细节同时可见。我发现自己经常遇到这个问题,我通过用两个ListView控件和一些事件处理程序代码替换SemanticZoom控件来解决这个问题。

创建列表上下文示例

为了演示两个ListView控件的排列,我在名为ListContext.html的示例项目的pages目录中添加了一个新文件,其内容如清单 16-11 所示。

清单 16-11 。ListContext.html 文件的内容

`

                        
        
            
        
    


        

            

        

    


        
Letter:

    


        

        


        

    

`

这个文件包含两个ListView元素,它们使用我为SemanticZoom控件定义的相同数据源。与SemanticZoom的例子一样,我使用了一个ListView来分组显示数据项(创建放大视图),使用了一个ListView来只显示分组本身(创建缩小视图)。

定义 CSS

我在这个例子中重用了semanticzoom.css文件,这样就不必改进我在模板中使用的样式。我还添加了/css/listcontext.css文件来去掉ListView对象,并定义了一些我将在本章后面使用的附加样式。您可以在清单 16-12 中看到listcontext.css文件的内容。这个文件中没有新的技术,所有的样式都很简单,并且使用标准的 CSS。

清单 16-12 。listcontext.css 文件的内容

`#contextContainer { height: 100%; display: -ms-flexbox;
    -ms-flex-direction: row; -ms-flex-align: center; -ms-flex-pack: center;    
}

contextContainer div[data-win-control="WinJS.UI.ListView"] {

border: thick solid white; height: 650px; padding: 20px; margin: 10px;
}

zoomedOut

zoomedIn

zoomedIn .win-groupheader

*.contextListItem {width: 170px;}
*.contextListData { background-color: black; text-align: center;
    border: solid medium white; font-size: 20pt;}

*.highlighted { color: #4cff00; font-weight: bold;}
*.notHighlighted { color: white; font-weight: normal;
    -ms-transition-delay: 100ms; -ms-transition-duration: 500ms;}`

定义 JavaScript 代码

这个例子的 JavaScript 代码在/js/pages/listcontext.js文件中。首先,这个文件只包含一个空的自执行函数,但是我将在完成这个示例的过程中添加代码。你可以在清单 16-13 中看到这个文件的初始内容。

清单 16-13 。listcontext.js 文件的初始内容

`(function () {

WinJS.UI.Pages.define("/pages/ListContext.html", {
    ready: function () {       
        // ...code will go here...
    }
});

})();`

这个例子没有定义对象,因为我不需要演示任何特定的 UI 控件特性。这意味着我需要采取的唯一额外步骤是确保用户可以通过导航条导航到ListContext.html文件,我已经对清单 16-14 中的文件做了添加。

清单 16-14 。通过导航栏启用 ListContext.html 文件导航

... var navBarCommands = [     //{ name: "AppTest", icon: "target" },     { name: "ToggleSwitch", icon: "\u0031" },     { name: "Rating", icon: "\u0032" },     { name: "Tooltip", icon: "\u0033" },     { name: "TimePicker", icon: "\u0034" },     { name: "DatePicker", icon: "\u0035" },     { name: "Flyout", icon: "\u0036" },     { name: "Menu", icon: "\u0037" },     { name: "MessageDialog", icon: "\u0038" },     { name: "FlipView", icon: "pictures" },     { name: "Listview", icon: "list" },     { name: "SemanticZoom", icon: "zoom" }, **    { name: "ListContext", icon: "list" },** ]; ...

如果运行该示例并导航到ListContext.html文件,您将看到如图图 16-8 所示的布局。两个ListView控件已经就位并填充了数据,但是它们还没有链接在一起,所以当您调用单个项目时,它们之间没有交互。

images

图 16-8。【ListContext.html 文件的布局

为列表上下文示例添加事件处理程序代码

本例的构建模块已经就绪——剩下的是纯粹的 JavaScript 来创建两个ListView控件之间的关系,模拟SemanticZoom控件的基本行为,但确保用户可以看到组的上下文和单个元素的细节。在接下来的部分中,我将所需的代码添加到/js/pages/listcontext.js文件中,并解释每次添加是如何构建ListView控件之间交互的关键部分的。

从缩小的 ListView 控件中选择组

我想做的第一件事是通过在放大视图中显示相应的组来响应在缩小视图中调用的项目。你可以在清单 16-15 中看到我是如何做到这一点的,这里我添加了一个函数来处理ListContext.js文件中的iteminvoked事件。

清单 16-15 。在缩小视图中处理调用的项目

`(function () {

WinJS.UI.Pages.define("/pages/ListContext.html", {
    ready: function () {

zoomedOut.addEventListener("iteminvoked", function (e) {
            e.detail.itemPromise.then(function (item) {
                var invokedGroup = item.key;
                zoomedIn.winControl.groupDataSource.itemFromKey(invokedGroup)
                .then(function (item) {
                    var index = item.firstItemIndexHint;
                    zoomedIn.winControl.indexOfFirstVisible = index;
                });
            });
        });
    }
});

})();`

正如你在第十五章中回忆的那样,当用户点击一个项目时,就会触发iteminvoked事件。传递给处理函数的Event对象的detail属性是一个Promise对象,它在项目完成时返回项目。我使用then方法获取项目,并使用key属性获取被调用项目的键。

属性告诉我用户调用了哪个组。为了在放大的视图中显示这个组,我在分组的数据源上使用了itemFromKey方法。这给了我另一个Promise,它在完成时返回一个项目。不同之处在于,从分组数据源返回的项包含一个firstItemIndexHint属性,该属性返回组中第一项的索引。我通过设置indexOfFirstVisible属性的值来确保用户在缩小视图中调用的组显示在放大视图中,这将导致ListView跳转到数据中的正确位置。

这种添加的结果是调用缩小的ListView中的项目将导致在放大的ListView中向用户显示相应的组。你可以在图 16-9 的缩小视图中看到调用J组的效果。

images

图 16-9。在缩小的 ListView 控件中调用一个组的效果

响应放大的 ListView 控件中的滚动

我现在想设置互补关系,以便在放大的视图中滚动内容。我在ListContext.js文件中为放大的ListView控件上的scroll事件添加了一个处理函数,如清单 16-16 所示

清单 16-16 。为放大的 ListView 控件添加滚动事件处理程序

`(function () {

WinJS.UI.Pages.define("/pages/ListContext.html", {
    ready: function () {

zoomedOut.addEventListener("iteminvoked", function (e) {
            e.detail.itemPromise.then(function (item) {
                var invokedGroup = item.key;
                zoomedIn.winControl.groupDataSource.itemFromKey(invokedGroup)
                .then(function (item) {
                    var index = item.firstItemIndexHint;
                    zoomedIn.winControl.indexOfFirstVisible = index;
                });
            });
        });

**        zoomedIn.addEventListener("scroll", function (e) {             var firstIndex = zoomedIn.winControl.indexOfFirstVisible;**
**            zoomedIn.winControl.itemDataSource.itemFromIndex(firstIndex)**
**            .then(function (item) {**
**                zoomedOut.winControl.itemDataSource.itemFromKey(item.groupKey)**
**                .then(function (item) {**
**                    zoomedOut.winControl.ensureVisible(item.index);**
**                });**
**            });**
**        }, true);**
    }
});

})();`

要从一个ListView控件接收scroll事件,你必须将addEventListener方法的可选第三个参数设置为true,这样事件就会从包含在生成事件的ListView中的元素(称为视窗)中冒出来:

`...
zoomedIn.addEventListener("scroll", function (e) {
       //...function statements go here...

}, true);
...`

如果省略该参数,该值默认为false,并且该事件不会触发您的处理函数。在我的代码中,我通过读取图 16 中indexOfFirstVisible属性的值来响应scroll事件——找出哪个元素在放大的ListView中位于最左侧。

我使用这个元素的索引,通过调用数据源上的itemFromIndex方法来获取数据项本身——这是另一个在满足Promise时返回数据项的方法。

一旦有了条目,我就使用groupKey属性(在分组数据源中的条目上定义)来标识最左边的组,并确保相应的条目在缩小的ListView中可见。结果是两个ListView项目现在同步了。如果您在缩小视图中调用一个项目,相应的组将显示在放大视图中。同样,当您滚动放大视图时,代表当前显示组的项目总是可见的。

处理最后一项

我还没有完全达到我想要的效果。我编写的代码确保了与放大的ListView中的第一个组相对应的缩小项目是可见的,但是它没有很好地处理数据源中的最后几个组。你可以在图 16-10 中看到这个问题,它显示了当我滚动放大的ListView来显示最终的元素组时的效果。

images

图 16-10。最后几组内容有问题

如图 16-所示,缩小后的ListView没有显示与放大视图中最后几组相对应的项目。为了解决这个问题,我需要添加一个事件处理程序代码来专门检查显示中的最后一组,如清单 16-17 中的所示。

清单 16-17 。确保最后一组得到正确处理

`(function () {

WinJS.UI.Pages.define("/pages/ListContext.html", {
    ready: function () {

zoomedOut.addEventListener("iteminvoked", function (e) {
            e.detail.itemPromise.then(function (item) {
                var invokedGroup = item.key;
                zoomedIn.winControl.groupDataSource.itemFromKey(invokedGroup)
                .then(function (item) {
                    var index = item.firstItemIndexHint;
                    zoomedIn.winControl.indexOfFirstVisible = index;
                });
            });
        });

**        zoomedIn.addEventListener("scroll", function (e) {**

**            var firstIndex = zoomedIn.winControl.indexOfFirstVisible;**
**            var lastIndex = zoomedIn.winControl.indexOfLastVisible;             zoomedIn.winControl.itemDataSource.getCount().then(function (count) {**
**                var targetIndex = lastIndex == count - 1 ? lastIndex : firstIndex;**

**                zoomedIn.winControl.itemDataSource.itemFromIndex(targetIndex)**
**                .then(function (item) {**
**                    zoomedOut.winControl.itemDataSource.itemFromKey(item.groupKey)**
**                    .then(function (item) {**
**                        zoomedOut.winControl.ensureVisible(item.index);**
**                    });**
**                });**
**            });**
**        }, true);**
    }
});

})();`

与上一个例子的不同之处在于,我通过使用ListView控件的count方法和indexOfLastVisible属性来检查数据源中的最后一项是否可见,这两者我都在第十五章的中描述过。如果最后一个数据项可见,那么我确保缩小视图显示最后一组组。您可以在图 16-11 的中看到这种变化的结果,其中您可以看到将放大视图滚动到数据集的末尾会导致缩小控件显示最后几组。

images

图 16-11。确保最后的数据项被正确处理

在缩小的控件中强调过渡

当放大视图滚动时,缩小视图是否保持最新对于用户来说不是很明显。用户的注意力会集中在正在滚动的ListView上,他们不会总是注意到其他地方的细微变化。

为了帮助吸引用户的注意力并强调两个ListView控件之间的双向关系,当放大视图中显示的组发生变化时,我将为缩小的ListView添加一个简短的颜色高亮。

为此,我将使用来自/css/listContext.css文件的两个样式,这是我在本章前面创建示例时添加的,我在清单 16-18 中再次展示了这两个样式。这些样式使用 CSS3 过渡,使样式中定义的值逐渐应用。

清单 16-18 。缩小视图中高亮过渡的样式

`...
*.highlighted {
    color: #4cff00;
    font-weight: bold;
}

*.notHighlighted {
    color: white;
    font-weight: normal;    
    -ms-transition-delay: 100ms;
    -ms-transition-duration: 500ms;
}
...`

应用了highlighted类的元素将有绿色和粗体文本。notHighlighted类逆转这些改变,但是在 100 毫秒的延迟之后,在 500 毫秒的方向上进行。为了应用这些样式,我对清单 16-19 中的文件进行了修改。

清单 16-19 。应用 CSS 样式来强调列表视图控件中的项目

`...
zoomedIn.addEventListener("scroll", function (e) {

var firstIndex = zoomedIn.winControl.indexOfFirstVisible;
    var lastIndex = zoomedIn.winControl.indexOfLastVisible;

zoomedIn.winControl.itemDataSource.getCount().then(function (count) {
        var targetIndex = lastIndex == count - 1 ? lastIndex : firstIndex;

**        var promises = {**
**            hightlightItem: zoomedIn.winControl.itemDataSource.itemFromIndex(firstIndex),**
**            visibleItem: zoomedIn.winControl.itemDataSource.itemFromIndex(targetIndex)**
**        };**

**        WinJS.Promise.join(promises).then(function (results) {**
            zoomedOut.winControl.itemDataSource.itemFromKey(results.visibleItem.groupKey)
            .then(function (item) {                 zoomedOut.winControl.ensureVisible(item.index);
            });

**        zoomedOut.winControl.itemDataSource.itemFromKey(results.hightlightItem.groupKey)**
**            .then(function (item) {**
**                var elem = zoomedOut.winControl.elementFromIndex(item.index);**
**                $('*.highlighted').removeClass("highlighted")**
**                    .removeClass("notHighlighted");**
**                WinJS.Utilities.addClass(elem, "highlighted");**
**                WinJS.Utilities.addClass(elem, "notHighlighted");**
**            });**

});
    });
}, true);
...`

我使用了WinJS.Promise.join方法的一个特性,它允许您传递一个属性值为Promise对象的对象。由join方法返回的Promise产生的结果包含相同的属性名,但是每个属性名的值是由您在对象中传递的相应的Promise产生的结果——这是使用数组索引的一个很好的替代方法。

否则,添加的代码很简单,当放大视图显示的组发生变化时,我应用 CSS 类来突出显示缩小视图中的项目。这种效果如此之快,以至于我无法轻松地在截图中展示给你,但如果你启动示例应用并滚动浏览放大的内容,你会看到在缩小的视图中出现强调的闪光。

总结

在本章中,我向您展示了SemanticZoom控件,它在整个 Windows 用户体验中扮演着重要的角色,它将两个ListView控件结合在一起,允许用户从一个控件缩放到另一个控件,以在两个不同的细节级别上查看相同的数据。

我还向您展示了一种替代方法,其中整体上下文和详细视图同时显示。当您正在处理的数据在没有更广泛的上下文的情况下有意义时,您应该使用标准的SemanticZoom控件——在我看来,这意味着数据中应用组的方式和在内容中的相对位置是不言而喻的。对于其他数据集,您应该考虑使用一种方法来确保广泛的上下文和精细的细节并排显示。在下一章,我将描述 Windows 应用处理输入事件和触摸手势的方式。

十七、使用指针和手势

到目前为止,在本书中,我一直依赖标准的 DOM 事件,如clickmoveover来响应用户输入。对于简单的交互来说,这是一种可行的方法,但是需要不同的技术来充分利用 Windows 应用,特别是支持触摸手势,这使得用户可以轻松地表达复杂的命令。在本章中,我将向您展示如何确定 Windows 8 设备支持哪些输入技术,解释 Windows 如何使用一个通用的指针系统来表示这些输入,以及如何识别触摸手势。表 17-1 对本章进行了总结。

images

创建示例项目

我创建了一个名为AppInput的项目来演示 Windows 应用可以支持的不同事件和手势。我将在自己的内容页面中展示每个主要功能,因此我创建了一个熟悉的带有NavBar的主内容页面的应用结构,这将允许用户在应用中导航。在清单 17-1 中,您可以看到default.html文件的内容,它将作为示例应用的母版页。

清单 17-1 。default.html 文件的内容

`

         AppInput                              
        

Select a page from the NavBar

    
     `

这个文件包含给用户的初始消息和导航条命令,这些命令允许用户导航到我将在本章中添加的五个内容页面。这是一个比我用于 UI 控件更简单的应用结构,例如,我不需要从定义对象生成元素。

定义 CSS

所有内容页面的 CSS 都可以在/css/default.css文件中找到,其内容可以在清单 17-2 中看到。这个文件中没有新的技术,我把它包括进来只是为了让你能看到这个示例应用的每个方面。

清单 17-2 。/css/default.css 文件的内容

`body { background-color: #5A8463; display: -ms-flexbox;
    -ms-flex-direction: column; -ms-flex-align: center; -ms-flex-pack: center;}

.container { display: -ms-flexbox; -ms-flex-direction: row;
    -ms-flex-align: stretch; -ms-flex-pack: center; }

.panel { border: medium white solid; margin: 10px; padding: 20px;    
    display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: stretch;
    -ms-flex-pack: center; text-align: center;}

.sectionHeader { font-size: 30pt; text-align: center; padding-bottom: 10px;}

.coloredRect { background-color: black; color: white; width: 300px; height: 300px;
    margin: 20px; font-size: 40pt; display: -ms-flexbox;
    -ms-flex-direction: column; -ms-flex-align: stretch; -ms-flex-pack: center; }

eventList

.eventDisplay { background-color: #5A8463;}
.pointerDetail, .eventDetail, .primaryDetail {
    display: inline-block; width: 250px; font-size: 20pt; text-align: left;}
.pointerDetail {width: 100px;}
.primaryDetail {width: 75px;}

input.cinput {width: 75px;display: inline-block;margin-left: 20px;font-size: 18pt;}
.imageRect {width: 600px;height: 80vh;}

capabilitiesContainer div.panel

.capabilityTitle {text-align: right; width: 250px; }
span.capabilityResult { text-align: left; font-weight: bold; width: 80px; }
div.capability {font-size: 20pt;width: 350px;}
div.capability > * {display: inline-block;padding-bottom: 10px;}`

定义 JavaScript

/js/default.js文件包含应用的导航代码,使用了您现在熟悉的相同模式。这是一个稍微简化的版本,因为我在这个应用中使用的唯一数据绑定是在模板中,所以当我加载新内容页面时,我不必调用WinJS.Binding.processAll方法。与我的其他示例页面一样,导航代码从 Visual Studio 项目的pages文件夹中加载内容文件。您可以在清单 17-3 中看到default.js文件的内容。

清单 17-3 。/js/default.js 文件的内容

`(function () {
    "use strict";

var app = WinJS.Application;

WinJS.Navigation.addEventListener("navigating", function (e) {

WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {
            WinJS.Utilities.empty(contentTarget);
            WinJS.UI.Pages.render(e.detail.location, contentTarget)
            .then(function () {
                return WinJS.UI.Animation.enterPage(contentTarget.children)
            });
        });
    });

app.onactivated = function (eventObject) {
        WinJS.UI.processAll().then(function () {
            navbar.addEventListener("click", function (e) {
                var navTarget = "pages/" + e.target.winControl.id + ".html";
                WinJS.Navigation.navigate(navTarget);
            });
        })
    };

app.start();
})();`

图 17-1 显示了应用的基本布局,如果您此时运行应用并调出导航条,您将会看到这个布局。在接下来的部分中,我将向应用添加内容页面,以演示应用支持指针和手势的不同方式。

images

图 17-1。app 的初始布局

确定设备的输入能力

本章的起点是计算用户设备支持何种输入形式的技术。您可以通过使用Windows.Devices.Input名称空间中的对象来获取这些信息,该名称空间提供了关于设备对键盘、鼠标和触摸交互的支持的信息。

确定哪些输入功能可用对于定制应用呈现的用户体验非常有用。例如,如果没有键盘,您可能不想显示允许用户配置键盘快捷键的设置。(题外话,我在第二十章中向你展示了如何管理应用设置并呈现给用户)。

为了演示如何确定设备输入功能,我在示例 Visual Studio 项目的 pages 文件夹中添加了一个名为DeviceCapabilities.html的新文件。你可以在清单 17-4 中看到这个文件的内容。

清单 17-4 。DeviceCapabilities.html 文件的内容

`

         

    
        

    


        

:
        
    

`

我已经使用WinJS.Binding.Template功能生成了一个显示设备功能的布局。generateCapabilityPanel函数接受一个标题和一组功能,并在 DOM 中生成元素来显示它们。我采用这种方法,这样我就不必列出大量的 HTML 标记,因为我想展示这种形式的模板绑定。

当我调用Template.render方法时,我接收到一个Promise对象,当从模板中生成新元素时,该对象被满足。传递给then函数的对象是已经创建的顶级元素(这是因为我省略了render方法的第二个参数,它指定了模板元素将要插入的容器元素——如果我指定了一个元素,那么这个容器元素将被传递给then函数)。

我使用这种方法是因为Template控件在从模板创建元素时去掉了class属性的值。为了解决这个问题,我使用了WinJS.Utilities.addClass方法,并使用 DOM appendChild方法将模板元素插入到文档中。这些都与设备功能没有直接关系,但是我忍不住展示了使用 WinJS 模板的另一种方法。

确定键盘功能

您可以通过创建一个新的Windows.Devices.Input.KeyboardCapabilities对象来获取有关用户设备键盘功能的信息。该对象只定义一个属性,该属性指示设备上是否有键盘。我已经在表 17-2 中总结了这个属性,以便您在以后返回本章时可以轻松找到它,并且不想阅读文本来查找属性名称。

images

在清单 17-5 中,您可以看到我是如何创建KeyboardCapabilities对象并读取属性的值的。注意,这个属性返回的是1而不是true,因此为了得到一个true / false值,我必须将属性值与数字文字值1进行比较。

清单 17-5 。检测硬件键盘的存在

... var kbd = new input.KeyboardCapabilities(); generateCapabilityPanel("Keyboard", [{     name: "Keyboard Present", value: kbd.keyboardPresent == 1 }]); ...

keyboardPresent属性与硬件键盘相关。没有硬件键盘的设备仍然支持通过软件键盘输入文本,当合适的 HTML 元素(如inputtextarea元素)获得焦点时,软件键盘会自动显示文本。

如果此时运行应用,并使用Capabilities NavBar 命令导航,您将会看到如图图 17-2 所示的内容。图 17 显示了在我的开发 PC 上运行的应用,正如你所料,它有一个键盘。

images

图 17-2。确定设备是否有键盘

确定鼠标功能

您可以通过Windows.Devices.Input.MouseCapabilities对象获得关于鼠标的信息,该对象定义了表 17-3 中所示的属性。

注意不要因为SwapButtons属性返回1就认为用户是左撇子。如果你有一个可以从左手和右手配置中受益的界面,那么把SwapButtons值作为一个提示,但是要求用户明确地确认他们想要一个替代的配置。在清单 17-6 中,你可以看到我是如何确定和显示由DeviceCapabilities.html文件的脚本元素中的示例应用中的MouseCapabilities对象定义的属性值的。

清单 17-6 。确定鼠标的存在和功能

`...

...`

我建议在对鼠标做假设时要谨慎。虽然大多数用户以标准的方式使用鼠标,但是惊人数量的用户重新配置鼠标的操作方式,以便在某些情况下重新映射按钮或执行宏。重新配置鼠标设备的软件质量变化很大,好的软件通过重新映射操作系统的硬件功能工作得很好。写得很差的代码(到处都有)使用了各种令人讨厌的黑客手段,你从用户那里得到的输入并不总是与 Windows 报告的功能相对应。我的建议是不要假设用户将能够使用垂直滚轮,例如,当verticalWheelPresent属性返回1时,它很可能完全被重新映射到一些其他函数,因此您应该对如何在您的应用中导航内容保持灵活。在图 17-3 中,你可以看到在我的开发 PC 上运行应用并选择Capabilities导航条命令的结果。

images 提示Visual Studio 模拟器将总是报告一个不带滚轮的 2 键鼠标,而不管连接到运行模拟器的 PC 的硬件如何。

images

图 17-3。确定当前设备的鼠标功能

确定触摸能力

最后一类输入是触摸,包括触摸屏和数字化平板电脑在内的各种设备类型。您可以使用Windows.Devices.Input.TouchCapabilities对象查看设备是否支持触摸,该对象定义了表 17-4 中所示的属性。

images

如果设备有多个触摸面,那么contacts属性返回能力最差的一个支持的接触点的数量。你可以看到我是如何使用清单 17-7 中的TouchCapabilities对象的,它显示了我对DeviceCapabilities.html文件的script元素的添加。

images 注意我发现当TouchPresent属性描述的设备有更多接触点时,它经常返回1

清单 17-7 。使用 TouchCapabilities 对象

`...

...`

你可以在图 17-4 中看到这段代码生成的结果。我已经在我的开发 PC 上运行了示例应用,我在 PC 上连接了一个便宜的数字化平板。

images

图 17-4。显示设备触摸功能的详细信息

处理指针事件

您可以在 Windows 应用中愉快地使用标准的 DOM 事件,如clickmouseover。负责运行 JavaScript Windows 应用的 Internet Explorer 10 将生成这些事件,而不管用于生成它们的输入设备是什么。这意味着,例如,当点击一个button元素时,它将触发一个click事件,而不管用户是用鼠标、触摸屏上的手指还是数字化仪上的笔进行交互。这些事件由 IE10 生成,以提供与 web 应用的兼容性,您可以在您的应用代码中非常安全地使用它们,正如我在本书的示例应用中所做的那样。

然而,如果你想使用 Windows 触摸手势,那么你需要使用MSPointer事件。这些事件与 web 应用开发中的标准 DOM 事件相对应,但是它们具有额外的属性,这些属性提供了所使用的输入类型的详细信息。在表 17-5 中,我列出了MSPointer事件,并描述了它们被触发的情况。没有叫MSPointer的事件——这个名字指的是事件名称MSPointerMSPointerDownMSPointerUp等开始

images 提示MSPointer事件与 DOM 兼容性事件一起生成。您可以毫无问题地监听像MSPointerMovemousemove这样的混合事件,尽管有时在MSPointer事件和被触发的标准 DOM 事件之间会有一点延迟。

对这些事件的描述必然是模糊的,因为它们涉及广泛的交互类型。当用户点击鼠标时,当手指或手写笔触摸屏幕时,或者使用不太常见的设备进行其他交互时,指针可以触摸一个元素。为了演示这些事件是如何工作的,我在示例 Visual Studio 项目的pages文件夹中添加了一个名为PointerEvents.html的文件,您可以在清单 17-8 中看到。

清单 17-8 。PointerEvents.html 文件的内容

`

         

    
        
            
            
            
        
    
    
        
            
                Basic Pointer Events             
        
        
            
            
        
    
`

这个页面的布局由一个简单的彩色块组成,您可以与它交互以生成事件。我通过将事件添加到一个WinJS.Binding.List对象来处理它们,该对象是一个ListView控件的数据源(你可以在第八章的中了解到WinJS.Binding.List对象,在第十五章的中了解到ListView UI 控件)。你可以在图 17-5 中看到结果,显示了简单交互后的内容。

images

图 17-5。响应 MSPointer 事件

您可以像注册常规 DOM 事件一样注册您对指针事件的兴趣——但是您必须注意使用正确的大写。例如,事件名称是MSPointerDown,而不是mspointerdownMsPointerDown或任何其他排列。你可以在清单 17-9 中看到我是如何设置我的处理函数的。我已经注释掉了MSPointerMove事件,因为任何交互都会产生许多这样的事件,很难发现其他类型的事件。

清单 17-9 。注册一个函数来处理 MSPointer 事件

`...
var eventTypes = [
    "MSPointerUp", "MSPointerDown","MSPointerOut",
    "MSPointerOver","MSPointerCancel","MSPointerHover",
**    /*"MSPointerMove" /*
    "MSGotPointerCapture", "MSLostPointerCapture"];

eventTypes.forEach(function (eventType) {
    targetElem.addEventListener(eventType, function (e) {
        eventList.unshift(e);
    }), true;
});
...`

MSPointer事件并不直接等同于 HTML DOM 中的事件,但在很大程度上,它们非常相似,并且以与仅使用鼠标的交互一致的方式触发。例如,当用户触摸屏幕上的元素时,就会触发MSPointerDownMSPointerUp事件,无论用户使用的是鼠标还是触摸屏,都是如此。

有两件事很麻烦。MSPointerHover事件是指当一个点在一个元素上移动而没有接触到屏幕时被触发。这听起来很合理,但我无法在真实的硬件上触发这个事件——尽管在应用模拟器中触发它很容易(只需选择模拟器窗口右边缘的Basic Touch Mode按钮,并将鼠标指针移动到元素上)。

我也不能触发MSPointerCancel事件。当设备中止交互时会触发此事件,微软给出的例子是当同时触摸点的数量超过触摸屏或数字化仪处理它们的能力时。我已经在我能找到的所有硬件上测试了清单 17-8 中的代码,但是还不能触发这个事件。

获取指针信息

一个MSPointer事件的处理函数被传递一个MSPointerEvent对象,它实现了一个常规 DOM Event的所有方法和属性,但是增加了一些内容。许多附加功能被系统用来计算多个事件是如何形成一个手势的,但是有一些属性可能更有用,我已经在表 17-6 中描述了这些属性。

images

你可以看到我是如何通过我的ListView模板显示这些属性的,如清单 17-10 所示。

清单 17-10 。在 ListView 模板中显示选中的 MSPointerEvent 值

`...

    
        
        
        
    
...`

通过选择不同的输入模式,您可以在模拟器中生成不同类型的事件。模拟器窗口右侧的按钮允许您在鼠标和触摸输入之间移动。

导致MSPointer事件被触发的指针类型可通过pointerType属性获得。该属性返回一个数值,您可以将其与由MSPointerEvent对象定义的此类值的枚举进行比较。在这个例子中,我使用了一个绑定转换器将数值转换成有意义的文本,如清单 17-11 中的所示。

清单 17-11 。确定 MSPointer 事件的类型

... var pointerTypeConverter = WinJS.Binding.converter(function (typeCode) {     switch (typeCode) {         case MSPointerEvent.MSPOINTER_TYPE_MOUSE:             return "Mouse";         case MSPointerEvent.MSPOINTER_TYPE_PEN:             return "Pen";         case MSPointerEvent.MSPOINTER_TYPE_TOUCH:             return "Touch";         default:             return "Unknown";     } }); ...

MSPOINTER_TYPE_MOUSEMSPOINTER_TYPE_PENMSPOINTER_TYPE_TOUCH对应于pointerType属性返回的值。

images 提示来自数字化平板的事件通常被报告为MSPOINTER_TYPE_MOUSE事件,而不是MSPOINTER_TYPE_PEN事件。这取决于数字化仪硬件是如何被识别的——我测试过的许多输入设备对系统来说就像鼠标一样,大概是为了更广泛的兼容性。

我在模板中显示的第三个字段指示事件是否由输入设备上的主要点生成。这与多点触摸设备有关,其中第一个接触点(通常是触摸屏幕的第一个手指)被认为是主要点。响应其他手指的移动或接触而触发的事件将为isPrimary属性返回false

处理手势

手势是以特定顺序接收的一系列事件。因此,例如,一个点击手势由一个MSPointerDown事件组成,在某个时刻,后面跟着一个MSPointerUp事件。我说在某些时候是因为手势交互是复杂的——用户的手指可能会在指针被按下然后释放的时刻之间移动,指针可能会移动到你正在收听的事件所在的元素之外,等等。微软已经包含了一些有用的工具,可以更容易地处理手势,而不必处理它们派生的单个事件。在接下来的部分中,我将向您展示如何在用户执行手势时接收通知,以及您可以在应用中响应手势的一些不同方式。

处理手势可能相当复杂,所以我将从最简单的手势开始,逐步增加到更复杂的。为了演示基础知识,我在 Visual Studio 项目的pages目录中添加了一个名为Gestures.html的文件,你可以在清单 17-12 中看到。

清单 17-12 。Gestures.html 文件的内容

`

         

    
        
            
            
        
    
    
        
            
                Gestures             
        
        
            
            
        
    
`

该内容遵循我用于指针事件的相同模式。有一个彩色的矩形,我监听它的事件——但是在这个例子中有一些关键的不同。

最基本的手势是taphold,分别用MSGestureTapMSGestureHold事件表示。为了从一个元素接收这些事件,我必须创建一个MSGesture对象,并告诉它我希望它对哪个元素进行操作,如清单 17-13 所示。

清单 17-13 。创建 MSGesture 对象

... var ges = new MSGesture(); ges.target = targetElem; ...

MSGesture对象内置于 Internet Explorer 10 中,因此您不需要使用名称空间来引用它。使用target属性设置想要接收手势事件的元素——在本例中,我指定了作为我的彩色矩形的div元素。

images 提示MSGesture对象只处理单一元素。如果您想要接收多个元素的手势事件,那么您需要为每个元素创建MSGesture对象。

MSGesture对象解除了您跟踪单个MSPointer事件的责任,但是您需要告诉它,通过addPointer方法传递来自MSPointerDown事件的细节,一个新的手势可能正在开始,该方法接受由MSPointerEvent对象的pointerId属性返回的值,如清单 17-14 所示。

清单 17-14 。开始一个手势

... ges.addPointer(e.pointerId); ...

此时,您不需要做任何其他事情——MSGesture对象将跟踪来自元素的事件,并在手势出现时生成事件。手势事件的处理函数被传递一个MSGestureEvent对象,该对象包含关于手势的信息。点击手势没有额外的细节,但是保持手势可以导致多个MSGestureHold事件被触发。您可以通过读取detail属性并将其与MSGestureEvent对象枚举的值进行比较来确定这些事件的重要性,如表 17-7 中所述。

images

在例子中,我使用一个ListView和一个非常简单的项目模板来响应事件的基本手势和细节作为列表。我已经定义了一个绑定转换器,这样我就可以读取detail值并显示一个有意义的字符串,如清单 17-15 中的所示。

清单 17-15 。通过读取细节属性确定手势事件的重要性

... var holdConverter = WinJS.Binding.converter(function (detail) {     if (detail == MSGestureEvent.MSGESTURE_FLAG_BEGIN) {         return "Start";     } else if (detail == MSGestureEvent.MSGESTURE_FLAG_END) {         return "End";     } else if (detail == MSGestureEvent.MSGESTURE_FLAG_CANCEL) {         return "Cancel";     } else {         return "";     } }); ...

你可以在图 17-6 中看到这些值是如何显示的,以及手势事件的类型。

images

图 17-6。响应顶部和保持手势

表演基本手势

为了测试这个例子,您需要知道如何执行手势。在介绍每个手势时,我将向您展示如何使用鼠标在触摸屏上创建它,以及如何使用鼠标在 Visual Studio 模拟器中模拟触摸。

要使用鼠标执行点击手势,只需在元素上单击鼠标按钮并立即释放——对于鼠标使用,执行点击手势与生成click事件并触发MSGestureTap事件是一样的。要执行保持手势,单击鼠标按钮并按住它——几秒钟后会触发MSGestureHold事件。

在触摸屏上,用一个手指按下并立即释放元素以执行点击手势,然后按住(即,不要将手指从屏幕上移开)以执行保持手势。

在模拟器中,使用模拟器窗口右边缘的按钮选择Basic Touch Mode,如图 17-7 中的所示。光标变为代表手指(显示为带有十字光标的大圆)。您的光标现在是一个手指——按下鼠标按钮模拟用手指触摸屏幕,松开按钮模拟移开手指。如果你想回到模拟器中常规的鼠标交互,那么选择Mouse Mode,它就在Basic Touch Mode按钮的正上方。

images

图 17-7。在 Visual Studio 模拟器中选择基本触摸模式

搬运操作

操作是更复杂的手势,允许用户缩放、旋转、平移和滑动元素。为了演示操作手势,我在 Visual Studio 项目中添加了一个名为Manipulations.html的文件。你可以在清单 17-16 中看到这个文件的内容。

清单 17-16 。Manipulations.html 文件的内容

`

         

    
        
            
Rotate
        
        
            
Scale
        
        
            
Pan
        
    
`

对于这个例子,我已经创建了三个目标元素,每个元素都用我将在接下来的小节中应用的操作进行了标记。你可以在图 17-8 中看到初始布局。

images

图 17-8。可以应用操作手势的元素

表演操纵手势

这个示例演示的操作手势是旋转缩放平移,在深入研究示例代码之前,我将向您展示如何执行每个手势。您需要在示例中的相应元素上执行每个手势。

images 注意您可以使用触摸或鼠标进行平移动作,但旋转和缩放手势仅适用于触摸。

旋转元素

要旋转一个元素,用两个手指触摸屏幕,并围绕一个中心点做圆周运动。要在模拟器中执行该手势,从模拟器窗口的右边选择Rotation Touch Mode按钮。光标将变为两个圆圈,代表手势的两个手指。将光标放在元素上,并按住鼠标按钮。向上滚动垂直鼠标滚轮执行逆时针旋转,向下滚动垂直鼠标滚轮执行顺时针旋转。释放鼠标按钮以完成手势。在图 17-9 中可以看到Rotation Touch Mode按钮、模拟光标和效果。

images 提示不按鼠标键滚动鼠标滚轮,改变模拟手指的初始位置。

images

图 17-9。选择旋转触摸模式,旋转一个元素

缩放元素

要缩放一个元素(也称为捏/缩放手势)将两个手指放在显示屏上,将它们分开以放大元素。一起移动手指会缩小元素。要在模拟器中模拟该手势,选择Pinch/Zoom Touch Mode按钮。光标将变为代表两个手指。在元素上按住鼠标按钮以开始手势,并使用鼠标滚轮调整接触点–向上滚动滚轮会将接触点分开,向下滚动滚轮会将它们一起移动。释放鼠标按钮以完成手势。在图 17-10 中可以看到Pinch/Zoom Touch Mode按钮、模拟光标和缩放手势的效果。

images 提示两个接触点都需要在元素内才能发起手势。在不按下按钮的情况下使用鼠标滚轮来调整接触点之间的距离,以使它们合适。

images

图 17-10。选择挤压/缩放触摸模式并缩放一个元素

平移元素

平移手势是唯一可以使用鼠标执行的操作:只需在元素上按住鼠标按钮并移动鼠标,元素就会跟随鼠标指针移动。手势的工作方式与触摸非常相似——触摸元素,然后移动手指在屏幕上移动元素。要模拟触摸手势,选择Basic Touch Mode并按下鼠标按钮,模拟将手指放在屏幕上。

处理操纵手势事件

使用手势时,您需要为每个想要变换的元素创建一个MSGesture对象,并使用target元素来关联它所应用的元素。在清单 17-17 中,您可以看到我是如何在示例中做到这一点的,在数组上使用了forEach方法,这样我就可以用相同的方式设置所有的元素,并表达对相同事件集的兴趣。

清单 17-17 。设置 MSGesture 对象并监听操作手势事件

`...
var eventTypes = ["MSPointerDown", "MSGestureStart", "MSGestureEnd", "MSGestureChange"];
...
var ids = ["rotate", "scale", "pan"];
var elems = [];
var gestures = [];

ids.forEach(function (id) {
    elems[id] = document.getElementById(id);
    gestures[id] = new MSGesture();
    gestures[id].target = elems[id];
    eventTypes.forEach(function (eventType) {
        elems[id].addEventListener(eventType, handleGestureEvent);
    });
});
...`

操纵手势有它们自己的一套事件,我在表 17-8 中描述了这些事件。当用户开始执行操作手势时触发MSGestureStart事件,当手势完成时触发MSGestureEnd事件。当用户移动指针时,使用MSGestureChange事件发送关于手势的更新。

images

系统不会区分不同的手势。这些事件的处理函数被传递给一个包含附加属性的MSGestureEvent对象,该属性包含用户所做的旋转、缩放和平移的详细信息。由你来决定你想对这些运动的哪些方面做出反应。我在表 17-9 中总结了这些特性。

images

这种方法的好处是用户可以同时执行多个手势。您可以选择要读取的属性值,并忽略那些表示您不感兴趣的手势的属性值。这是我在示例中采用的方法,我只想为布局中的每个元素支持一种手势。

使用 CSS3 转换响应操作

你可以在应用中以任何有意义的方式响应操作手势,但如果你想让用户直接操作布局中的元素,那么最简单的方法是使用 CSS3 转换,这在 Internet Explorer 10 中是受支持的。你可以在例子中看到我是如何使用清单 17-18 中的函数来完成的。

清单 17-18 。处理操作手势事件的细节

... function filterGesture(e) {     var matrix = new **MSCSSMatrix**(e.target.style.transform);     switch (e.target.id) {         case "rotate":             return **matrix.rotate**(e.rotation * 180 / Math.PI);             break;         case "scale":             return **matrix.scale**(e.scale);             break;         case "pan":             return **matrix.translate**(e.translationX, e.translationY)             break;     }; } ...

表示布局中元素的每个 DOM 对象都有一个style.transform属性,您可以将它用作MSCSSMatrix对象的构造函数参数。传递给事件处理函数的每个MSGestureEvent对象包含自上次事件以来每次操作的变化量,而MSCSSMatrix对象使得通过rotatescaletranslate方法应用这些变化变得容易。

images 提示CSSMatrix对象和元素style.transform属性是 CSS3 过渡和转换特性的一部分,我将在第十八章的中详细介绍。

唯一不方便的是,MSGestureEvent.rotation值是用弧度表示的,而 CSS3 转换是用度数表示的——您可以在清单中看到我是如何从一种转换到另一种的。你必须记住将rotatescaletranslate方法的结果设置为用户正在操作的元素的transform属性,如清单 17-19 所示。

清单 17-19 。将更新后的变换应用于被操纵的元素

function handleGestureEvent(e) {     if (e.type == "MSPointerDown") {         gestures[e.target.id].addPointer(e.pointerId);     } else { **        e.target.style.transform = filterGesture(e);**     } }

你可以在图 17-11 中看到操纵所有三个元素的结果。

images

图 17-11。操纵示例中的元素

使用内容缩放

内容缩放允许你缩放一个元素的内容,而不是元素本身。你可以在图 17-8 中看到,我放大了其中一个元素,以至于它溢出了初始边界——这是一个巧妙的技巧,但有时你只是想让用户在原位缩放内容,这就是内容缩放允许的。为了演示这个特性,我在示例 Visual Studio 项目的pages文件夹中添加了一个名为CSSGestures.html的新文件。你可以在清单 17-20 中看到这个文件的内容。

清单 17-20 。CSSGestures.html 文件的内容

`

              
        
                     
    
`

使用 CSS 配置内容缩放功能,本例中没有script块。我依赖于一个名为aster.jpg的图像文件,我已经将它添加到 Visual Studio 项目的images目录中——你可以在这个例子中使用任何图像,我使用的图像包含在本章的源代码下载中,可以从Apress.com获得(并且是我在第十四章的中用来演示WinJS.UI.FlipView控件的图像之一)。

本章这一节的内容非常简单。图像以相当大的格式显示。用户可以触摸屏幕并做出缩放手势来缩放图像。在图 17-12 中可以看到布局的原始状态和放大后的内容。该手势与标准缩放手势的主要区别在于,图像已在其容器的边界内缩放,但容器保持不变。

images

图 17-12。使用内容缩放手势

控制内容缩放手势的 CSS 属性在表 17-10 中进行了描述,在接下来的章节中,我将向您展示如何使用它们,并解释支持值的范围。

images

启用内容缩放功能

启用内容缩放手势需要两个属性。首先,您必须将 CSS overflow属性设置为scroll。该属性并不特定于内容缩放,但它是启用该功能所必需的。第二个属性是–ms-content-zooming,必须设置为zoom。结合这些属性可以启用元素的内容缩放功能。

你必须将这些属性应用到一个有内容的元素上,如清单 17-21 所示。您可以看到内容是一个img元素,它包含在一个 div 中。这是内容缩放手势应用到的div元素。

清单 17-21 。对包含内容的元素应用内容缩放功能

`

              
        
                     
    
`

应用缩放限制

您可以使用-ms-content-zoom-limit-minms-content-zoom-limit-max属性来限制内容的缩放量。这些以原始大小的百分比表示。用户可以在执行捏合/缩放手势时将内容缩放到这些限制之外,但当手势结束时,它会迅速恢复到该限制。在这个例子中,我设置了 50%的最小比例和 200%的最大比例,如清单 17-22 中的所示。设置这些属性时,不要忘记%符号。

清单 17-22 。应用缩放限制

... -ms-content-zoom-limit-min: 50%; -ms-content-zoom-limit-max: 200%; ...

images 提示-ms-content-zoom-limit便利属性允许您在一条语句中指定最小值和最大值。

设置缩放内容样式

-ms-overflow-style属性允许您配置内容缩放后的显示方式。该属性支持的值如表 17-11 所示。

images

我倾向于使用-ms-autohiding-scrollbar值,它可以确保用户意识到他们可以在元素的内容周围平移,但是使用滚动条,滚动条覆盖在内容的顶部,并且只在用户与内容交互时显示。相比之下,由scrollbar值应用的滚动条增加了元素的大小,并且总是被显示。你可以在图 17-13 中看到不同之处。左图显示了-ms-autohiding-scrollbar值的效果,右图显示了scrollbar值。

images

图 17-13。使用内容缩放功能时滚动条的不同样式

限制缩放级别的范围

您可以通过应用-ms-content-zoom-snap-points属性来限制缩放级别的范围。用户在执行收缩/缩放手势时可以缩放到任何级别,但是内容将会靠齐该属性指定的最接近的缩放级别。有两种方法指定捕捉点,如表 17-12 所述。

以编程方式使用内容缩放

虽然 CSS 用于设置内容缩放,但它还有一些编程特性。当用户改变内容的比例时,MSContentZoom事件被触发,您可以使用msContentZoomFactor属性获取或设置缩放因子。清单 17-23 显示了对CSSGestures.html文件的一些添加,以演示事件和属性的使用。

清单 17-23 。程序化内容缩放功能

`

          **    **

    
        
                     

**        

**
**          
**
**                

Zoom Factor:

**
**                **
**            
**
**        
**
    

`

我在布局中添加了一个input元素,当MSContentZoom事件被触发时,这个元素会被更新。当输入元素的值改变时,我通过更新msContentZoomFactor属性来处理change事件。在图 17-14 中可以看到修改后的布局。

images 注意msContentZoomFactor是直接在HTMLElement对象上定义的,而不是由 WinJS 定义的。这意味着您不需要使用winControl来访问属性——您可以直接从document.getElementById方法返回的对象中获取或设置值。

images

图 17-14。在 JavaScript 中使用内容缩放功能

总结

在这一章中,我向您展示了如何确定设备上可用的输入形式,以及如何使用MSPointer事件来响应用户交互。这些事件提供了对常规 DOM 事件的重要增强,因为它们包含了输入机制的细节,可以用来识别手势。我向您展示了手势系统的工作原理,并演示了简单的手势和操作。在本章的最后,我演示了如何使用内容缩放功能来创建另一种效果。这个特性依赖于 CSS——我将在下一章回到这个主题,在那里我将描述 WinJS 动画系统。

十八、使用动画和工具

在这一节中,我将向您展示如何使用 WinJS 动画特性。Windows 应用通常具有的颜色和排版的一致性可能会使用户难以意识到内容已被更新或替换,因此用一个简短的动画来突出显示这种变化可能会很有用。我在前面的章节中提到了一些基本的动画特性,但是现在我要回到这个主题上来。我首先向您展示如何直接使用 WinJS 所依赖的 CSS 特性,然后解释这些特性是如何被 WinJS 动画便利方法打包的。WinJS 包含一些用于常见布局更新场景的预打包动画,我将向您展示如何使用这些动画——包括如何为动画准备元素以及如何在之后进行整理。

在本章的第二部分,我描述了一些在WinJS.Utilities名称空间中有用的方法。我在整本书中一直在使用这些方法,并且我已经描述了当我第一次使用它们时它们做了什么。在这一章中,我给出了一个更完整的使用指南,并演示了一些额外的功能,包括计算布局中元素的大小和位置,以及创建一个灵活的日志记录系统,即使应用没有连接到调试器,您也可以使用该系统。表 18-1 对本章进行了总结。

images

使用动画

出于我不完全理解的原因,当涉及到应用中的动画时,一些程序员变得有点疯狂。不仅仅是 Windows 应用——你可以在网络应用、桌面应用以及任何地方看到这种疯狂的结果。你可以很快疏远你的用户,特别是如果动画阻止用户继续当前的任务。过多的动画是业务线应用中的一种残酷形式,用户将日复一日地重复执行相同的任务。在开发过程中看起来很酷很刺激的动画,当你的用户每天看一百遍的时候,会让他们筋疲力尽。当涉及到 Windows 应用中的动画时,我遵循一套简单的规则,我建议你也这样做:

  1. Only use animation to attract users' attention to changes or results that they may miss.
  2. Keep the animation short to ensure that it won't interfere with the user's work. Use standard animations where they exist, and create subtle effects when they do not exist.

这三条规则会让你的应用看起来不像维加斯的老丨虎丨机,让你的应用使用起来更愉快。

创建示例项目

为了演示不同的动画和技术,我创建了一个名为Animations的示例项目。这遵循了我在前面章节中使用的模式,并使用单页模型为本章中的每个示例导入内容,并支持通过 NavBar 导航。你可以在清单 18-1 中看到这个项目的default.html文件的内容,它包含了我将在本章中添加的三个内容页面的导航条命令。

清单 18-1 。动画项目中 default.html 文件的内容

`

         Animations                         ` `     
        

Select a page from the NavBar

    
    

`

为了布局这个例子中的元素,我在清单 18-2 中定义了样式,它显示了/css/default.css文件的内容。这都是标准的 CSS,在这个文件中没有特定于应用的技术。

清单 18-2 。/css/default.css 文件的内容

body {background-color: #5A8463;} .column { display: -ms-flexbox; -ms-flex-direction: column;     -ms-flex-align: center; -ms-flex-pack: center;} div.panel { border: medium white solid; margin: 10px; padding: 20px;} .outerContainer { display: -ms-flexbox; -ms-flex-direction: row;     -ms-flex-align: stretch; -ms-flex-pack: center;} .coloredRect { background-color: black; color: white; width: 300px;     height: 300px; margin: 20px; font-size: 40pt;text-align: center; } .coloredRectSmall { width: 200px; height: 200px;} .buttonPanel button { width: 200px; font-size: 20pt; margin: 20px; height: 85px;}

内容页面之间的导航由/js/default/js文件处理,其内容可以在清单 18-3 中看到。这是我在前一章中使用的相同的基本导航代码。

清单 18-3 。default.js 文件的内容

(function () {     "use strict"; `    var app = WinJS.Application;
    window.$ = WinJS.Utilities.query;

WinJS.Navigation.addEventListener("navigating", function (e) {
        var elem = document.getElementById("contentTarget");

WinJS.UI.Animation.exitPage(elem.children).then(function () {
            WinJS.Utilities.empty(elem);
            WinJS.UI.Pages.render(e.detail.location, elem)
                .then(function () {
                    return WinJS.UI.Animation.enterPage(elem.children)
                });
        });
    });

app.onactivated = function (eventObject) {
        WinJS.UI.processAll().then(function () {
            document.getElementById("navbar").addEventListener("click",
                function (e) {
                    var navTarget = "pages/" + e.target.winControl.id + ".html";
                    WinJS.Navigation.navigate(navTarget);
                });
        })
    };

app.start();
})();`

直接使用元素

CSS3 定义了对过渡转换的支持,以动画化 HTML 标记中的元素。过渡允许您更改 CSS 属性的值,转换允许您平移、缩放和旋转元素。

images 提示在这一节中,我主要关注与 WinJS 动画直接相关的 CSS3 特性。关于 CSS3 更完整的报道,请参阅我的另一本书html 5的权威指南,这本书也是由 Apress 出版的。

过渡和转换是 WinJS 动画使用的底层机制。通过理解 CSS3 的功能是如何工作的,你将会对 WinJS 的功能有更好的理解,并且如果需要的话能够创建你自己的效果。为了演示如何直接使用这些 CSS 特性,我在示例 Visual Studio 项目的pages文件夹中添加了一个文件。这个文件叫做CSSTransitions.html,如清单 18-4 所示。

清单 18-4 。使用 CSS 过渡和转换

`

` `              

    
        
            
Transition
        
        
            
Transform
        
    
`

这个例子在布局中有两个明确标记的元素,我将用它们来演示转换和变换。你可以在图 18-1 中看到初始布局,我将在接下来的章节中解释其功能。

images

图 18-1。过渡和变换的初始布局示例

应用过渡

当您应用转换时,您告诉浏览器您希望一个或多个属性的值在一段时间内逐渐改变。您可以将过渡应用于颜色和任何具有数值的属性。

images 提示有两种方法可以应用过渡和变换:通过 CSS 类或者使用 JavaScript 直接应用于元素。我使用了基于类的方法进行转换,并在本章的后面向您展示了基于代码的方法进行转换。

有几个 CSS 属性可以用来定义一个过渡的特征,我已经在表 18-2 中描述了这些属性。

images

在这个例子中,我定义了一个 CSS 类,它包含了我的转换。我省略了transition-property,这意味着任何值被改变的属性都服从于transition-directiontransition-delay属性,如清单 18-5 所示。

清单 18-5 。使用 CSS 过渡属性

`...

...`

当这个类被应用到一个元素时,colorbackground-colorfont-size属性的新值将在 500 毫秒的延迟后 1 秒内被应用。我将这个类应用于目标元素以响应click事件,如清单 18-6 所示。

清单 18-6 。响应于点击事件应用转换类

... WinJS.Utilities.toggleClass(e.target, "colorTransition"); ...

如果指定的元素上没有colorTransition类,则WinJS.Utilities.toggleClass方法会添加它,如果有,则移除它。这意味着元素将随着每个click在正常状态和转换状态之间移动。你可以在图 18-2 的中看到这个效果,它显示了应用过渡时从初始状态的渐进过程。这些只是快照,如果你用示例应用进行实验,你会看到完整的效果是多么平滑。

images

图 18-2。一个 HTML 元素的过渡

该图显示了应用过渡时产生的逐渐过渡。请注意,colorTransition类中所有属性的值被同时修改。

如果再次单击该元素,它会立即恢复到原始状态。这是因为transition-delaytransition-duration属性是colorTransition类的一部分,当该类被删除时,应用渐变的指令也被删除。通过在colorTransition类之外设置transition-duration属性,可以确保所有的属性更改都是逐步进行的。

应用变换

在第十七章中,当我修改元素来响应手势时,我使用了一个变换。在这一节中,我将向您展示实现相同结果的不同方法,并演示变换和过渡如何协同工作。变换是通过transform属性来控制的,我在表 18-3 中总结了这个属性,这样你将来可以很容易地找到参考。

images

transform 属性的值可能相当复杂,这取决于您想要进行的更改的性质。我在表 18-4 中列出了不同的变换值。

images

您可以通过将一个或多个单独的转换连接起来作为 transform 属性的值来指定一个转换(这就是我在《??》第十七章中使用的CSSMatrix对象的作用)。例如,我定义了三个不同的transform值,如清单 18-7 所示。

清单 18-7 。定义变换值

... var transforms = ["", "translateX(100px) rotate(45deg)", "translateY(50px) scale(1.2)"]; ...

这个数组中有三个transform值。第一个是空字符串,这意味着没有应用任何转换。第二个将元素沿 X 轴移动 100 个像素,并将其旋转 45 度。旋转用 CSS 度数单位表示(即90deg为 90 度),正值表示顺时针旋转。最后的转换将元素沿 Y 轴移动 50 个像素,并将元素的大小增加 20%。

这些是绝对变换,意味着当你应用其中一个值时,任何先前的变换都被撤销。如果你想累积应用变换,那么你可以使用CSSMatix对象,我在第十七章的中提到过。

在本例中,每次单击元素时,我都会依次遍历这些转换。变换和过渡可以组合在一起,为了演示这一点,我还为transitionDurationbackgroundColor属性应用了一系列值。我使用 DOM 属性应用了这个值,如清单 18-8 所示。

清单 18-8 。使用 DOM 应用变换

... var curr = e.target.style.transform; var index = (transforms.indexOf(curr) + 1) % 3; **e.target.style.transitionDuration = durations[index];** e.target.style.transform = transforms[index]; e.target.style.backgroundColor = colors[index]; ...

我突出显示了为属性transitionDuration赋予新值的语句。当通过 DOM 使用转换和过渡时,在将更改应用到其他属性之前设置这个值是很重要的——如果您不采取这种预防措施,那么更改将会立即应用,不会有任何渐进的效果。你可以看到一组变换和过渡是如何应用到元素图 18-3 中的,尽管你应该用示例应用来体验一下真实的效果有多平滑和优雅。

images

图 18-3。通过 DOM 应用变换和过渡

使用 WinJS 核心动画方法

WinJS.UI名称空间包含一组方法,使得在 Windows 应用中应用过渡和变换更加容易。这些是控制 WinJS 动画整体状态的包装器函数,并为我在上一节描述的 CSS 功能提供了一个方便的包装器。表 18-5 总结了核心动画制作方法。

images

前三种方法允许您控制应用中的所有动画。将此功能作为设置向用户公开是一个好主意,这样他们就可以禁用动画——我在第二十章中向您展示如何显示用户设置。

images 提示这里有一个术语不匹配。您可以使用executeAnimationexecuteTransition方法来执行 CSS 过渡和转换。不同之处在于操作完成时元素所处的状态。使用executeAnimation方法,元素被返回到它们的原始状态,而executeTransition使元素保持修改后的状态,这与前面的例子非常相似。微软并不想在这些名字上为难——executeAnimation方法支持一个叫做关键帧的特性,它执行更复杂的效果,被称为动画——这就是方法名称的来源。我没有在这一章描述关键帧,因为它们不太适合更广泛的 Windows UX 主题,但你可以在[www.w3.org/TR/css3-transforms](http://www.w3.org/TR/css3-transforms)获得更多细节。

executeAnimationexecuteTransition方法将 CSS 变换和过渡应用于元素。这些方便的方法比直接使用 CSS 类或style属性更容易使用。为了演示这些方法,我在示例项目中添加了一个名为CoreFunctions.html的新页面,它使用executeAnimationexecuteTransition方法再现了早期的内容。你可以在清单 18-9 中看到这个新文件的内容。

清单 18-9 。使用 executeAnimation 和 executeTransition 方法

`

              

    
        
            
Transition
        
        
            
Transform
        
    
`

这两种方法的第一个参数是您想要操作的元素集。这可以是单个元素(这是我在示例中使用的),也可以是元素的数组,这将导致相同的效果应用于数组中的所有元素。第二个参数是一个对象,其属性包含要应用的过渡或变换的详细信息。我已经在表 18-6 中描述了支持的属性名称。

images

使用 executeAnimation 方法

如果您想一次操作多个属性,那么您可以将包含表中属性的对象数组作为第二个参数传递给executeAnimationexecuteTransition方法。这两个方法都返回WinJS.Promise对象,你可以用它们来链接效果。如清单 18-10 所示,我在示例中使用了两种技术,你可以在第九章的中了解更多关于Promise对象的信息。

images 提示当指定属性值时,使用 CSS,而不是 DOM,属性名。所以,比如用background-color而不用backgroundColor。如果使用 DOM 属性名称,效果将无法正确应用。

清单 18-10 。预先制作多个同步效果和连锁效果

... WinJS.UI.executeAnimation(e.target, [{     property: "background-color", to: "white", duration: 500, delay: 0, timing: "ease" }, {     property: "color", to: "black", duration: 500, delay: 0, timing: "ease" }]) .then(function () {     return WinJS.UI.executeAnimation(e.target, {         property: "font-size", to: "50pt", duration: 500, delay: 0, timing: "ease"     }); }); ...

这是处理左边元素的片段——标记为Transition的那个。我首先使用executeAnimation方法来转换background-colorcolor属性——我是通过将两个对象的数组作为第二个参数传递给该方法来完成的。我在返回的Promise上使用了then方法来链接对executeAnimation方法的第二次调用——这次是为了转换font-size属性。结果是background-colorcolor属性被一起转换,当两个转换都完成时,font-size属性被转换。

executeAnimationexecuteTransition方法的区别在于,在动画结束时,元素会返回到其原始状态。最好的方法是使用示例应用并点击Transition元素,但我试图捕捉图 18-4 中的效果。不会逐渐返回到初始状态——元素只是快速返回到调用executeAnimation方法之前的位置。

images

图 18-4。使用 executeAnimation 方法

元素的状态在每次调用executeAnimation结束时被重置,您可以在图中看到它的效果。在font-size属性转换之前,background-colorcolor属性的值被重置。

使用 executeTransition 方法

除了被操作的一个或多个元素保持在它们的转换状态之外,executeTransition方法的工作方式与executeAnimation方法类似。你可以在清单 18-11 的中看到我是如何使用executeTransition方法的。

清单 18-11 。使用 executeTransition 方法

... var curr = e.target.style.transform; **var index = (transforms.indexOf(curr) + 1) % 3;**                              WinJS.UI.executeTransition(e.target, [     { property: "transform", to: transforms[index], duration: durations[index],           delay: 0, timing: "ease"     }, { property: "background-color", to: colors[index],           duration: durations[index], delay: 0, timing: "ease"}     ]); ...

使用这个方法就像使用executeAnimation方法一样,您可以从清单中看到,我向该方法传递了一个对象数组,这样就可以转换transformbackground属性。当直接使用 CSS 属性时,使用transform属性具有相同的效果,并允许将变换应用于元素。

事实上,executeTransform方法只是我之前向您展示的 CSS 功能的包装,我在清单中突出显示的语句说明了这一点。我可以从元素中读取transform属性的值,以确定我在循环中的位置。

使用 WinJS 动画

WinJS.UI.Animation名称空间包含一组在应用内容上执行预定义动画的方法。这些是标准的 Windows 动画,您应该在应用中执行与它们相关的活动时使用它们,例如,显示新的内容页面。使用这些方法有两个好处。首先,它们使用起来非常简单,比使用executeTransition方法或直接使用 CSS 要简洁得多。第二个原因是,你的应用将与其他 Windows 应用保持一致,并受益于用户先前对这些动画的意义的体验。表 18-7 描述了 WinJS 内容动画。

images 提示WinJS.UI.Animation名称空间中还有其他方法,但是它们被系统用来在 UI 控件中应用动画,并不是通用的。

images

这些是标准化的动画,通常成对使用。作为一个例子,你可以看到我是如何在清单 18-12 中的default.js中使用enterPageexitPage动画方法的。我调用这些方法来响应navigating事件(我在第七章的中描述过)以引起用户对内容变化的注意。

清单 18-12 。使用 default.js 文件中的 enterPage 和 exitPage 方法

`...
WinJS.Navigation.addEventListener("navigating", function (e) {
    var elem = document.getElementById("contentTarget");

WinJS.UI.Animation.exitPage(elem.children).then(function () {
        WinJS.Utilities.empty(elem);
        WinJS.UI.Pages.render(e.detail.location, elem)
            .then(function () {
                WinJS.UI.Animation.enterPage(elem.children)
            });
    });
});
...`

表中的所有方法都返回一个WinJS.Promise对象,当动画完成时,该对象被实现。在接下来的章节中,我将向您展示如何使用其他动画。

使用内容进入和退出动画

为了演示内容动画,我添加了一个名为ContentAnimations.html的新 HTML 页面。我将为每个动画构建这些内容,从enterContentexitContent方法开始。你可以在清单 18-13 中看到ContentAnimations.html文件的初始版本。

清单 18-13 。使用内容输入和内容退出动画

`

         

    
        
            
One
            
Two
            
                             
        
    
`

尽管动画方法是成对出现的,但您仍然要负责协调对这些方法的调用,并准备将被动画化的元素。使用exitContent方法制作动画的元素只需要在页面上可见即可。将由enterContent方法引入的元素需要被添加到 DOM 中,但不可见,这是通过将display属性设置为none来实现的,如下所示:

... content2.style.display = "none"; ...

要协调动画序列,以便从一个元素到另一个元素的过渡是平滑的,请遵循以下顺序:

  1. Call the exitContent method, passing in and out elements as parameters.
  2. Use then method for WinJS.Promise returned by exitContent:
    1. Outgoing elements
    2. Set the display property on to none to clear the incoming element.
    3. The display property on calls the enterContent method, passing in the method as a parameter.

这些方法在改变opacity属性的值时翻译元素。这个过程非常快——exitContent动画耗时 80 毫秒,而enterContent动画耗时 370 毫秒。

images 提示通过在 Visual Studio 项目的References部分的ui.js文件中搜索方法名,可以看到每个动画是如何设置的细节。

点击示例中的button元素触发动画。对于每个click事件,我计算出哪个元素是可见的(因此是传出的),哪个元素是隐藏的(因此是传入的)——这允许示例在元素之间交替。

使用淡入和淡出动画

fadeInfadeOut方法操作动画元素的opacity属性。这意味着在动画开始之前,输出元素需要有一个值为1opacity和一个值为0的输入元素。如果您希望一个元素替换另一个元素,那么您需要确保它们在布局中占据相同的空间-一种方法是使用网格布局并将两个元素分配给同一个网格单元。你可以看到我是如何应用这种技术,并调用动画方法的,在建立在前面例子基础上的清单 18-14 中。

清单 18-14 。使用渐强和渐弱方法

`

         

    
        
            
One
            
Two
            
                             
        

**        

**
**            
**
**                
One
**
**                
Two
**
**            
**
**            
**
**                **
**            
**
**        
**
    

`

通过将display属性设置为–ms-grid,可以很容易地将元素放置在同一位置。如果没有为行或列设置任何值,也没有为单元格分配任何元素,那么结果将是一个 1 x 1 的网格,所有内容元素都在同一个单元格中。在这个例子中,我通过将style属性应用到容器元素来设置这种排列。我通常不直接对元素应用 CSS,但是我破例了,因为这是一个如此简单的例子。

fadeInfadeOut不会变换元素的位置(因此不需要显式地改变动画之间的display属性)。你可以在图 18-5 的中看到示例的布局。

使用交叉渐变动画

crossFade方法转换一对元素的opacity属性,这样一个变得透明,而另一个变得不透明。crossFade方法所需的准备与fadeInfadeOut方法相同,因为您需要为输出元素设置opacity1,为输入元素设置0。不同之处在于两个动画是同时开始的。你可以看到我是如何在清单 18-15 中添加对crossFade方法的支持的。

清单 18-15 。使用交叉渐变方法

`

         

    
        
            
One
            
Two
            
                             
        


            

                
One
                
Two

            

            

                
            

        

**        

**
**            
**
**                <div id="crossfade1"**
**                    class="coloredRect coloredRectSmall column">One
**
**                <div id="crossfade2"**
**                    class="coloredRect coloredRectSmall column">Two
**
**            
**
**            
**
**                **
**            
**
**        **
    

`

交叉淡入淡出动画非常快。淡入和淡出动画都持续 167 毫秒,因此过渡是即时的。我发现效果有点快,倾向于将fadeOutfadeIn方法链接在一起。你可以在图 18-5 中看到触发动画的元素和按钮的布局。我建议您花一些时间对这三个元素进行实验,以感受用户将如何看到从一个元素到另一个元素的转换。

images

图 18-5。使用 enterContent 和 exitContent 动画

使用 WinJS 工具

在本书这一部分的例子中,我一直在使用WinJS.Utilities名称空间的特性。在这一节中,我将描述其中最有用的特性,如果您使用过 jQuery 之类的 DOM 操作库,就会对其中的许多特性很熟悉。

查询 DOM

在本书的许多例子中,我给符号$起了别名,这样它就可以引用WinJS.Utilities.query方法。这是一个类似 jQuery 的方法,它在 DOM 中搜索匹配 CSS 选择器字符串的元素。结果作为一个QueryCollection对象从query方法返回——该对象定义了表 18-8 中描述的方法。

images

到目前为止,我已经在本书的例子中使用了几乎所有这些方法,并且在接下来的章节中也使用了它们。在这一章中,我不打算给出任何具体的例子,因为这些方法是不言而喻的,而且大多数 web 程序员至少对 jQuery 或类似的库有一点熟悉。

images 提示如果你正在做 web 开发而没有使用 jQuery 之类的东西,那么你就错过了。更多细节请见我的书 Pro jQuery,这本书也是由 Apress 出版的。你的网络开发将会改变。

您可以在 Windows 应用项目中使用 jQuery。不过,在很大程度上,我倾向于坚持使用WinJS.Utilities方法。它们完成了 jQuery 支持的大部分基本功能,我发现它们又快又可靠。

确定元素的大小和位置

除了 DOM 查询之外,WinJS.Utilities名称空间还包含帮助确定应用布局中元素的大小和位置的方法。这些方法在表 18-9 中描述。这些方法对单个元素进行操作,这些元素可以使用 DOM 方法如getElementById或从QueryCollection对象中获得。

为了演示这些方法,我创建了一个名为SizeAndPosition的新 Visual Studio 项目。整个项目包含在default.html文件中,如清单 18-16 所示,我已经移除了 Visual Studio 默认添加到新项目中的其他文件。

清单 18-16 。来自 SizeAndPosition 项目的 default.html 文件

`

         SizeAndPosition                         

    
        
Here is some content
    
`

在这个例子中,我把所有的东西都放在了一起,所以你可以看到 CSS 如何影响元素的布局,以及这个布局如何影响WinJS.Utilities方法。我定义了一个包含嵌套的div元素的简单布局。外部元素设置为网格布局,其中可用空间以不同的数量分配给行和列。网格中空间的部分分配以及填充和边距的使用使得很难从标记中计算出内部元素的位置。在script元素中,我使用了表 18-9 中的方法来获取职位的详细信息,并将它们写入 JavaScript 控制台窗口。你可以在图 18-6 中看到布局是如何出现的。

images

图 18-6。创建展示 WinJS 的布局。使用方法

运行这个示例应用会产生下面的输出。根据您使用的设备或模拟器配置,您的结果会有所不同。该输出将显示在 Visual Studio JavaScript 控制台窗口中。


Content Height: 55 Content Width: 608 Total Width: 698 Total Height: 145 Position Top: 59 Position Left: 284 Position Width: 658 Position Height: 105 Rel Left: 264 Rel Top: 39


记录消息

WinJS.Utilities名称空间包含三种方法,可用于记录来自应用的消息。乍一看这似乎没什么用——但是有一个巧妙的技巧可以让这些方法比乍看起来更有趣。这些方法用于通过WinJS.log方法设置日志记录。表 18-10 总结了这些方法,我将在下面的章节中解释。

images 提示注意,log方法在WinJS名称空间中,但是其他方法在WinJS.Utilities中。

images

编写日志消息

在这种情况下,您最常用的方法是WinJS.log,它有三个参数:一个写入日志的消息,一个包含一个或多个标签(用空格分隔)的字符串,这些标签对消息进行分类,以及一个消息类型,如infoerrorwarn。您可以使用任何字符串作为标签和类型,只要对您的应用有意义。

WinJS.log方法不会总是被定义(原因我将很快解释),所以您需要在编写日志消息之前确保它存在。你可以在清单 18-17 的中看到我是如何做到的,在那里我修改了来自SizeAndPosition项目的 default.html 文件,使用了WinJS.log方法。

清单 18-17 。将 WinJS.log 方法应用于 SizeAndPosition 示例

`

         SizeAndPosition                         

    
        
Here is some content
    
`

我发现使用WinJS.log方法最简单的方法是通过应用代码中定义的函数,这就是我在示例中使用logSizeAndPos函数所做的。传递给该函数的任何消息都使用标签winjsappinfo写入日志,并指定类型info。如果在这种状态下运行应用,您将看不到任何输出,因为默认情况下,WinJS.log 方法尚未定义。我将在下一节解释如何设置。

启动日志

在调用WinJS.Utilities.startLog方法之前,没有定义WinJS.log方法。这将告诉系统您对哪种类型的日志消息感兴趣,从而允许您过滤被记录的内容。startLog方法的参数是一个对象,它的属性有特殊的含义——我已经在表 18-11 中列出了公认的属性名。

images

从本质上讲,startLog方法设置了一个过滤器来捕获某些日志消息,并在默认情况下将它们写入 JavaScript 控制台。清单 18-18 显示了对添加到SizeAndPosition项目的脚本块中的startLog的调用。

清单 18-18 。启动日志

`...

...`

在清单 18 中——我调用了startLog方法,指定我对类型为info且标签为appbugsinfo的消息感兴趣。一个日志消息只需要有一个您指定给startLog方法的标签就可以被捕获和处理。对startLog的调用设置了过滤器,在中,创建了WinJS.log方法。如果您现在运行应用,您将在 JavaScript 控制台窗口中看到输出。以下是输出的示例行:

winjs:app:info: Position Top: 59

Windows 应用日志记录系统已格式化输出,以便包含标记。如果您没有看到该消息,请检查JavaScript Console窗口顶部的按钮。这些按钮可用于过滤控制台中显示的消息类型,您可能会发现有一个或多个按钮未按下。您可以在图 18-7 中看到按钮。

images

图 18-7。确保显示日志信息

创建自定义日志记录操作

JavaScript 控制台的问题在于,当应用在调试器之外部署和运行时,它是不可用的。这就是传递给startLog方法的对象的action属性发挥作用的地方——它允许你创建一个定制的日志记录方法,该方法与你的应用的其余部分集成,并且可以在调试器之外工作。action属性被设置为一个函数,该函数被传递了日志消息、标签和类型,如清单 18-19 中的所示,这里我向SizeAndPosition示例的script块添加了一个自定义日志动作。

清单 18-19 。添加自定义日志动作

`

         SizeAndPosition                         

    
        
Here is some content
    
**    
    ** `

在这个例子中,我使用了startLog来创建一个动作,该动作捕获那些类型为info、标签为app并且消息以工作Position开始的消息。对于每个匹配的消息,我创建一个新的div元素,并将其作为子元素添加到我添加到布局中的新容器元素中。这并不是一种特别有用的显示日志消息的方式,但是它确实证明了您可以对您需要的日志信息做几乎任何事情——这可能包括向用户显示它、将它保存到一个文件或者将它上传到一个服务器。

您可以通过调用formatLog方法来创建与写入 JavaScript 控制台的字符串格式相同的字符串——这与默认操作使用的方法相同,并生成包含消息和标签细节的字符串。当然,您可以完全忽略这个方法,生成对您的应用有意义的任何消息格式。你可以在图 18-8 的中看到这些增加的结果,它显示了作为布局一部分的日志信息。(布局元素没有正确对齐,因为我在最初的示例中使用了奇怪的网格布局。)

images 提示注意,我在示例中保留了对startLog的原始调用。WinJS 日志记录系统支持多种过滤器和操作,这意味着消息仍然被写入 JavaScript 控制台,这在开发和测试过程中非常有用。

images

图 18-8。在应用布局中显示选中的日志信息

总结

在这一章中,我通过描述动画特性结束了对 WinJS UI 特性的介绍。我向您展示了如何直接使用 CSS3 特性来转换属性和元素。我转到了 WinJS 便利功能,它可以使 CSS 功能更容易使用,并且包含您希望向用户发出信号的常见情况的预定义效果,例如应用布局中出现的新内容。一如往常的效果,WinJS 动画应该简短,简单,少用。

本章结束时,我详细介绍了WinJS.Utilities名称空间中最有用的方法。我向您展示了如何查询元素、操作 DOM、获取应用布局中元素的大小和位置,以及如何创建一个灵活的日志系统,供您在整个应用生命周期中使用。我在前面的章节中解释了这些方法的用途,并把它们包含在这里,这样你将来可以很快地引用它们。在本书的下一部分,我将向您展示如何将您的应用集成到 Windows 中,并通过这样做来改善您的应用向用户提供的体验。

十九、了解应用生命周期

我将关注的第一个集成领域是 Metro 应用生命周期。到目前为止,在本书中,我一直在掩饰应用的启动和管理方式,我一直依赖于 Visual Studio 在创建新项目时添加到default.js文件中的一部分代码,为了简洁起见,我删除了其他部分。

在这一章中,我将解释 Metro 应用生命周期中的不同阶段,解释它们是如何发生的以及为什么会发生,并向您展示如何理解您的应用何时从一个阶段进入另一个阶段。在这个过程中,我将解释为什么 Visual Studio 添加的代码不是那么有用,向您展示如何修复它,并向您展示如何确保您的应用适合整体 Metro 和 Windows 8 生命周期模型。表 19-1 对本章进行了总结。

images

了解应用生命周期

Windows 8 积极管理 Metro 应用,以保持设备内存空闲。这样做是为了确保最大限度地利用可用内存,并减少电池消耗。作为这一战略的一部分,Metro 应用拥有明确的生命周期。除了最简单的应用,所有应用都需要知道 Windows 8 何时将应用从生命周期的一个阶段转移到另一个阶段,这是通过侦听一些特定的系统事件来完成的。我将描述这些事件,并向您展示如何处理它们,但在这一部分,我将描述生命周期,以便您了解您的 Metro 应用运行的环境。

激活

一个应用在启动时被激活,通常是当用户点击开始屏幕上的图标时。这是 Metro 应用的基本初始化,就像任何类型的应用启动时一样,您需要创建布局、加载数据、设置事件监听器等等。简而言之,激活的应用从不运行变为运行,并负责引导其状态以向用户提供服务。

images 注意并非所有的激活都是为了启动一个应用——正如我将在本章后面解释的,Windows 也会激活你的应用,这样它就可以执行其他任务,包括那些由契约定义的任务。契约是本书这一部分的主题,你将在第二十四章开始看到它们是如何工作的。

暂停

当一个应用不被使用时,它会被 Windows 8 暂停,最常见的原因是用户已经切换到使用另一个应用。这就是应用管理中积极的部分:在用户切换到另一个应用后几秒钟,一个应用就被挂起了。简而言之,一旦你的应用对用户不再可见,你就可以期待它被暂停。

暂停的应用被冻结。应用的状态被保留,但应用代码不会被执行。用户不会意识到某个应用已被暂停,该应用的缩略图仍会出现在正在运行的应用列表中,以便可以再次选择它。

恢复

当用户选择一个暂停的应用并再次显示它时,暂停的应用被恢复。Windows 会保留应用的布局和状态,因此在恢复应用时,您不需要加载数据和配置 HTML 元素。

终止

如果 Windows 需要释放内存,暂停的 Metro 应用将被终止。暂停的应用在终止时不会收到任何通知,应用的状态和任何未保存的数据都会被丢弃。应用布局的快照将从向用户显示的运行应用列表中移除,并替换为作为占位符的闪屏。如果用户再次启动应用,应用将返回激活状态。

images 注意 Windows 没有为 Metro 应用开发者提供关于应用何时终止的明确政策。这是最后的手段,但 Windows 可以在任何需要的时候自由终止应用,并且您不能根据可用的系统资源来假设应用被终止的可能性。

与 WinJS 合作。应用

既然您已经理解了生命周期的不同阶段,我将向您展示如何在应用 JavaScript 代码中处理它们。这个部分的关键对象是WinJS.Application,它提供了对 JavaScript Metro app 的生命周期事件和一些相关特性的访问。

images 提示 WinJS.Application只是包装了Windows.UI.WebUI.WebUIApplication对象中的功能。使用WinJS.Application的价值在于它以一种更符合 JavaScript 和 web 应用开发的方式呈现了生命周期特性。你可以直接使用WebUIApplication类,但是我发现有一些有用的WinJS.Application特性值得使用。

我将从WinJS.Application支持的事件开始,我已经在表 19-2 中描述过了。对于处理应用生命周期,重要的事件是activatedcheckpoint。事实上,我很少使用其他事件。

images

当您创建一个新的 Metro 项目时,Visual Studio 会向js/default.js文件添加一些代码,使用WinJS.Application对象提供一些基本的生命周期事件处理。对于这一章,我已经创建了一个名为EventCalc的新 Visual Studio Metro 应用项目,你可以在清单 19-1 中看到 Visual Studio 创建的default.js文件。(我已经编辑了清单中的注释,并突出显示了与WinJS.Application对象相关的部分。)

清单 19-1 。Visual Studio 添加到 default.js 文件中的代码

// For an introduction to the Blank template, see the following documentation: // http://go.microsoft.com/fwlink/?LinkId=232509 `(function () {
    "use strict";

WinJS.Binding.optimizeBindingReferences = true;

**    var app = WinJS.Application;**
    var activation = Windows.ApplicationModel.Activation;

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !==
                activation.ApplicationExecutionState.terminated) {
**                // app has been launched**
            } else {
**               // app has been resumed**
            }
            args.setPromise(WinJS.UI.processAll());
        }
    };

app.oncheckpoint = function (args) {
**        // app is about to be suspended**
    };

**    app.start();**
})();`

你可以使用addEventListener方法来设置你的事件监听器函数,但是微软已经为 Visual Studio 生成的代码使用了on*属性,我将在本章中遵循这个约定。

images 提示注意,清单中的最后一条语句是对WinJS.Application.start方法的调用。WinJS.Application对象将事件排队,直到start方法被调用,此时事件被传递给你的代码。调用stop方法会导致WinJS.Application再次开始对事件进行排队。一个常见的问题是忘记调用start,创建一个激活时不做任何事情的应用。

这个清单中有几个问题。首先,Visual Studio 添加到新项目中的代码不是很有帮助。第二,WinJS.Application对象不会转发WebUIApplication对象发送的所有事件。如果你使用/js/default.js文件中的代码构建一个复杂的应用而不解决这两个问题,你迟早会碰壁。在我构建出EventCalc示例应用后,我将解释这两个问题并演示它们的解决方案。

构建示例应用

示例应用是一个非常简单的计算器,允许您将两个数字相加。该应用还显示其接收的生命周期事件的详细信息。在图 19-1 中可以看到没有任何内容的 app 布局。

images

图 19-1。event calc 应用

定义视图模型

这是一个基本的简单应用,但如果我添加一个简单的视图模型,它将帮助我演示应用的生命周期。清单 19-2 显示了viewmodel.js文件的内容,我将它添加到了 Visual Studio 项目的js文件夹中。

清单 19-2 。EventCalc 应用的视图模型

`(function () {

WinJS.Namespace.define("ViewModel", WinJS.Binding.as({
        State: {
            firstNumber: null,
            secondNumber: null,
            result: null,
            history: new WinJS.Binding.List(),
            eventLog: new WinJS.Binding.List()
        }
    }));

WinJS.Namespace.define("ViewModel.Converters", {
        calcNumber: WinJS.Binding.converter(function (val) {
            return val == null ? "" : val;
        }),
        calcResult: WinJS.Binding.converter(function (val) {
            return val == null ? "?" : val;
        }),
    });
})();`

ViewModel.State对象为相加的两个值和计算结果定义属性。我还使用了两个WinJS.Binding.List对象来记录计算和保存应用接收的生命周期事件的细节。除了状态数据之外,我还定义了一对简单绑定转换器,当视图模型属性没有赋值时,它们将阻止null显示在布局中。

定义标记

我的示例应用只包含一组内容,所以我在default.html文件中定义了标记,而不是使用单页内容模型和导航 API。你可以在清单 19-3 的中看到default.html的内容。我没有在这个文件中使用任何新技术。布局分为三个部分。第一部分和最后一部分包含ListView控件,该控件使用视图模型列表对象作为数据源,中间部分包含基本的 HTML 元素,用于捕获用户的计算输入并显示结果。

清单 19-3 。default.html 文件的内容

`

<html> <head>     <meta charset="utf-8" />     <title>EventCalc</title>


    
    
    


    
    
    

    
        
    


        Events
        

        

    


        Calculator
        

            
            +
            
            =
            
            
        

    


        History
        

        

    

`
定义 CSS

为了管理 HTML 元素的布局,我在/css/default.css文件中定义了如清单 19-4 所示的样式。这些样式依赖于标准的 CSS 属性,没有使用特定于 Metro 的特性。

清单 19-4 。/css/default.css 文件的内容

`body {display: -ms-flexbox;-ms-flex-direction: row;
    -ms-flex-align: stretch;-ms-flex-pack: center;}
div.container {width: 30%; display: inline-block;margin: 0 20px;    
    text-align: center;font-size: 35pt;}

eventList, #historyList, #calcElems {

border: thin solid white;height: 85%;padding-top: 20px;
}

calcButton

calcContainer input

*.calcSymbol, #result {font-size: 35pt;}
.message {font-size: 20pt;margin: 5px;text-align: left;}`

定义 JavaScript

这个应用所有有趣的部分都发生在 JavaScript 代码中。清单 19-5 建立在 Visual Studio 添加到default.js文件的代码上,因此生命周期事件的细节显示在一个ListView控件中,并且可以执行计算。

清单 19-5 。构建在 Visual Studio 创建的 default.js 文件上

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    WinJS.strictProcessing();

**    function writeEventMessage(msg) {**
**        ViewModel.State.eventLog.push({ message: msg });**
**    };**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !==
                activation.ApplicationExecutionState.terminated) {
**                // app has been launched**
**                writeEventMessage("Launched");**
**                performInitialization();**
            } else {
**                // app has been resumed**
**                writeEventMessage("Resumed");**
            }

args.setPromise(WinJS.UI.processAll().then(function () {
**                return WinJS.Binding.processAll(calcElems, ViewModel.State);**
            }));
        }
    };

**    app.oncheckpoint = function (args) {**
**        // app is about to be suspended**
**        writeEventMessage("Suspended");**
**    };**

**    function performInitialization() {**
**        calcButton.addEventListener("click",**
**            function (e) {**
**                var first = ViewModel.State.firstNumber = Number(firstInput.value);**
**                var second = ViewModel.State.secondNumber = Number(secondInput.value);**
**                var result = ViewModel.State.result = first + second;**
**            });**

**        ViewModel.State.bind("result", function (val) {             if (val != null) {**
**                ViewModel.State.history.push({**
**                    message: ViewModel.State.firstNumber + " + "**
**                        + ViewModel.State.secondNumber + " = "**
**                        + val**
**                });**
**            }**
**        });**
    };

app.start();
})();`

writeEventMessage函数向视图模型添加一个项目,报告生命周期事件的接收,格式化消息,以便它可以与我在default.html文件中定义的模板一起工作。

performInitialization函数包含我希望在应用启动时执行的代码——对于这个应用,这意味着为由button元素发出的click事件设置一个处理程序,并设置一个编程数据绑定,以便在视图模型result属性更改时生成计算历史。

images 提示注意,在onactivated处理程序中,我对传递给函数的参数对象调用了setPromise方法。我将在本章后面的处理闪屏部分解释这个方法。

触发生命周期变更

将应用从生命周期的一部分转移到另一部分的最简单方法是使用 Visual Studio。如果你查看工具栏,你会看到一个标有Suspend的菜单项。如果你点击标签右边的小箭头,你会看到菜单也包含了ResumeSuspend and?? 的选择,如图图 19-2 所示。(您可能需要从 Visual StudioViewToolbars菜单中选择Debug位置才能看到工具栏。)

images

图 19-2。使用 Visual Studio 控制应用生命周期

菜单项迫使应用从一个生命周期阶段转移到另一个阶段。它们很有用,因为它们允许您在附加调试器的情况下测试代码。在应用开发的早期阶段,我经常使用这个功能。

然而,Visual Studio 只能模拟生命周期事件,这意味着调试器运行时和不运行时会有细微的差别。这意味着您应该花时间通过直接从操作系统生成事件来测试您的应用的行为方式,而不依赖于 Visual Studio 调试器。

问题是,当调试器没有被使用时,一个影响是没有JavaScript Console窗口可用于显示调试消息,这使得更难弄清楚发生了什么。正是因为这个原因,我在示例应用中添加了一个ListView控件,这样我就可以在应用本身的布局中记录生命周期事件的到来——这是我经常用于应用最终测试的一种技术。

在 Windows 8 中生成生命周期事件

生成生命周期事件实际上非常简单,只要您耐心等待,看看 Windows 8 显示的指示生命周期变化的指示。在接下来的小节中,我将向您展示如何在不使用 Visual Studio 的情况下生成每个activatedresumingsuspending事件。正如我所说的,这是测试你的用户将会看到什么的唯一现实的方法。

启动应用

触发activated事件最简单的方法是启动应用,尽管重要的是不要用 Visual Studio 调试器来做这件事。你可以从开始屏幕中选择应用的磁贴,或者从 Visual Studio Debug菜单中选择Start Without Debugging

images 提示在 Visual Studio 中至少启动一次之前,示例应用的磁贴不会添加到开始屏幕。之后,您应该会看到应用的磁贴列在屏幕的最右侧。有时磁贴会不可见,特别是如果你一直在模拟器和本地机器之间切换以运行应用——在这种情况下,我发现通过键入应用名称的前几个字母并从结果列表中选择它来搜索应用足以使文件正确显示。

当你启动EventCalc应用时,你会看到默认的闪屏(因为我没有改变所用颜色或图标的清单设置),然后看到如图图 19-3 所示的应用布局。请注意,左侧的ListView控件中已经记录了activated事件。我用消息Launched记录了这个事件,以表明接收到的事件是一个启动应用的请求——这一点我将在本章后面详细解释。

images

图 19-3。在没有调试器的情况下启动示例应用

暂停应用

让 Windows 暂停应用最简单的方法就是按Win+D切换到桌面。您可以通过启动Task Manager,切换到Details选项卡并找到WWAHost.exe进程来跟踪应用生命周期的进程(这是用于运行 JavaScript Metro 应用的可执行文件的名称——因此,您启动的每个 Metro 应用都会有一个进程)。您可能需要点击任务管理器中的More Details按钮才能看到Details选项卡。

images 注意您需要在本地机器上启动任务管理器,即使示例应用正在模拟器中运行。任务管理器不能在模拟器中运行,但是WWAHost.exe进程在本地机器的任务管理器中仍然可见。

几秒钟后,Windows 将暂停应用,进程状态将在Task Manager中变为Suspended。你可以在图 19-4 中看到一个暂停的应用是如何显示在任务管理器中的。

images

图 19-4。使用任务管理器观察一个 Metro 应用被挂起

images 注意连接到 Visual Studio 调试器的应用永远不会在Task Manager中显示为挂起,因为它们保持活动状态,以便调试器可以控制应用。如果你没有看到挂起的消息,通常是因为应用是用调试器启动的。

我需要使用任务管理器来检查应用的状态,因为我再也看不到应用的布局,而且由于我没有使用调试器,我也看不到任何调试消息。在下一节中,当我恢复应用时,您将看到收到了suspending事件的证据。

恢复应用

你可以通过将应用带回到前台来恢复应用,最简单的方法是将鼠标移动到左上角(或在触摸屏上从左边缘滑动)并单击缩略图。应用将返回并填满屏幕,您将看到在应用布局的左侧ListView控件中记录了一个新事件,如图图 19-5 所示。

images

图 19-5。app 收到暂停和恢复事件

图中显示suspending事件被 app 接收。这发生在应用对用户隐藏之后,但在进程在Task Manager中显示为暂停之前。这个小间隔是我将在本章后面返回的东西,因为它为应用提供了一个准备被挂起的机会,这对某些类型的应用来说是无价的。

没有显示的是resuming事件的接收。这是 Visual Studio 添加到项目中的代码的问题之一。我将向您展示如何确保您的应用很快获得该事件。

终止应用

你可以通过输入Alt + F4来终止应用的执行。应用会突然终止,并且不会发送警告事件。以这种方式退出应用允许您确保您的应用在下次启动时正确恢复,并检查它使用的任何远程服务(web 服务、数据库等。)能够正确地恢复资源。

这是终止你的应用的两种方式之一。另一种情况发生在应用暂停,Windows 需要释放一些资源的时候。在这两种情况下,您的应用将被终止,而不会收到任何通知事件。在这一章的后面,我将向你展示当你的应用被暂停时如何准备终止,我也将向你展示当你的应用下次启动时如何判断它是如何被终止的。

您可以通过使用 Visual Studio 工具栏菜单中的Suspend and Shutdown项(包含SuspendResume项的菜单)来模拟 Windows 终止应用的情况。

获取激活类型和以前的应用状态

在我可以修复default.js文件中的代码以便获得所有的生命周期事件之前,我需要做一些准备工作,以便我可以弄清楚当事件到来时我被要求做什么。为此,我需要深入研究事件的细节,以发现激活类型和我的应用在事件被调度之前所处的状态。

当一个应用被发送activated事件时,传递给onactivated函数的事件对象有一个detail属性,该属性返回一个Windows.ApplicationModel.Activation.IActivatedEventArgs对象。

IActivatedEventArgs对象定义了我在表 19-3 中描述的三个属性,这些属性为你提供了所有你需要的信息,让你知道 Windows 要求你的应用做什么。我将在接下来的章节中描述其中的两个属性,并在本章的后面返回到第三个属性。

images

确定激活的种类

一个应用可以被激活用于一系列不同的目的,例如在应用之间共享数据,从设备接收数据,以及处理搜索查询。这些激活目的是由 Windows 契约定义的,它允许你将你的应用集成到 Windows 中,我将在这一章的后面返回——你将能够在后面的章节中看到我如何实现契约的例子。对于这一章,我关心的是启动激活,这发生在用户已经启动应用或者应用在暂停后已经恢复的时候。

您可以通过从activated事件中读取kind属性来确定您正在处理的激活类型。kind属性返回由Windows.ApplicationModel.Activation.ActivationKind枚举定义的值之一。我在launch的这一章中寻找的唯一值,它告诉我应用已经被启动或恢复。你可以看到我如何在清单 19-6 的函数中检查这一点。

清单 19-6 。检查启动激活类型

... app.onactivated = function (args) {     if (**args.detail.kind === activation.ActivationKind.launch**) {        // the app has been launched        writeEventMessage("Launched");     } }; ...

确定以前的应用状态

在处理一个启动激活请求时,你需要知道应用在激活前是什么状态,这可以通过读取previousExecutionState属性来确定。该属性返回由Windows.ApplicationModel.Activation.ApplicationExecutionState枚举定义的值之一,我已经在表 19-4 中列出了这些值。

images

如果你的应用之前的状态是notRunningclosedByUser,那么你正在处理一个的重新开始的发布。您需要初始化您的应用(设置 UI 控件、事件监听器等。)并为应用的首次用户交互做准备。

如果之前的状态是suspended,那么应用将已经被初始化,并且应用的状态将与应用被挂起时一样。terminated状态是两种情况的奇怪组合。应用是由系统终止的,而不是用户,所以这个想法是,如果用户再次启动应用,他们应该能够像暂停和终止发生之前一样继续。但是,应用的状态在终止时会丢失。为了实现正确的行为,您需要存储应用挂起时的状态,以防发生终止。我将在本章的后面解释如何做到这一点。

当 Windows 希望您的应用履行其契约义务时,通常会遇到running状态,并且该应用已经在运行,因为用户之前已经启动了它。当您使用 Visual Studio Refresh按钮重新加载应用的内容时,您也会遇到这种情况——当然,这不是您的应用在部署给用户时会遇到的情况。你如何响应运行状态将取决于你的应用和它所支持的契约,但是在这一章中,我将把running状态视为与notRunning相同的状态,只是为了确保应用与 Visual Studio Refresh按钮一起正常工作。这可能对所有的应用都没有意义,但是因为我的用户——也就是你——可能会在 Visual Studio 中运行应用,所以这是最明智的做法。

应对不同的投放类型

现在您已经理解了 Windows 如何在activated事件中提供细节,我可以在我的/js/default.js文件中添加一些代码来响应不同的情况。你可以在清单 19-7 中看到我添加的内容。

清单 19-7 。区分激活类型和以前的执行状态

`...
app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
**        switch (args.detail.previousExecutionState) {**
**            case activation.ApplicationExecutionState.suspended:**
**                writeEventMessage("Resumed from Suspended");**
**                break;**
**            case activation.ApplicationExecutionState.terminated:**
**                writeEventMessage("Launch from Terminated");**
**                performInitialization();**
**                break;**
**            case activation.ApplicationExecutionState.notRunning:**
**            case activation.ApplicationExecutionState.closedByUser:**
**            case activation.ApplicationExecutionState.running:**
**                writeEventMessage("Fresh Launch");**
**                performInitialization();**
**                break;**
**        }**

args.setPromise(WinJS.UI.processAll().then(function () {
            return WinJS.Binding.processAll(calcElems, ViewModel.State);
        }));
    }
};
...`

当我收到一个启动激活事件时,我使用一个 switch 语句来检查所有的ApplicationExecutionState值。我将notRunningclosedByUserrunning的值视为相同,并调用performInitialization函数来设置我的事件监听器和数据绑定。我已经更改了传递给writeEventMessage函数的消息,以使我收到的事件的意义更加清晰。

我也是在前一个状态是terminated的时候调用performInitialization函数。我将在本章后面添加一些额外的代码,以区分我处理这种状态的方式与notRunningrunningclosedByUser。对于所有这些先前的状态,我需要初始化我的应用,以确保我的事件处理程序和数据绑定已经设置好。

我还没有对suspended值做任何事情,除了调用writeEventMessage函数来记录事件的接收。我不需要初始化我的应用,因为系统正在恢复执行,我的事件处理程序和数据绑定已经存在。当我谈到后台活动时,我会在这一部分添加一些代码,但是目前,什么也不做是将对suspended状态的响应区分开来的原因。

images 提示你会注意到我调用了WinJS.UI.processAllWinJS.Binding.processAll方法,而不管我正在处理哪种激活。我将在本章的后面回到这段代码。

你可以在图 19-6 中看到我显示的三个激活事件消息中的两个。您可以使用 Visual Studio 非常容易地自己重新创建这些事件。首先,使用Debug菜单中的Start Debugging项启动应用——这将产生如图左侧所示的Fresh Launch消息。

images

图 19-6。显示启动激活事件的详细信息

现在从工具栏菜单中选择Suspend and Shutdown项,这将终止应用。再次启动应用,activated事件的先前执行状态将被设置为terminated,导致出现图中右侧所示的消息。

捕获恢复事件

我的default.js文件中的代码可以区分不同类型的激活事件,并根据应用之前的状态做出响应——但当应用恢复时,我仍然没有收到事件。为了补救这一点,我在default.js文件中添加了如清单 19-8 所示的内容。

清单 19-8 。捕捉恢复事件

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    WinJS.strictProcessing();

function writeEventMessage(msg) {
        ViewModel.State.eventLog.push({ message: msg });
    };

app.onactivated = function (args) {
        // ...code removed for brevity...
    };

app.oncheckpoint = function (args) {
        // app is about to be suspended
        writeEventMessage("Suspended");
    };

function performInitialization() {
        // ...code removed for brevity...
    };

**    Windows.UI.WebUI.WebUIApplication.addEventListener("resuming", function (e) {**
**        WinJS.Application.queueEvent({**
**            type: "activated",             detail: {**
**                kind: activation.ActivationKind.launch,**
**                previousExecutionState: activation.ApplicationExecutionState.suspended**
**            }**
**        });**
**    });**

app.start();
})();`

正如我之前提到的,WinJS.Application对象是由Windows.UI.WebUI.WebUIApplication对象提供的功能的包装器。出于我不明白的原因,WinJS.Application类在由WebUIApplication对象发送恢复事件时不会将其转发给应用,我对default.js文件的添加修复了这一遗漏:我监听由WebUIApplication对象发出的resuming事件,并通过调用queueEvent对象将其送入由WinJS.Application维护的事件队列。

我传递给queueEvent方法的对象符合被激活事件的模式,即传递——类型属性被设置为activated,detail属性具有返回预期值的kindpreviousExecutionState属性。

这并不理想,但是如果您希望能够响应整个生命周期的变化,这是很重要的。图 19-7 展示了修复的效果,您可以通过启动示例应用,然后从 Visual Studio 工具栏菜单中选择SuspendResume项来复制这个结果。

images

图 19-7。说明捕捉恢复事件的效果

响应生命周期变化

当然,应用响应生命周期变化的方式会有所不同,但是无论你的应用提供什么工具,你都需要考虑一些核心行为。在接下来的章节中,我描述了一些关键技术,当你响应生命周期事件时,这些技术可以让你的应用适应生命周期模型。

处理闪屏

你可能已经注意到我在本章的onactivated处理函数中使用了setPromise方法,如清单 19-9 所示。这是一个有用的WinJS.Application功能,可以防止应用初始化前闪屏消失。

清单 19-9 。使用 setPromise 方法保留闪屏

`...
app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        switch (args.detail.previousExecutionState) {
            case activation.ApplicationExecutionState.suspended:
                writeEventMessage("Resumed from Suspended");
                break;
            case activation.ApplicationExecutionState.terminated:
                writeEventMessage("Launch from Terminated");
                performInitialization();
                break;
            case activation.ApplicationExecutionState.notRunning:
            case activation.ApplicationExecutionState.closedByUser:
            case activation.ApplicationExecutionState.running:
                writeEventMessage("Fresh Launch");
                performInitialization();
                break;
        }

**        args.setPromise(WinJS.UI.processAll().then(function () {**
**            return WinJS.Binding.processAll(calcElems, ViewModel.State);**
**        }));**
    }
};
...`

闪屏将一直显示,直到传递给args.setPromise方法的Promise完成。在这个清单中,我使用了一系列的Promise对象,它们分别调用WinJS.UI.processAllWinJS.Binding.processAll方法。只有当这两种方法都完成时,应用布局才会替换闪屏。

images 提示我在第十章中描述了WinJS.UI.processAll法,在第八章中描述了WinJS.Binding.processAll法。

目前,示例应用中没有任何东西会延迟闪屏的移除,因此,为了演示这个特性,我将在接下来的部分中添加一些新功能。

添加到示例应用

我的示例应用非常简单,但是在真正的应用中可能需要大量的初始设置。为了模拟这种情况,我更改了示例应用,以便在应用首次启动时计算前 5000 个整数值相加的结果,然后在用户执行计算时使用这些数据来产生计算结果。如果你愿意的话,忘记这样做没有什么好的理由,除了这是一个有用的演示。首先,我向视图模型添加了一个新值来包含缓存的结果,如清单 19-10 所示。

清单 19-10 。为缓存数据添加视图模型属性

... WinJS.Namespace.define("ViewModel", WinJS.Binding.as({     State: {         firstNumber: null,         secondNumber: null,         result: null,         history: new WinJS.Binding.List(),         eventLog: new WinJS.Binding.List(), **        cachedResult: null**     } }));

为了生成缓存的结果,我在项目中添加了一个名为tasks.js的新文件,其中包含一个定制的WinJS.Promise实现。你可以在清单 19-11 中看到tasks.js文件的内容,它展示了doWork函数的实现。(我在第九章中解释了Promise对象如何工作以及如何实现自己的对象)。

清单 19-11 在 tasks.js 文件中实现自定义承诺

`(function () {
    WinJS.Namespace.define("Utils", {
        doWork: function (count) {
            var canceled = false;

return new WinJS.Promise(function (fDone, fError, fProgress) {
                var blockSize = 500;

var results = {};

(function calcBlock(start) {
                    for (var first = start; first < start + blockSize; first++) {
                        results[first] = {};
                        for (var second = start; second < start + blockSize; second++) {
                            results[first][second] = first + second;
                        }
                    }
                    if (!canceled && start + blockSize < count) {
                        fProgress(start);
                        setImmediate(function () {
                            calcBlock(start + blockSize);
                        });
                    } else {
                        fDone(results);
                    }
                })(1);

}, function () {
                canceled = true;
            });
        }     });
})();`

定制的Promise代码可能很难阅读,但是清单中的代码在调用setImmediate函数之前一次将 500 个块中的成对数字加在一起,以避免锁定 JavaScript 运行时。我将calcBlock函数定义为一个自执行函数,并在用参数1定义后立即调用它来计算第一组结果。

此代码创建的数据结构是一个对象,它具有每个数值的属性,每个属性的值是另一个对象,它的属性对应于第二个数值,其值是总和,如下所示:

results = {     1 = {         1: 2,         2: 3,         3: 4,     } }

完整的结果集被传递给Promise完成函数,这意味着可以使用doWork函数返回的Promise对象的then方法来访问它。我已经将tasks.js文件添加到了default.html文件的头部分,如清单 19-12 所示。

清单 19-12 。将 tasks.js 文件添加到 default.html 头文件

`...

<head>     <meta charset="utf-8" />     <title>EventCalc</title>


    
    
    


    
    
**    **
    

...`
生成缓存的结果

要查看setPromise对象解决的问题,它有助于查看当不使用方法时会发生什么。为此,我在onactivated处理程序中调用了doWork方法,但是没有使用setPromise对象。你可以在清单 19-13 中看到对default.js文件的添加。

清单 19-13 。不使用 setPromise 对象生成缓存结果

`...
app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        switch (args.detail.previousExecutionState) {
            case activation.ApplicationExecutionState.suspended:
                writeEventMessage("Resumed from Suspended");
                break;
            case activation.ApplicationExecutionState.terminated:
                writeEventMessage("Launch from Terminated");
                performInitialization();
                break;
            case activation.ApplicationExecutionState.notRunning:
            case activation.ApplicationExecutionState.closedByUser:
            case activation.ApplicationExecutionState.running:
                writeEventMessage("Fresh Launch");
                performInitialization();
                break;
        }

**        Utils.doWork(5000).then(function (data) {**
**            ViewModel.State.cachedResult = data;**
**        });**

args.setPromise(WinJS.UI.processAll().then(function () {
            return WinJS.Binding.processAll(calcElems, ViewModel.State);
        }));
    }
};
...`

只有在计算完所有值后,才会将缓存的结果分配给视图模型属性。风险在于,在所有数据可用之前,用户将看到应用的布局。

images我发现一项任务花费大约 5-10 秒钟是合适的。

你可以在performInitialization函数中看到潜在的问题,我在清单 19-14 中修改了这个函数以使用缓存的结果。

清单 19-14 。使用 performInitialization 函数中的缓存数据

... function performInitialization() {     calcButton.addEventListener("click", function (e) {         var first = ViewModel.State.firstNumber = Number(firstInput.value);         var second = ViewModel.State.secondNumber = Number(secondInput.value); `**        if (first < 5000 && second < 5000) {**
**            ViewModel.State.result = ViewModel.State.cachedResult[first][second];**
**        } else {**
**            ViewModel.State.result = first + second;**
**        }**
    });

ViewModel.State.bind("result", function (val) {
        if (val != null) {
            ViewModel.State.history.push({
                message: ViewModel.State.firstNumber + " + "
                    + ViewModel.State.secondNumber + " = "
                    + val
            });
        }
    });
};
...`

如果用户试图在缓存数据准备好之前执行计算,试图从视图模型中读取结果将会产生一个异常,如图 19-8 所示。在缓存数据准备就绪之前,尝试执行 1 + 1 计算后,显示了图中的错误消息。click事件处理程序中的代码,如清单 19-14 所示,试图访问一个名为 1 的变量,但它还不存在。

images

图 19-8。试图在缓存结果生成之前使用它们

维护闪屏

另一种方法是使用setPromise方法,这将确保闪屏一直显示到它所经过的Promise完成。你可以在清单 19-15 中看到我是如何为示例应用做这些的。

清单 19-15 。使用 setPromise 方法保持闪屏显示

`...
app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        switch (args.detail.previousExecutionState) {
            case activation.ApplicationExecutionState.suspended:
                writeEventMessage("Resumed from Suspended");
                break;
            case activation.ApplicationExecutionState.terminated:
                writeEventMessage("Launch from Terminated");
                performInitialization();
                break;
            case activation.ApplicationExecutionState.notRunning:
            case activation.ApplicationExecutionState.closedByUser:
            case activation.ApplicationExecutionState.running:
                writeEventMessage("Fresh Launch");
                performInitialization();
                break;
        }

**        var cachedPromise = Utils.doWork(5000).then(function (data) {**
**            ViewModel.State.cachedResult = data;**
**        });**

**        var processPromise = WinJS.UI.processAll().then(function () {**
**            return WinJS.Binding.processAll(calcElems, ViewModel.State);**
**        });**

**        args.setPromise(WinJS.Promise.join([cachedPromise, processPromise]));**
    }
};
...`

doWork函数生成缓存的结果时,WinJS.UI.processAllWinJS.Binding.processAll方法没有理由不能对标记进行操作,所以我使用了WinJS.Promise.join方法来创建一个Promise,让这两个活动交错进行——正是这个组合的Promise被我传递给了setPromise方法,它确保了闪屏将一直显示,直到doWork和两个processAll调用都完成了它们的工作。

当然,我还没有完全正确的行为——每当我的应用启动时,我的结果都会被计算,即使它是从suspended状态恢复的。我需要更好地选择何时执行初始化,最简单的方法是开始在performInitialization函数中生成缓存的结果,当应用还没有从挂起状态恢复时就会调用这个函数。你可以在清单 19-16 中看到我是如何做到这一点的,它显示了对default.js文件的进一步修改。

清单 19-16 。确保恢复应用时不会生成缓存结果

`(function () {
    "use strict";

var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation;
    WinJS.strictProcessing();

function writeEventMessage(msg) {
        ViewModel.State.eventLog.push({ message: msg });
    };

app.onactivated = function (args) {
**        var promises = [];**

if (args.detail.kind === activation.ActivationKind.launch) {
            switch (args.detail.previousExecutionState) {
                case activation.ApplicationExecutionState.suspended:
                    writeEventMessage("Resumed from Suspended");
                    break;
                case activation.ApplicationExecutionState.terminated:
                    writeEventMessage("Launch from Terminated");
**                    promises.push(performInitialization());**
                    break;
                case activation.ApplicationExecutionState.notRunning:
                case activation.ApplicationExecutionState.closedByUser:
                case activation.ApplicationExecutionState.running:
                    writeEventMessage("Fresh Launch");
**                    promises.push(performInitialization());**
                    break;
            }

**            promises.push(WinJS.UI.processAll().then(function () {**
**                return WinJS.Binding.processAll(calcElems, ViewModel.State);**
**            }));**

args.setPromise(WinJS.Promise.join(promises));
        }
    };

app.oncheckpoint = function (args) {
        // app is about to be suspended
        writeEventMessage("Suspended");
    };

function performInitialization() {
        calcButton.addEventListener("click", function (e) {
            var first = ViewModel.State.firstNumber = Number(firstInput.value);
            var second = ViewModel.State.secondNumber = Number(secondInput.value);
            if (first < 5000 && second < 5000) {
                ViewModel.State.result = ViewModel.State.cachedResult[first][second];
            } else {
                ViewModel.State.result = first + second;
            }
        });         ViewModel.State.bind("result", function (val) {
            if (val != null) {
                ViewModel.State.history.push({
                    message: ViewModel.State.firstNumber + " + "
                        + ViewModel.State.secondNumber + " = "
                        + val
                });
            }
        });

**        return Utils.doWork(5000).then(function (data) {**
**            ViewModel.State.cachedResult = data;**
**        });**
    };

Windows.UI.WebUI.WebUIApplication.addEventListener("resuming", function (e) {
        WinJS.Application.queueEvent({
            type: "activated",
            detail: {
                kind: activation.ActivationKind.launch,
                previousExecutionState: activation.ApplicationExecutionState.suspended
            }
        });
    });

app.start();
})();`

了解闪屏何时关闭

如果初始化需要几秒钟,保持闪屏可见是可以接受的。超过这一点,看起来你的应用在启动时就挂起了,变得没有响应。如果您有很多初始化要执行,那么您应该避免使用setPromise对象,而是向用户显示某种进度显示。要做到这一点,你需要知道闪屏什么时候被关闭,什么时候被你的应用的布局所取代。

当闪屏被替换为应用布局时,IActivatedEventArgs对象的splashScreen属性发出一个dismissed事件。你可以通过传递给onactivated处理函数的Event对象的detail.splashScreen属性获得这个值,如清单 19-17 所示。

清单 19-17 。收到闪屏已被取消的通知

`...
app.onactivated = function (args) {
    var promises = [];

if (args.detail.kind === activation.ActivationKind.launch) {
        switch (args.detail.previousExecutionState) {
            case activation.ApplicationExecutionState.suspended:
                writeEventMessage("Resumed from Suspended");
                break;             case activation.ApplicationExecutionState.terminated:
                writeEventMessage("Launch from Terminated");
                promises.push(performInitialization());
                break;
            case activation.ApplicationExecutionState.notRunning:
            case activation.ApplicationExecutionState.closedByUser:
            case activation.ApplicationExecutionState.running:
                writeEventMessage("Fresh Launch");
                promises.push(performInitialization());
                break;
        }

**        if (args.detail.splashScreen) {**
**            args.detail.splashScreen.addEventListener("dismissed", function (e) {**
**                writeEventMessage("Splash Screen Dismissed");**
**            });**
**        }**

promises.push(WinJS.UI.processAll().then(function () {
            return WinJS.Binding.processAll(calcElems, ViewModel.State);
        }));

args.setPromise(WinJS.Promise.join(promises));
    }
};
...`

应用恢复时没有闪屏——不仅仅是因为 Metro 没有使用闪屏,还因为我将恢复事件提供给了WinJS.Application,并且没有定义splashScreen属性。这意味着您需要检查一下splashScreen属性是否存在。

如果您启动该应用,您将看到在左侧ListView控件中显示一条通知消息,报告闪屏已被取消,如图图 19-9 所示。如果您仍然有应用初始化要执行,那么这将提示您向用户显示一些临时消息或内容。

images

图 19-9。闪屏关闭时显示消息

处理 App 终止

处理一个终止后又启动的 app,需要做一些工作。一个应用不会被警告即将被终止——系统会暂停应用,然后,如果资源紧张,终止应用进程以释放内存。Windows 无法告诉应用它将被终止,除非让它退出挂起状态,这将需要一些 Windows 试图回收的资源。

处理应用终止需要代表应用做一些仔细的工作,因为微软想对用户隐藏终止。这是一个明智的想法,当一个应用遵循这种模式时,它会为用户创造更好的整体地铁体验,用户不会在意有限的设备资源导致的应用终止。用户没有关闭应用,当他们从“正在运行”的应用列表中选择闪屏占位符时,他们会期望应用会让他们从停止的地方继续。

要做到这一点,您需要存储应用挂起时的状态。这就像购买应用终止保险一样——你希望你的应用只是被恢复,你不必恢复状态,但你需要采取预防措施,以防你的应用被终止。

WinJS.Application对象通过它的sessionState属性帮助你存储你的应用状态。通过将一个对象分配给该属性来存储状态,该对象将作为 JSON 数据永久存储。使用的 JSON 解析器非常简单,不会处理复杂的对象,包括可观察的对象,这意味着您通常需要创建一个新的对象来表示您的应用的状态——当然,如果您的应用被终止然后被激活,请准备好使用该对象来恢复该状态。

为了演示这项技术,我在viewmodel.js文件中添加了两个新函数来保存和恢复应用状态数据,如清单 19-18 所示。

images 注意sessionState属性应该只用于存储应用状态,比如用户输入到 UI 控件中的值,用户在应用中导航到的点,以及将应用恢复到其先前状态所需的其他数据。用户数据和设置应该而不是使用sessionState属性存储。我将在第二十章中向您展示如何处理设置,以及如何在第二十二章–24 章中使用文件系统。

清单 19-18 。创建和恢复状态对象的功能

`(function () {

WinJS.Namespace.define("ViewModel", WinJS.Binding.as({
        State: {
            firstNumber: null,
            secondNumber: null,
            result: null,
            history: new WinJS.Binding.List(),
            eventLog: new WinJS.Binding.List(),
            cachedResult: null
        }
    }));

WinJS.Namespace.define("ViewModel.Converters", {
        calcNumber: WinJS.Binding.converter(function (val) {
            return val == null ? "" : val;
        }),
        calcResult: WinJS.Binding.converter(function (val) {
            return val == null ? "?" : val;
        }),
    }); **    WinJS.Namespace.define("ViewModel.State", {**
**        getData: function () {**
**            var data = {**
**                firstNumber: ViewModel.State.firstNumber,**
**                secondNumber: ViewModel.State.secondNumber,**
**                history: [],**
**                events: []**
**            };**
**            ViewModel.State.history.forEach(function (item) {**
**                data.history.push(item);**
**            });**
**            ViewModel.State.eventLog.forEach(function (item) {**
**                data.events.push(item);**
**            });**
**            return data;**
**        },**
**        setData: function (data) {**
**            data.history.forEach(function (item) {**
**                ViewModel.State.history.push(item);**
**            });**
**            data.events.forEach(function (item) {**
**                ViewModel.State.eventLog.push(item);**
**            });**
**            ViewModel.State.firstNumber = data.firstNumber;**
**            ViewModel.State.secondNumber = data.secondNumber;**
**        }**
**    });**

})();`

请注意,我对包含的数据是有选择性的。例如,我不包括result属性。如果我这样做了,并在应用再次启动时恢复了该值,那么设置该属性将触发我在default.js中定义的数据绑定,并在历史记录中创建一个新的(意外的)条目。不设置这个值的缺点是,最后提供给用户的结果不会显示在中间的列中。

这是处理应用状态数据的典型方式——你通常可以非常接近地恢复到应用暂停前的状态,但总有一些事情很难恢复。至于你在处理这些问题时能走多远,这是一个判断的问题。

images 提示我在这个例子中也省略了 app 状态下缓存的计算数据。决定在应用状态中包含相对大量的数据是另一个判断。如果数据是我可以快速再现的东西,就像在这种情况下,那么我倾向于从状态中忽略它,因为我预计终止是一种相对罕见的情况,我宁愿承受再现数据的代价,而不是存储数据的代价,后者无限期地保留在设备上,可能会给用户带来其他问题。这个问题没有简单的答案,您必须查看您正在处理的数据,并找出给用户最佳体验的方法。在你阅读了第二十章之后,你将能够就如何存储更大数量的数据做出明智的决定,我在其中描述了存储应用数据的其他方法。

为了保存和恢复应用状态,我从default.js文件中调用视图模型方法,以响应适当的生命周期事件,如清单 19-19 所示。

清单 19-19 。存储和恢复状态数据

`...
app.onactivated = function (args) {
    var promises = [];

if (args.detail.kind === activation.ActivationKind.launch) {
        switch (args.detail.previousExecutionState) {
            case activation.ApplicationExecutionState.suspended:
                writeEventMessage("Resumed from Suspended");
                break;
            case activation.ApplicationExecutionState.terminated:
**                ViewModel.State.setData(app.sessionState);**
                writeEventMessage("Launch from Terminated");
                promises.push(performInitialization());
                break;
            case activation.ApplicationExecutionState.notRunning:
            case activation.ApplicationExecutionState.closedByUser:
            case activation.ApplicationExecutionState.running:
                writeEventMessage("Fresh Launch");
                promises.push(performInitialization());
                break;
        }

if (args.detail.splashScreen) {
            args.detail.splashScreen.addEventListener("dismissed", function (e) {
                writeEventMessage("Splash Screen Dismissed");
            });
        }

promises.push(WinJS.UI.processAll().then(function () {
            return WinJS.Binding.processAll(calcElems, ViewModel.State);
        }));

args.setPromise(WinJS.Promise.join(promises));
    }
};

app.oncheckpoint = function (args) {
    // app is about to be suspended
**    app.sessionState = ViewModel.State.getData();**
    writeEventMessage("Suspended");
};
...`

就在应用挂起之前,调用了oncheckpoint处理函数,这提示我通过将getData方法的结果赋给sessionState属性来存储我的应用状态。如果我的应用启动了,并且之前的执行状态是terminated,我通过读取sessionState属性的值来恢复状态数据。

这些变化的效果是,当应用终止时,用户获得了(几乎)无缝的体验。你可以在图 19-10 的中看到状态是如何恢复的,以及生命周期事件通知,显示应用被启动之前已经被终止。

images

图 19-10。保存和恢复 app 状态的效果

处理 App 暂停

除了存储应用的状态,你还可以使用oncheckpoint处理函数来释放应用正在使用的资源,并控制应用正在执行的任何后台任务。

例如,在资源方面,您可能需要关闭与服务器的连接或安全地关闭文件。你不知道你的应用将被挂起多长时间,也不知道它是否会在挂起时被终止,所以使用oncheckpoint处理程序让你的应用进入安全状态是有意义的。如果您使用远程服务器,最好尝试关闭连接,以便其他用户可以使用它们–大多数服务器最终会发现您的应用已经消失,但这可能需要一段时间,而且仍然有一些企业服务器对并发连接的数量有严格的限制,即使这些连接没有主动处理请求。

就后台任务而言,如果在应用暂停时,您有使用setImmediatesetIntervalsetTimeout方法延迟的工作,那么这项工作将在应用再次恢复时执行——这并不总是有用的,尤其是当应用恢复时,状态可能会发生变化,这使得处理被延迟执行的函数的结果更加困难。

在这一节中,我将向您展示如何在应用挂起之前进行准备。我将使用一个简单的后台任务来完成这项工作,因为这意味着您不必按照示例设置服务器。后台任务将计算出自应用上次激活以来已经过了多少秒——这在现实世界中并不是一件有用的事情,但它是一个有用的示例,因为它允许我向您展示当后台活动的上下文变得陈旧时会发生什么。清单 19-20 显示了我对tasks.js文件所做的更改,以定义新的活动。

清单 19-20 。为示例应用定义后台活动

`(function () {
    WinJS.Namespace.define("Utils", {
        doWork: function (count) {
            // ...statements removed for brevity...
        },

**        doBackgroundWork: function () {**
**            var interval = 1000;**
**            var canceled = false;**
**            var timeoutid;             return new WinJS.Promise(function (fDone, fError, fProgress) {**
**                var startTime = Date.now();**
**                (function getElapsedTime() {**
**                    var elapsed = Date.now() - startTime;**
**                    fProgress(elapsed / 1000);**
**                    if (!canceled) {**
**                        timeoutid = setTimeout(getElapsedTime, interval);**
**                    } else {**
**                        fDone();**
**                    }**
**                })();**
**            }, function () {**
**                canceled = true;**
**                if (timeoutid) {**
**                    clearTimeout(timeoutid);**
**                }**
**            });**
**        }**
    });
})();`

当调用doBackgroundWork函数时,我创建了一个新的Promise,它对当前时间进行快照,然后生成进度消息来指示自任务开始以来已经过去了多少秒。我将更新的时间间隔设置为一秒钟,这样在测试生命周期事件时,我就不必为更新等待太长时间。

我还在default.js文件中添加了代码,以启动后台工作并显示结果。您可以在清单 19-21 中看到这些变化。

清单 19-21 。开始后台工作并显示进度信息

`...
function performInitialization() {
    calcButton.addEventListener("click", function (e) {
        var first = ViewModel.State.firstNumber = Number(firstInput.value);
        var second = ViewModel.State.secondNumber = Number(secondInput.value);
        if (first < 5000 && second < 5000) {
            ViewModel.State.result = ViewModel.State.cachedResult[first][second];
        } else {
            ViewModel.State.result = first + second;
        }
    });

ViewModel.State.bind("result", function (val) {
        if (val != null) {
            ViewModel.State.history.push({
                message: ViewModel.State.firstNumber + " + "
                    + ViewModel.State.secondNumber + " = "
                    + val
            });
        }
    }); **    startBackgroundWork();**

return Utils.doWork(5000).then(function (data) {
        ViewModel.State.cachedResult = data;
    });
};

var backgroundPromise;

function startBackgroundWork() {
**    backgroundPromise = Utils.doBackgroundWork();**
**    var updatedExistingEntry = false;**
**    backgroundPromise.then(function () { }, function () { }, function (progress) {**
**        var newItem = {**
**            message: "Activated: " + Number(progress).toFixed(0) + " seconds ago"**
**        };**
**        ViewModel.State.eventLog.forEach(function (item, index) {**
**            if (item.message.indexOf("Activated:") == 0) {**
**                updatedExistingEntry = true;**
**                ViewModel.State.eventLog.setAt(index, newItem);**
**            }**
**        });**
**        if (!updatedExistingEntry) {**
**            ViewModel.State.eventLog.push(newItem);**
**        }**
**    });**
}
...`

startBackgroundWork函数调用doBackgroundWork并使用then方法接收进度更新,每个更新包含自激活以来的秒数。我将这些信息写入视图模型中的事件日志,但是因为我不想让日志中每一秒都充满消息,所以我在日志中创建了一个对象,并在每次收到进度更新时替换它。

doBackgroundWork函数中,我已经用后台任务开始的时间来表示 app 被激活的时刻。为了做出合理的估计,我从performInitialization函数中调用了startBackgroundWork函数,该函数在应用启动时被调用,并且之前不处于suspended状态。你可以在图 19-11 中看到这些变化的结果,图中显示了一个已经运行了一段时间的应用实例。

images

图 19-11。显示应用激活后经过的时间

要查看过时的上下文问题,请启动应用并运行一段时间。然后暂停和恢复应用。应用暂停时不会进行任何后台工作,但应用恢复后会立即重新开始。从字面上看,该应用会从停止的地方继续运行,这意味着后台任务生成的进度更新是基于任务首次启动的时间,而不是该应用最近一次激活的时间。

示例中的这个问题是微不足道的,但是现实世界中经常会出现类似的问题。执行oncheckpoint函数是停止后台任务的机会,这些任务依赖于应用再次启动时将失效的数据。有两种停止后台工作的方法—取消并忘记停止并等待。我将在接下来的章节中解释这两个问题。

使用取消和忘记技术

这是最简单的技术——您只需调用执行后台工作的Promisecancel方法。你可以在清单 19-22 中的函数中看到我是如何做到这一点的——快速、简单、容易。

清单 19-22 。取消 oncheckpoint 处理函数中的承诺

... app.oncheckpoint = function (args) {     // app is about to be suspended     app.sessionState = ViewModel.State.getData(); **    backgroundPromise.cancel();**     writeEventMessage("Suspended"); }; ...

当应用从暂停状态恢复时,你可以再次开始工作,如清单 19-23 所示。

清单 19-23 。恢复应用时开始后台工作

... switch (args.detail.previousExecutionState) {     case activation.ApplicationExecutionState.suspended: **        startBackgroundWork();**         writeEventMessage("Resumed from Suspended");         break;     case activation.ApplicationExecutionState.terminated:         ViewModel.State.setData(app.sessionState);         writeEventMessage("Launch from Terminated");         promises.push(performInitialization());         break;     case activation.ApplicationExecutionState.notRunning:     case activation.ApplicationExecutionState.closedByUser:     case activation.ApplicationExecutionState.running:         writeEventMessage("Fresh Launch");         promises.push(performInitialization());         break; } ...

这种方法的缺点是,在Promise检查应用是否被取消之前,或者在工作正在进行期间,应用可能会被暂停。这可能意味着当应用恢复时,将有一个或多个过时的更新。您可以通过在您的Promise代码中添加一个额外的检查来轻松地解决这个问题,如清单 19-24 所示,它显示了我对tasks.js文件所做的更改。

清单 19-24 。执行额外检查,以避免应用恢复时更新过时

`...
doBackgroundWork: function () {
    var interval = 1000;
    var canceled = false;
    var timeoutid;

return new WinJS.Promise(function (fDone, fError, fProgress) {
        var startTime = Date.now();
        (function getElapsedTime() {
            var elapsed = Date.now() - startTime;
**            if (!canceled) {**
**                fProgress(elapsed / 1000);**
                timeoutid = setTimeout(getElapsedTime, interval);
            } else {
                fDone();
            }
        })();
    }, function () {
        canceled = true;
        if (timeoutid) {
            clearTimeout(timeoutid);
        }
    });
}
...`

我已经将调用转移到了进度更新函数,这样只有在Promise没有被取消的情况下才会调用它。这降低了向用户显示过时更新的可能性,因为即使在应用暂停时正在处理一个工作单元,当应用恢复时也不会显示该工作的结果。

使用停止和等待技术

我对自定义Promise中调用 progress 函数的方式所做的更改减少了过时更新的机会,但并没有完全消除它——当应用暂停时,可能会调用 progress 处理程序,当应用恢复时,调用将会完成。这意味着只有当一个过时的更新不会导致严重的问题时,才应该使用“取消并忘记”技术。

如果您需要确保不使用过时的数据,那么您需要停止并等待技术。向oncheckpoint处理函数传递一个支持setPromise方法的对象。如果您将执行后台工作的Promise传递给此方法,应用的暂停将被延迟最多 5 秒钟,以便让Promise完成。

在这种情况下,你不能取消Promise。对Promise.cancel方法的调用立即返回并将Promise置于错误状态,而Promise正在执行的后台工作将继续,直到取消状态被下一次检查——这挫败了目标。

相反,你必须向Promise发出信号,表示它应该终止,但要以一种将承诺留在常规完成状态的方式进行。我对tasks,js文件做了一些进一步的修改来演示这种技术,你可以在清单 19-25 中看到。

清单 19-25 。修改提前完工的背景承诺

`...
doBackgroundWork: function () {
    var interval = 1000;
    var canceled = false;
    var timeoutid;

var p = new WinJS.Promise(function (fDone, fError, fProgress) {
        var startTime = Date.now();
        function getElapsedTime() {
            var elapsed = Date.now() - startTime;
**            if (!canceled && !p.stop) {**
                fProgress(elapsed / 1000);
                timeoutid = setTimeout(getElapsedTime, interval);
            } else {
                fDone();
            }
        };
**        setImmediate(getElapsedTime);**
    }, function () {
        canceled = true;
        if (timeoutid) {
            clearTimeout(timeoutid);
        }
    });

**    p.stop = false;**
**    return p;**
}
...`

我在由doBackgroundWork函数返回的Promise对象上定义了一个附加属性,并在自定义的Promise代码中引用它。这允许我有一个 per- Promise标志,表示后台工作应该停止,Promise应该表明它已经完成。

为了做到这一点,我改变了最初执行getElapsedTime函数的方式,从自执行函数切换到调用setImmediate方法。推迟执行getElapsedTime函数意味着在最初调用getElapsedTime之前执行在Promise对象上创建stop属性的代码,这意味着我可以检查函数中stop属性的值,安全地知道到那时它已经被定义了。

您可以在清单 19-26 的中看到我是如何使用stop属性的,它显示了我对default.js文件中的oncheckpoint函数所做的修改。

清单 19-26 。使用 setPromise 方法推迟应用暂停

... app.oncheckpoint = function (args) {     // app is about to be suspended     app.sessionState = ViewModel.State.getData(); **    backgroundPromise.stop = true;** **    args.setPromise(backgroundPromise);**     writeEventMessage("Suspended"); }; ...

oncheckpoint函数中的args对象上可用的setPromise方法与onactivated函数中的同名方法扮演着不同的角色:当你将一个Promise传递给该方法时,它会在你的应用被挂起之前给它片刻的宽限,等待承诺的兑现。

使用这种技术时,有几个要点需要记住。首先,使用setPromise方法只会推迟暂停你的应用 5 秒钟。在此之后,应用将被暂停,当应用恢复时,您将面临状态更新的风险——这意味着您需要确保您的后台工作在短时间内执行,并且您需要足够频繁地检查stop标志,以确保没有超过 5 秒期限的机会。第二点是,你不能用这种方法取消Promise——只有当你能安排好事情,使Promise在不进入错误状态的情况下完成时,这种方法才会起作用——否则,在Promise.cancel方法被调用之后,在Promise正在做的工作被暂停之前,你的应用会被挂起。

总结

在这一章中,我解释了 Metro 应用的不同生命周期阶段,并向您展示了这些阶段是如何由 Windows 发出信号的。我向您展示了如何解决 Visual Studio 添加到新项目中的WinJS.Application对象和代码的问题,以便您的应用能够获得全方位的生命周期通知并做出适当的响应。我演示了应用基于其先前状态启动时所需的不同操作,并向您展示了如何确保在应用初始化时显示闪屏,以及如何存储和恢复状态数据,以便您可以在应用被 Windows 终止后启动时正确响应。

应用生命周期的主题贯穿全书的这一部分。我将向您展示用于支持契约的不同类型的激活事件,这是 Windows 平台的一个关键特性。首先,我将在下一章向您展示如何使用用户设置和应用数据。

二十、使用设置和应用数据

在这一章中,我将向您展示如何向用户展示应用设置,以及如何使它们持久化。几乎每个应用都有一些用户可以自定义的功能,以与其他应用一致的方式向用户提供选项是一种重要的方式,可以确保用户在使用应用时能够建立在他们以前的 Windows 体验上。

对应用有特殊的规定,使设置工作变得简单和相对容易。我将向您展示呈现设置的不同技术以及持久存储设置(和其他数据)的 Windows 特性。表 20-1 提供了本章的总结。

images

准备示例 App

我将建立在我在第十九章中用来解释生命周期事件的EventCalc例子之上。在那一章中,我向你展示的一个特性是当应用即将被挂起时如何存储会话状态。我当时提到,您不应该将用户设置存储为会话数据,所以我构建同一个示例应用来告诉您故事的其余部分是合适的。提醒一下,你可以看到EventCalc是如何出现在图 20-1 中的。

images

图 20-1。event calc 应用

准备示例应用

为了准备演示设置的应用,我在项目中添加了一个名为/js/settings.js的新文件,你可以在清单 20-1 中看到。这个文件定义了一个名为ViewModel.Settings的名称空间,我将在这里存储用户首选项。

清单 20-1 。settings.js 文件的内容

`(function () {
    WinJS.Namespace.define("ViewModel", {
        Settings: WinJS.Binding.as({
**            backgroundColor: "#1D1D1D",**
**            textColor: "#FFFFFF",**
**            showHistory: true**
        })
    });

WinJS.Namespace.define("ViewModel.Converters", {
        display: WinJS.Binding.converter(function (val) {
            return val ? "block" : "none";
        }),
    });

})();`

ViewModel.Settings名称空间中的属性将控制应用布局的背景和前景色,以及显示计算历史的面板的可见性。我修改了default.html文件,将settings.js文件纳入范围,并添加了一些新的数据绑定,以便将ViewModel.Settings属性应用于适当的元素,如清单 20-2 所示。

清单 20-2 。将设置绑定添加到 default.html 文件

`

<html> <head>     <meta charset="utf-8" />     <title>EventCalc</title>


    
    
    


    
    
    
**    **
    

    
        
    


        Events
        

        

    


        Calculator
        

            
            +
            
            =
                         
        

    

<div id="historyContainer" class="container"
**            data-win-bind="style.display: showHistory ViewModel.Converters.display"**>
        History
        


        

    

`

我要做的最后一个更改是添加一个对WinJS.Binding.processAll方法的额外调用,以便激活标记中的数据绑定。您可以在清单 20-3 中看到我添加到/js/default.js文件中的附加语句。

清单 20-3 。激活设置数据绑定

`...
app.onactivated = function (args) {
    var promises = [];

if (args.detail.kind === activation.ActivationKind.launch) {
        switch (args.detail.previousExecutionState) {
            case activation.ApplicationExecutionState.suspended:
                startBackgroundWork();
                writeEventMessage("Resumed from Suspended");
                break;
            case activation.ApplicationExecutionState.terminated:
                ViewModel.State.setData(app.sessionState);
                writeEventMessage("Launch from Terminated");
                promises.push(performInitialization());
                break;
            case activation.ApplicationExecutionState.notRunning:
            case activation.ApplicationExecutionState.closedByUser:
            case activation.ApplicationExecutionState.running:
                writeEventMessage("Fresh Launch");
                promises.push(performInitialization());
                break;
        }

if (args.detail.splashScreen) {
            args.detail.splashScreen.addEventListener("dismissed", function (e) {
                writeEventMessage("Splash Screen Dismissed");
            });
        }         promises.push(WinJS.UI.processAll().then(function() {
**            return WinJS.Binding.processAll(document.body, ViewModel.Settings);**
        }).then(function () {
            return WinJS.Binding.processAll(calcElems, ViewModel.State);
        }));

args.setPromise(WinJS.Promise.join(promises));
    }
};
...`

这些更改和添加的结果是一组可观察的属性,在ViewModel.Settings名称空间中定义。当这些属性改变时,default.html文件中标记的数据绑定会更新关键元素的 CSS 值。您可以通过使用调试器启动应用(从 Visual Studio Debug菜单中选择Start Debugging)并在JavaScript Console窗口中输入以下语句来测试这些属性:


ViewModel.Settings.showHistory = false **ViewModel.Settings.backgroundColor = "#317f42"**


改变设置值会触发数据绑定的更新,改变背景颜色并隐藏计算历史,如图图 20-2 所示。

images

图 20-2。改变设置属性的值

在接下来的小节中,我将向您展示向用户呈现这些设置属性并持久存储用户选择的值的机制。

向用户呈现设置

Windows 提供了一个处理设置的标准机制,它是通过设置魅力(你可以通过魅力栏或使用Win+I快捷方式打开它)来触发的。

设置通过设置窗格呈现,如图图 20-3 所示,默认设置窗格显示了应用的一些基本细节,并包含一个Permissions链接,向用户显示应用在其清单中声明了哪些功能。“设置”面板的底部有一组标准按钮,用于配置系统范围的设置。

images

图 20-3。默认设置窗格

如果你点击Permissions链接,你会看到一个设置弹出按钮的例子,如图 20-4 中的所示。这就是设置的处理方式——它们被分组到类别中,在设置窗格中显示为链接,单击链接会显示一个设置弹出按钮,其中包含更多详细信息,通常还包含允许用户更改应用设置的控件。

images

图 20-4。权限弹出按钮

images 提示您可以通过更改应用清单的Packaging部分中的值来更改Permissions弹出按钮中显示的详细信息。

如图所示,设置弹出按钮有一个标题区,其中包含一个后退按钮——单击此按钮将返回设置窗格。设置窗格和设置弹出按钮都是轻触式的,这意味着用户可以通过单击或触摸屏幕上的其他地方来关闭它们,就像我在本书的前一部分描述的弹出 UI 控件一样。

在这一部分,我将在设置窗格中添加额外的链接,以允许用户查看和更改我在/js/settings.js file中定义的设置。我将向窗格添加两个新链接:Colors链接将打开一个窗格,让用户设置backgroundColortextColor设置属性,History链接将打开一个窗格,让用户设置showHistory设置属性。在这个过程中,我将演示你的应用如何将自己集成到标准设置系统中,以及你如何创建设置弹出按钮,其外观和行为与图 20-4 中的默认Permissions弹出按钮相同。

images 通过设置魅惑辅助设置是契约的一个例子。契约是一组定义明确的交互、事件和数据对象,允许应用在标准的 Windows 功能中具体化。设置只是我在本书中解释的契约之一——当我向你展示如何将你的应用集成到 Windows 搜索功能中时,你会在第二十一章中看到一个更复杂的例子。

定义弹出型 HTML

我需要做的第一件事是为每个设置面板创建一个 HTML 文件。首先,我创建了一个名为colorsSettings.html的文件,其内容可以在清单 20-4 中看到。这是一个相对简单的文件,所以我将 CSS 和 JavaScript 放在 HTML 标记所在的文件中。

清单 20-4 。colorsSettings.html 文件的内容

`

               ` `     
        
                         
Colors
        
        
            
                

Background Color

                             
            
                

Text Color

                             
        
    
`

这个 HTML 的核心是一个名为SettingsFlyout的 WinJS UI 控件。该控件应用于div元素,仅用于向用户呈现设置。width选项是由SettingsFlyout定义的唯一配置选项,它允许你请求一个标准的弹出按钮(使用narrow值)或一个有额外空间的按钮(使用wide值)。

设置弹出文件的内容通常需要一些 JavaScript 来处理用户输入,您可以看到我已经使用了WinJS.UI.Pages.define方法(我在第七章中描述过)来确保在我设置我的事件处理程序和应用数据绑定之前加载文档中的元素。

images 提示我建议你在创建设置弹出按钮时使用列表中的 HTML 作为模板。它包含了你需要的一切——包括标题和后退按钮的标题,以及一个用于设置内容的区域。

对于这个设置弹出按钮,我定义了两个input元素,它们是绑定到ViewModel.Settings属性的数据,并且在change事件被触发时更新这些属性。对于另一个设置弹出按钮,我已经创建了一个非常相似的文件,叫做historySettings.html,它的内容你可以在清单 20-5 中看到。

清单 20-5 。historySettings.html 文件的内容

`

                   
        
                         
History
        
        
            
            
        
    
`

对于这个弹出按钮,我为width配置选项选择了wide值。ToggleSwitch控件(我在第十一章中描述过)允许用户切换计算历史的可见性。当切换值改变时,ViewModel.Settings名称空间中相应的属性也会更新。

images 注意我的设置弹出按钮有点稀疏,在一个真实的项目中,我可以很容易地在一个弹出按钮上显示所有三个选项。我这样做是为了演示如何添加多个类别,但在实际项目中,如果需要将设置组合在一起,我建议您合并设置并使用弹出按钮中的标准 HTML 元素来创建部分。我在这一章中采用的方法对于一个例子来说是有用的,但是让用户更难配置应用。

响应设置事件

定义了弹出 HTML 文件后,下一步是在设置窗格中注册它们。我这样做是为了响应由WinJS.Application对象发出的settings事件,该事件在用户激活设置符时被触发。

您可以在清单 20-6 的中看到我对此事件的反应。为了将设置代码与应用的其他部分分开,我在settings.js文件中响应这个事件,但在实际项目中,我会在default.js文件中这样做,在那里我处理WinJS.Application发出的其他事件。

清单 20-6 。处理设置事件

`(function () {
    WinJS.Namespace.define("ViewModel", {
        Settings: WinJS.Binding.as({
            backgroundColor: "#1D1D1D",
            textColor: "#FFFFFF",
            showHistory: true
        })
    });

WinJS.Namespace.define("ViewModel.Converters", {
        display: WinJS.Binding.converter(function (val) {
            return val ? "block" : "none";
        }),
    });

**    WinJS.Application.onsettings = function (e) {**
**        e.detail.applicationcommands = {**
**            "colorsDiv": { href: "colorsSettings.html", title: "Colors" },**
**            "historyDiv": { href: "historySettings.html", title: "History" }**
**        };**
**        WinJS.UI.SettingsFlyout.populateSettings(e);**
**    };**
})();`

我通过给WinJS.Application.onsettings属性分配一个处理函数来处理settings事件。当用户激活 Settings Charm 时,我的函数将被调用,向我提供向 Settings 窗格添加附加类别设置的机会。

这是一个两步过程。首先,我将一个对象分配给传递给处理函数的对象的detail.applicationcommand属性。我的对象定义的属性的名称对应于我的 HTML 文件中的div元素,其中已经应用了WinJS.UI.SettingsFlyout控件。

我给这些div名称属性中的每一个分配一个具有hreftitle属性的对象。必须将href属性设置为您想要显示的弹出文件的名称,并将title属性设置为您想要包含在设置窗格链接中的文本。每个div名称必须是唯一的,如果div元素名称或href值不正确,您的链接将从设置窗格中被忽略。

第二步是调用WinJS.UI.SettingsFlyout.populateSettings方法,传入传递给处理函数的对象(其detail.applicationcommands属性已经被分配了我想要的链接和弹出按钮的详细信息)。第二步加载弹出 HTML 文件的内容并处理它们。这些增加的结果是当用户激活设置符时,设置窗格包含一些新的链接,如图图 20-5 所示。

images

图 20-5。向设置面板添加自定义链接

如果您点击或触摸这些链接,则会显示相应的设置窗格。图 20-6 显示了我创建的两个窗格,展示了width设置的narrowwide值之间的差异。

images

图 20-6。点击链接时显示的自定义设置弹出按钮

我没有做太多的工作来使设置弹出按钮吸引人,因为我在这一章的重点是窗口设置机制。但是在一个真实的项目中,如果我只有一个ToggleSwitch要显示,我不会使用 wide 设置,我也不会期望用户通过输入十六进制代码或 CSS 颜色名称来选择颜色。这些都是显而易见的,你应该考虑如何分组和安排弹出按钮的内容,以使配置应用的过程尽可能简单和轻松。

使设置持久

我已经关联了“设置”弹出按钮中的控件和元素,以便视图模型在它们发生变化时立即更新。您可以尝试更改设置的效果,并立即看到效果——但下次启动应用时,将使用我在settings.js文件中定义的默认值,您的更改将会丢失。

这就把我带到了应用数据应用数据的话题上,这些数据是你的应用运行所需要的,但你不想让用户直接访问——比如设置。当用户与应用数据交互时,是通过某种形式的中介,比如设置弹出按钮。在接下来的部分中,我将向您展示如何存储和检索应用数据设置。

images 提示应用数据的替代品是用户数据,用户可以直接使用这些数据——我将在第二十二章–24 章中解释应用如何操作用户数据,届时我将描述 Windows 对使用文件的支持。

存储设置

用于处理应用数据的 Windows 功能非常出色,设置是最容易处理的应用数据。清单 20-7 显示了我对settings.js文件所做的添加,以持久存储设置数据。

清单 20-7 。添加到 settings.js 文件以永久存储设置数据

`(function () {
    WinJS.Namespace.define("ViewModel", {
        Settings: WinJS.Binding.as({
            backgroundColor: "#1D1D1D",
            textColor: "#FFFFFF",
            showHistory: true
        })
    });

WinJS.Namespace.define("ViewModel.Converters", {
        display: WinJS.Binding.converter(function (val) {
            return val ? "block" : "none";
        }),
    });

WinJS.Application.onsettings = function (e) {
        e.detail.applicationcommands = {
            "colorsDiv": { href: "colorsSettings.html", title: "Colors" },
            "historyDiv": { href: "historySettings.html", title: "History" }
        };
        WinJS.UI.SettingsFlyout.populateSettings(e);
    };

**    var storage = Windows.Storage;**
**    var settingNames = ["backgroundColor", "textColor", "showHistory"];**

**    settingNames.forEach(function (setting) {**
**        ViewModel.Settings.bind(setting, function (newVal, oldVal) {**
**            if (oldVal != null) {**
**                storage.ApplicationData.current.localSettings.values[setting] = newVal;**
**            }**
**        });**
**    });**
})();`

images 注意除非另有说明,否则我在本节中引用的所有新对象都是Windows.Storage名称空间的一部分。

这项技术的核心是ApplicationDataContainer对象,其中数据存储为键/值对。有两个内置容器——第一个允许您存储数据,以便数据位于运行应用的设备上,另一个存储漫游数据,这些数据会自动复制到用户登录的任何设备上。

稍后我将回到漫游数据容器,但是对于这个例子,我从最简单的选项开始,使用本地容器。为了获得本地容器对象,我读取了ApplicationData.current.localSettings属性,如下所示:

... storage.ApplicationData.current.localSettings.values[setting] = newVal; ...

属性返回一个可以用来存储数据的对象。容器中的数据是永久存储的,这意味着您不必担心显式指示 Windows 保存您的应用数据-一旦您在存储容器中设置了值,您的数据就会被存储。

ApplicationData定义了许多有用的属性,我在表 20-2 中总结了这些属性,并在接下来的章节中进行了描述。

images

一旦获得了想要使用的容器,就可以通过values属性存储键/值对,该属性返回一个ApplicationDataContainerSettings对象。在这个例子中,我像使用数组一样使用这个对象,并给它赋值如下:

... storage.ApplicationData.current.localSettings.**values[setting] = newVal;** ...

你可以使用数组符号来赋值和读取值,但是一个ApplicationDataContainerSettings对象并不能实现一个真实数组的所有行为,你可能需要使用我在表 20-3 中描述的方法和属性来代替。

现在您已经理解了所涉及的对象,您可以看到我是如何在示例中持久存储设置值的。我使用 WinJS 编程数据绑定(如第八章中的所述)来监控ViewModel.Settings属性,并在设置改变时存储新值,这一点我在清单 20-8 中已经强调过。

清单 20-8 。绑定查看模型更改以保持用户选择

`...
var storage = Windows.Storage;
var settingNames = ["backgroundColor", "textColor", "showHistory"];

settingNames.forEach(function (setting) {
**    ViewModel.Settings.bind(setting, function (newVal, oldVal) {
        if (oldVal != null) {
**            storage.ApplicationData.current.localSettings.values[setting] = newVal;

        }
    });
});
...`

images 提示我忽略了任何可观察属性的旧值为null的更新。这是因为编程数据绑定在首次创建时会被发送一个更新,提供初始值和旧值的null。我只想在值改变时存储它们,因此检查了null

恢复设置

如果你不能在需要的时候读取设置,那么将设置存储为应用数据就没有多大用处。在清单 20-9 中,你可以看到我在/js/settings.js文件中添加的内容,以便在应用首次加载时恢复任何保存的设置。

images 注意我定义了恢复设置的函数,并在settings.js文件中调用该函数,但在实际项目中,我会从 default.js 文件中调用该函数来响应正在启动的应用。我已经把这个项目中的所有东西放在一起,所以我不必列出代码页来显示简单的变化。

清单 20-9 。加载应用数据设置

`(function () {
    WinJS.Namespace.define("ViewModel", {
        Settings: WinJS.Binding.as({
            backgroundColor: "#1D1D1D",
            textColor: "#FFFFFF",
            showHistory: true
        })
    });

WinJS.Namespace.define("ViewModel.Converters", {
        display: WinJS.Binding.converter(function (val) {
            return val ? "block" : "none";
        }),
    });

WinJS.Application.onsettings = function (e) {
        e.detail.applicationcommands = {
            "colorsDiv": { href: "colorsSettings.html", title: "Colors" },
            "historyDiv": { href: "historySettings.html", title: "History" }
        };
        WinJS.UI.SettingsFlyout.populateSettings(e);
    };

var storage = Windows.Storage;
    var settingNames = ["backgroundColor", "textColor", "showHistory"];
**    var loadingSettings = false;**

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
                storage.ApplicationData.current.localSettings.values[setting] = newVal;
            }
        });
    });

**    function loadSettings() {**
**        loadingSettings = true;**
**        var container = storage.ApplicationData.current.localSettings;**
**        settingNames.forEach(function (setting) {**
**            value = container.values[setting];**
**            if (value != null) {**
**                ViewModel.Settings[setting] = value;**
**            }**
**        });**
**        setImmediate(function () {**
**            loadingSettings = false;**
**        })**
**    };     loadSettings();**

})();`

我通过读取ApplicationData.current.localSettings属性获得本地设置容器。然后,我使用数组符号来检查是否有我感兴趣的每个设置的存储值。如果有,那么我使用该值来更新相应的ViewModel.Settings属性值,这将触发我定义的数据绑定,将应用返回到其先前的配置。

为了避免存储我正在加载的相同值,我定义了loadSettings变量,在读取存储的设置之前,我将它设置为true。当我收到数据绑定通知时,我会检查这个变量的值,如果加载正在进行,我会放弃更新。

images 提示注意,在我传递给setImmediate方法的函数中,我将变量loadingSettings设置为false。我这样做是为了确保在我再次开始存储值之前,所有的数据绑定事件都得到处理。我在第九章的中解释了setImmediate的方法。

这些添加的结果是对显示在“设置”弹出按钮上的设置的更改现在是持久的。为了测试这一点,启动应用,激活设置符,选择History链接,并将ToggleSwitch控制改为Off位置。

计算历史将立即隐藏。现在重启应用,要么重启调试器,要么使用Alt + F4并再次启动应用。你会看到,当应用启动时,计算历史并没有显示。

使用漫游设置

在前面的例子中,我使用本地应用存储进行设置。这意味着数据仅存储在当前设备上——如果用户在另一台设备上运行该应用,则将使用默认设置。通过这种方式,用户可以在两个设备上运行相同的应用,而每个实例都有完全不同的配置。

如果您想在用户登录的任何地方应用相同的设置,那么您需要使用漫游设置。这是 Windows 应用最有前途的功能之一,对于将其 Windows 帐户与 Microsoft 帐户相关联的用户,应用和用户数据可以无缝复制。

作为一名 Windows 应用程序员,您不必担心帐户登录过程的细节、正在使用的 Microsoft 帐户或如何复制数据的细节。相反,您只需将设置存储在漫游容器中,而不是本地容器中。你可以在清单 20-10 的中看到我对/js/settings.js文件所做的更改,以使用漫游设置。

清单 20-10 。使用漫游设置容器

(function () {     WinJS.Namespace.define("ViewModel", {         Settings: WinJS.Binding.as({             backgroundColor: "#1D1D1D",             textColor: "#FFFFFF",             showHistory: true         })     });
`    WinJS.Namespace.define("ViewModel.Converters", {
        display: WinJS.Binding.converter(function (val) {
            return val ? "block" : "none";
        }),
    });

WinJS.Application.onsettings = function (e) {
        e.detail.applicationcommands = {
            "colorsDiv": { href: "colorsSettings.html", title: "Colors" },
            "historyDiv": { href: "historySettings.html", title: "History" }
        };
        WinJS.UI.SettingsFlyout.populateSettings(e);
    };

var storage = Windows.Storage;
    var settingNames = ["backgroundColor", "textColor", "showHistory"];
    var loadingSettings = false;

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
                storage.ApplicationData.current.roamingSettings.values[setting] = newVal;
            }
        });
    });

function loadSettings() {
        loadingSettings = true;
        var container = storage.ApplicationData.current.roamingSettings;
        settingNames.forEach(function (setting) {
            value = container.values[setting];
            if (value != null) {
                ViewModel.Settings[setting] = value;
            }
        });
        setImmediate(function () {
            loadingSettings = false;
        })
    };
    loadSettings();

**    storage.ApplicationData.current.addEventListener("datachanged", function (e) {**
**        loadSettings();
    });**

})();`

要切换到漫游,而不是本地存储,我只需使用ApplicationData.current.roamingSettings属性。存储和检索单个设置的方法是相同的。

使用漫游设置时,Windows 将在用户登录的任何位置复制你的应用数据,这意味着你可以在多个设备上创建一致的体验。

images 注意如果用户没有与其 Windows 登录相关联的 Microsoft 帐户,则无需处理任何错误——在这种情况下,数据不会被复制。

如果在应用运行时漫游设置被修改,ApplicationData对象将触发datachanged事件。这使您有机会更新您的应用状态以反映新数据,确保用户在其他地方所做的更改尽快得到应用。

images 提示在示例中,我简单地调用了loadSettings函数来在收到datachanged事件时应用更新,但是您可能想在实际应用中进行更改之前询问用户是否要应用修改后的设置。

要在示例应用中测试对漫游数据的支持,您需要有两台 Windows 8 设备或虚拟机,并使用相同的 Microsoft 帐户登录。在复制应用数据之前,该帐户必须明确信任这两个设备(要信任一个设备,打开设置图标,选择Change PC Settings Users,然后点击Trust this PC链接)。在两台设备上运行应用,然后在其中一台设备上更改应用设置。几分钟后,您将看到同样的更改应用于另一台设备。

了解漫游数据的工作原理

漫游数据功能使用简单,但有一些重要的限制。首先,数据在最大努力的基础上复制。漫游数据旨在让您的应用在不同设备上提供一致的体验,但没有性能保证。当适合 Windows 时,您的数据将被复制,并且您无法控制复制过程(有一个例外,我将在下一节中描述)。Windows 可能会选择无限期推迟数据复制,尤其是在电量或资源不足的情况下。Windows 不承诺在设备关闭或进入睡眠状态之前执行复制,这意味着在存储新设置值和将其复制到其他设备之间可能会经过很长一段时间。

第二,漫游数据是一种低流量、低频率的服务。如果您频繁更新漫游容器,Windows 将暂时停止复制您的数据。这意味着你不应该使用漫游数据来保持一个应用的多个实例同步-在最初几次更新后,Windows 将开始推迟你的应用的更新。

第三,Windows 复制的数据量是有限制的。您可以通过读取ApplicationData.current.roamingStorageQuota属性来确定配额是多少,但在 Windows 8 的初始版本中它被设置为 100KB。这可能看起来很多,但是,正如我在本章后面解释的,您也可以使用漫游存储来复制文件,所以配额可以很快用完。当您超过漫游配额时,不会出现警告,Windows 将会(静默地)停止为您的应用复制数据。此外,由于没有可靠的方法来计算您的应用存储了多少数据,所以在计划哪些数据将被漫游,哪些数据将被存储在本地时,您需要非常保守。

最后,漫游功能旨在让用户在不同设备间移动时获得一致的体验,而不是同时运行同一个应用。对于在不同设备上更改的漫游设置,没有冲突解决方案–Windows 只是丢弃除了最近所做的更改之外的任何更改。

当你考虑到这几点,你就明白漫游数据要慎用了。您应该发送尽可能少的数据,仅在必要时存储更新,并仔细考虑发送数据引用而不是数据本身的机会(例如复制 URL 而不是网页内容)。这并不是说你不应该使用漫游——这是一个很好的功能,它可以改变在多个设备上安装你的应用的用户的体验——但要慎重而谨慎地使用。

使用高优先级设置

漫游设置容器以不同的方式处理一个设置。如果您给HighPriority设置赋值,Windows 将更努力地快速复制该设置。仍然没有性能保证,配额和频繁更新限制仍然适用,但是您可以使用这个特殊设置来尝试确保最重要的信息尽快在其他设备上可用。

HighPriority设置旨在让您创建流畅的用户体验,您应该使用此设置来复制应用状态的关键部分,以便用户在移动到新设备时可以无缝地继续他们的工作流程。这意味着什么将取决于你的应用的性质,但微软给出的一个例子是将一封由部分内容组成的电子邮件从一台设备复制到另一台设备,例如,允许用户开始在家里的 PC 上写邮件,然后在上班途中继续使用平板电脑。

为了演示HighPriority设置的使用,我优先考虑了showHistory设置。当这个设置改变时,我将新值赋给HighPriority,以便尽可能快地复制它。您可以在清单 20-11 中看到我对settings.js文件所做的更改。

清单 20-11 。使用高优先级设置

`(function () {
    WinJS.Namespace.define("ViewModel", {
        Settings: WinJS.Binding.as({
            backgroundColor: "#1D1D1D",
            textColor: "#FFFFFF",
            showHistory: true
        })
    });

// ...statements removed for brevity...

var storage = Windows.Storage;
    var settingNames = ["backgroundColor", "textColor", "showHistory"];
    var loadingSettings = false;

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
**                if (setting == "showHistory") {**
**                    setting = "HighPriority";**
**                }**
                storage.ApplicationData.current.roamingSettings.values[setting] = newVal;
            }
        });
    });

function loadSettings() {
        loadingSettings = true;         var container = storage.ApplicationData.current.roamingSettings;
        settingNames.forEach(function (setting) {
**            value = container.values[setting == "showHistory" ?**
**                "HighPriority" : setting];**
            if (value != null) {
                ViewModel.Settings[setting] = value;
            }
        });
        setImmediate(function () {
            loadingSettings = false;
        })
    };
    loadSettings();

storage.ApplicationData.current.addEventListener("datachanged", function (e) {
        loadSettings();
    });

})();`

我在这里采用的方法是截取对showHistory设置的更新,并使用HighPriority键将新值存储在容器中。它是触发加速行为的密钥名称,并且不需要明确的动作来触发复制。

images 注意虽然使用复制时没有性能保证,但我的经验是常规设置大约每 5 分钟复制一次,而HighPriority设置在几秒钟内复制一次。在高峰时间,常规设置的复制通常会减慢到大约每 10 分钟一次。

确保数据一致性

某些应用设置无法自行安全复制。考虑示例应用的以下场景:

  1. On the host computer, the user sets backgroundColor to white and textColor to green. The user was about to leave the house when they turned off the PC before Windows copied the changes. This means that the settings are stored locally, which will affect the application when it runs on the PC, but will not be copied to affect other devices.
  2. The user gets on the train and starts the same application on another device. They changed backgroundColor to green, but did not set the textColor attribute to the default value (white). Windows copies the new backgroundColor value.
  3. The user goes home and starts the application on the PC again. Windows will not copy the setting values in step 1 because they have been replaced by the values changed in step 2.
  4. The application program applies the new value set by backgroundColor, presents green text to the user on the green background, and makes the application program unavailable.

从这种情况中可以吸取几个教训。首先,明确询问用户是否想要应用更新的设置可能很重要——对于我的示例应用,我只是应用更新,在我在本节中提出的场景中,用户不会了解他们几个小时前所做的一系列设置更改已经组合在一起使应用变得无用。同样,您应该花时间确保有一种简单的方法将应用重置为默认设置,或者防止选择会导致应用不可用的设置组合。

然而,最重要的教训是,一些设置需要成组复制,尤其是当这些设置可以以危险的方式组合时。您可以通过使用一个ApplicationDataCompositeValue对象来做到这一点,它允许您将几个设置合并到一个对象中,并确保它们作为一个单一的原子单元被复制。你可以看到我如何在清单 20-12 中使用这个对象来确保我在本节开始描述的场景不会出现。

清单 20-12 。使用 ApplicationDataCompositeValue 对象复制多个值

`(function () {
    WinJS.Namespace.define("ViewModel", {
        Settings: WinJS.Binding.as({
            backgroundColor: "#1D1D1D",
            textColor: "#FFFFFF",
            showHistory: true
        })
    });

// ...statements removed for brevity...

var storage = Windows.Storage;
    var settingNames = ["backgroundColor", "textColor", "showHistory"];
    var loadingSettings = false;

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
**                var container = storage.ApplicationData.current.roamingSettings;**
**                if (setting == "showHistory") {**
**                    container.values["HighPriority"] = newVal;**
**                } else if (setting == "backgroundColor" || setting == "textColor") {**
**                    var comp = new storage.ApplicationDataCompositeValue();**
**                    comp["backgroundColor"] = ViewModel.Settings.backgroundColor;**
**                    comp["textColor"] =  ViewModel.Settings.textColor;**
**                    container.values["colors"] = comp;**
**                }**
            }
        });
    });

function loadSettings() {
        loadingSettings = true;
        var container = storage.ApplicationData.current.roamingSettings;
**        ["HighPriority", "colors"].forEach(function (setting) {**
**            value = container.values[setting];**
**            if (value != null) {                 if (setting == "HighPriority") {**
**                    ViewModel.Settings.showHistory = value;**
**                } else {**
**                    ViewModel.Settings.backgroundColor = value["backgroundColor"];**
**                    ViewModel.Settings.textColor = value["textColor"];**
**                }**
**            }**
**        });**
        setImmediate(function () {
            loadingSettings = false;
        })
    };
    loadSettings();

storage.ApplicationData.current.addEventListener("datachanged", function (e) {
        loadSettings();
    });
})();`

ApplicationDataCompositeValue对象本身就像一个容器,您可以使用数组符号样式为它分配多个设置。然后,您可以将组合的属性集添加为单个设置,并依赖它们一起被复制。我向清单中的ApplicationDataCompositeValue对象添加了两个属性,但是您可以根据需要为您的应用添加任意多个属性(受 Windows 应用于漫游数据的配额和容量限制的限制)。

images 提示你可以使用一个ApplicationDataCompositeValue对象作为HighPriority设置的值。这允许您优先选择一组相关属性,以在设备之间保留您的应用状态。

有了这一改变,ViewModel.Settings名称空间中的属性和应用数据容器中的设置名称之间没有直接的联系。我使用特殊的HighPriority设置复制了showHistory属性的值,并将backgroundColortextColor属性复制为一个名为colors的设置。这是一个非常合理的方法,但是您需要确保正确映射您的设置和属性,并进行彻底的测试。请注意,测试是一个痛苦的过程,因为没有办法强制复制-这意味着您必须在一个设备上进行更改,然后等待几分钟才能看到它在其他设备上的反映(当然,除非您正在使用HighPriority设置)。

使用应用数据文件

并不是所有的数据都可以用键/值对来表示。幸运的是,设置并不是应用可以使用的唯一应用数据——你也可以使用文件来存储你需要的数据。为了演示这个特性,我在js项目文件夹中添加了一个名为appDataFiles.js的新文件,其内容可以在清单 20-13 中看到。

清单 20-13 。appDataFiles.js 的内容

(function () {
`    var storage = Windows.Storage;
    var historyFileName = "calcHistory.json";
    var folder = storage.ApplicationData.current.localFolder;

ViewModel.State.history.addEventListener("iteminserted", function (e) {
        folder.createFileAsync(historyFileName,
                storage.CreationCollisionOption.openIfExists)
        .then(function (file) {
            var stringData = JSON.stringify(e.detail.value);
            storage.FileIO.appendLinesAsync(file, [stringData]);
        });
    });
})();`

这段代码使用数据绑定来观察ViewModel.State.history对象,它是一个WinJS.Binding.List对象。每当用户执行一个计算时,一个新的项目被添加到List中,并且appDataFiles.js文件中的代码创建一个用户操作的持久记录。在本章的后面,我将扩展这个例子,并使用这个文件来填充应用启动时的历史记录。但是,首先,我将介绍示例中的代码,并解释所有对象是如何组合在一起的。当你理解了基本的技术,在 Windows 应用中处理文件是非常简单的,但是它不同于我见过的任何其他处理文件系统的方法。在接下来的部分中,我将向您展示每个步骤,并详细解释选项。

images 注意除非我明确声明,否则我在本节中引用的所有新对象都是Windows.Storage名称空间的一部分。

在清单 20-14 中,您可以看到我添加到default.html文件头部分的脚本元素,以将appDataFiles.js文件中的代码引入示例应用。

清单 20-14 。为 appDataFiles.js 文件向 default.html 添加脚本元素

`...

<head>     <meta charset="utf-8" />     <title>EventCalc</title>


    
    
    


    
    
    
    
**    **
    

...`

获取文件夹和文件对象

起点是由ApplicationData.current.localFolder属性返回的StorageFolder对象。使用StorageFolder对象时有很多选项,在这一章中我将只关注基本的选项。我将在第二十二章–24 章中向您展示更多可用的功能,届时我将描述如何使用用户数据,而不是应用数据。

images 注意与设置一样,应用对文件的数据支持可以是本地的,也可以是漫游的,我选择了本地选项。两个实例中的 API 是相同的,不同之处在于漫游文件是复制的。如果要使用漫游 app 数据文件,那么应该使用ApplicationData.current.roamingFolder属性返回的StorageFolder。注意不要超过漫游数据报价,否则您的文件将不会被复制。

表 20-4 总结了使用StorageFolder对象打开、创建或删除文件的基本方法。(我在第二十二章中描述了其他的StorageFolder方法,但是这些是你最常用于 app 数据文件的方法。)

images

Windows 应用中几乎所有与文件相关的操作都是异步执行的,这就是为什么表中的所有方法都返回一个Promise对象。完成后,这些Promise对象将把一个或多个StorageFile对象传递给then方法,代表指定的文件。你可以看到我是如何使用清单 20-15 中的createFileAsync方法得到一个StorageFile对象的。

images 提示本章的剩余部分严重依赖于WinJS.Promise对象。如果你还没有阅读第九章,那么你应该现在就阅读,并在完成后返回这里。无法回避Windows.Storage名称空间对象的异步特性。微软为 Windows 应用引入异步支持的主要目标之一是防止应用在执行文件操作时挂起。在处理文件时,你必须使用承诺,即使一开始可能会感觉有点反直觉。

清单 20-15 。获取存储文件夹和存储文件对象

... folder.**createFileAsync**(historyFileName, storage.CreationCollisionOption.openIfExists)     .**then**(function (**file**) {         // statements to operate on the StorageFile go here }); ...

images 注意异步文件操作中出现的任何错误都会传递给then方法的error函数。我在这一节中没有定义错误处理程序,因为我想向您展示如何处理文件,而不是处理错误,但是您应该小心处理真实项目中的错误。关于使用Promise时如何报告错误的详细信息,参见第九章。

createFileAsync方法的可选参数是来自CreationCollisionOption对象的一个值,该对象枚举了一些值,这些值决定了当您试图创建一个已经存在的文件时,Windows 将采取什么操作。表 20-5 描述了可用的值。

images

我使用了openIfExists值,这意味着如果没有文件,示例应用将创建一个新文件,如果有,将重用现有文件。

写入文件

StorageFile对象支持对文件的所有操作——你可以重命名或删除它所代表的文件,打开允许你读写数据的流等等。

我不会直接做这些事情,因为FileIO对象定义了一组非常方便的方法,使得执行基本的读写选项变得简单(比直接使用StorageFile对象方法容易得多)。表 20-6 描述了FileIO定义的便利方法。

images

images 提示读写缓冲区的方法本身并不是特别有用。它们旨在与来自Windows.Storage.Streams名称空间的对象一起使用,这允许您以更传统的方式执行操作(例如,读取一个字节或一串数据的调用)。FileIO中的便利方法对于大多数情况来说已经足够了,我鼓励你先看看它们是否能满足你的需求。

我发现最有用的方法是appendLinesAsync,因为我可以用它将 JSON 数据写入文件,而不必担心行终止符。你可以看到这是我用来在清单 20-16 中写计算细节的方法,尽管我一次只写一项。

清单 20-16 。使用 FileIO 对象将 JSON 数据写入文件

... ViewModel.State.history.addEventListener("iteminserted", function (e) {     folder.createFileAsync(historyFileName,             storage.CreationCollisionOption.openIfExists)     .then(function (file) { **        var stringData = JSON.stringify(e.detail.value);** **        storage.FileIO.appendLinesAsync(file, [stringData]);**     }); }); ...

请注意,我不必显式地打开我正在处理的文件,将写光标移动到文件的末尾,或者在完成后关闭文件。这些平凡的(并且容易出错的)任务由FileIO对象替我处理。

结果是,每次用户执行计算时,我都会在文件中添加一个字符串。这是一个真实的常规文件,您可以通过在 Visual Studio JavaScript 控制台窗口中输入Windows.Storage.ApplicationData.current.localFolder.path在 Windows 文件系统中找到该文件。对于我的系统,该属性的值是:


"C:\Users\adam\AppData\Local\Packages\a52d9e6e-bba3-4774-a824b26e77499de7_6fxp0bkxjs8ye \LocalState"


当然,在您的系统上,路径会有所不同。如果您使用应用执行一些计算,然后打开文件夹,您将看到calcHistory.json文件,它将包含每个计算的简单 JSON 描述,类似于清单 20-17 中显示的内容。

清单 20-17 。calcHistory.json 文件的内容

{"message":"1 + 2 = 3"} {"message":"1 + 3 = 4"} {"message":"1 + 4 = 5"}

message属性的存在是因为我在default.html文件中使用了一个 WinJS 模板来显示事件消息和计算历史。在这个例子中,为了简单起见,我只是将对象转换为 JSON 并将其写入文件,但是如果需要,您可以重新格式化对象或以完全不同的方式表达数据。

从文件中读取

从文件中读取计算历史所需的代码很容易理解,因为您已经看到了将数据写入文件所涉及的对象。清单 20-18 显示了我在appDataFiles.js文件中添加的内容,以便在应用启动时读取历史记录。(我已经在保存新数据的代码之前插入了将数据添加到视图模型的代码,以便在加载初始数据之前不会对更改做出响应。)

清单 20-18 。从应用数据文件中读取计算历史

`(function () {

var storage = Windows.Storage;
    var historyFileName = "calcHistory.json";
    var folder = storage.ApplicationData.current.localFolder;

**    function readHistory() {**
**        folder.getFileAsync(historyFileName)**
**        .then(function (file) {**
**            var fileData = storage.FileIO.readLinesAsync(file)**
**            .then(function (lines) {**
**                lines.forEach(function (line) {**
**                    ViewModel.State.history.push(JSON.parse(line));**
**                });**
**            })**
**        });**
**    }**

**    readHistory();**

ViewModel.State.history.addEventListener("iteminserted", function (e) {
        folder.createFileAsync(historyFileName,
                storage.CreationCollisionOption.openIfExists)
        .then(function (file) {
            var stringData = JSON.stringify(e.detail.value);
            storage.FileIO.appendLinesAsync(file, [stringData]);
        });
    });

})();`

我使用FileIO.readLinesAsync方法获取字符串数组形式的文件内容。这使我能够很好地将每个 JSON 字符串解析成一个 JavaScript 对象,并将其推送到ViewModel.State.history对象中,从而将每个项目显示给用户。

images 注意如果你在添加readHistory函数之前没有运行这个 app,那么 Visual Studio 会报错。这是因为readHistory函数是在 app 刚启动的时候调用的,文件不存在。readHistory函数处理丢失的文件,您可以单击Continue按钮忽略该文件一次,或者取消选中复选框以防止 Visual Studio 在将来报告相同的异常。

从 App 包中加载文件

您不必从头开始生成设置和文件,您也可以将数据文件包含在您的应用包中,并将其处理为其他应用数据文件。当所有用户都需要相同的数据,并且只需要很少或不需要定制时,这非常有用。

我在本章中作为例子使用的EventCalc应用生成一组存储在缓存中的计算结果。我在第十九章中添加了这个特性,当时我正在演示如何在应用生命周期的范围内执行任务。在本章中,我将更新应用,以便它从数据文件加载预先计算的结果,该数据文件包含在应用部署到用户设备中。首先,我在项目中创建了一个data文件夹,并添加了一个名为calcData.json的新文件。这个文件中的每一行都包含一个计算的 JSON 表示,你可以在清单 20-19 中看到这个数据的例子。在第十九章中,我想要一个需要一段时间才能完成的任务,所以我生成了前 5000 个整数的和。对于这一章,我想要一个可管理的文件大小,所以数据文件只包含前 100 个整数值相加的结果。

清单 20-19 。calcData.json 文件中的 JSON 数据示例

... {"first":3,"second":50,"result":53} {"first":3,"second":51,"result":54} {"first":3,"second":52,"result":55} {"first":3,"second":53,"result":56} {"first":3,"second":54,"result":57} {"first":3,"second":55,"result":58} {"first":3,"second":56,"result":59} {"first":3,"second":57,"result":60} ...

这个文件太长了,我无法在这一章中全部列出,所以如果你想按照这个例子学习,有两种方法可以得到这个文件的内容。首先是下载本书附带的源代码,其中包含了每章中所有示例的所有文件。第二种方法是自己生成数据,您可以在生成示例数据侧栏中找到相关说明。

生成样本数据

如果你不想从apress.com下载calcData.json文件,那么你可以自己轻松生成。在 Visual Studio 中创建一个新的 Windows 应用项目,并用以下内容替换default.html文件的内容:

`

         DataGen                               `

这个清单定义了一个简单的应用,其中 HTML、CSS 和 JavaScript 都定义在同一个文件中。应用的布局包含一个单独的Generate按钮,当点击它时,会提示你在你的系统上保存calcData.json文件,然后你可以将它复制到你的 Visual Studio 项目的data文件夹中。结果将被生成并保存到文件中。这个应用使用了我在本章后面才描述的特性,所以为了创建例子所需的数据,把它当作一个黑盒。

为了禁用缓存后台任务并减少预期缓存结果的数量,我更新了default.js文件中的performInitialization函数,如清单 20-20 所示。这些变化缩小了预计算数据的范围,并停止在每次启动应用时生成结果。

清单 20-20 。修改 default.js 文件,准备加载计算结果

... function performInitialization() {     calcButton.addEventListener("click", function (e) { `        var first = ViewModel.State.firstNumber = Number(firstInput.value);
        var second = ViewModel.State.secondNumber = Number(secondInput.value);
        if (first < 100 && second < 100) {
            ViewModel.State.result = ViewModel.State.cachedResult[first][second];
        } else {
            ViewModel.State.result = first + second;
        }
    });

ViewModel.State.bind("result", function (val) {
        if (val != null) {
            ViewModel.State.history.push({
                message: ViewModel.State.firstNumber + " + "
                    + ViewModel.State.secondNumber + " = "
                    + val
            });
        }
    });

startBackgroundWork();

**    //return Utils.doWork(5000).then(function (data) {**
**    //    ViewModel.State.cachedResult = data;**
**    //});**
};
...`

这个例子的关键部分显示在清单 20-21 中,它详细说明了我对appDataFiles.js文件所做的添加。我添加了一个自执行函数,它打开data/calcData.json文件,解析内容,并将结果作为缓存数据。

清单 20-21 。添加到 appDataFiles.json 文件以加载预先计算的数据

`(function () {

var storage = Windows.Storage;
    var historyFileName = "calcHistory.json";
    var folder = storage.ApplicationData.current.localFolder;

ViewModel.State.history.addEventListener("iteminserted", function (e) {
        folder.createFileAsync(historyFileName,
                storage.CreationCollisionOption.openIfExists)
        .then(function (file) {             var stringData = JSON.stringify(e.detail.value);
            storage.FileIO.appendLinesAsync(file, [stringData]);
        });
    });

function readHistory() {
        folder.getFileAsync(historyFileName)
        .then(function (file) {
            var fileData = storage.FileIO.readLinesAsync(file)
            .then(function (lines) {
                lines.forEach(function (line) {
                    ViewModel.State.history.push(JSON.parse(line));
                });
            })
        });
    }

readHistory();

**    (function () {**
**        storage.StorageFile.getFileFromApplicationUriAsync(**
**            Windows.Foundation.Uri("ms-appx:///data/calcData.json"))**
**        .then(function (file) {**
**            var cachedData = {};**
**            storage.FileIO.readLinesAsync(file).then(function (lines) {**
**                lines.forEach(function (line) {**
**                    var calcResult = JSON.parse(line);**
**                    if (cachedData[calcResult.first] == null) {**
**                        cachedData[calcResult.first] = {};**
**                    }**
**                    cachedData[calcResult.first][calcResult.second] = calcResult.result;**
**                });**
**            });**
**            ViewModel.State.cachedResult = cachedData;**
**        });**
**    })();**
})();`

这类似于我在上一节中读取历史文件的方式,但是关键的区别是我获取对应于calcData.json文件的StorageFile对象的方式。在这种情况下,我不能使用ApplicationData对象,因为应用包中的文件被不同地处理。

第一步是创建一个Windows.Foundation.Url对象。名称空间包含的对象大部分是。NET 编程语言,并且与 JavaScript 关系不大。Uri对象接受一个 URI 字符串,并以一种可以和Windows.Storage对象一起使用的方式准备它。它不做任何对 JavaScript 程序员有用的事情。

URI 的格式很重要。协议组件必须设置为ms-appx,您必须使用三个/字符,然后包括您想要加载的文件的路径。对于data/calcData.json文件,这意味着我需要使用的字符串是:

ms-appx:///data/calcData.json

一旦有了一个Windows.Foundation.Uri对象,就可以把它作为StorageFile.getFileFromApplicationUriAsync方法的一个参数,该方法返回一个代表应用包中文件的StorageFile。从这一点开始,您可以使用FileIO对象来读取文件的内容,就像读取常规应用数据文件一样。(不要试图写入这些文件——它们是只读的。)这些变化的结果是,我能够将数据作为我的应用分发的一部分进行部署,并在应用启动时加载数据——对于我的示例应用,这意味着我不必依赖用户设备的潜在有限功能来生成缓存数据。

总结

在这一章中,我向你展示了如何实现设置契约,以便将你的应用集成到标准的 Windows 模型中,向用户呈现设置。契约是一种强大的技术,可以确保你的应用向用户呈现一系列一致的交互,正如你将在后面的章节中看到的,有些联系可能非常复杂。

如果您不能持久地存储用户选择的值,那么向用户提供设置是没有意义的。为此,我解释了 app 数据系统的工作原理,允许您存储键/值对和数据文件。Windows 应用可以存储设置和文件,以便它们位于当前设备的本地,或者在用户登录时漫游并跟随用户。漫游功能很容易使用,但在使用上有一些限制,我向您展示了一些高级功能,以帮助您获得特定种类的漫游行为。

本章结束时,我向您展示了如何加载作为应用包的一部分分发给用户的数据。当所有用户都需要相同的数据时,这很有用,并且避免了在应用启动时生成或下载数据的需要。

在下一章,我将向你展示如何使用 Windows 搜索功能,让用户搜索你的应用数据。

二十一、搜索契约

Windows 定义了一系列的契约,Windows 应用可以实现这些契约来集成关键的平台范围的服务。这些契约为特定功能的应用和操作系统之间的交互设置了一个模型。在这一章中,我将向您介绍搜索契约,它允许一个应用无缝地参与 Windows 搜索机制,允许用户在您的应用中查找数据,就像他在操作系统中定位文件和应用一样。

这一章不是关于给你的应用添加搜索功能。这是一款已经有能力处理搜索的应用,并使用契约向用户展示这种能力。您可以提供对您的应用和应用数据有意义的任何类型的搜索功能,并且,正如您将看到的,使用户能够轻松访问和使用它。本章开始时,我将构建一个能够搜索其应用数据的简单应用,然后我将使用该应用来演示如何实现搜索契约。

images

创建示例应用

在《??》第十六章中,我使用了一系列流行的名字来帮助演示SemanticZoom UI 控件。我将在本章中使用相同的数据作为基础,来演示支持搜索契约的不同方式。我为这一章编写的示例应用叫做SearchContract,你可以在的清单 21-1 中看到 default.html 文件的内容。

清单 21-1 。来自 SearchContract 应用的 default.html 文件的内容

`

<html> <head>     <meta charset="utf-8" />     <title>SearchContract</title>


    
    
    


    
**    **
    

    
        
            
        
    


        

    


**        <div id="nameList" data-win-control="WinJS.UI.ListView"**
**            data-win-options="{itemTemplate: NameItemTemplate,**
**                itemDataSource: ViewModel.filteredNames.dataSource}">**
**        
**
    


**        <div id="messageList" data-win-control="WinJS.UI.ListView"**
**            data-win-options="{itemTemplate: MessageTemplate,**
**                itemDataSource: ViewModel.messages.dataSource,**
**                layout: {type: WinJS.UI.ListLayout}}">**
**        
**
    

`

这个应用布局的关键部分是两个ListView控件。第一个ListView将显示一组名称,我将使用第二个ListView控件来显示一系列消息,类似于我在第十九章中向您展示 UI 中的应用生命周期事件时采用的方法。该标记还包含几个我将用于ListView内容项的模板和一个导入viewmodel.js文件的脚本元素,我将很快创建该文件。

为了创建这个应用的布局,我在/css/default.css文件中定义了一些样式,如清单 21-2 所示。这些样式是使用常规 CSS 属性构建的,不依赖于任何特定于 Windows 的功能。

清单 21-2 。/css/default.css 文件的内容

`body { background-color: #5A8463; display: -ms-flexbox;
    -ms-flex-direction: row; -ms-flex-align: center; -ms-flex-pack: center;}
.listContainer {height: 80%; margin: 10px; border: medium solid white; padding: 10px;}

nameContainer

messageContainer

nameList, #messageList

*.ListData, .message { background-color: black; text-align: center;
    border: solid medium white; font-size: 20pt; padding: 10px; width: 140px;}

.message { width: 95%; font-size: 18pt; padding: 5px;}

buttonContainer > button {font-size: 20pt; margin: 10px;}`

定义视图模型

为了定义数据并准备好它,以便它可以与ListView控件一起使用,我添加了一个名为js/viewmodel.js的文件,其内容可以在清单 21-3 中看到。

清单 21-3 。viewmodel.js 文件的初始内容

`(function () {

var rawData = ['Aaliyah', 'Aaron', 'Abigail', 'Abraham', 'Adam', 'Addison', 'Adrian',
        'Adriana', 'Aidan', 'Aiden', 'Alex', 'Alexa', 'Alexander', 'Alexandra', 'Alexis',
        'Allison', 'Alyssa', 'Amelia', 'Andrew', 'Angel', 'Angelina', 'Anna', 'Anthony',
        'Ariana', 'Arianna', 'Ashley', 'Aubrey', 'Austin', 'Ava', 'Avery', 'Ayden',
        'Bella', 'Benjamin', 'Blake', 'Brandon', 'Brayden', 'Brian', 'Brianna', 'Brooke',
        'Bryan', 'Caleb', 'Cameron', 'Camila', 'Carter', 'Charles', 'Charlotte', 'Chase',
        'Chaya', 'Chloe', 'Christian', 'Christopher', 'sClaire', 'Connor', 'Daniel',
        'David', 'Dominic', 'Dylan', 'Eli', 'Elijah', 'Elizabeth', 'Ella', 'Emily',
        'Emma', 'Eric', 'Esther', 'Ethan', 'Eva', 'Evan', 'Evelyn', 'Faith', 'Gabriel',
        'Gabriella', 'Gabrielle', 'Gavin', 'Genesis', 'Gianna', 'Giovanni', 'Grace',
        'Hailey', 'Hannah', 'Henry', 'Hunter', 'Ian', 'Isaac', 'Isabella', 'Isaiah',
        'Jack', 'Jackson', 'Jacob', 'Jacqui', 'Jaden', 'Jake', 'James', 'Jasmine',
        'Jason', 'Jayden', 'Jeremiah', 'Jeremy', 'Jessica', 'Joel', 'John', 'Jonathan',         'Jordan', 'Jose', 'Joseph', 'Joshua', 'Josiah', 'Julia', 'Julian', 'Juliana',
        'Julianna', 'Justin', 'Kaitlyn', 'Katherine', 'Kayla', 'Kaylee', 'Kevin',
        'Khloe', 'Kimberly', 'Kyle', 'Kylie', 'Landon', 'Lauren', 'Layla', 'Leah', 'Leo',
        'Liam', 'Lillian', 'Lily', 'Logan', 'London', 'Lucas', 'Luis', 'Luke',
        'Mackenzie', 'Madeline', 'Madelyn', 'Madison', 'Makayla', 'Maria', 'Mason',
        'Matthew', 'Max', 'Maya', 'Melanie', 'Mia', 'Michelle', 'Miriam', 'Molly',
        'Morgan', 'Moshe', 'Naomi', 'Natalia', 'Natalie', 'Nathan', 'Nathaniel',
        'Nevaeh', 'Nicholas', 'Nicole', 'Noah', 'Oliver', 'Olivia', 'Owen', 'Paige',
        'Patrick', 'Peyton', 'Rachel', 'Rebecca', 'Richard', 'Riley', 'Robert', 'Ryan',
        'Samantha', 'Samuel', 'Sara', 'Sarah', 'Savannah', 'Scarlett', 'Sean',
        'Sebastian', 'Serenity', 'Sofia', 'Sophia', 'Sophie', 'Stella', 'Steven',
        'Sydney', 'Taylor', 'Thomas', 'Tristan', 'Tyler', 'Valentina', 'Victoria',
        'Vincent', 'William', 'Wyatt', 'Xavier', 'Zachary', 'Zoe', 'Zoey'];

WinJS.Namespace.define("ViewModel", {
        allNames: [],
        filteredNames: new WinJS.Binding.List(),
        messages: new WinJS.Binding.List(),
        writeMessage: function (msg) {
            ViewModel.messages.push({ message: msg });
        },
        searchTerm: ""
    });

rawData.forEach(function (item, index) {
        var item = { name: item, firstLetter: item[0] };
        ViewModel.allNames.push(item);
    });

ViewModel.search = function (term) {
        ViewModel.writeMessage("Searched for: " + (term == "" ? "empty string" : term));
        term = term.toLowerCase();
        ViewModel.filteredNames.length = 0;
        ViewModel.allNames.forEach(function (item) {
            if (item.name.toLowerCase().indexOf(term) > -1) {
                ViewModel.filteredNames.push(item)
            }
        });
        ViewModel.searchTerm = term;
    };
})();`

rawData数组包含作为一组字符串的名字列表。这些对我来说没有多大用处,所以我处理这些值来创建两组对象,我可以将它们用于数据绑定模板。第一个集合ViewModel.allNames,包含一个完整的对象集合——这将是我的参考数据,我将根据它执行搜索。第二组对象是ViewModel.filteredNames,我将它用作布局中左侧ListView控件的数据源。在本章中,我将使用数据源来显示一些搜索的结果。

我还使用viewmodel.js文件来定义ViewModel.search函数,该函数通过依次检查每个名字来执行简单的搜索——这是一种低效的搜索技术,但对于我的示例应用来说已经足够了。

定义 JavaScript 代码

现在剩下的就是实现default.js文件。我只使用了最小生命周期事件处理代码,在本章中我不会担心应用暂停或终止的影响。您可以在清单 21-4 中看到我修改后的default.js文件的内容。

清单 21-4 。初始 default.js 文件

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    WinJS.strictProcessing();

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState
                    != activation.ApplicationExecutionState.suspended) {

args.setPromise(WinJS.UI.processAll().then(function () {
**                    ViewModel.writeMessage("App Launched");**
**                    ViewModel.search("");**
                }));
            }
        }
    };

app.start();
})();`

当应用启动时,我调用ViewModel.search方法来搜索空字符串——这样可以在应用首次启动时匹配所有名称并显示完整的名称集。你可以在图 21-1 中看到应用的布局以及首次启动时显示的数据和消息。

images

图 21-1。示例 app 的初始布局和数据

添加应用图片

我在项目的images文件夹中添加了一些文件,如图图 21-2 所示。我已经使用了文件名以tile开头的文件来设置应用清单的应用 UI 部分的字段,就像我在第四章的中所做的一样。这些图像包含如图所示的放大镜图标。另一个文件,user.png,包含一个人物图标,我将在本章后面使用 Windows 显示我的搜索结果时使用这个文件。您可以在图中看到这两个图标(我添加了黑色背景以使图标可见—实际文件是透明的)。我已经将这些文件包含在本书的源代码下载中,您可以从apress.com获得。

images

图 21-2。向示例应用添加磁贴和用户图标

测试示例应用

正如我在介绍中提到的,这一章不是关于你如何在你的应用中实现搜索,而是关于你如何将搜索功能集成到 Windows 中,以便你的用户有一致和丰富的搜索体验。我在示例应用中执行搜索的方式非常简单,您可以通过使用 Visual Studio JavaScript Console窗口看到它是如何工作的。启动应用(确保使用Start Debugging菜单项)并进入 JavaScript 控制台窗口。在提示符下输入以下内容:


ViewModel.search("jac")


(JavaScript 控制台也会说Undefined——你可以忽略这个。)当你按下 Enter 键时,你会看到左边的ListView显示的名字集合将被限制为包含搜索词jac的名字,如图图 21-3 所示。右边的ListView将显示一条新消息,报告所请求的搜索词。

images

图 21-3。测试 app 搜索能力

你必须使用JavaScript Console来执行搜索,因为我没有在布局中添加任何搜索元素或控件——当我将应用搜索功能集成到更广泛的 Windows 搜索体验中时,这将由操作系统提供。在接下来的小节中,我将向您展示如何执行这种集成,并解释您可以采用的不同方法。

实施搜索契约

第一步是更新应用清单,以声明您打算支持搜索契约。为此,双击 Visual Studio Solution Explorer窗口中的package.appxmanifest文件,并选择Declarations选项卡。从Available Declarations菜单中选择Search并按下Add 按钮。你会看到Search被添加到Supported Declarations列表中,如图图 21-4 所示。你可以忽略页面的Properties部分——只有当你想使用一个单独的应用来处理搜索时,这些才是有用的,在这一章,我将向你展示如何直接向应用添加搜索支持。

images

图 21-4。宣布支持搜索契约

处理激活事件

Windows 通过发送一个activated事件通知你的应用它需要执行一个搜索操作。这与您的应用启动时发送的事件相同,为了区分这两种情况,您必须读取事件的kind属性。处理一个契约的激活需要一种不同的方法来实现你的应用中的onactivated功能,如清单 21-5 中的所示。

清单 21-5 。default.js 文件中的契约激活处理

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    WinJS.strictProcessing();

app.onactivated = function (args) {

**        var searchTerm;**
**        var promise;**

**        switch (args.detail.kind) {**
**            case activation.ActivationKind.search:**
**                ViewModel.writeMessage("Search Activation");**
**                searchTerm = args.detail.queryText;**
**                break;**
**            case activation.ActivationKind.launch:**
**                ViewModel.writeMessage("Launch Activation");                 searchTerm = "";**
**                break;**
**        }**

**        if (args.detail.previousExecutionState**
**            != activation.ApplicationExecutionState.suspended) {**
**            ViewModel.writeMessage("App was not resumed");**
**            promise = WinJS.UI.processAll();**
**        } else {**
**            ViewModel.writeMessage("App was resumed");**
**            promise = WinJS.Promise.as(true);**
**        }**

**        args.setPromise(promise.then(function () {**
**            ViewModel.search(searchTerm);**
**        }));**
    };

app.start();
})();`

为了解释这是如何工作的,我将详细地遍历代码,并向您展示它适合的场景范围。这一点也不复杂,但有一些细节需要考虑,如果你理解如何处理这个契约,你会发现处理其他契约更简单、更容易。

启动应用

首先要做的是启动 app。如何做并不重要——您可以使用 Visual Studio Debug菜单中的Start Debugging项,或者,如果您之前已经运行了该示例,可以使用开始屏幕上的磁贴。当应用启动时,您会在右侧的ListView控件中看到如图图 21-5 所示的消息。

images

图 21-5。应用正常启动时显示的消息

在这种情况下,我在onactivated处理函数中的switch语句将读取detail.kind属性并获得launch值。在这种情况下,我将搜索字符串设置为空字符串(""),如下所示:

... switch (args.detail.kind) {     case activation.ActivationKind.search:         ViewModel.writeMessage("Search Activation");         searchTerm = args.detail.queryText;         break;     case activation.ActivationKind.**launch**:         ViewModel.writeMessage("Launch Activation"); **        searchTerm = "";**         break; } ...

这给了我两样我需要的东西之一:搜索词。为了得到Promise,我需要查看之前的执行状态。由于 app 已经重新启动,之前的状态会是notRunning。对于除suspended之外的每个州,我想调用WinJS.UI.processAll方法来执行应用的初始设置:

... if (args.detail.previousExecutionState     != activation.ApplicationExecutionState.**suspended**) {     ViewModel.writeMessage("App was not resumed"); **    promise = WinJS.UI.processAll();** } else {     ViewModel.writeMessage("App was resumed");     promise = WinJS.Promise.as(true); } ...

其效果是,搜索将匹配所有数据项(因此最初显示所有名称),并且应用被初始化,以便 WinJS UI 控件被激活。你可以看到我是如何在整个代码中编写消息来显示我正在处理的情况,这就是我如何得到如图 21-4 所示的消息。

执行搜索

启动应用当然很好,但是你已经知道怎么做了。要查看新的东西,选择搜索符,通过键入Win+Q或激活符栏并选择Search图标,如图图 21-6 所示。

images

图 21-6。选择搜索符

当你激活搜索功能时,一个名为搜索窗格的新显示会覆盖在应用上,允许你进行搜索。有一个搜索词的文本输入框,在它下面你会看到一系列图标。这些图标代表搜索目标的范围,包括SearchContract示例 app,如图图 21-7 所示。(您可能需要向下滚动列表才能看到该应用。)

images 提示如果搜索窗格报告无法搜索到应用,您需要停止 Visual Studio 调试器,卸载SearchContract应用,然后再次启动 Visual Studio 调试器。在开发过程中,Windows 并不总是能够正确响应明显的更改,但对于通过 Windows 应用商店安装的应用来说,这不是问题。(我在本书的第五部分中向你展示了将你的应用发布到商店的过程。)

images

图 21-7。搜索窗格

您会注意到列表中已经选择了SearchContract应用,这向用户表明搜索请求将被传递给该应用进行处理。

输入jac和搜索词,点击回车或点击文本输入右侧的图标进行搜索。该应用将执行指定术语的搜索,并显示匹配的名称,但搜索窗格将保持可见。

单击应用上的任意位置关闭搜索窗格,您将看到右侧ListView控件中显示的消息已经更新,如下所示:


Launch Activation App was not resumed Searched for: empty string **Search Activation** **App was not resumed** **Searched for: jac**


感兴趣的是我用粗体标记的新条目。当你提交搜索时,Windows 向应用发送了另一个activated事件,但这次args.detail.kind属性被设置为search。众所周知,当执行搜索激活时,系统包含用户正在搜索的字符串作为detail.queryText属性的值。当我收到一个搜索激活事件时,这是我在我的onactivated处理程序中搜索的术语:

... switch (args.detail.kind) {     case activation.ActivationKind.**search**:         ViewModel.writeMessage("Search Activation"); **        searchTerm = args.detail.queryText;**         break;     case activation.ActivationKind.launch:         ViewModel.writeMessage("Launch Activation");         searchTerm = "";         break; } ...

当然,此时应用正在运行,所以我不需要调用WinJS.UI.processAll方法,因为我所有的 UI 控件都已经应用并正常工作。我仍然希望有一个Promise对象来处理,所以我使用Promise.wrap方法(我在第九章中描述过)来创建一个将被立即实现的Promise,如下所示:

... if (args.detail.previousExecutionState     != activation.ApplicationExecutionState.suspended) {     ViewModel.writeMessage("App was not resumed");     promise = WinJS.UI.processAll(); } else {     ViewModel.writeMessage("App was resumed"); **    promise = WinJS.Promise.as(true);** } ...

每当用户执行额外的搜索时,该应用将接收额外的activated事件,其kindsearch。出于这个原因,我一直小心地构建我的代码,这样我就不会对我将要处理的事件的数量或种类做任何假设。

应用不运行时进行搜索

我想探索的最后一个场景要求应用已经关闭。停止调试器或在应用中按Alt+F4关闭应用。无需重启应用,从开始屏幕选择搜索符。

当您从开始屏幕中选择 Search Charm 时,搜索范围将设置为安装在设备上的应用的名称(视觉提示是在文本输入框下的列表中选择了Apps)。输入jac作为搜索词,并按回车键执行搜索。你会在图 21-8 中看到结果(除非你刚好有一个 app 的名字包含jac)。

images

图 21-8。通用视窗搜索

现在点击列表中SearchContract应用的图标。(如果你一直遵循本章中的示例,应用将是列表中的第一项,因为 Windows 根据使用情况对应用进行排序——你或许可以从图中看出这一点。)

单击列表中的应用条目会将搜索范围更改为示例应用。该应用将被启动,显示在左侧ListView的名字将是那些匹配的搜索词。点击应用布局,关闭搜索窗格,查看右侧ListView显示的消息,如下所示:


Search Activation App was not resumed Searched for: jac


需要注意的重要一点是,应用没有接收到launch激活事件,只有search激活是由系统发送的。如果当用户执行针对该应用的搜索时,该应用没有运行,则系统将启动该应用,但它不会发送与通过其磁贴或 Visual Studio 调试器启动该应用时相同的事件。

知道了这一点,我需要确保当我得到一个搜索激活事件并且应用之前没有运行时,我可以正确地初始化应用——对于这个简单的应用,这仅仅意味着调用WinJS.UI.processAll方法,然后进行搜索。您现在可以理解为什么我把对前一个执行状态的检查从检查我接收到的事件的kind的代码中分离出来了:

... if (args.detail.previousExecutionState     != **activation.ApplicationExecutionState.suspended**) {     ViewModel.writeMessage("App was not resumed"); **    promise = WinJS.UI.processAll();** } else {     ViewModel.writeMessage("App was resumed");     promise = WinJS.Promise.as(true); } ...

总结搜索契约激活场景

重要的是要确保你知道什么时候初始化你的应用,什么时候你可以通过queryText属性获得一个搜索词。您已经在前面的章节中看到了各种场景,为了将来快速参考,我在表 21-2 中总结了这些排列。

images

使用搜索窗格

我在前面几节中向您展示的技术是搜索契约的基本实现,一旦您的应用中有了搜索功能,大部分工作就是确保您正确处理激活事件。

您还可以通过直接使用搜索窗格来创建定制的搜索体验,搜索窗格可通过Windows.ApplicationModel.Search名称空间中的对象获得。这个名称空间中的关键对象是SearchPane,它允许您访问 Windows 搜索窗格并与之集成。SearchPane对象定义了表 21-3 中所示的方法和属性。

images

此外,SearchPane对象支持几个事件,我已经在表 21-4 中描述过了。这些事件在搜索过程的关键时刻触发,允许您以比使用基本契约实现更复杂的方式做出响应。

在接下来的部分中,我将向您展示一些使用这些方法、属性和事件的高级搜索技术,包括如何从应用布局中触发搜索过程,并提供 Windows 可以用来在搜索过程中帮助用户的不同类型的建议。

images 注意除非我另有说明,否则我在本节中引用的对象都在Windows.ApplicationModel.Search名称空间中。

激活搜索

用户并不总是理解 Windows 8 搜索是如何工作的,这是一个遗憾,因为从系统范围的窗格中触发特定应用搜索的想法是一个很好的想法。如果搜索是应用功能的一个关键部分,您可能需要在应用布局中为用户提供一个用于打开搜索窗格的控件。为了演示这一点,我在default.html文件的标记中添加了一个button元素,如清单 21-6 所示。

清单 21-6 。添加一个打开搜索窗格的按钮

`...

<body>     <div id="NameItemTemplate" data-win-control="WinJS.Binding.Template">         <div class="ListItem">             <div class="ListData" data-win-bind="innerText: name"></div>         </div>     </div>


        

    


        

        
    

**    

**
**        **
**    
**


        

        

    

...`

这并没有创造出最优雅的应用布局,但对我来说已经足够了。你可以在图 21-9 中看到结果。

images

图 21-9。在应用布局中添加显示搜索按钮

我已经将清单 21-7 中显示的语句添加到default.js文件中,以响应被点击的button

清单 21-7 。响应被点击的按钮

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
**    var search = Windows.ApplicationModel.Search;**
    WinJS.strictProcessing();

app.onactivated = function (args) {
        var searchTerm;
        var promise;

switch (args.detail.kind) {
            case activation.ActivationKind.search:
                ViewModel.writeMessage("Search Activation");                 searchTerm = args.detail.queryText;
                break;
            case activation.ActivationKind.launch:
                ViewModel.writeMessage("Launch Activation");
                searchTerm = "";
                break;
        }

if (args.detail.previousExecutionState
            != activation.ApplicationExecutionState.suspended) {
            ViewModel.writeMessage("App was not resumed");
            promise = WinJS.UI.processAll().then(function () {
**                showSearch.addEventListener("click", function (e) {**
**                    search.SearchPane.getForCurrentView().show(ViewModel.searchTerm);**
**                });**
            });
        } else {
            ViewModel.writeMessage("App was resumed");
            promise = WinJS.Promise.as(true);
        }

args.setPromise(promise.then(function () {
            ViewModel.search(searchTerm);
        }));
    };

app.start();
})();`

使用SearchPane对象的第一步是调用getForCurrentView方法,该方法返回一个SearchPane对象,您可以在其上执行操作。这意味着,如果您想要显示搜索窗格,就像我在示例中所做的那样,您必须使用:

... search.SearchPane.**getForCurrentView()**.show(ViewModel.searchTerm); ...

如果你试图在一个SearchPane对象上使用一个不是通过getForCurrentView方法获得的方法或属性,你将会创建一个异常。在清单中,我通过使用show方法显示搜索窗格来响应新添加的button中的click事件。我可以通过向show方法传递一个字符串来设置搜索窗格中的初始查询字符串,这允许我确保搜索窗格与应用布局左侧ListView中显示的数据一致。

如果您运行示例应用并单击Show Search按钮,将会显示标准的 Windows 搜索窗格。Search示例应用将被自动选择为搜索范围,就像您在本章前面通过 Search Charm 显示搜索窗格一样。

提供查询建议

使用搜索窗格时,我最喜欢的功能是直接在窗格中显示结果。这让用户可以在你的应用上执行渐进式搜索,每次查询框中的文本改变时,可能匹配的范围都会更新。

当搜索窗格可见,用户正在输入搜索词时,SearchPane对象将触发suggestionsrequested事件,这是邀请你向用户提供建议。你可以在清单 21-8 中的示例应用中看到我是如何做到这一点的,我已经为这个事件在default.js文件中添加了一个处理函数。

清单 21-8 。添加对建议请求事件的支持

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var search = Windows.ApplicationModel.Search;
    WinJS.strictProcessing();

app.onactivated = function (args) {
        var searchTerm;
        var promise;

switch (args.detail.kind) {
            case activation.ActivationKind.search:
                ViewModel.writeMessage("Search Activation");
                searchTerm = args.detail.queryText;
                break;
            case activation.ActivationKind.launch:
                ViewModel.writeMessage("Launch Activation");
                searchTerm = "";
                break;
        }

if (args.detail.previousExecutionState
            != activation.ApplicationExecutionState.suspended) {
            ViewModel.writeMessage("App was not resumed");
            promise = WinJS.UI.processAll().then(function () {
**                var sp = search.SearchPane.getForCurrentView();**
                showSearch.addEventListener("click", function (e) {
                    sp.show(ViewModel.searchTerm);
                });
**                sp.addEventListener("suggestionsrequested", function (e) {**
**                    var query = e.queryText;**
**                    var suggestions = ViewModel.search(query, true);**
**                    suggestions.forEach(function (item) {**
**                        e.request.searchSuggestionCollection**
**                            .appendQuerySuggestion(item.name);**
**                    });**
**                });**
            });
        } else {
            ViewModel.writeMessage("App was resumed");
            promise = WinJS.Promise.as(true);
        }         args.setPromise(promise.then(function () {
            ViewModel.search(searchTerm);
        }));
    };

app.start();
})();`

注意,我在从SearchPane.getForCurrentView方法返回的对象上调用了addEventListener方法。为了能够提供建议,我必须更新viewmodel.js文件,这样我的search方法就可以执行更新ListView控件的搜索以及仅仅生成建议的搜索。您可以在清单 21-9 的中看到search方法的新版本。

清单 21-9 。更新搜索方法以生成建议

... ViewModel.search = function (term, **suggestions**) {     ViewModel.writeMessage("Searched for: " + (term == "" ? "empty string" : term));     term = term.toLowerCase(); **    var target = suggestions ? [] : ViewModel.filteredNames;**     target.length = 0;     ViewModel.allNames.forEach(function (item) {         if (item.name.toLowerCase().indexOf(term) > -1) {             target.push(item)         }     }); **    if (!suggestions) {** **        ViewModel.searchTerm = term;** **    }**     return target; }; ...

如果suggestions参数存在并且true,那么该方法将生成并返回一个匹配数组,而不更新ListView使用的数据源。这允许我添加对建议的支持,而不必修改任何对search方法的现有调用。

对于本节,向您展示结果,然后解释所有涉及的对象是如何组合在一起的会更容易。要查看这些更改的效果,请启动应用,打开搜索窗格,然后键入jac。当您输入每个字母时,您会看到在文本输入框下方显示一个可能匹配的列表,如图图 21-10 所示。Windows 将显示您的应用提供的最多五个建议。

images

图 21-10。搜索窗格中显示的建议

每当您键入一个额外的字母时,该列表都会被优化。当你输入所有三个字母时,会显示四个建议,所有建议都包含术语jac。示例应用中的ListView在你输入时不会受到影响——但是如果你点击这些建议中的任何一个,系统将触发搜索激活,这将具有搜索该术语的效果。

理解建议示例

要想给系统提供建议,需要一长串的对象,但是请相信我——它并不像看起来那么复杂。一个SearchPaneSuggestionsRequestedEventArgs对象被传递给suggestionrequired事件的处理函数。除了有一个长得离谱的名字,这个对象定义了两个有用的只读属性,我已经在表 21-5 中描述过了。

images

每次用户修改搜索文本时,queryText属性都会更新。当用户缩小搜索范围时,通常会收到一系列事件——可能从jqueryText值开始,然后是ja,最后是jac。您读取queryText属性的值,并使用 request 属性返回的SearchPaneSuggestionsRequest对象向系统提供可以呈现给用户的建议。SearchPaneSuggestionsRequest定义了我在表 21-6 中描述的方法和属性。

images

我们已经到达了这个链中的重要对象,即SearchSuggestionCollection对象,它可以通过传递给处理函数的事件对象的request.searchSuggestionCollection属性获得。SearchSuggestionCollection对象定义了四个方法和一个属性,我已经在表 21-7 中进行了总结。

images

最简单的方法是appendQuerySuggestion,它将一个字符串作为参数,并在搜索窗格中作为一个可能的查询呈现给用户。这是我在例子中使用的方法,正如你在清单 21-10 中看到的,在那里我重复了关键语句。

清单 21-10 。为用户提供查询建议

... sp.addEventListener("suggestionsrequested", function (e) {     var query = e.queryText;     var suggestions = ViewModel.search(query, true);     suggestions.forEach(function (item) {         e.request.searchSuggestionCollection.**appendQuerySuggestion**(item.name);     }); }); ...

images 提示我为从ViewModel.search方法得到的每个建议调用appendQuerySuggestion方法。这对于我的示例应用来说是可行的,因为搜索 200 个名字没有任何显著的成本。但是,Windows 将显示不超过五个建议,因此如果生成结果的成本很高,您可以通过只生成五个匹配来节省资源。

给建议添加分隔符

方法允许你给你的建议增加一些结构。如果将对appendSearchSeparator的调用与对appendQuerySuggestion方法的调用交错,就可以创建一组结构化的建议。然而,在搜索窗格上仍然只有五个位置用于建议,所以您向建议添加的每一个分隔符都意味着您可以少提供一个结果。清单 21-11 显示了我对default.js文件中的suggestionsrequested处理函数所做的修改,以演示appendSearchSeparator方法的使用。

清单 21-11 。使用 appendSearchSeparator 方法向建议添加结构

`...
sp.addEventListener("suggestionsrequested", function (e) {
    var query = e.queryText;
    var suggestions = ViewModel.search(query, true);
**    var lastLetter = null;**
    suggestions.forEach(function (item) {
**        if (item.firstLetter != lastLetter) {**
**            e.request.searchSuggestionCollection.appendSearchSeparator(item.firstLetter);**
**            lastLetter = item.firstLetter;**
**        }**
        e.request.searchSuggestionCollection.appendQuerySuggestion(item.name);

});
});
...`

搜索建议没有固定的结构,所以你可以在应用中以任何有意义的方式应用分隔符。在这个例子中,我根据名字的第一个字母来分隔名字(这样做时,我依赖于这样一个事实,即由ViewModel.search方法返回的名字是按字母顺序排序的)。你可以在图 21-11 中看到结果。

images

图 21-11。搜索建议添加分隔符的效果

要查看示例应用中的效果,您需要找到一个匹配以不同字母开头的名称的搜索字符串。在示例中,我使用了字符串aa,它匹配AaliyahAaronIsaac。您可以在图中看到,这些名称已经使用我传递给appendSearchSeparator方法的值进行了分隔。

images 提示我建议你在提供搜索结果时使用分隔符之前仔细考虑一下,因为它们会占用宝贵的空间,而这些空间可以用来给用户提供额外的建议。只有当没有分隔符的建议对用户没有意义时,才使用分隔符。

提供结果建议

当用户正在搜索的术语与你的应用中的数据项完全匹配时,你可以给出一个结果建议。这为用户提供了该项目的概述,并帮助他决定他是否已经找到了他正在寻找的东西。你可以在图 21-12 中看到一个例子,我在这里搜索过alexAlex是我正在处理的列表中的一个名字,也是其他名字的一部分,比如AlexaAlexander,这就是为什么你会看到混合的查询建议(到目前为止我一直在做的那种建议)和结果建议。

images

图 21-12。搜索窗格中显示的结果建议

使用SearchSuggestionCollection.appendResultSuggestion方法提出结果建议,该方法需要表 21-8 中描述的参数。这些附加参数用于提供您可以在图中看到的有关该项目的附加信息。

所有参数都是必需的。对于这个例子,我在示例项目中添加了一个简单的图像作为img/user.png,这个图像显示在建议的Alex结果旁边。您可以在清单 21-12 中看到我是如何创建建议的,它显示了我对default.js文件所做的更改。

清单 21-12 。增加对结果建议的支持

... sp.addEventListener("suggestionsrequested", function (e) {     var query = e.queryText;     var suggestions = ViewModel.search(query, true);     var lastLetter = null;     suggestions.forEach(function (item) { **        if (query.toLowerCase() != item.name.toLowerCase()) {**             e.request.searchSuggestionCollection.appendQuerySuggestion(item.name); **        } else {** **            var imageSource = Windows.Storage.Streams.RandomAccessStreamReference.** **                createFromUri(Windows.Foundation.Uri("ms-appx:img/user.png"));** **            e.request.searchSuggestionCollection.appendResultSuggestion(** **                item.name,** **                "This name has " + item.name.length + " chars",** **                item.name, imageSource, item.name);** **        }**     }); }); ...

我的数据并不完全适合这个模型——这并不罕见。我发现数据项要么太复杂而不能用作结果建议,要么太简单,导致我不得不添加一些填充数据。在这个例子中,数据过于简单。

对于textdetailText参数,我使用了匹配的名称和一个报告名称中有多少字符的字符串——这不是有用的数据,但它是您最终使用的填充类型。如果您真的没有什么有用的东西要说,您可以将detailText参数设置为空字符串,但是这样会产生一个看起来有点奇怪的建议。

我将暂时跳过tag参数,来看看image参数。我包含了我想作为项目一部分使用的图像,但是图像参数必须是一个Windows.Storage.Streams.IRandomAccessStreamReference对象。

我在《??》第二十二章中介绍了 Windows 对文件处理的支持,所以现在我将把image参数所需的代码作为黑盒咒语呈现出来。从应用包加载图像所需的咒语类似于我在加载应用数据文件时向您展示的技术,但是将Windows.Foundation.Uri对象传递给Windows.Storage.Streams. RandomAccessStreamReference.createFromUri方法,以创建加载建议图像所需的对象类型:

... Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(     Windows.Foundation.Uri("**ms-appx:img/user.png**")); ...

images 注意不要忘记在指定图像文件的 URL 中有三个///字符——一个常见的错误是只使用两个。这不会产生错误,但是您不会看到作为结果建议的一部分显示的图像。

选择建议后做出回应

您可以在清单中看到,我也为tag参数使用了这个名称。此参数必须唯一标识您建议的数据项。系统并不关心这个值是什么——如果用户点击了建议,它就把它返回给你。当这种情况发生时,SearchPane对象触发一个resultsuggestionchosen事件,并通过事件对象的tag属性将您提供的tag值传递回您的应用。清单 21-13 显示了为resultsuggestionchosen事件添加一个处理函数,其中我将标签值传递给ViewModel.search方法以反映用户的选择。

清单 21-13 。为 resultsuggestionchosen 事件添加事件处理程序

... sp.addEventListener("suggestionsrequested", function (e) {     var query = e.queryText;     var suggestions = ViewModel.search(query, true);     var lastLetter = null;     suggestions.forEach(function (item) {         if (query.toLowerCase() != item.name.toLowerCase()) {             e.request.searchSuggestionCollection.appendQuerySuggestion(item.name);         } else {             var imageSource = Windows.Storage.Streams.RandomAccessStreamReference                 .createFromUri(Windows.Foundation.Uri("ms-appx:img/user.png"));             e.request.searchSuggestionCollection.appendResultSuggestion(                 item.name,                 "This name has " + item.name.length + " chars",                 item.name, imageSource, item.name);         }     }); }); **sp.addEventListener("resultsuggestionchosen", function (e) {** **    ViewModel.search(e.tag);** **});** ...

处理建议历史

Windows 维护用户接受的建议结果和查询的历史记录,并在再次执行相同的搜索时给予他们优先权。要了解这是如何工作的,请启动应用并搜索jo。您将看到搜索窗格中显示的建议是:


Joel John Jonathan Jordan Jose


点击Jordan完成搜索——你会看到应用布局中的ListView控件被更新以匹配你的搜索,搜索框也被更新以显示Jordan。清除搜索框,再次输入jo。这一次,名字出现的顺序有所不同:


**Jordan** Joel John Jonathan Jose


这对用户来说是一个有用的帮助,并且搜索历史是持久的,这意味着当用户做出更多选择时,它将向用户提供更好的建议。

您可以通过将SearchPane.searchHistoryEnabled属性设置为false来禁用建议历史。这可以防止用户的选择被添加到历史记录中,并确保您的建议按照您提供的顺序呈现给用户。您可以在清单 21-14 中看到我是如何使用searchHistoryEnabled属性的。

清单 21-14 。禁用搜索历史

... if (args.detail.previousExecutionState     != activation.ApplicationExecutionState.suspended) {     ViewModel.writeMessage("App was not resumed");     promise = WinJS.UI.processAll().then(function () {         var sp = search.SearchPane.getForCurrentView(); **        sp.searchHistoryEnabled = false;**         showSearch.addEventListener("click", function (e) {             sp.show(ViewModel.searchTerm);         });         sp.addEventListener("suggestionsrequested", function (e) {             // *... code removed for brevity...*         });         sp.addEventListener("resultsuggestionchosen", function (e) {             ViewModel.search(e.tag);         });     }); } else {     ViewModel.writeMessage("App was resumed");     promise = WinJS.Promise.as(true); } ...

如果您启动应用并再次运行对jo的搜索,您将会看到Jordan在建议列表中没有被优先考虑。

images 提示如果你的应用支持不同的数据搜索方式或者支持搜索不同的数据集,你可以使用SearchPane.searchHistoryContext属性为每个数据集保存单独的搜索历史。给这个属性分配一个代表用户将要执行的搜索类型的值,下次使用相同的值给searchHistoryContext属性时,Windows 将只考虑用户所做的选择。

异步提出建议

到目前为止,我已经同步生成了我的所有建议,这很好,因为我的所有数据都存储在内存中,可以立即使用。

如果生成建议所依赖的代码返回一个Promise而不是直接给你数据,你就需要采取不同的方法。当您的数据包含在文件或一个WinJS.UI.IListDataSource对象中时,您会经常发现这种情况,该对象从它的许多方法中返回Promise对象,并在满足Promise时产生数据。

为了演示这个问题,我在我的viewmodel.js类中添加了一个ViewModel.asyncSuggest方法。这个方法使用了我在这一章中一直使用的数据,但是它通过一个Promise来呈现结果,这个结果只有在搜索完成后才能实现。为了更真实地展示这一点,搜索是作为一系列小操作来执行的,这些小操作与对setImmedate方法的调用交织在一起,以允许 JavaScript 运行时执行其他操作。你可以在清单 21-15 中看到增加的内容。

images 提示关于WinJS.Promise对象和setImmediate方法的详细信息,请参见第九章。

清单 21-15 。一种异步搜索方法

`...
ViewModel.asyncSuggest = function (term) {
    return new WinJS.Promise(function (fDone, fError, fProgress) {
        var index = 0;
        var blockSize = 10;
        var matches = [];
        term = term.toLowerCase();

function searchBlock() {
            for (var i = index; i < index + blockSize; i++) {
                if (ViewModel.allNames[i].name.toLowerCase().indexOf(term) > -1) {
                    matches.push(ViewModel.allNames[i].name);
                }
            }
            index += blockSize;
            if (index < ViewModel.allNames.length) {                 setImmediate(searchBlock);
            } else {
                fDone(matches);
            }
        }
        setImmediate(searchBlock);
    });
}
...`

该方法返回一个WinJS.Promise对象,当查询词的所有名称都被搜索到时,该对象被满足。搜索本身是在 10 个名字的块中执行的,并且在每个块之后调用setImmediate方法,以便 JavaScript 运行时可以执行其他未完成的工作,比如流程事件。

要使用异步方法生成建议,您必须调用SearchPaneSuggestionRequest.getDeferral方法。通过传递给suggestionsrequested事件处理函数的对象的request属性可以获得SearchPaneSuggestionRequest对象。getDeferral方法返回一个SearchPaneSuggestionsRequestDeferral对象,该对象定义了一个方法:complete。当异步建议生成方法返回的Promise被满足时,调用complete方法。清单 21-16 展示了我是如何将这项技术应用到例子中的suggestionsrequested处理程序的。

清单 21-16 。使用异步方法生成建议

... sp.addEventListener("suggestionsrequested", function (e) { **    var deferral = e.request.getDeferral();**     ViewModel.asyncSuggest(e.queryText).then(function (suggestions) {         e.request.searchSuggestionCollection.appendQuerySuggestions(suggestions); **        deferral.complete();**     }); }); ...

当满足Promise时,记住调用complete方法是很重要的。如果您忘记这样做,不会报告错误,但是用户不会看到您提供的建议。

总结

在这一章中,我已经向你展示了如何实现第一个 Windows 契约,它允许一个应用与系统范围的特性相集成。我向你展示了搜索契约,通过实施这一契约,一个应用能够无缝地参与搜索,提供其内容和数据以及其他应用的内容和数据。

我还向您展示了如何通过直接使用搜索窗格来定制搜索体验。使用搜索窗格,我向您展示了如何提供查询和结果建议,以帮助用户在您的应用中找到他想要的东西。Windows 对搜索的支持非常灵活,我建议你花时间以一种有用和有意义的方式将其集成到你的应用中。在下一章,我将向你展示如何在 Windows 应用中处理文件。这是一个重要的功能领域,我将在接下来的三章中全面介绍。

二十二、使用文件

这是关注用户文件的两章中的第一章,也就是说用户创建、更改和存储的文件不同于我在第二十一章中描述的应用数据和文件。

在这一章中,我将向您展示如何直接操作文件和文件夹来执行基本的文件操作,例如复制和删除文件。接下来,我将向您展示 Windows 应用的功能,这些功能可让您更全面地查看文件,包括对文件列表进行排序、过滤文件夹内容、执行复杂的文件搜索、创建虚拟文件夹以将相关文件分组在一起,以及监控文件夹以便在添加新文件时通知您。

这些技术让我为下一章做好了准备,下一章将展示如何将您的应用及其与文件和文件夹相关的功能集成到更广泛的 Windows 体验中。因此,简而言之,这一章的所有内容都是关于处理你的应用中的文件系统,而下一章是关于向用户展示这些功能。表 22-1 对本章进行了总结。

images

创建示例应用

对于这一章,我已经创建了一个名为UserFiles的新示例项目。我需要一些示例文件,所以我创建了一个img/flowers文件夹,并添加了一些 JPG 图片。这些是我在第二十一章中使用的相同图像,我已经将它们包含在本书附带的源代码下载中(可从apress.com获得)。您可以在图 22-1 中看到文件名列表,它显示了UserFiles项目的解决方案资源管理器窗口。

images

图 22-1。向示例应用添加图像文件

该项目的其余部分是一个简单的应用。您可以在清单 22-1 中看到default.html文件的内容。布局中有两个面板——一个包含一系列触发本章示例代码的按钮,另一个包含一个我用来显示消息的WinJS.UI.ListView控件。WinJS.Binding.Template控件用于显示ListView中的信息。

清单 22-1 。用户文件项目的 default.html 文件

`

         UserFiles                                   ****      ` `     **
**         **
**     **
**


        
        
        
        
        
        
        
        
    


        <div id="list" data-win-control="WinJS.UI.ListView"
            data-win-options="{
                itemDataSource: ViewModel.State.messages.dataSource,
                itemTemplate: template,
                layout: {type:WinJS.UI.ListLayout}
           ** }">**
        

    

`

你可以看到我在css/default.css文件中定义的 CSS 来管理这些元素在清单 22-2 中的布局。

清单 22-2 。css/default.css 文件的内容

`body {
    display: -ms-flexbox;
    -ms-flex-direction: row;
    -ms-flex-align: center; -ms-flex-pack: center;
}

.container {
    height: 80%; margin: 10px; padding: 10px;
    border: medium solid white;
}

buttonsContainer button, .message {

display: block; font-size: 20pt;
    width: 100%; margin-top: 10px;
}

listContainer

list { height: 100%;}`

我在js/viewmodel.js文件中定义了一些基本的应用状态和一些实用函数,你可以在清单 22-3 中看到。这些支持显示在ListView控件中的信息。

清单 22-3 。示例应用的 viewmodel.js 文件

`(function () {

WinJS.Namespace.define("ViewModel", {
        State: WinJS.Binding.as({
            messages: new WinJS.Binding.List()
        })
    });

WinJS.Namespace.define("App", {
        writeMessage: function (msg) {
            ViewModel.State.messages.push({ message: msg });
        },
        clearMessages: function () {
            ViewModel.State.messages.length = 0;
        }
    });

App.writeMessage("Ready");
})();`

最后,你可以在清单 22-4 中看到js/default.js文件。在这里,我将通过响应default.html文件中按钮元素的click事件来添加示例代码,但目前该文件只包含基本的应用管道。

清单 22-4 。示例应用的初始 default.js 文件

`(function () {

var $ = WinJS.Utilities.query;
    var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var storage = Windows.Storage;
    var search = Windows.Storage.Search;

var imageFileNames = ["astor.jpg", "carnation.jpg", "daffodil.jpg",
        "lily.png","orchid.jpg", "peony.jpg", "primula.jpg", "rose.png", "snowdrop.jpg"];

app.onactivated = function (args) {
        if (args.detail.previousExecutionState !=
            activation.ApplicationExecutionState.suspended) {

args.setPromise(WinJS.UI.processAll().then(function() {
                $('#buttonsContainer > button').listen("click", function (e) {
                    App.clearMessages();
                    switch (e.target.id) {
                        // ...code for examples will go here...
                    }                 });
            }));
        }
    };
    app.start();
})();`

我定义了一个名为imageFileNames的数组,它包含了img/flowers文件夹中的文件名。在接下来的几节中,我将在switch语句中添加代码来响应各个按钮。如果你现在启动示例应用,你会看到如图图 22-2 所示的基本布局。

images

图 22-2。示例 app 的布局

执行基本的文件操作

在接下来的小节中,我将向您展示如何执行一些基本的文件操作。这将允许我演示使用Windows.Storage对象处理用户文件而不是应用文件的区别,并提供一些关于应用在处理文件系统时所受限制的细节。

images 提示我在本章中没有提到的一个基本文件操作是读取文件的内容。你可以在第二十一章(我演示了如何读取应用数据文件的内容)和第二十三章(我展示了如何在应用布局中使用文件内容)中看到这一点。

复制文件

在这一节中,我将向您展示如何将应用包中包含的花卉图像文件复制到用户的My Pictures文件夹中。您可以在清单 22-5 的中看到我对default.js文件中的switch语句所做的补充。在 Windows 应用中处理文件的代码相当冗长,所以我将只展示我对每个示例所做的修改。

images 注意除非我另外明确说明,否则我在本章中描述的新对象都在Windows.Storage名称空间中,在本例中我将其别名为storage

清单 22-5 。增加从应用包复制文件到文件系统的支持

... switch (e.target.id) {     **case 'copyFiles':**         **storage.KnownFolders.picturesLibrary.createFolderAsync("flowers",**             **storage.CreationCollisionOption.replaceExisting)**         **.then(function(folder) {**             **imageFileNames.forEach(function (filename) {**                 **storage.StorageFile.getFileFromApplicationUriAsync(**                     **Windows.Foundation.Uri("ms-appx:img/" + filename))**                 **.then(function (file) {**                     **file.copyAsync(folder).then(function () {**                         **App.writeMessage("Copied: " + filename);**                     **}, function (err) {**                         **App.writeMessage("Error: " + err);**                     **});**                 **});**             **});**         **});**         **break;** } ...

这比看起来要复杂得多,因为大多数与文件相关的方法都使用了Promise对象。我将一步一步地浏览这段代码,并解释发生了什么。一旦理解了基本结构,您会发现本节中的其余示例更容易解析。

定位文件夹

我想做的第一件事是找到我的目标文件夹。当涉及到使用文件系统时,Windows 应用被置于严格的限制之下,只有某些位置你可以在没有用户明确许可的情况下写入(这种许可是通过文件选择器来表达的,我在第二十三章中进行了演示)。在第二十三章之前,我不会向您介绍文件拾取器,这意味着我被限制在一个非常小的预定义位置集合中,可以通过KnownFolder对象访问这些位置。该对象定义了表 22-2 中所示的属性,这些属性与常见的 Windows 文件位置相关。

images 提示甚至访问由KnownFolder对象定义的位置集也受到限制。想要读写这些位置的应用必须在它们的清单中做一个声明,我将在本章后面演示。

images

在这个例子中,我将应用包中的文件复制到用户的Pictures文件夹中,如清单 22-6 所示。

清单 22-6 。使用用户的图片文件夹

... storage.**KnownFolders.picturesLibrary**.createFolderAsync("flowers",     storage.CreationCollisionOption.replaceExisting) ...

每个属性返回一个代表文件系统位置的StorageFolder对象。一旦我得到代表Pictures文件夹的StorageFolder,我就调用createFolderAsync方法来创建一个flowers文件夹。这将是我复制图像文件的位置。在第二十一章中,我向你展示了StorageFolder对象定义的创建或定位StorageFile对象的基本方法,但是StorageFolder也为其他文件夹定义了有用的方法。表 22-3 描述了这些方法中最有用的。

images

当使用createFoldersAsyncrenameAsync方法时,你可以使用一个由CreationCollisionOption对象定义的值来提供一个可选参数,我在表 22-4 中总结了这个值。

在示例中,我使用了replaceExisting选项,这将删除任何名为flowers的现有文件夹,并用一个新的空文件夹替换它。由createFolderAsync方法返回的Promisethen方法传递一个StorageFolder对象,该对象代表我感兴趣的flowers文件夹。

images 警告删除文件夹当然是破坏性的。在运行此示例应用之前,您应该确保您的Pictures文件夹中没有名为flowers的文件夹;否则你会丢失你的文件。

复制文件

现在我有了一个代表文件目标的StorageFolder对象,我可以开始复制它们了。我通过代表单个文件的StorageFile对象来实现。首先,我为我的应用包中的每个文件获取一个StorageFile,然后依次为它们调用copyAsync方法,如清单 22-7 所示。

清单 22-7 。将 app 包中的每个文件复制到目标文件夹

... switch (e.target.id) {     case 'copyFiles':         storage.KnownFolders.picturesLibrary.createFolderAsync("flowers",             storage.CreationCollisionOption.replaceExisting)         .then(function(folder) {             **imageFileNames.forEach(function (filename) {**                 **storage.StorageFile.getFileFromApplicationUriAsync(**                     **Windows.Foundation.Uri("ms-appx:img/" + filename))**                 **.then(function (file) {**                     **file.copyAsync(folder).then(function () {**                         **App.writeMessage("Copied: " + filename);**                     **}, function (err) {**                         **App.writeMessage("Error: " + err);**                     **});**                 **});**             **});**         });         break; } ...

这是一个很好的例子,说明当您使用许多异步方法来执行相关操作时,会产生冗长的代码。确定给定的Promise对象上运行的函数可能需要一段时间——尽管随着你习惯于编写 Windows 应用,这变得更加容易。

我在清单中突出显示的代码列举了我在imageFileNames数组中定义的每个文件名,并使用StorageFile.getFileFromApplicationUriAsync方法获得一个依次代表每个图像的StorageFile对象。这是我在《??》第二十一章中展示的技术,依靠Windows.Foundation.Uri物体。

StorageFile对象是对StorageFolder对象的补充,代表文件系统中的单个文件。一旦我有了一个StorageFile对象,我就可以调用copyAsync方法。这个方法的参数是StorageFolder对象,它代表文件应该被复制到的位置。我提供了我在上一节中创建的flowers文件夹。我已经在由copyAsync方法返回的Promise上使用了then方法来编写成功和错误消息,这些消息将显示在应用布局的ListView控件中。我在表 22-5 中总结了常用的StorageFile方法。

images

启用清单中的功能

如果您在此时运行示例应用并单击Copy Files按钮,您将会看到 Visual Studio 中显示的一条消息,如图 22-3 中的所示。

images

图 22-3。试图访问文件系统时显示错误

正如我前面提到的,应用对文件系统的访问非常有限。如果你想访问一个不是由用户通过文件选择器提供的位置(我会在第二十三章中解释),那么你必须使用由KnownFolders对象定义的一个位置。您还必须在应用清单中声明您的应用能够访问该位置。如果您不这样做,一旦您的应用尝试访问文件系统,您就会看到如图所示的错误。

要声明该功能,在 Visual Studio Solution Explorer窗口中双击package.appxmanifest文件打开清单,并导航到Capabilities选项卡。你会看到能力列表包括KnownFolders位置的项目,我在图 22-4 中突出显示了这些项目。如图所示,我已经检查了与KnownFolders.picturesLibrary位置相对应的Pictures Library能力。(Internet (Client)功能在您创建新的 Windows 应用项目时默认选中。)

images

图 22-4。允许访问应用清单中的位置

您必须为每个想要使用的位置启用该功能。这将为您的应用提供该位置的访问权限,并向您的应用的权限窗格(可通过 Settings Charm 获得)添加一个条目,如果您通过 Windows 应用商店分发您的应用,该条目将显示给用户。

images 提示您应该只检查您需要的功能。这个想法是为了给用户提供他们需要的信息,让他们在把你的应用安装到他们的设备上之前,对他们的信任程度做出明智的选择。实际上,用户在任何平台上都不会太关注这些信息,他们会下载并安装任何看起来有趣的东西。即使如此,要求您需要的最低能力也是一个好的做法。

通过添加响应按钮点击的代码并在清单中声明该功能,我已经可以将项目中的文件复制到设备文件系统中了。

为了测试这个例子,启动应用并点击Copy Files按钮。你会在布局中间面板的ListView控件中看到一系列消息,报告每个文件都被复制,如图图 22-5 所示。如果您打开文件浏览器并导航到您的Pictures库,您将看到一个包含样本图像的 flowers 文件夹。

images

图 22-5。将文件从 app 包复制到文件系统

在我继续之前,有几点需要注意。首先,我已经从应用包中复制了文件,因为这是我设置和分发示例的最简单的方法。虽然这在实际项目中可能是一项有用的技术,但是您当然可以在您的应用清单中声明的任何已知位置上执行文件操作(我将在第二十三章中向您展示如何获得用户许可来处理其他位置的文件)。

要注意的第二点是,由于我在使用createFolderAsync方法时指定的碰撞选项,单击Copy Files按钮将删除flowers文件夹,创建一个新文件夹,并再次复制文件。

确保复制顺序

如果您重复点击Copy Files按钮,您将会看到ListView控件中显示的消息以不同的顺序显示。你可以在图 22-6 中看到效果,这里我已经显示了三次点击按钮的消息顺序。

images

图 22-6。消息顺序的变化

在第九章的中,当我解释Promise对象如何工作时,我说过 JavaScript 维护一个要完成的工作队列,使用Promise s 通过将工作放在那个队列中进行后续处理来推迟工作——这就是setImmediate方法所做的。

然而,当您使用 Windows API(包括Windows.Storage)时,您的工作可以并行完成,因为 API 的这一部分中的对象是使用支持并行执行任务的语言实现的。JavaScript 隐藏了这方面的细节,它没有对并行任务的内置语言支持,但这确实意味着操作完成的顺序可以不同。

如果您不关心操作执行和完成的顺序,那么您不必采取任何特殊的操作。如果你真的在乎,那么你需要改变你调用异步方法的方式,这样你只需要在前一个操作完成时复制一个文件,如清单 22-8 所示。我添加了这段代码,以便在点击Copy Files (Ordered)按钮时执行。

清单 22-8 。强制按给定顺序执行复印操作

... switch (e.target.id) {     case 'copyFiles':         // *...statements removed for brevity...*         break;     **case 'copySeq':**         **var index = 0;**         **function copyFile(index, folder) {**             **return storage.StorageFile.getFileFromApplicationUriAsync(**                 **Windows.Foundation.Uri("ms-appx:img/" +**                    **imageFileNames[index]))**             **.then(function(file) {**                 **return file.copyAsync(folder).then(function () {**                     **App.writeMessage("Copied: " + imageFileNames[index]);**                 **}).then(function() {**                     **index++;**                     **if (index < imageFileNames.length) {**                         **return copyFile(index, folder);**                     **}**                 **})**             **});**         **}**         **storage.KnownFolders.picturesLibrary.createFolderAsync("flowers",**             **storage.CreationCollisionOption.replaceExisting)**         **.then(function (folder) {**             **copyFile(index, folder).then(function () {**                 **App.writeMessage("All files copied");**             **});**         **});**         **break;** } ...

在这个清单中,我使用我在第二十一章中描述的技术将复制操作链接在一起,其效果是按照在imageFileNames数组中定义的顺序复制图像。这是另一段看起来曲折的代码,但是基本模式——定义一个返回Promise并递归调用自身的函数——与《??》第九章中的链接示例是一样的。

只有在一致性非常重要的情况下,才应该强制异步操作的顺序,因为 Windows 能够并行执行多种操作,这种技术会强制序列化,因此会严重影响性能。

删除文件

既然您已经看到了几个文件操作示例,您会发现本节的其余部分更容易理解。如果你还没有理解使用异步方法处理文件的想法,你应该考虑重温一下第九章——如果你还没有很好地掌握Promise对象如何工作,你会发现例子越来越难理解。

在这一节中,我将演示如何删除文件。这很简单,但是我想强调这种方法的共性——一旦你理解了一个文件操作是如何工作的,你就可以很快地创建其他的文件操作。在清单 22-9 中,您可以看到我添加到default.js文件中的代码,以响应被点击的Delete Files按钮。

清单 22-9 。删除文件

... switch (e.target.id) {    // *...statements omitted for brevity...*    **case 'deleteFiles':**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**         **.then(function (folder) {**             **folder.getFilesAsync().then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **storageFile.deleteAsync(storage.StorageDeleteOption.default);**                         **App.writeMessage("Deleted: " + storageFile.name);**                     **});**                 **}**             **});**         **})**         **break;** } ...

在这个清单中,我使用了getFilesAsync方法来获取代表 flowers 目录中文件的一组StorageFile对象。如果数组是空的,那么我知道文件夹中没有文件,并在ListView控件中显示一条适当的消息。如果数组中有StorageFile对象,那么我使用forEach方法枚举数组内容并调用deleteAsync方法,这将删除文件。该方法的参数是来自StorageDeleteOption对象的一个值,它指定了执行的删除类型。我在表 22-6 中描述了这些值。

images

例如,我指定了default值,这意味着如果你启动应用并单击Delete Files按钮,你将能够在 Windows 回收站中找到已删除的文件。

images 提示您会注意到,在这个例子中,当我向ListView写消息时,我使用了StorageFile.name属性。StorageFile对象定义了许多属性,您可以用它们来获取关于文件的信息——我将在下一节更详细地解释这一点。

整理和过滤文件

在前一个例子中,我称为StorageFolder.getFilesAsync方法的StorageFile对象数组有两个定义特征。第一个是我得到了文件夹中所有文件的和第二个是数组中按字母顺序排列的对象,基于文件名。

在这一节中,我将向您展示如何执行文件查询,这允许您对检索的文件进行更多的选择,并以不同的方式对结果进行排序。本节中的新对象位于Windows.Storage.Search名称空间中,在示例中我将其别名为search

对文件进行排序和过滤有两个原因。第一个是应用中的通用工具——你可能只想对满足某种标准的文件执行操作,对从StorageFolder中获得的文件进行过滤和排序是一种很好的方式。对文件进行分类和过滤的另一个原因是,这些技术支撑了一些关键的接触点,使你能够将你的应用集成到更广泛的 Windows 平台中,并向你的用户呈现一个处理文件的一致模型——这是我在第二十三章中再次提到的主题。

整理文件

在 Windows 应用中,对文件的选择顺序进行排序是文件查询的一种简单形式。在后面的小节中,我将向您展示一些功能,这些功能更像是与单词 query 相关联的东西,但是从排序开始,让我介绍所需的对象,并解释它们是如何组合在一起的。首先,清单 22-10 显示了我添加到default.js文件中的内容,以便在点击Sort Files按钮时做出响应。(如果您最近删除了文件,您需要再次点击Copy Files按钮,以便在flowers文件夹中有要处理的文件。)

清单 22-10 。使用文件查询对文件进行分类

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'sortFiles':**         **var options = new search.QueryOptions();**         **options.sortOrder.clear();**         **options.sortOrder.push({**             **ascendingOrder: false,**             **propertyName: "System.ItemNameDisplay"**         **});**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**         **.then(function (folder) {**             **folder.createFileQueryWithOptions(options).getFilesAsync()**             **.then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **App.writeMessage("Found: " + storageFile.name);**                     **});**                 **}**             **});**         **});**         **break;** } ...

您使用一个QueryOptions对象控制文件的选择和排序方式,创建其中一个是我在清单中采取的第一个动作。我创建了这个没有任何构造函数参数的对象,它使用默认设置。

属性返回一个对象数组,这些对象按顺序应用来对文件夹中的文件进行排序。每个对象都有一个propertyName属性和一个ascendingOrder属性,前者指定执行排序的文件的属性,后者指定排序的方向。

在示例中,您可以看到我已经通过调用QueryOptions.sortOrder.clear方法开始了。这是因为默认的QueryOptions对象是用一个sortOrder对象创建的,该对象根据文件名对文件进行升序排序。如果不调用clear方法,您定义的任何自定义排序都将仅在默认排序之后应用。

images 提示sortOrder属性是只读的。这意味着您需要通过更改属性返回的数组的内容来创建排序指令,而不是分配一个新的数组。

清除默认排序后,我添加自己的排序,如下所示:

... options.sortOrder.push({     ascendingOrder: false,     propertyName: "**System.ItemNameDisplay**" }); ...

该语句向sortOrder数组添加了一个新对象,该对象按照名称以逆序对文件进行排序,这是默认排序的一个简单变体。propertyName属性值System.ItemNameDisplay是 Windows 定义的一组广泛的文件属性之一。这样的房产有 125 个,太多了,我无法在此一一列举。相反,我在表 22-7 中列出了常用的。对于列表中的每一个条目,我都详细描述了当示例应用中的snowdrop.jpg文件被复制到Pictures库中并且拥有完整的路径C:\Users\adam\Pictures\flowers\snowdrop.jpg时,属性会显示什么。

除了 125 个基本属性(包括表中的属性)之外,还有数百个更具体的属性。例如,您可以在音频文件中找到一整套的System.Music属性,在视频文件中可以找到System.Media属性。你可以在[msdn.microsoft.com/en-us/library/windows/desktop/ff521735(v=vs.85).aspx](http://msdn.microsoft.com/en-us/library/windows/desktop/ff521735(v=vs.85).aspx)获得完整名单的详细信息。

images 提示这些是字符串值,尽管它们看起来像是对System名称空间中对象的引用。在将这些值分配给propertyName属性时,必须用引号将它们括起来。

images

一旦我定义并配置了我的QueryOptions对象,我就调用我对其内容感兴趣的StorageFolder对象的getFilesAsync方法,并返回一个由按照我指定的顺序排序的StorageFile对象组成的数组。

images 提示应用排序时,使用有效的文件系统属性名很重要。如果您提供的名称无效,应用将被终止,不会发出警告。

在示例中,我枚举了排序数组的内容,并显示了每个文件的名称。为了得到这个名字,我使用了一个由StorageFile对象定义的描述性属性,我在表 22-8 中总结了这个属性。我再次展示了每个属性为snowdrop.jpg文件返回的值。

images

其中一些属性提供了用户友好的值,您可以直接将其包含在应用布局中,而其他属性则更具技术性和细节性,旨在用于您的代码中。

文件系统属性和通过StorageFile属性可用的属性之间有很好的映射,但是并不是所有 125 个文件系统属性都可用。如果你想得到一个特定的值,那么你可以使用StorageFile.properties.retrievePropertiesAsync方法,如清单 22-11 所示。

清单 22-11 。从 StorageFile 对象获取文件系统属性值

... file.properties.retrievePropertiesAsync(["System.ItemType"]).then(function (props) {      var value = props["System.ItemType"]; }); ...

我没有将这种技术结合到例子中,因为StorageFile属性足以满足我的需要。如果你启动 app,点击Sort Files按钮,你会看到文件按名字降序排列(即按字母顺序倒序排列),如图图 22-7 所示。

images

图 22-7。列出文件时应用自定义排序顺序

过滤文件

除了排序,您还可以使用QueryOptions对象来过滤从getFilesAsync方法获得的StorageFile对象。QueryOptions对象定义了许多支持过滤的属性,我已经在表 22-9 中描述过了。

images

这些过滤属性分为两类——基本AQS——我将在接下来的章节中解释这两种属性。三个基本的过滤属性是fileTypeFilterfolderDepthindexOption。您可以在清单 22-12 中看到这些正在使用的属性,它列出了我添加到default.js文件中的代码,以响应被点击的Filter (Basic)按钮。

清单 22-12 。执行基本过滤

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'filterBasic':**         **var options = new search.QueryOptions();**         **options.fileTypeFilter.push(".doc", ".jpg", ".pdf");**         **options.folderDepth = search.FolderDepth.shallow;**         **options.indexerOption = search.IndexerOption.useIndexerWhenAvailable;**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**         **.then(function (folder) {**             **folder.createFileQueryWithOptions(options).getFilesAsync()**             **.then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **App.writeMessage("Found: " + storageFile.name);**                     **});**                 **}**             **});**         **});**         **break;** } ...

这个例子在结构上类似于我之前展示的排序例子,除了我使用了基本的过滤属性。当然,您可以将排序和过滤属性结合起来——我将它们分开显示,只是因为这样便于解释属性值。

过滤文件类型

在清单中,我从QueryOptions.fileTypeFilter属性开始,它过滤掉没有您指定的文件前缀的任何文件。这是另一个需要修改从属性获取的数组内容的实例,而不是分配一个新的数组。

在这个例子中,我使用了由 JavaScript 数组定义的push方法来指定我想要的文件扩展名为.doc.jpg.pdf的文件。我的示例应用只有.jpg.png文件,因此效果是从查询中排除了.png文件,但是我已经指定了多个值来向您展示这是如何做到的。该属性的默认值是一个空数组,表示不应根据文件扩展名排除任何文件。

images 提示文件扩展名以前导句点指定,即。.pdf而不是pdf

设置搜索深度

使用由FolderDepth对象定义的值之一来设置QueryOptions.folderDepth属性,我已经在表 22-10 中描述过了。

我在清单中选择了shallow值,它将我通过getFilesAsync方法接收的文件限制在flowers目录中(当然,虽然没有子目录可供我操作我的示例文件)。使用deep值时要小心——您最终可能会查询大量文件,这可能是一个耗时且消耗资源的操作。

使用以前的索引数据

Windows 为用户的内容编制索引以加快搜索速度,从而避免了通过文件系统来获取文件属性和内容的详细信息。问题是,索引是作为后台任务完成的,对文件的最新更改可能不会反映在索引中—本质上,索引数据是在更快的搜索和准确性之间的权衡。您可以使用QueryOptions.indexerOption属性指定查询是否使用索引数据,该属性设置为来自IndexerOption对象的值。我已经在表 22-11 中描述了可用值。

images

我在示例中使用了useIndexerWhenAvailable值。当您对文件的内容感兴趣时,索引数据的影响最大,访问文件系统意味着依次搜索每个文件。使用以前的索引数据可以大大加快这种搜索的速度。

查看过滤结果

我为各种筛选器属性选择的值的效果是,我的查询选择了正在搜索的文件夹中的 JPG、DOC 和 PNG 文件,并且 Windows 应该使用缓存的文件数据(如果可用)。你可以在图 22-8 中看到结果。

images

图 22-8。文件过滤结果

使用高级查询语法属性

高级查询语法 (AQS)允许表达复杂的查询,超出了使用其他QueryOptions属性所能管理的范围。有两个属性可用于指定 AQS 查询:applicationSearchFilter属性用于您在应用中定义的 AQS 查询,而userSearchFilter属性用于用户定义的查询。这种分离不是强制性的,无论如何,当查询文件系统时,两个查询字符串会自动合并。

清单 22-13 显示了添加到default.js文件中的switch语句,我添加它是为了在点击Filter (AQS)按钮时做出响应。这段代码执行了一个相当简单的 AQS 查询。

清单 22-13 。执行 AQS 查询

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'filterAQS':**         **var options = new search.QueryOptions();**         **options.folderDepth = search.FolderDepth.deep;**         **options.applicationSearchFilter**             **= 'System.ItemType:=".jpg" AND System.Size:>300kb AND folder:flowers';**         **storage.KnownFolders.picturesLibrary.createFileQueryWithOptions(options)**             **.getFilesAsync().then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **App.writeMessage("Found: " + storageFile.name);**                 **});**             **}**         **});**         **break;** } ...

对于这个例子,我在代表Pictures库的StorageFolder对象上调用了getFilesAsync方法。这允许我将flowers文件夹指定为 AQS 查询的一部分,如下所示:

System.ItemType:=".jpg" AND System.Size:>300kb AND folder:flowers

这是一个典型的 AQS 查询,包含两个搜索词和一个约束。搜索词基于我前面描述的文件系统属性。我正在查询其System.ItemType属性为.jpg并且其System.Size属性大于300kb的文件。条款和约束与关键字AND组合在一起,该关键字必须总是大写(您也可以在查询中使用ORNOT)。

请注意,每个搜索属性后跟一个冒号,然后是比较符号,如下所示:

System.ItemType:=".jpg" AND System.Size:>300kb AND folder:flowers

约束使用folder关键字表示,并将查询限制在路径包含名为flowers的文件夹的文件上。(这匹配路径中任何名为flowers的文件夹,而不仅仅是直接的父文件夹)。

如果运行应用并点击Filter (AQS)按钮,AQS 查询将用于过滤Pictures库中的文件,产生如图图 22-9 所示的结果。

images

图 22-9。使用 AQS 过滤文件

AQS 也可以在 Windows 搜索窗格中使用。我想我从来没有见过用户使用 AQS,但它可以在应用开发过程中测试你的查询。图 22-10 显示了先前清单 22 中的 AQS 查询——在搜索窗格中使用。如果您的文件系统包含 Visual Studio 项目中图像的额外副本,您可能会看到更多结果(我清理了我的文件以获得此结果)。

images

图 22-10。在 Windows 搜索窗格中使用 AQS 查询查找文件

在我的例子中,我使用了基于文件属性和路径的 AQS 查询,但是您也可以使用 AQS 来搜索文件内容,只需在查询中包含一个带引号的短语。因此,例如,如果您想要查找包含短语“我喜欢苹果”的所有 PDF 文件,您的查询应该是:

System.ItemType:=".pdf" AND "I like apples"

AQS 可以用来创建极其精确的查询,在[msdn.microsoft.com/en-us/library/aa965711(v=VS.85).aspx](http://msdn.microsoft.com/en-us/library/aa965711(v=VS.85).aspx)可以找到一个了解 AQS 更多信息的好起点。

使用便捷查询

CommonFileQuery对象定义了六个常用的查询,您可以使用它们来创建预配置的QueryOptions对象。清单 22-14 显示了我添加到default.js文件中的switch语句中的内容,以响应点击Common Query按钮。

清单 22-14 。使用 CommonFileQuery 对象

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'commonQuery':**         **var options = new search.QueryOptions(**             **search.CommonFileQuery.orderByName, [".jpg", ".png"]);**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**                 **.then(function (folder) {**                     **folder.createFileQueryWithOptions(options).getFilesAsync()**                     **.then(function (files) {**                         **if (files.length == 0) {**                             **App.writeMessage("No files found");**                         **} else {**                             **files.forEach(function (storageFile) {**                                 **App.writeMessage("Found: " + storageFile.name);**                             **});**                         **}**                     **});**                 **});**         **break;** } ...

为了使用方便的查询,可以将一个值从CommonFileQuery对象作为构造函数参数传递给QueryOptions对象。您还必须提供一个文件扩展名数组,用于设置fileTypeFilter属性。

在这个例子中,我使用了CommonFileQuery.orderByName属性,该属性将QueryOptions对象配置为包含StorageFolder中的所有文件,并按照名称的字母顺序对它们进行排序。我已经过滤了文件,只接受了jpgpng文件扩展名(在示例文件夹中只有这种类型的文件,但是您已经明白了)。我已经在表 22-12 中描述了所有六个CommonFileQuery值。其中一些查询只有在应用于音乐文件时才有意义,因为它们依赖于System.Music文件属性。

images

使用虚拟文件夹

对象过着双重生活。在本章的前面,我向您展示了可以用来在文件系统上定位或创建新文件夹的StorageFolder方法。这是文件夹的传统用法,也是你对一个叫做StorageFolder的对象的期望。

然而,StorageFolder对象也可以在文件查询的结果中使用,其中它们表示用于按照共同特征或排序顺序将文件分组在一起的虚拟文件夹。清单 22-15 展示了我是如何使用这个特性将我的示例图像文件按照年份分组到虚拟文件夹中,以响应点击Group (Type)按钮。

清单 22-15 。将文件分组到虚拟文件夹中

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'groupType':**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**             **.then(function (flowersFolder) {**                 **flowersFolder.getFoldersAsync(search.CommonFolderQuery.groupByType)**                 **.then(function (typeFolders) {**                     **var index = 0;**                     **(function describeFolders() {**                         **App.writeMessage("Folder: " + typeFolders[index].displayName);**                         **typeFolders[index].getFilesAsync().then(function (files) {**                             **files.forEach(function (file) {**                                 **App.writeMessage("--File: " + file.name);**                             **});**                             **if (index < typeFolders.length -1) {**                                 **index++;**                                 **describeFolders();**                             **}**                         **});**                     **})();**                 **});**         **});**         **break;** } ...

清单中的代码有点复杂,因为我想以特定的顺序显示输出,但是我执行的操作是异步的。解释发生了什么的最简单的方法是将代码分成两部分。建议你运行 app,点击Group (Type)按钮。您将看到图 22-11 中所示的结果。

images 注意如果您已经删除了示例文件,您需要先点击Copy Files按钮。

images

图 22-11。根据类型对示例文件进行分组

将文件分组

将文件分组到虚拟文件夹的过程非常简单,你可以在清单 22-16 中看到重要的陈述。这是完整清单中语句的子集,这样我就可以专注于分组特性。

清单 22-16 。分组文件

... storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")     .then(function (flowersFolder) {         **flowersFolder.getFoldersAsync(search.CommonFolderQuery.groupByType)**         .then(function (typeFolders) {            // …code removed...         }); }); ...

我通过对由KnownFolders.picturesLibrary属性返回的StorageFolder对象调用getFolderAsync方法来获得flowers文件夹,就像我在前面的例子中所做的一样。

这给了我一个代表flowers文件夹的StorageFolder对象。为了对文件夹包含的文件进行分组,我调用了getFoldersAsync方法并传入了由CommonFolderQuery对象定义的一个值。CommonFolderQuery对象定义了许多允许你以不同方式对文件进行分组的值,如表 22-13 所总结的。

images

在这个例子中,我使用了groupByType值,顾名思义,它根据文件扩展名对文件进行分组。用一个CommonFolderQuery值调用getFoldersAsync方法的结果是一个Promise对象,它在完成时将一个StorageFolder对象数组传递给 then 函数。

每个StorageFolder对象代表为组属性找到的一个值。在这个例子中,我按照类型对文件进行了分组,有两种类型的示例文件——JPG 文件和 PNG 文件。我将收到一个包含所有 JPG 文件的StorageFolder和一个包含所有 PNG 文件的StorageFolder

处理分组后的文件

一旦我获得了虚拟StorageFolder对象的数组,我需要在应用布局中显示结果。这是示例代码的第二部分,我已经在清单 22-17 中展示过了。

清单 22-17 。显示每个虚拟文件夹的内容

... .then(function (typeFolders) {     **var index = 0;**     **(function describeFolders() {**         **App.writeMessage("Folder: " + typeFolders[index].displayName);**         **typeFolders[index].getFilesAsync().then(function (files) {**             **files.forEach(function (file) {**                 **App.writeMessage("--File: " + file.name);**             **});**             **if (index < typeFolders.length -1) {**                 **index++;**                 **describeFolders();**             **}**         **});**     **})();** }); ...

您使用getFilesAsync方法获取每个虚拟文件夹中的文件。我已经调用了不带参数的方法,但是您可以使用QueryOptions对象过滤或排序文件,如本章前面所述。

这段代码解决的问题是,getFilesAsync方法返回一个提供文件数组的Promise,但是我需要确保在移动到下一个虚拟文件夹之前,我已经为一个虚拟文件夹中的所有文件调用了App.writeMessage方法。

为了解决这个问题,我使用了我在第二十一章中介绍的技术的一个小变体,通过定义一个自我执行的函数,当它遍历虚拟文件夹时调用它自己。这段代码的结果是,我对虚拟文件夹及其包含的文件的处理进行了序列化,这样我就可以确保描述性消息以正确的顺序显示,从而产生您在本节开始的图 22-11 中看到的结果。

监控文件夹中的新文件

我要展示的最后一个功能是监视文件夹中的新文件。为了演示这一点,我已经将清单 22-18 中的代码添加到了default.js文件中。与本章中的其他例子不同,这个添加的代码不是响应按钮点击而触发的,而是作为onactivated函数的一部分执行的。

清单 22-18 。监控文件

... app.onactivated = function (args) {     if (args.detail.previousExecutionState         != activation.ApplicationExecutionState.suspended) {
`        args.setPromise(WinJS.UI.processAll().then(function () {

storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")
            .then(function (folder) {
                var query = folder.createFileQuery();
                query.addEventListener("contentschanged", function (e) {
                    App.writeMessage("New files!");
                });

setTimeout(function () {
                    query.getFilesAsync();
                }, 1000);
            });

$('#buttonsContainer > button').listen("click", function (e) {
                App.clearMessages();
                switch (e.target.id) {
                    // ... statements removed for brevity
                }
            });
        }));
    }
};
...`

监控一个文件夹的起点是获得一个代表它的StorageFolder对象。我将再次监控flowers文件夹,所以我首先对由KnownFolders.picturesLibrary属性返回的StorageFolder调用getFolderAsync

下一步是创建一个StorageFileQueryResult对象,通过调用您想要监控的StorageFolder上的createFileQuery方法可以获得这个对象。

当一个新文件被添加到被监控的目录中时,StorageFileQueryResult对象发出contentschanged事件。在这个例子中,我提供了一个函数来处理这个事件,在应用布局的ListView控件中向用户显示一条消息。

最后一步是调用StorageFileQueryResult.getFilesAsync方法,开始监控文件夹。我使用setTimeout函数在一秒钟的延迟后调用这个方法——这是因为contentschanged功能不是特别可靠,我发现这是增加工作几率的最好方法(尽管它仍然不时地失败,并且我没有在应该得到通知的时候得到通知)。

要在示例中测试该功能,请启动应用,单击Delete Files按钮从 flowers 文件夹中删除文件,然后单击Copy Files将新文件添加到受监控的文件夹中。几秒钟后你会在应用布局中看到一条消息,就像图 22-12 中的一样。

images

图 22-12。监控文件夹中的新文件

这种技术有一些严重的局限性。首先,contentschanged事件只有在新文件被添加到文件夹中时才会被触发。当文件被删除或修改时,您不会收到该事件。其次,在添加新文件和触发事件之间可能会有几秒钟的延迟。第三,这不是一个健壮的特性,并且contentschanged事件并不总是在它应该触发的时候被触发,并且对于一个单独的改变经常被触发多次。但如果你能忍受这些问题,那么监控文件夹可能是一种有用的方法,可以确保你的应用让用户了解最新的内容。

总结

在这一章中,我向您展示了如何使用文件和文件夹,从复制和删除文件等基本功能开始。然后,我向您展示了如何对文件进行排序、过滤和查询,如何创建虚拟文件夹来将相关文件分组在一起,以及如何监视文件夹中的新文件。

所有这些特性和技术的共同点是它们都是在你的应用内部实现的,用户看不到。但由于我们处理的是用户的文件和内容,所以我们以一种清晰、明显、与其他应用和 Windows 本身一致的方式来表达文件和文件夹处理功能是很重要的。在下一章中,我将向您展示如何做到这一点,使用 Windows 为处理设备文件系统提供的大量集成功能。

二十三、集成文件服务

在这一章中,我将基于第二十二章中的技术向你展示如何向用户公开文件操作。我将向您展示如何使用文件选择器来请求用户选择文件和文件夹,如何缓存位置以便您的应用保留对它们的访问,以及如何使用文件系统作为 WinJS 数据驱动 UI 控件(如FlipViewListView)的数据源。表 23-1 对本章进行了总结。

Images

创建示例应用

对于这一章,我已经创建了一个名为FileServices的示例应用,它遵循单页内容模型并使用WinJS.Navigation名称空间,由应用导航栏上的按钮驱动。本章中的例子不容易放入一个布局中,所以这种方法将让我在同一个应用中向你展示多个内容页面。您可以在清单 23-1 的中看到我对default.html文件所做的修改。

清单 23-1 。来自文件服务项目的 default.html 文件

`

    ` `    FileServices                                    **    
** **        
** **            

Select a page from the NavBar

** **        
** **    
**

**    <div id="navbar" data-win-control="WinJS.UI.AppBar"**
**        data-win-options="{placement:'top'}">**
**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'displayFile', label:'Display File',**
**                icon:'\u0031', section:'selection'}">**
**        **
**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'pickers', label:'Pickers',**
**                icon:'\u0032', section:'selection'}">**
**        **
**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'access', label:'Access Cache',**
**                icon:'\u0033', section:'selection'}">**
**        **
**        <button data-win-control="WinJS.UI.AppBarCommand"**
**            data-win-options="{id:'dataSources', label:'Data Sources',**
**                icon:'\u0034', section:'selection'}">**
**        **
**    **

`

idcontentTargetdiv元素将成为其他页面内容的目标,这些内容将被导入到文档中以响应单击导航条按钮。你可以在default.html文件中看到 NavBar 命令,我将在本章中添加它们相关的文件。初始内容是一条提示用户去导航条的消息,如图图 23-1 所示。

images

图 23-1。示例应用的布局

您可以在清单 23-2 的中看到/css/default.css文件的内容。该文件包含示例应用中使用的常见样式,我将在示例的内容页面中添加特定于元素的内容。

清单 23-2 。css/default.css 文件的内容

`body {display: -ms-flexbox; -ms-flex-direction: column;
    -ms-flex-align: center; -ms-flex-pack: center;    }

contentTarget { display: -ms-flexbox; -ms-flex-direction: row;

-ms-flex-align: center; -ms-flex-pack: center; text-align: center;}

.container {border: medium solid white; margin: 10px; padding: 10px;}
.container button {font-size: 25pt; margin: 10px; display: block; width: 300px;}

*.imgElem {height: 500px;}
*.imgTitle { color: white; background-color: black;font-size: 30pt; padding-left: 10px;}`

CSS 中没有新的技术或特性——样式和属性仅用于展示示例中的元素。对于这个项目来说,/js/default.js文件的内容非常简单,只包含设置和导航代码——所有有趣的特性都在我在本章每一节添加的单独文件中。您可以在清单 23-3 中看到default.js文件的内容。

清单 23-3 。default.js 文件的内容

(function () {
`    var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;

WinJS.Navigation.addEventListener("navigating", function (e) {
        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {
            WinJS.Utilities.empty(contentTarget);
            WinJS.UI.Pages.render(e.detail.location,
                contentTarget, WinJS.Navigation.state)
                .then(function () {
                    return WinJS.UI.Animation.enterPage(contentTarget.children)
                });
        });
    });

app.onactivated = function (args) {
        if (args.detail.previousExecutionState
            != activation.ApplicationExecutionState.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function() {
                navbar.addEventListener("click", function (e) {
                    var navTarget = "pages/" + e.target.winControl.id + ".html";
                    WinJS.Navigation.navigate(navTarget);
                    navbar.winControl.hide();
                });
            }));
        }
    };

app.start();
})();`

声明文件定位能力

让基本示例工作的最后一步是启用对Pictures库的访问。为此,我打开package.appxmanifest文件,切换到Capabilities选项卡并检查Pictures Library能力,如图图 23-2 所示。

images

图 23-2。启用对图片库的访问

我将在本章的后面返回到清单来声明额外的功能,但是对Pictures库的访问已经足够开始了。

显示图像文件

对于本章的第一个例子,我将从一些非常简单的事情开始:在应用布局中显示一个图像文件。这是一个很好的切入点,因为它让我在第二十二章中向您展示的文件系统功能的基础上进行构建,并演示它们如何与 JavaScript Metro 应用中的 HTML 布局相关联。

Images 注意除非我另有说明,否则我在本章中使用的对象都在Windows.Storage名称空间中。

对于这一节,我已经在项目中添加了一个名为pages/displayFile.html的文件。这是一个包含标记、CSS 和 JavaScript 的一体化文件,你可以在清单 23-4 中看到内容。

清单 23-4 。displayFile.html 文件的内容

`

               ` `    
                 
    
`

这个内容的简单布局由一个用于显示文件的img元素和一个用于显示文件名的div元素组成。如果你启动应用,通过点击Display File命令使用导航条导航到这个文件,你会看到类似于图 23-3 的东西。

Images 提示如果您的图片库中没有文件,您会看到一条No Files Found消息。只需重启应用,点击Copy Sample Files按钮,然后选择Display File导航条命令来补救这种情况。

images

图 23-3。显示在图片库中找到的第一个文件

我说您将看到类似于下图的内容,因为本例中的代码对Pictures库中的文件进行了深度查询,并显示了第一个文件的内容。(我在第二十二章中描述了文件查询。)对于我的机器,找到的文件是我在前一章中使用的一组示例图像中的astor.jpg文件,但是它可能是您机器上的一个完全不同的图像。

Images 提示如果Pictures库中的第一个文件不是浏览器可以显示的图像,您会看到一条Not an image file消息。Windows 并没有对Pictures库强制执行只显示图像的策略,所以我在显示文件之前检查了文件类型。

displayFile.html文件中的script元素代码应该很熟悉。我在由KnownFolders.picturesLibrary属性返回的StorageFolder上调用getFilesAsync方法,指定 CommonFileQuery.orderByName值来排序文件并设置文件夹深度。该示例的关键部分是以下语句:

imgElem.src = **URL.createObjectURL**(files[0]);

getFilesAsync方法返回的Promise产生一个代表图片库中文件的StorageFile对象数组。问题是我在布局中使用的img元素不知道如何显示StorageFile对象的内容,因为它们是一个 Windows 概念,不是 HTML 的一部分。

URL.createObjectURL方法通过充当文件和我的 HTML 标记之间的桥梁解决了我的问题。URL对象是 W3C 文件 API 规范的一部分,它是 HTML5 的附属标准草案。它是由浏览器实现的,这就是为什么我能够在不使用名称空间的情况下调用该方法。

createObjectURL方法获取一个文件并返回一个 URL,该 URL 可用于在 HTML 元素中访问该文件,这就是为什么我将示例中的createObjectURL调用的结果赋给了我的img元素的src属性。如果您使用 Visual Studio DOM Explorer窗口查看示例应用中的 HTML,您会看到img元素最终看起来像这样:

<img id="imgElem" src="blob:9A3CB2C5-9526-49E3-A8C3-6F0FFC3DCD66"></img>

Images 提示createObjectURL方法返回的 URL 特定于创建它的系统——您不能与其他设备共享该 URL。

使用文件和文件夹选择器

应用只能自由访问由KnownFolders对象定义的文件系统位置,这些位置在应用清单的功能部分中声明。为了访问其他位置,您需要用户的明确许可,这是使用文件和文件夹选择器获得的。使用选择器,您可以要求用户指定要打开或保存的文件,或者选择一个文件夹。

为了演示拣选器的使用,我在 Visual Studio 项目中添加了一个名为pages/pickers.html的新文件,其内容如清单 23-5 所示。当点击导航栏中的Pickers命令时,会显示该内容。首先,这个文件只包含演示其中一个选择器的代码,稍后我会为其他选择器添加代码。

清单 23-5 。pages/pickers.html 文件的初始内容

`

              

    
                               
    
                 
Select an image file
    
`

这个例子包含三个按钮,我将用它们来激活拣选器。还有一个imgdiv元素,我将使用它们来显示选定的文件。此文件的代码演示了文件打开选择器的使用,它允许用户选择一个或多个文件。因为这是我第一次向您展示一个选择器,所以我将介绍它是如何呈现给用户的,然后解释代码是如何工作的。

举个例子

当您第一次选择导航栏上的命令时,您会看到一个非常基本的布局。img元素被隐藏,仅显示按钮和一条消息,如图 23-4 中的所示。

images

图 23-4。pickers.html 文件的初始布局

点击Open File Picker按钮显示拾取器,如图图 23-5 所示。选取器填充屏幕,允许用户在文件系统中导航,并显示当前文件夹中文件的缩略图。

images

图 23-5。文件打开选择器

导航到图中所示的flowers文件夹,选择其中一幅图像(该图像将被检查)。激活Open按钮—点击它选择文件。文件拾取器消失,选中的图像显示在应用布局中,如图图 23-6 所示。

images

图 23-6。显示用文件拾取器选择的图像

理解代码

现在您已经看到了文件选择器是如何呈现给用户的,我将解释示例中的代码是如何创建您所看到的行为的。当您需要用户选择一个或多个文件时,您可以使用位于Windows.Storage.Pickers名称空间中的FileOpenPicker对象。要使用选取器,您需要创建对象的新实例,然后为它定义的属性设置值。您可以在表 23-1 中看到FileOpenPicker属性列表。

Images

我已经重复了创建和配置 23-清单 23-6 中的FileOpenPicker的语句。您可以看到我已经将pngjpg扩展添加到了fileTypeFilter数组中,并且我没有为commitTextButton属性设置值,而是依赖于默认值Open

清单 23-6 。创建和配置拾取器

... var openPicker = Windows.Storage.Pickers.FileOpenPicker(); openPicker.fileTypeFilter.push(".png", ".jpg"); openPicker.suggestedStartLocation = pickers.PickerLocationId.picturesLibrary; openPicker.viewMode = pickers.PickerViewMode.thumbnail; ...

您使用suggestedStartLocation属性来指示应该在选择器中显示的初始文件夹。这只是一个建议,选取器可能会显示另一个值-如果之前使用过选取器并且它记住了最后一个位置,或者因为您指定的位置不可用,则可能会发生这种情况。你从Windows.Storage.Pickers.PickerLocationId对象中设置初始位置的值,我已经在表 23-2 中列出。在这个例子中,我使用了picturesLibrary值,这样提货人最初打开了Pictures位置。

Images

使用来自Windows.Storage.Pickers.PickerViewMode对象的值设置viewMode属性,它允许您指定选取器如何显示文件和文件夹。有两个定义的值,我已经在表 23-3 中给出。

Images

我在示例中使用了thumbnail值,这在您希望处理图像文件时非常理想,我的示例应用就是这样。用户不能在选取器中更改视图,因此您必须确保为您希望处理的内容选取一个合理的值。

挑选文件

FileOpenPicker对象定义了两个方法,当您准备好显示选择器并让用户做出选择时,您可以调用这两个方法。这些方法在表 23-4 中描述。

Images

在这个例子中,我使用了pickSingleFileAsync方法,它允许用户选择单个文件。该方法返回一个Promise,当用户做出选择时,它被满足,并通过then方法产生一个StorageFile对象。(提醒一下,你可以在第九章中阅读所有关于Promise对象和then方法的内容。)如果用户没有选择就取消了选取器,那么传递给then方法函数的对象将是null。在清单 23-7 中,我重复了示例中显示选取器和处理用户选择的代码。

清单 23-7 。显示选取器并处理用户的选择

... openPicker.**pickSingleFileAsync()**.then(function (**pickedFile**) {     if (pickedFile != null) {         loadedFile = pickedFile;         pickerImgElem.style.display = "block";         **pickerImgElem.src = URL.createObjectURL(pickedFile);**         pickerTitleElem.innerText = pickedFile.displayName;         save.disabled = false;     } else {         pickerImgElem.style.display = "none";         pickerTitleElem.innerText = "No file selected";     } }); ...

为了显示选中的文件,我使用了URL.createObjectURL方法,并将结果赋给布局中img元素的src属性。使用pickMultipleFilesAsync方法是类似的,除了当用户做出选择时,then方法被传递一个StorageFile对象的数组。

Images 提示在这个例子中,我限制了用户可以选择的文件类型,这意味着我可以安全地在一个img元素中显示文件的内容。在这一章的后面,我将向你展示如何处理不是图像的文件。

使用文件保存选择器

现在,您已经看到了其中一个拣选器是如何工作的,我可以介绍其他的拣选器,而不必进入相同的细节层次。清单 23-8 显示了我添加到 pickers.html 文件中的内容,当点击Save File Picker按钮时,它会做出响应。这段代码使用了Windows.Storage.Pickers.FileSavePicker对象,它允许用户选择一个位置并保存文件。

清单 23-8 。用文件保存选择器保存文件

`...

**

...`

要测试这段代码,请启动应用,单击Open File Picker按钮,然后选择一个图像文件。当显示图像文件时,Save File Picker按钮将被激活,允许您将加载的图像文件保存到新位置。当您点击Save File Picker按钮时,将显示拾取器,如图图 23-7 所示。

images

图 23-7。用拾取器保存文件

除了用户能够为将要保存的文件指定名称和类型之外,保存选取器的外观类似于打开选取器。我已经将选取器的初始位置设置为Documents库,这就是为什么会列出这么多不同的文件夹。您可以通过创建一个新的FileSavePicker对象来创建一个新的选取器,并通过该对象的属性来配置它。我已经在表 23-5 中列出了房产,其中部分房产与FileOpenPicker共有。

Images

在示例中,我使用了属性来约束用户的选择,以便他们可以选择文件的位置和名称,但只能选择用打开的选择器加载的文件类型。这是因为我不想进入文件类型转换的世界,而是想演示一下选择器是如何工作的。

挑选文件

当您准备好向用户显示选取器时,调用pickSaveFileAsync方法。这个方法返回一个Promise,它将一个代表用户选择的StorageFile对象传递给then方法函数。如果用户点击Cancel按钮,则null被传递到then功能。

选取器只向用户请求一个位置——应用有责任对它做些什么。例如,我将先前加载的文件复制到所选的位置。我已经重复了清单 23-9 中的代码。

清单 23-9 。处理用户选择的位置

... savePicker.pickSaveFileAsync().then(function (saveFile) {     if (saveFile) {         loadedFile.copyAndReplaceAsync(saveFile).then(function () {             pickerImgElem.style.display = "none";             pickerTitleElem.innerText = "Saved: " + saveFile.name;         });     } }); ...

使用文件夹选择器

第三个选择器允许用户选择一个文件夹。到目前为止,您已经理解了配置和使用选择器的模式,所以我在这个例子中添加了一项新技术,只是为了让事情变得更有趣。

到目前为止,我假设用户只想处理图像文件。这是一个方便的快捷方式,因为它与应用布局中的img元素配合得很好。当然,现实是大多数应用需要处理不同类型的文件,所以在这个例子中,我向你展示了如何显示你可能遇到的任何文件的缩略图。清单 23-10 显示了pickers.html文件的附加内容,用于在点击文件夹选择器按钮时使用文件夹选择器(并处理缩略图)。

Images 注意当然,显示缩略图并不等同于阅读文件内容。正如我在前面的例子中演示的那样,处理图像文件的内容很容易,因为您可以依赖 IE10 对使用 HTML img元素显示图像的内置支持。在第二十二章中,我向你展示了如何读取文件内容,并提到了对处理二进制内容的支持,这可能是你所需要的,取决于你的应用所操作的文件类型。

清单 23-10 。对 pickers.html 文件的补充,增加了对文件夹选择器的支持

`...

...`

使用文件夹选择器的技术与前面的示例类似。你创建一个Windows.Storage.Pickers.FolderPicker对象并通过它的属性配置它,我已经在表 23-6 中列出了。

Images

在这个例子中,我将初始位置设置为Pictures库,并使用一个星号(*字符)来指定应该向用户显示所有类型的文件。

Images 提示如果fileTypeFilter数组没有包含至少一项,那么FolderPicker对象将抛出一个异常,因此你必须列出你想要显示的文件类型或者使用一个星号。

pickSingleFolderAsync方法显示选取器并允许用户选择一个文件夹。你可以在图 23-8 中看到拾取器是如何出现在用户面前的,它显示了包含本书前一章手稿的文件夹的内容。

images

图 23-8。使用文件夹选择器选择文件夹

FolderPicker看起来很像FileOpenPicker,但是用户不能选择单个文件,按钮文本清楚地表明正在选择一个文件夹。pickSingleFolderAsync方法返回一个Promise,当用户选择一个文件夹时,这个值就会被满足。用户的选择作为一个StorageFolder对象传递给then方法函数(我在第二十二章中介绍过)。

使用缩略图图像

我重复了前一个例子中处理清单 23-11 中的StorageFolder的代码。当用户选择一个文件夹时,我调用getFilesAsync方法来获取文件夹中的文件。

清单 23-11 。使用缩略图

... folderPicker.pickSingleFolderAsync().then(function(selectedFolder) {     if (selectedFolder != null) {         selectedFolder.getFilesAsync().then(function (files) { **            files[0].getThumbnailAsync(storage.FileProperties.ThumbnailMode.singleItem,** **                 500)**             .then(function (thumb) {                 pickerImgElem.style.display = "block";                 pickerImgElem.src = URL.createObjectURL(thumb);                 pickerTitleElem.innerText = files[0].displayName;             });         });     } }); ...

我获取文件夹中的第一个StorageFile对象并调用getThumbnailAsync方法。此方法生成一个图像,可用于直观地引用文件。对于图像文件,缩略图将是文件内容,而对于其他文件,缩略图通常是系统用来打开文件类型的默认应用的应用图标。

getThumbnailAsync方法有两个参数。第一个参数是来自Windows.Storage.FileProperties.ThumbnailMode对象的一个值,它指定了将要生成的缩略图的种类。我已经在表 23-7 中列出并描述了ThumbnailMode值。

Images

在示例中,我选择了用户选择的文件夹中的第一个文件,并在创建缩略图时使用了singleItem值,这意味着我将收到一个具有文件原始纵横比的大图像(这在显示图像文件时很重要)。第二个参数是您希望最长边的缩略图的大小—我已经指定了500,这意味着我的缩略图的最长边将是 500 像素。

getThumbnailAsync方法返回的Promise被满足时,我使用URL.createObjectURL方法为缩略图创建一个 URL,如下所示:

... pickerImgElem.src = URL.createObjectURL(thumb); ...

createObjectURL方法接受一系列不同的对象类型,包括由getThumbnailAsync方法产生的Windows.Storage.FileProperties.StorageItemThumbnail对象。

为了演示如何为非图像文件生成缩略图,我在我的Music库中选择了包含手稿文件的文件夹。使用文件夹选取器选择文件夹是一个两阶段的过程。

首先,导航到想要选择的文件夹后,点击Choose this folder按钮。这做了一个临时的选择,但是执行了一个微妙的 UI 更新,文件夹显示在屏幕的底部边缘,按钮的文本变成了OK,如图图 23-9 所示。

images

图 23-9。临时挑选文件夹

你可以在图 23-10 中看到拾取一个非图像文件的效果。我选择了包含手稿章节文本的 Microsoft Word 文件,该文件使用与 Word 文件相关的缩略图显示。

images

图 23-10。Word 文件的缩略图

缓存位置访问

正如您多次看到的那样,Metro 应用在文件系统方面受到严格的访问限制。您的应用可以自由访问由KnownFolders对象定义的位置,但前提是您想要访问的每个位置都在应用清单中声明为一项功能。如果您需要处理不同位置的文件,那么您需要使用选择器来获得用户授予的显式访问权限。

如果您的应用允许用户创建一些内容,然后将其保存到单个文件中,这种模式就很好——在这种情况下,使用选择器是非常合理的,因为每个文件的打开或保存位置可能会有所不同。

但是许多 Metro 应用将需要持久访问用户选择的位置,为了管理这一点,您必须使用Windows.Storage.AccessCache名称空间中的对象。首先,我向示例 Visual Studio 项目添加了一个名为access.html的新文件,您可以在示例应用中使用Access Cache NavBar 命令来访问该文件。你可以在清单 23-12 中看到内容。

清单 23-12 。access.html 文件的初始内容

`

` `          

` `    
                      
    
                 
Ready
    
`

这个例子背后的想法是展示两个相关的文件操作。布局中有两个button元素,为了测试这个例子,启动应用,在导航栏上选择适当的 common,然后单击Pick Folder按钮。该应用将显示文件夹选择器,以便您可以选择一个位置。选择任何不在Pictures库中的文件夹(因为应用已经在清单中声明了对Pictures的访问,所以已经可以访问那个位置)。

选择文件夹后,点击Load File按钮。该应用显示所选文件夹中第一个文件的名称——但它还没有显示缩略图,如图图 23-11 所示。

images

图 23-11。使用示例应用选择文件夹

现在,为了看看这个例子有什么不同,从 Visual Studio Debug菜单中选择Restart来重新启动应用。(重要的是重启,而不是刷新app。)

点击Load File按钮(不是Pick Folder按钮),你会看到缩略图和先前选择的文件夹中第一个文件的名称被显示出来,如图图 23-12 所示。我在文档库中选择了一个文件夹,应用通常无法访问该文件夹。

images

图 23-12。使用缓存文件位置

这个例子有两点很重要。第一个是所选位置被持久存储,第二个是访问该位置的许可也是持久的。这个应用不需要返回给用户并显示选取器来再次获取位置(以及访问它的权限)。

使用访问缓存

这个过程有两个部分——缓存对位置的访问和检索缓存的数据。我重复了清单 23-13 中处理缓存部分的例子中的关键语句。

清单 23-13 。缓存对文件系统位置的访问

... folderPicker.pickSingleFolderAsync().then(function (folder) {     if (folder != null) { **        var token = access.StorageApplicationPermissions.futureAccessList.add(folder);** **        storage.ApplicationData.current.localSettings.values["folder"] = token;**         accessTitleElem.innerText = "Selected: " + folder.displayName;     } **});** **...**

名称空间Windows.Storage.AccessCache(在例子中我将其别名为access)包含了StorageApplicationPermissions对象。该对象定义了两个属性,如表 23-8 所述。

Images

在这个例子中,我使用了futureAccessList属性,它返回一个Windows.Storage.AccessCache.StorageItemAccessList对象。我通过调用add方法存储我的位置,传入我希望在将来再次使用其位置的StorageFileStorageFolder对象。

add方法返回了一个我需要注意的字符串令牌——我使用了我在第二十章的中描述的应用设置特性来持久地存储选择的文件夹作为folder设置。StorageItemAccessList对象定义了我在表 23-9 中列出的方法和属性。

Images

当需要我显示文件的缩略图和名称时,我检索存储在应用设置中的令牌,并将其传递给getFolderAsync方法。这个方法返回一个Promise,当它被满足时,产生一个对应于缓存位置的StorageFolder对象。我重复了清单 23-14 中获取令牌和检索位置的例子中的语句。

清单 23-14 。从访问缓存中检索位置

... var token = storage.ApplicationData.current.localSettings.values["folder"]; var folder = **access.StorageApplicationPermissions.futureAccessList.getFolderAsync(token)** .then(function (folder) {     // *...statements that process StorageFolder omitted for brevity...* }); ...

通过使用访问缓存,我能够保留用户授予我的访问位置的权限,这样我就不必在每次应用启动时都使用选择器,我只需获得一次我需要的位置,然后就可以继续使用它们。

当然,有几个考虑因素。首先,也是最重要的,我不能滥用用户的信任,在我的应用被授权访问的位置上执行意想不到的操作。根据经验,我继续使用缓存位置来执行非破坏性操作,比如读取文件、监视文件夹的更改或创建新文件。如果我需要对文件系统执行任何类型的更改,包括重命名、移动和(尤其是)删除文件,我会提示用户获得明确的许可。

提示用户不仅给了他们说不的机会,还意味着他们清楚地知道是我的应用做了一系列的改变,避免了当他们试图找到已经被归档到不同地方或者更糟的是已经被删除的文件时令人讨厌的意外。

使用访问缓存时的第二个考虑是确保缓存中的位置不超过 1,000 个。一千个位置听起来很多,但缓存的条目会很快增加,特别是如果你的应用经常使用,并且操作单个文件而不是文件夹。有两种方法可以处理这个问题——您可以手动管理futureAccessList.entries数组的内容,并确保它不超过maximumItemsAllowed值。每个条目由一个AccessListEntry项表示,其token属性通过getFileAsyncgetFolderAsync方法返回访问缓存位置所需的令牌。

作为替代,您可以使用StorageApplicationPermissions.mostRecentlyUsedList。这就像futureList一样,但是它只包含 25 个最近使用的位置。当您添加第 26 个项目时,最少使用的项目会被自动删除,使您无需直接管理内容。

使用文件数据源

在第十四章、第十五章和第十六章中,我解释了如何通过数据驱动的 WinJS UI 控件FlipViewListViewSemanticZoom使用数据源。在那些章节中,我使用了WinJS.Binding.List对象作为数据源,即使是在我演示如何显示图像的时候。

更好的方法是创建由文件系统查询直接驱动的数据源,允许您对用户存储在设备上的文件进行操作。为了演示这一点,我将pages/dataSources.html 文件添加到示例 Visual Studio 项目中,其内容可以在清单 23-15 中看到。

清单 23-15 。dataSources.html 文件的内容

`

              

    
        
                         
        
    


        

        

    

`

这个例子在Pictures库中查询 PNG 图像文件,生成一个数据源,然后与FlipView控件一起使用,这样用户就可以浏览他们的图像。你可以在图 23-13 中看到应用的布局,你可以使用Data Sources导航条命令将其加载到示例应用中。

images

图 23-13。【dataSources.html 页面的布局

布局很简单,但是在这个简短的例子中有惊人的数量,所以我将详细检查代码。

创建数据源

简单的部分是创建数据源本身,这是使用WinJS.UI.StorageDataSource对象完成的。你首先创建一个QueryOptions对象,我在第二十二章中介绍过,然后把它传递给StorageFolder.createFileQueryWithOptions方法。这将返回一个StorageFolderQueryResult对象,您可以将它作为参数传递给StorageDataSource构造函数。我重复了清单 23-16 中设置StorageDataSource对象的例子中的语句。

清单 23-16 。创建存储数据源对象

`...
var options = new search.QueryOptions();
options.fileTypeFilter = [".png"];
options.folderDepth = search.FolderDepth.deep;

var query = folder.createFileQueryWithOptions(options);
flip.winControl.itemDataSource = new WinJS.UI.StorageDataSource(query, {
     mode: storage.FileProperties.ThumbnailMode.picturesView,
     requestedThumbnailSize: 400,
     thumbnailOptions: storage.FileProperties.ThumbnailOptions.resizeThumbnail,
     synchronous: false
});
...`

在这个例子中,我创建了一个QueryOptions对象,它执行深层文件夹查询,并将其匹配限制在 PNG 文件。我将StorageDataSource对象赋给了FlipView控件的itemDataSource属性,这意味着通过QueryOptions匹配的文件将由 UI 控件显示。

StorageDataSource构造函数有两个参数:QueryOptions对象和一个具有四个特定属性的对象。这些属性在表 23-10 中描述。

Images

对于这个例子,我使用了ThumbnailMode.picturesView值来获得一个宽高比的缩略图,并指定大小为 400 像素。我使用了在表 23-11 中描述的ThumbnailOptions.resizeThumbnail值,并将synchronous属性设置为false,这意味着我需要满足那些缩略图尚未被加载的数据源项目——我将很快解释如何做到这一点。

生成模板数据

创建数据源只是这个过程的一部分——我还需要使用 WinJS 数据绑定特性来填充由FlipView控件使用的模板。这并没有想象中那么简单,因为文件系统查询返回的对象不能用新的属性来扩展,这正是WinJS.Binding.converter方法试图使数据对象的属性可观察到的事情。

相反,我必须使用一个开放的值转换器,这让我可以更松散地映射函数。我在第八章中描述了这种技术,处理不能扩展的对象是这种技术有用的一种情况。首先,我将data-win-bind属性添加到 HTML 模板的元素中,如清单 23-17 中的所示,这里我重复了示例中的模板元素。

清单 23-17 。向 HTML 模板元素添加 data-win-bind 属性

`...

    
                 
    
...`

为了支持这个模板,我定义了两个开放的转换器,我在清单 23-18 中重复了这两个转换器。

清单 23-18 。开放数据转换器以支持 HTML 模板

WinJS.Namespace.define("Converters", {     img: function (src, srcprop, dest, destprop) {         if (src.thumbnail == null) {             src.addEventListener("thumbnailupdated", function (e) {               dest[destprop] = URL.createObjectURL(src.thumbnail);             });         } else {             dest[destprop] = URL.createObjectURL(src.thumbnail);         }     },     general: function (src, srcprop, dest, destprop) {         dest[destprop] = src[srcprop];     } }); Converters.img.supportedForProcessing = true; Converters.general.supportedForProcessing = true;

最简单的转换器叫做Converters.general,它只是将指定属性的值设置为指定的数据对象值。第二个称为Converters.img,需要稍微多解释一下。对于不能使用文件系统对象作为数据绑定值的来源这一问题,这是一个简单的解决方案。general转换器简单地使用源属性的值来设置目标属性的值,不进行转换或格式化,并充当Windows.StorageWinJS.Binding名称空间之间的桥梁。

Converters.img转换器处理两个问题。首先,它使用URL.createObjectURL方法创建引用文件缩略图的 URL,这样我就可以在 HTML img元素中使用它们。

第二个问题是 Windows 只在需要的时候才生成文件缩略图。这意味着,如果您正在浏览数据源中的文件(在本例中,这是用户将使用FlipView控件进行的操作),那么您将会遇到缩略图尚未加载的数据对象。

使用StorageDataSource对象时,数据源将是一个Windows.Storage.BulkAccess.FileInformation对象。Windows.Storage.BulkAccess名称空间提供了可以用来执行大规模高效文件系统操作的对象,这通常对创建数据源提供者很有用,但通常对常规应用开发没什么用处(这就是为什么在本节之外我不详细介绍这个名称空间)。

缩略图可通过FileInformation.thumbnail属性获得,但如果 Windows 尚未生成并缓存合适的图像,这将返回null。为了解决这个问题,我监听了thumbnailupdated事件,当 Windows 生成缩略图时,FileInformation对象将发出该事件。结果是 HTML 元素将被更新,即使 Windows 需要一点时间来生成所需的缩略图。所有这些组合在一起提供了一个数据源,可以用于数据驱动的 WinJS UI 控件,它建立在我在《??》第二十二章中介绍的对象和技术之上。

总结

在这一章中,我已经向你展示了一些方法,你可以将文件系统的底层支持集成到你的应用中。我向您展示了如何创建引用图像文件内容的 URL,如何向用户呈现打开文件、保存文件和文件夹选择器,以及如何缓存对位置的访问,以便您可以向用户提供服务,而不必不断地纠缠他们让您访问文件系统位置。同时,我还向您展示了如何生成缩略图,以及如何创建查询文件系统的数据源,并以 WinJS 数据驱动 UI 控件可以使用的方式呈现结果。在下一章,我将向您展示如何实现与文件系统相关的 Metro 契约。

二十四、文件激活和选取器契约

在这一章中,我将向你展示如何实现允许 Windows 应用将文件相关功能集成到 Windows 中的三个契约。我将向您展示如何使用文件激活契约注册一个应用来处理特定类型的文件,以及如何使用保存选取器打开选取器契约向其他应用提供存储服务。像所有契约一样,实现这些契约是可选的,但如果你的应用以任何方式处理文件,你应该仔细查看它们,看看它们是否提供了集成,使用户使用你的应用更加简单和容易。表 24-1 提供了本章的总结。

Images

创建示例应用

本章的示例应用将提供一些基本功能,我将通过实现文件契约来增强这些功能。我要建立一个简单的相册应用,它的初始化身将让用户从文件系统中选择图像文件添加到相册中。每个图像的缩略图将显示在一个ListView UI 控件中(我在第十五章中描述过)。我将使用Windows.Storage.AppCache名称空间中的对象缓存用户选择的文件位置(我在第二十三章中描述过),这将使用户的文件选择持久化。

我使用Blank App模板创建了一个名为PhotoAlbum的新 Visual Studio 项目。您可以在清单 24-1 中看到default.html文件的内容。

清单 24-1 。来自相册 app 的 default.html 文件

`

` `         PhotoAlbum                               **    ** **    **      **    
** **        
** **            ** **            ** **        
** **    
**

**    

**

`

该应用将使用标准内容导航模型,通过使用WinJS.Navigation名称空间将内容页面引入应用布局。与我使用WinJS.Navigation的其他例子不同,内容转换将由应用触发,以响应文件契约。这个default.html文件还包含了一个WinJS.Binding.Template的元素,我将用它来显示整个应用中图像文件的缩略图。模板显示图像和包含文件名的标签。

内容文件可以在pages文件夹中找到,我只从一页内容开始,名为pages/albumView.html,我将在应用启动时加载它。这将提供基本的相册特性,你可以在清单 24-2 中看到这个文件的内容。

清单 24-2 。albumView.html 文件的内容

`

              
    
    
                      
`

除了显示图像缩略图的ListView控件之外,该文件还包含两个按钮。Open按钮显示文件打开选择器(在第二十三章的中描述),以便用户可以在应用中打开图像文件。Clear按钮从ListView中移除图像并清除位置缓存,将应用重置为初始状态。你可以在清单 24-3 中看到我为这些元素定义的样式,它显示了css/default.css文件的内容。CSS 中没有特殊的功能或特定于应用的技术。

清单 24-3 。default.css 文件的内容

`#contentTarget {
    width: 100%; height: 100%;
    display: -ms-flexbox; -ms-flex-direction: column;
    -ms-flex-align: center; -ms-flex-pack: center;}

listView { border: medium solid white; margin: 10px;

height: 80%; width: 80%; padding: 20px;}

buttonContainer button

.imgContainer { border: thin solid white; padding: 2px;}

.listTitle { font-size: 18pt; max-width: 180px;
    text-overflow: ellipsis; display: block; white-space: nowrap;
    margin: 0 0 5px 5px; height: 35px;}

.listImg {height: 200px; width: 300px;}
.title { font-size: 30pt;}`

定义 JavaScript

我想让这个项目中的default.js文件尽可能简单,因为它将是本章中变化最大的文件,我不想重复列出作为基本的非契约功能一部分的代码。为此,我创建了几个 JavaScript 文件来执行应用的基本设置,并提供管理相册所需的功能。这些文件中的第一个,/js/setup.js,如清单 24-4 所示。

清单 24-4 。setup.js 文件的内容

`(function () {

WinJS.Namespace.define("ViewModel", {
        fileList: new WinJS.Binding.List()
    });

WinJS.Navigation.addEventListener("navigating", function (e) {
        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {
            WinJS.Utilities.empty(contentTarget);
            WinJS.UI.Pages.render(e.detail.location, contentTarget,
                WinJS.Navigation.state)
                .then(function() {
                    WinJS.Binding.processAll(document.body, ViewModel);
                }).then(function () {
                    return WinJS.UI.Animation.enterPage(contentTarget.children)
                });
        });
    });
})();`

这个文件创建了ViewModel.fileList对象,我将它用作ListView控件的数据源,这样我就可以显示图像缩略图。我还为navigating事件设置了事件处理程序,它将内容加载到主页中。我提到的函数在/js/app.js文件中,其内容如清单 24-5 所示。

清单 24-5 。app.js 文件的内容

`(function () {

var storage = Windows.Storage;
var access = storage.AccessCache;
var cache = access.StorageApplicationPermissions.futureAccessList;
var pickers = storage.Pickers;

WinJS.Namespace.define("App", {

loadFilesFromCache: function () {
        return new WinJS.Promise(function (fDone, fErr, fProg) {
            if (cache.entries.length > 0) {
                ViewModel.fileList.length = 0;
                var index = cache.entries.length - 1;
                (function processEntry() {
                    cache.getFileAsync(cache.entries[index].token)
                        .then(function (file) {
                            App.processFile(file, false);
                            if (--index != -1) {
                                processEntry();
                            } else {
                                fDone();
                            }
                        });
                })();
            } else {
                fDone();
            }         });
    },

processFile: function (file, addToCache) {
        ViewModel.fileList.unshift({
            img: URL.createObjectURL(file),
            title: file.displayName,
            file: file
        });
        if (addToCache !== false) {
            cache.add(file);
        }
    },

pickFiles: function () {
        var picker = pickers.FileOpenPicker();
        picker.fileTypeFilter.replaceAll([".jpg", ".png"]);
        picker.pickMultipleFilesAsync().then(function (files) {
            if (files != null) {
                files.forEach(function (file) {
                     App.processFile(file);
                });
            }
        });
    },

clearCache: function () {
        cache.clear();
        ViewModel.fileList.length = 0;
    }
});

})();`

我不打算详细介绍这段代码,因为它建立在我在前面的章节中描述和演示的功能之上,我只是将它作为演示文件契约的基础。

也就是说,这个清单中的一些技术是我在前面章节中展示的技术的微小变化,值得指出。首先,我在FileOpenPicker对象上使用了pickMultipleFilesAsync方法,这样用户可以一次选择多个文件。这不是一个复杂的特性,与使用pickSingleFileAsync的唯一区别是,方法返回的Promise通过then方法产生一个StorageFile对象的数组(而不是单个StorageFile对象)。

第二个变化是我不存储由futureAccessList.add方法返回的令牌。相反,我处理的是entries数组中的项目,使用token属性从getFileAsync方法中获取我需要的字符串来获取StorageFile对象。(我反向枚举缓存位置,以便最近添加的图像显示在ListView显示屏的顶部。)最后一个文件是js/default.js,如清单 24-6 所示。

清单 24-6 。default.js 文件的初始内容

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;

app.onactivated = function (args) {

if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                if (ViewModel.fileList.length == 0) {
                    App.loadFilesFromCache();
                }

switch (args.detail.kind) {
                    default:
                        WinJS.Navigation.navigate("/pages/albumView.html");
                        break;
                }
            }));
        }
    };
    app.start();
})();`

我尽可能保持简单,依靠我在viewmodel.js文件中创建的ViewModelApp名称空间中的对象来实现相册的基本功能,我将在下一节中演示。switch语句可能看起来有点奇怪,因为它只有一个default块,但是我将在本章中为不同类型的激活事件添加处理程序。

测试示例应用

为了测试这个例子,启动应用并点击Open按钮。导航到一个有一些图像文件的文件夹(我使用了我在前面章节中创建的Pictures/flowers文件夹),用选择器选择一个或多个图像。点击选择器中的Open按钮打开文件,你会看到缩略图显示在应用布局中,如图图 24-1 所示。

images

图 24-1。将图像载入示例应用

从 Visual Studio Debug菜单中选择Stop Debugging停止应用,然后再次启动应用。由于文件位置缓存在futureAccessList对象中,你会看到你之前加载的图像再次显示。

对于示例应用来说,这是一个很长的设置过程,但是它为我实现文件契约提供了一个很好的基础,现在我可以用最少的额外代码来实现它。

添加图像

我在这个示例应用中使用了许多图像,所有这些图像都可以在 Visual Studio 项目的 images 文件夹中找到。前两个图像被称为jpgLogo.pngpngLogo.png,我将在实现文件激活契约时使用它们。你可以在图 24-2 中看到这些图像。这两个文件显示相同的图像,但背景不同。

images

图 24-2。将在文件激活契约中使用的图像

我还将 Visual Studio 在images文件夹中创建的所有默认图像替换为具有相同尺寸的图像,但显示的图标与图中的图像相同。

对于logo.pngslashscreen.pngstorelogo.png文件,我添加到项目中的图像在透明背景上显示一个白色图标,这意味着它们不可能在白色页面上显示——但是如果你想象一下图 24-2 中的一个图像没有彩色背景,你就会明白了。

我已经删除了smalllogo.png文件,用一个名为small.png的 30×30 像素文件代替,它以白色显示相同的图标,但背景为黑色,看起来和图 24-2 中左边的图像一样。然后我更新了应用清单,为Small logo字段指定了 small.png 文件,如图 24-3 中的所示。

images

图 24-3。更改小 logo 文件

这个名称的改变很重要,因为它解决了一个奇怪的错误——我将在本章后面实现文件激活契约时解释这个问题。

images 提示你可以在本书附带的源代码下载中找到所有这些图像文件,从apress.com开始可以免费获得。

创建助手应用

我在本章中实现的一些契约为其他应用提供服务,我需要一个助手应用来演示这些功能。这个应用叫做FileHelper,它非常简单,使用我在第二十三章的中介绍的文件拾取器来加载和保存一个图像文件。您可以在清单 24-7 中看到FileHelper项目的default.html文件的内容。

清单 24-7 。file helper default.html 文件的内容

`

<html> <head>     <meta charset="utf-8" />     <title>FileHelper</title>


    
    
    


    
    

    
        ` `             
    
             
`

这个应用的布局是围绕两个button元素和一个img元素构建的。Open按钮使用文件打开选择器加载单个图像文件,该文件使用img元素显示。你可以在清单 24-8 中看到我用来样式化这些元素的 CSS,它显示了css/default.css文件的内容。

清单 24-8 。FileHelper 项目中的 default.css 文件

`body, div.container {display: -ms-flexbox;-ms-flex-direction: row;
    -ms-flex-align: center; -ms-flex-pack: center; }
div.container {margin: 10px; height: 80%; -ms-flex-direction: column;}
button {font-size: 25pt; width: 200px; margin: 10px;}

thumbnail {width: 600px; border: medium white solid;}`

这个项目中唯一的另一个文件是default.js,在这个文件中,我通过显示选择器来响应按钮点击,以便用户可以加载和保存图像文件。您可以在清单 24-9 中看到default.js文件的内容。

清单 24-9 。FileHelper 项目中 default.js 文件的内容

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var storage = Windows.Storage;
    var pickers = Windows.Storage.Pickers;

var pickedFile = null;

app.onactivated = function (args) {
        if (args.detail.previousExecutionState
            != activation.ApplicationExecutionState.suspended) {

args.setPromise(WinJS.UI.processAll().then(function() {
                WinJS.Utilities.query('button').listen("click", function (e) {
                    if (this.id == "open") {
                        var openPicker = new pickers.FileOpenPicker();
                        openPicker.fileTypeFilter.replaceAll([".png", ".jpg"]);
                        openPicker.pickSingleFileAsync().then(function (file) {
                            pickedFile = file;
                            save.disabled = false;
                            thumbnail.src = URL.createObjectURL(file);
                        });
                    } else {                         var savePicker = new pickers.FileSavePicker();
                        savePicker.defaultFileExtension = pickedFile.fileType;
                        savePicker.fileTypeChoices.insert(pickedFile.displayType,
                            [pickedFile.fileType]);
                        savePicker.suggestedFileName = "New Image File";
                        savePicker.pickSaveFileAsync().then(function (saveFile) {
                            if (saveFile) {
                                pickedFile.copyAndReplaceAsync(saveFile);
                            }
                        });
                    }
                });
            }));
        }
    };
    app.start();
})();`

在这个清单中没有新的技术,我使用了拣选器,就像我在第二十三章中所做的一样。助手应用的存在只是为了帮助我在PhotoAlbum应用中演示文件契约的实现。你可以在图 24-4 中看到助手应用,它显示了我在前面章节中使用的一个样本图像。

images

图 24-4。文件助手应用

实现文件激活契约

文件激活契约允许您声明您的应用愿意并且能够处理某种类型的文件。为了理解我的意思,打开桌面,使用文件浏览器找到一个 PNG 或 JPG 文件——前几章中的花卉图片是理想的。右键单击该文件并选择Open with菜单,您将看到一个应用列表,包括桌面和 Windows 应用商店应用,可以打开该文件。你可以在图 24-5 中看到在我的台式电脑上打开 PNG 文件的应用。

images

图 24-5。能够在我的系统上打开 PNG 文件的应用

如图所示,我可以使用 Microsoft Office、Paint、Paint.NET、Photos 应用、Snagit 编辑器(我用于截图)和 Windows Photo Viewer 打开一个 PNG 文件。在这一部分,我将向你展示如何将你的应用添加到列表中,并演示当你的应用被选中打开一个文件时,你如何回应。

声明文件类型关联

您可以在应用清单中声明想要支持的文件类型。从Solution Explorer打开package.appxmanifest文件,切换到Declarations选项卡。

Declarations列表中选择File Type Associations,点击Add按钮。Visual Studio 将为您提供一个表单,用于填写文件关联的详细信息。对于这个例子,我需要两个文件关联—一个用于 PNG 文件,一个用于 JPG 文件。表 24-2 描述了需要填充的字段的含义,并提供了每个关联的值。使用First Form列中的值填充完第一个表单后,单击Add按钮创建第二个关联。使用Second Form列中的值填写表单,然后键入Control+S保存对清单的更改。当你完成时,你会在声明列表中看到两个条目,如图图 24-6 所示。

Images

images 提示通过点击Add New按钮,您可以在同一个声明中支持多个文件扩展名,这将在表单中添加一个新的Supported File Type部分。我使用了两个声明,因为我希望每种文件类型有不同的图像和描述性文本。

images

图 24-6。向应用清单添加文件类型关联声明

当您使用 Visual Studio 启动应用时,应用包会安装到设备上,这包括创建 Windows 文件关联。尽管我只实现了契约的一部分,但您已经可以看到文件关联的效果:启动应用,然后导航到桌面(最简单的方法是键入Control+D)。打开Explorer并找到任何 PNG 或 JPG 文件。如果右击文件并选择Open with菜单,你会看到PhotoAlbum app 已经添加到列表中,如图图 24-7 所示。(显示的名称取自清单的Application UI部分的Display name字段——我更改了这个值,在单词PhotoAlbum之间添加了一个空格。)

images

图 24-7。浏览器中与文件类型相关联的应用

images 注意当我创建示例应用时,我特意更改了在Small logo清单字段中指定的文件名。有一个奇怪的错误,如果你不改变文件名,Windows 将显示你的项目images文件夹中的logo.png文件,它通常有一个透明的背景,以便它可以在开始屏幕上使用(这个主题我将在第十九章中深入讨论)。透明背景会阻止应用在Open with列表中正常显示。要避免此问题,请确保更改 30 x 30 像素文件的名称,以便使用该文件,为用户呈现具有纯色背景的图像。

处理文件激活事件

最后一步是当用户选择应用打开文件时做出响应,这是使用一个activated事件发出的信号,该事件的kind属性被设置为ActivationKind.file。你可以看到我对清单 24-10 中的default.js文件中的onactivated函数所做的修改,以支持这种事件。

清单 24-10 。响应文件激活事件

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;

app.onactivated = function (args) {
        if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {

var promise = ViewModel.fileList.length == 0 ?
                    App.loadFilesFromCache() : WinJS.Promise.wrap(false);

promise.then(function () {
                    switch (args.detail.kind) {
**                        case activation.ActivationKind.file:**
**                            args.detail.files.forEach(function (file) {                                 App.processFile(file);**
**                            });**
                        default:
                            WinJS.Navigation.navigate("/pages/albumView.html");
                            break;
                    }
                });

return promise;
            }));
        }
    };
    app.start();
})();`

粗体语句响应文件激活事件。事件对象的detail.files属性包含一个StorageFile对象的数组,每个对象都由用户选择由应用打开。在这个清单中,我使用forEach方法来枚举数组的内容,并为每个StorageFile调用App.processFile方法(我在app.js文件中定义了该方法),其效果是将缩略图添加到ListView控件并缓存文件位置,以便应用持久工作。

images 注意注意,处理文件激活事件的case块一直到default块。这意味着albumView.html文件将用于为file事件以及常规发布事件提供内容。你可以在第十九章中了解更多关于发布活动的信息。

要测试事件的处理,启动应用,导航到桌面,右键单击 PNG 或 JPG 文件。选择Open with菜单项,点击列表中的Photo Album。您打开的文件的缩略图将被添加到布局中的ListView,与文件的displayName值一起显示。

images 提示如果没有得到正确的结果,那么右击开始屏幕上的Photo Album应用图标,选择Uninstall,然后从 Visual Studio 再次启动应用。当应用重新启动时,Windows 并不总是会选择更改,卸载应用包可以解决这个问题。

将应用设为默认处理程序

我还想描述文件激活处理程序的另一个方面。为此,您需要将应用设置为文件类型的默认处理程序。从 Windows 桌面上,使用文件资源管理器找到并右键单击一个 PNG 文件,选择Open with Images Choose default program菜单项。你会看到一个弹出窗口,如图图 24-8 所示。(该应用显示有我在清单的应用 UI 部分中定义的徽标。我使用了与文件关联相同的图标,但是背景是透明的。)选择Photo Album项,使示例应用成为默认处理程序,然后对 JPG 文件重复这个过程。

images

图 24-8。让应用成为 PNG 文件的默认处理程序

这不会改变应用的行为方式,但它确实意味着 Windows 将使用我在本章前面添加到应用的图像,这些图像被指定为文件关联声明的一部分,作为向用户显示 PNG 和 JPG 文件时的文件图标。你可以在图 24-9 的中看到这是如何出现的。

images

图 24-9。Windows 资源管理器中使用的文件关联声明中的文件图标

因为我正在处理图像文件,Windows 会尽可能显示文件内容,但图标的使用范围更广,包括在开始屏幕上搜索文件时,以及搜索其他文件类型时。该图显示了文件资源管理器的List视图。

实施 App-to-App 提货契约

应用到应用的挑选契约允许其他应用通过你的应用加载和保存文件,而不是本地文件系统。如果你的应用提供的某种价值超出了用户以常规方式存储文件所能获得的价值,这将非常有用——一个很好的例子是支持 Dropbox 或 SkyDrive 风格的文件复制或提供对存储在远程位置的文件的访问的应用。

我需要一些更简单的东西来演示文件选择器契约,所以我的示例应用将文件存储在本地应用数据文件夹中。这不会给用户增加任何价值,但这意味着我可以专注于契约,而不会有太多的分心和转移。

在接下来的章节中,我将向您展示如何实现 app-to-app 提货契约:保存提货契约和开放提货契约。

实施保存提货人契约

保存选择器契约允许其他应用将文件保存到你的应用,就好像它是一个文件系统位置,就像一个文件夹一样。与大多数契约一样,您必须在应用清单中进行声明,并处理特定类型的激活事件。这份契约与您目前看到的略有不同,因为您还需要准备应用的布局,以便应用可以在选取器中显示内容。我将在接下来的章节中解释它是如何工作的。

宣布支持该契约

第一步是声明应用实现清单中的协定。这告诉 Windows 您的应用应该作为一个可以保存文件的位置呈现给用户。从 Visual Studio 的Solution Explorer窗口打开package.appxmanifest文件,点击Declaration部分的标签。

Available Declarations列表中选择File Save Picker,点击Add按钮。如果你希望你的应用能够处理任何类型的文件(这在 Dropbox/SkyDrive 场景中是有意义的),那么选择Supports any file type选项。PhotoAlbum示例应用将只支持 JPG 和 PNG 文件,因此在File type文本框中输入.png,单击Add New按钮获得另一个框,并在第二个File type文本框中输入.jpg。键入Control+S保存更改。您的清单应该看起来像图 24-10 中的清单。

images

图 24-10。将文件保存提货人契约声明添加到应用清单

测试保存选择器声明

仅仅进行清单声明就将应用定义为一个保存位置,尽管我还没有实现作为契约一部分的激活事件。现在看看这是如何工作的,将更容易理解应用为实现这个契约而必须做的其余工作。

测试 save picker 契约声明需要使用我在本章开始时创建的FileHelper应用以及PhotoAlbum本身,并且需要几个步骤。

首先启动PhotoAlbum app。该应用不需要运行到保存位置,但 Visual Studio 会在启动时安装该应用,这将向 Windows 注册清单声明中定义的文件类型。点击Open按钮,选择一些图像,这样应用中就会有一些内容。

其次,启动我在本章开始时创建的FileHelper应用,点击Open按钮,选择一个图像文件——最好是你刚才没有选择的文件。点击Save按钮。该应用将显示文件保存选择器。如果你点击Files链接旁边的箭头,你会看到一个保存目的地列表,包括Photo Album,如图图 24-11 所示。

images

图 24-11。在选取器中显示为保存位置的应用

如果从列表中选择Photo Album,你会看到类似于图 24-12 所示的布局。这是文件选择器中显示的PhotoAlbum应用布局的奇怪组合。

完成实现契约所需的工作是双重的:我需要更新应用,以便在选择器中显示更有用的布局,并支持从FileHelper应用保存文件。在下一节中,我将向您展示如何做到这两点。

images

图 24-12。保存选择器中显示的应用布局

处理被激活的事件

当用户选择应用作为保存位置时,Windows 会发送一个activated事件。事件的detail.kind属性被设置为ActivationKind.fileSavePicker,这是更改应用布局的提示,以便它适合选取器并为用户提供一些有意义的内容。你可以在清单 24-11 中的应用中看到我是如何响应事件的,它显示了我对/js/default.js文件所做的更改。

清单 24-11 。响应 default.html 保存提货人契约激活事件

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;

app.onactivated = function (args) {

if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {

if (ViewModel.fileList.length == 0) {
                    App.loadFilesFromCache();
                }

switch (args.detail.kind) {
**                    case activation.ActivationKind.fileSavePicker:**
**                        var pickerUI = args.detail.fileSavePickerUI;m,**
**                        WinJS.Navigation.navigate("/pages/savePickerView.html",**
**                            pickerUI);**
**                        break;**
                    case activation.ActivationKind.file:
                        args.detail.files.forEach(function (file) {
                            App.processFile(file);
                        });
                    default:
                        WinJS.Navigation.navigate("/pages/albumView.html");
                        break;
                }
            }));
        }
    };
    app.start();
})();`

除了这些更改之外,我还向项目添加了一个名为pages/savePickerView.html的新文件,当我获得文件保存选择器契约的激活事件时,我会显示这个文件。

注意,当我调用WinJS.Navigation.navigate方法时,我从激活事件中传递了detail.fileSavePickerUI属性的值。这个对象让我在用户保存文件时做出响应,通过将它传递给navigate方法,我将能够处理savePickerView.html文件中的对象,如清单 24-12 所示。

images 注意要明确的是,我能够像这样传递对象是因为我在js/setup.js文件中定义的函数将WinJS.Navigation.state属性的值传递给了WinJS.UI.Pages.render方法。你可以在第七章的中了解更多关于WinJS.Navigation.state属性和WinJS.UI.Pages.render方法的知识。

清单 24-12 。pages/savePickerView.html 文件的内容

`

                   
Add to the                   images in your album
    
    
`

这个文件有两个部分。该标记是自包含的,并向用户提供有用的消息和单行图像缩略图,以显示相册中已有的内容。当收到 activated事件时,我使用这个标记作为应用布局,数据绑定和ListView控件向用户提供内容。你可以在图 24-13 中看到结果。(我通过重启PhotoAlbum应用,切换到FileHelper应用,点击Save按钮,这样我就可以从文件位置列表中选择PhotoAlbum了。)

images

图 24-13。向用户呈现适合提货人的内容

在这种情况下,您可以使用任何您喜欢的布局,只要它适合文件选取器的信箱区。你展示的任何东西都应该是有用的——这通常意味着给用户一些已经可用的指示。在我看来,对用户的价值优先于严格准确的内容视图。在图中,你会看到我已经决定显示相册中的图片,而不是只关注那些作为保存位置存储在应用中的图片。

配置选取器

在选择器中向用户呈现内容只是任务的一部分。我还必须配置选择器本身,并在用户单击保存按钮时做出响应。在清单 24-13 的中,我重复了来自savePickerView.html文件的代码来完成这两项工作。

清单 24-13 。savePickerView.html 文件中处理拣选器的代码

... ready: function (element, pickerUI) {     pickerUI.title = "Save to Photo Album";     pickerUI.addEventListener("targetfilerequested", function (e) {         var deferral = e.request.getDeferral();         storage.ApplicationData.current.localFolder             .createFileAsync(pickerUI.fileName,                 storage.CreationCollisionOption.replaceExisting)                 .then(function (file) {                     e.request.targetFile = file;                     App.processFile(file);                     deferral.complete();                 });     }); } ...

我在我的ready函数中接收的pickerUI变量是来自激活事件的detail.fileSavePickerUI属性的值。该属性返回一个Windows.Storage.Pickers.Provider.FileSavePickerUI对象,用于配置呈现给用户的选择器。FileSavePickerUI对象定义了表 24-3 中描述的属性。

Images

在示例中,我使用了title属性来指定字符串Save to Photo Album,这可以在图 24-13 中看到。当您提供的存储存在某种层次结构时,此属性非常有用,因为您可以使用它来指示文件的保存位置。FileSavePickerUI对象还定义了两个事件,其中一个我在例子中使用了。您可以在表 24-4 中查看这些事件的详细信息。

Images

我对这一章感兴趣的是targetfilerequested事件,因为它向应用发出用户想要保存文件的信号。事件的处理程序被传递了一个Windows.Storage.Pickers.Provider.targetFileRequestedEventArgs对象。这个对象只定义了一个名为request的属性,这是完成契约所需要的。请求属性返回一个Windows.Storage.Pickers.Provider.TargetFileRequest对象,该对象定义了表 24-5 中显示的属性。

Images

一个简单的操作涉及到很多对象。当您接收到targetfilerequested事件时,您调用request.getDeferral方法来告诉 Windows 在您执行异步操作时等待。

然后,创建或获取将被传递给另一个应用的StorageFile对象,以便它可以写入其内容。在示例中,我在代表本地应用数据文件夹的StorageFolder对象上调用了createFileAsync方法(如第二十章所述)。对于文件名,我读的是FileSavePickerUI.fileName属性。将StorageFile赋给request.targetfile属性,然后调用之前调用getDeferral时获得的对象的 complete 方法——这告诉 Windows 您已经完成了,现在可以将StorageFile传递给想要保存数据的应用。

更新数据

完成契约的实现还需要一个步骤,这涉及到保持你的应用布局是最新的。

如果当用户选择它作为存储位置时,您的应用没有运行,则应用会启动并发送文件激活事件。一旦保存操作完成,应用就会终止。

但是,如果您的应用在用户选择它作为保存位置时正在运行,则会创建应用的第二个实例,具有完全独立的全局名称空间和变量。一旦保存操作完成,第二个实例就被终止,但是它留下了一个问题:您的应用的保持运行的实例如何发现新保存的文件?

你不能依靠视图模型来解决这个问题,因为视图模型是一个全局变量,应用的每个实例都有自己的副本。您必须使用某种共享存储来解决这个问题,通过它您可以发现文件。对于我的应用,这意味着我必须监控本地应用数据文件夹,并加载我在那里发现的任何新文件。你可以看到我是如何使用我在第二十二章和清单 24-14 中展示的文件夹监控技术做到这一点的,其中显示了我在PhotoAlbum应用中对default.js文件所做的添加。

清单 24-14 。监控本地应用数据文件夹的变化

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;

**    var query = storage.ApplicationData.current.localFolder**
**        .createFolderQuery();**
**    query.addEventListener("contentschanged", function () {**
**        App.loadFilesFromCache();**
**    });**
**    query.getFoldersAsync();**

app.onactivated = function (args) {

if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {

if (ViewModel.fileList.length == 0) {
                    App.loadFilesFromCache();
                }

switch (args.detail.kind) {
                    case activation.ActivationKind.fileSavePicker:
                        var pickerUI = args.detail.fileSavePickerUI;
                        WinJS.Navigation.navigate("/pages/savePickerView.html",                             pickerUI);
                        break;
                    case activation.ActivationKind.file:
                        args.detail.files.forEach(function (file) {
                            App.processFile(file);
                        });
                    default:
                        WinJS.Navigation.navigate("/pages/albumView.html");
                        break;
                }
            }));
        }
    };
    app.start();
})();`

每当我接收到contentschanged事件时,我就调用App.loadFilesFromCache函数,该函数在/js/app.js文件中定义(在本章前面显示过)。有了这个功能,你可以将文件从FileHelper应用保存到PhotoAlbum应用,并立即看到它们出现。对于这个简单的应用来说,重新加载所有文件比找出新内容更容易。有了这个附加功能,我有了一个很好的保存选择器契约的实现,并且可以从其他应用接收和存储文件。

现在,当您从FileHelper应用保存文件时,您可以切换到PhotoAlbum应用,并看到您保存的图像显示在应用布局中。你可以在图 24-14 中看到这个效果,它显示了一个图像添加到应用布局中。

images

图 24-14。通过保存选取器契约将图像保存到示例应用

实施开放提货人契约

既然您已经看到了保存选取器契约,那么它的补充——开放选取器契约——通过比较就很容易理解了。与所有契约一样,首先向应用清单添加一个声明。从 Visual Studio 的Solution Explorer窗口打开package.appxmanifest文件,点击Declarations部分的标签。

Available Declarations列表中选择File Open Picker,点击Add按钮。示例中我打算支持 JPG 和 PNG 文件,所以在现有的File type文本框中输入.jpg,点击Add New按钮,在新创建的File type文本框中输入.png。键入Control+S保存更改。清单应该类似于图 24-15 中所示的清单。

images

图 24-15。增加文件打开提货人契约声明

处理激活事件

当用户选择应用作为打开文件的位置时,Windows 会发送激活事件。激活事件的detail.kind属性被设置为ActivationKind.fileOpenPicker,就像处理保存选取器事件一样,这是改变应用布局的提示,以便它适合选取器并为用户提供一些有意义的内容。在清单 24-15 中,您可以看到我是如何响应PhotoAlbum应用中的激活事件的,它显示了我对default.html文件中的switch语句所做的更改。

清单 24-15 。响应 default.html 文件中的打开选取器激活事件

... switch (args.detail.kind) {     case activation.ActivationKind.fileOpenPicker: **        var pickerUI = args.detail.fileOpenPickerUI;** **        WinJS.Navigation.navigate("/pages/openPickerView.html",** **            pickerUI);**         break;     case activation.ActivationKind.fileSavePicker:         var pickerUI = args.detail.fileSavePickerUI;         WinJS.Navigation.navigate("/pages/savePickerView.html",             pickerUI);         break;     case activation.ActivationKind.file:         args.detail.files.forEach(function (file) {             App.processFile(file);         });     default:         WinJS.Navigation.navigate("/pages/albumView.html");         break; } ...

该契约的工作方式与保存选取器契约非常相似。该应用的一个新实例被启动并发送激活事件,布局被嵌入显示给用户的文件选取器中。

激活事件的detail.fileOpenPickerUI属性返回我需要管理文件打开过程的对象,所以我将它传递给 navigate 方法,请求用我添加到项目中的/pages/openPickerView.html文件填充布局,其内容可以在清单 24-16 中看到。

清单 24-16 。/pages/openPickerView.html 文件的内容

`

              

    
Select images from the album
    
    
`

该文件定义的布局非常类似于我用于保存选择器契约的布局——一个WinJS.UI.ListView UI 控件显示相册中当前的图像。用户将选择文件来挑选它们。

要看到这种布局,您需要经历一系列特定的事件。首先,重启PhotoAlbum应用——open picker 应用不需要运行才能工作,但您需要重启应用,以便 Visual Studio 通知 Windows 您的应用已声明支持 open picker 合约。

现在切换到FileHelper应用,点击Open按钮显示一个打开的拾取器。点击位置标题旁边的箭头,你会看到PhotoAlbum被列为文件来源,如图图 24-16 所示。

images

图 24-16。显示为要打开的文件源的示例应用

如果选择列表中的PhotoAlbum项,将触发激活事件,显示新的布局,如图图 24-17 所示。

images

图 24-17。使用开放选取器契约打开文件

选择其中一个文件并点击Open按钮,您将看到您选择的图像显示在FileHelper应用中。

PhotoAlbum应用的角度来看,激活事件的detail.fileOpenPickerUI属性返回的对象是一个Windows.Storage.Pickers.Provider.FileOpenPickerUI对象,它的工作方式与保存选取器契约的相应对象略有不同。为了演示它是如何工作的,我将分解对象并依次讨论它的属性、方法和事件。

FileOpenPickerUI对象定义的属性显示在清单 24-6 中,它们用于获取显示给用户的拣选器信息,并设置一些基本的配置选项。

Images

selectionMode属性对你呈现给用户的布局影响最大,因为它表明试图打开文件的应用是接受一个文件还是多个文件。该属性返回由Windows.Storage.Pickers.Provider.FileSelectionMode对象定义的值之一,我已经在表 24-7 中列出了这些值。

Images

确保您在打开的文件选择器中呈现给用户的布局遵循FileSelectionMode值是很重要的,否则您会让用户选择更多可以打开的文件,或者在他们应该可以选择多个文件时将他们限制为一个,从而使用户感到困惑。

我在示例中使用了selectionMode属性来改变ListView控件的selectionMode属性。即使属性名称相同,定义值的对象却不同,因此我必须从一个对象映射到另一个对象,如下所示:

... openListView.winControl.selectionMode = (pickerUI.selectionMode ==     provider.FileSelectionMode.single) ? WinJS.UI.SelectionMode.single         : WinJS.UI.SelectionMode.multi; ...

当用户选择或取消选择项目时,ListView控件触发selectionchanged事件,我使用由FileOpenPickerUI定义的方法来响应,以反映用户选择的文件。这些方法在表 24-8 中描述。

Images

当调用addFile方法时,传递一个代表用户选择的StorageFile和一个代表文件的惟一 ID。然后,您可以使用此 ID 来检查该文件是否已经是选择的一部分,或者将其从选择中删除。没有方法来枚举选择器中的文件,这是一个问题,因为selectionchanged事件并没有指出选择中发生了什么变化。这意味着我必须在每次用户改变ListView选择时清除选择的文件,并为每个选择的项目添加新的条目,以确保我没有在选择器中留下任何已被取消选择的文件。

除了调用addFile方法之外,不需要任何显式操作。你传递给addFileStorageFile对象被交给为用户打开文件的应用,所以你唯一的义务就是确保StorageFile对象与用户的选择相对应。通过监听FileOpenPickerUI定义的事件,你可以更深入地了解挑选过程,我已经在表 24-9 中描述了这些事件。

Images

总结

在这一章中,我向你展示了如何实现三个关键契约:文件激活、保存选择器和打开选择器。这些契约允许您将应用集成到 Windows 中,以便您可以代表用户处理文件,并为其他应用提供存储服务。在下一章中,我将向您展示共享契约,这是 Windows 8 的一项关键功能。

二十五、共享契约

共享契约允许用户在应用之间共享数据项。这是 Windows 应用的关键功能之一,它允许用户使用互不了解且仅共享相同数据格式的应用创建临时工作流。

需要两个应用参与共享契约。作为共享源的应用拥有用户想要共享的一项或多项数据。Windows 向用户提供了一个应用列表,称为共享目标,能够处理这些数据,用户选择他们想要从共享源接收项目的应用。在这一章中,我将向您展示如何在契约中创建两个参与者,并演示如何简化重复的操作,使用户的共享更简单。表 25-1 对本章进行了总结。

Images

创建示例应用

我将从创建一个共享源开始,这是一个提供数据供另一个应用使用的应用。我创建了一个简单的助手应用,名为ShareHelper。我将介绍不支持共享的基本应用,然后展示如何实现共享源代码功能。清单 25-1 显示了来自ShareHelper应用的default.html文件。

清单 25-1 。来自 ShareHelper 应用的 default.html 文件

`

         ShareHelper                                    **    
** **        
** **            ** **            ** **        
** **    
** **    
** **    
** `

这个应用的布局由一个大的ListView控件组成,使用了我在最近的其他例子中使用的相同类型的项目模板。这里的想法是向用户提供一组图像,他们可以从中进行选择,然后使用共享契约与PhotoAlbum应用共享这些图像,然后将共享的图像添加到相册中。

有了这样一个简单的布局,ShareHelper应用所需的 CSS 主要集中在模板中的元素上,正如你在清单 25-2 中看到的,它显示了css/default.css文件的内容。

清单 25-2 。来自 ShareHelper 应用的 css/default.css 文件

`body {
    display: -ms-flexbox;
    -ms-flex-direction: column;  -ms-flex-pack: center;}

listView

.imgContainer { border: thin solid white; padding: 2px; }

.listTitle { font-size: 18pt; max-width: 180px;
    text-overflow: ellipsis; display: block; white-space: nowrap;
    margin: 0 0 5px 5px; height: 35px;} .listImg {width: 300px; height: 200px;}            `

创建助手应用的下一步是加载一些图像并填充ListView控件。你可以在清单 25-3 中看到我是如何做的,它显示了js/default.js文件。我对Pictures库进行了深度查询,并显示了我在那里找到的所有图像文件。

清单 25-3 。ShareHelper 应用中 default.js 文件的内容

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;

app.onactivated = function (args) {
        if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                var list = new WinJS.Binding.List();
                listView.winControl.itemDataSource = list.dataSource;

storage.KnownFolders.picturesLibrary
                    .getFilesAsync(storage.Search.CommonFileQuery.orderByName)
                    .then(function (files) {
                        files.forEach(function (file) {
                            list.unshift({
                                img: URL.createObjectURL(file),
                                title: file.displayName, file: file
                            });
                        });
                    });
            }));
        }
    };
    app.start();
})();`

这是一个基本的应用,我以简洁的名义做了一些假设,其中最重要的是我假设Pictures库中的所有文件都是图像文件。

创建助手应用的最后一步是在清单中声明需要访问Pictures库。打开package.appxmanifest文件,导航到Capabilities部分,勾选Pictures Library选项,如图图 25-1 所示。

images

图 25-1。声明访问图片库

如果你启动应用,你会看到你的Pictures文件夹中的图片显示在ListView中,如图图 25-2 所示。

images

图 25-1。share helper app 的基本功能

创建共享源

尽管共享源是共享契约的关键部分,但不会对应用清单进行任何更改。相反,当应用启动时,一切都在 JavaScript 中处理。为了演示这一点,我对清单 25-4 中显示的ShareHelper应用中的/js/default.js文件进行了修改。

清单 25-4。在 ShareHelper 应用中创建共享源

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;
    var share = Windows.ApplicationModel.DataTransfer;     app.onactivated = function (args) {
        if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                var list = new WinJS.Binding.List();
                listView.winControl.itemDataSource = list.dataSource;

storage.KnownFolders.picturesLibrary
                    .getFilesAsync(storage.Search.CommonFileQuery.orderByName)
                    .then(function (files) {
                        files.forEach(function (file) {
                             list.unshift({
                                 img: URL.createObjectURL(file),
                                 title: file.displayName, file: file
                             });
                        });
                    });

**                    share.DataTransferManager.getForCurrentView()**
**                        .addEventListener("datarequested", function (e) {**

**                        var deferral = e.request.getDeferral();**

**                        listView.winControl.selection.getItems().then(function (items) {**
**                            if (items.length > 0) {**
**                                var datapackage = e.request.data;**

**                                var files = [];**
**                                items.forEach(function (item) {**
**                                    files.push(item.data.file);**
**                                });**
**                                datapackage.setStorageItems(files);**

**                                datapackage.setUri(new Windows.Foundation.Uri(**
**                                    "http://apress.com"));**

**                                datapackage.properties.title = "Share Images";**
**                                datapackage.properties.description**
**                                     = "Images from the Pictures Library";**
**                                datapackage.properties.applicationName = "ShareHelper";**
**                            } else {**
**                                e.request.failWithDisplayText(**
**                                    "Select the images you want to share and try again");**
**                            }**
**                        });**
**                        deferral.complete();**
**                    });**
            }));
        }
    };
    app.start();
})();`

我在清单中突出显示的代码做了两件事:它将应用注册为共享数据源,并在用户激活 Share Charm 时做出响应。我将在接下来的部分中解释每个活动。

注册为共享源

告诉 Windows 您的应用是共享数据的来源所需的技术不需要任何清单声明,也不是通过激活事件来完成的。相反,您必须处理位于Windows.ApplicationModel.DataTransfer名称空间中的DataTransferManager对象。(在示例中,我将这个名称空间别名为share。)对象DataTransferManager定义了我在表 25-2 中描述的事件。

images 注意在本章中,我引入的所有新对象都在Windows.ApplicationModel.DataTransfer名称空间中,除非我另有说明。

Images

要为这些事件注册一个处理函数,必须对由DataTransferManager.getForCurrentView方法返回的对象调用addEventListener方法。(如果你是一名. NET 程序员,这是另一个其结构更有意义的对象,但是只要你记得调用getForCurrentView,它在 JavaScript 中工作得非常好。)

对事件做出反应

我对本例中的datarequested事件感兴趣,该事件在用户激活 Windows Share Charm 时触发,提示我准备要共享的数据。传递给datarequested事件处理程序的对象有点奇怪,因为request属性,而不是detail属性,包含服务于共享操作所需的信息。

request属性返回一个DataRequest对象,用于创建一个数据包,该数据包将与另一个应用共享。我总结了表 25-3 中DataRequest对象定义的方法和属性。

Images

当您接收到datarequested事件时,您需要做的第一件事是调用getDeferral方法,如果您将执行任何异步方法调用的话。如果你不调用getDeferral,那么当你的处理函数执行完成时,Windows 会认为你没有提供任何数据。

getDeferral方法返回一个定义了complete方法的DataRequestDeferral对象。这是DataRequestDeferral定义的唯一方法,当您创建了数据包并准备好呈现给用户时,您可以调用它。

对于我的示例应用,我已经定义了一些代码来创建事件处理程序的概要,如清单 25-5 中的所示。

清单 25-5 。处理 datarequested 对象

`...
share.DataTransferManager.getForCurrentView().addEventListener("datarequested",
    function (e) {

**    var deferral = e.request.getDeferral();**

listView.winControl.selection.getItems().then(function (items) {
        if (items.length > 0) {
            // ...statements to prepare shared data go here...
        } else {
**            e.request.failWithDisplayText(**
**                "Select the images you want to share and try again");**
        }
    });
**    deferral.complete();**
});
...`

我调用getDeferral方法是因为我需要使用异步getItems方法从ListView控件中获取选择,如果用户没有选择任何要共享的图像,我调用failWithDisplayText方法。如果用户在没有选择ListView控件中的任何图像的情况下激活了 Share Charm,他们将会看到传递给failWithDisplayText方法的消息。这是将应用设置为共享数据提供者的基本模式,将数据的准备和打包工作留给自己。我将在下一节向您展示如何做到这一点。

打包共享数据

如果您的应用有可以共享的数据,那么您必须填充从DataRequest.data属性获得的DataPackage对象,以便可以定位共享目标并传递您的数据。通过读取传递给datarequested事件处理函数的对象的request属性来获得DataRequest对象。

DataPackage非常灵活,可以用来共享各种数据。我在表 25-4 中总结了向DataPackage添加数据的最有用的方法。

您可以使用表中所示的方法将数据添加到将与其他应用共享的包中。您的包可以包含不同类型的数据,但是如果您两次调用相同的方法,则第二次传递的数据将替换包中已有的数据。

images 提示你不必事先指定你的数据包的内容——你可以在datarequested事件到来的那一刻决定。这意味着你可以基于你的应用的状态共享不同种类的数据——例如,当用户在ListView控件中选择单个图像时,我可能会共享图像文件,如果选择了多个文件,我可能会共享文件名列表,如果用户导航到应用的不同部分,我可能会完全共享其他内容。共享契约的灵活性很大一部分来自于能够根据用户正在执行的任务选择共享的内容。

你可以在清单 25-6 中看到我如何使用setStorageItems方法将文件添加到数据包中,对应于用户在ListView中选择的图像。我还使用了setUri方法为用户提供一个链接,他们可以通过这个链接获得更多信息——在我的例子中,我使用了 URL apress.com作为占位符。

清单 25-6 。打包数据以供共享

`...
share.DataTransferManager.getForCurrentView()
    .addEventListener("datarequested", function (e) {

var deferral = e.request.getDeferral();

listView.winControl.selection.getItems().then(function (items) {
        if (items.length > 0) {
            var datapackage = e.request.data;

**            var files = [];**
**            items.forEach(function (item) {**
**                files.push(item.data.file);**
**            });             datapackage.setStorageItems(files);**

**            datapackage.setUri(new Windows.Foundation.Uri("http://apress.com"));**

datapackage.properties.title = "Share Images";
            datapackage.properties.description = "Images from the Pictures Library";
            datapackage.properties.applicationName = "ShareHelper";

} else {
            e.request.failWithDisplayText(
                "Select the images you want to share and try again");
        }
    });
    deferral.complete();
});
...`

Images 我在这个数据包上使用setUri方法的方式上故意引入了一个问题。在这一章的后面,我会回来解释这个问题以及为什么会经常遇到这个问题——所以,在你阅读完这一章的其余部分之前,不要使用setUri方法。

描述数据

DataPackage对象定义了一个名为properties,的属性,该属性返回一个DataPackagePropertySet对象(由于术语属性出现得如此频繁,本节中的对象和成员名称可能有点令人困惑)。

您使用DataPackagePropertySet来提供关于共享操作及其数据的附加信息,我已经在表 25-5 中描述了该对象定义的属性。

Images

这些属性不是为您设置的,尽管其中一些属性——如applicationListUrifileTypes——看起来应该设置。另一方面,您不需要为这些属性中的任何一个提供值,所以您只需要处理那些有助于用户理解共享操作的属性。

在我的例子中,我已经为其中三个属性设置了值,如清单 25-7 所示。当我在本章稍后将PhotoAlbum应用设为共享目标时,我将使用这些值来显示共享操作的细节,在下一节中您可以看到 Windows 是如何使用它们的。

清单 25-7 。用属性描述数据包

... datapackage.properties.title = "Share Images"; datapackage.properties.description = "Images from the Pictures Library"; datapackage.properties.applicationName = "ShareHelper"; ...

Images 提示您也可以使用insert方法向数据包添加自定义属性。在本章后面的添加自定义属性部分,我将向您展示一个这样的例子。

测试共享源代码应用

为了测试数据共享,使用 Visual Studio Debug菜单中的Start Debugging项启动ShareHelper应用,并在ListView中选择一个或多个图像。

打开魅力条,选择分享魅力(可以直接用Win + H激活分享魅力)。您将看到共享弹出按钮,其中包含可以处理由ShareHelper应用准备的数据包的应用列表,如图图 25-3 所示。

images

图 25-3。挑选应用的哪些数据将与哪些数据共享

我已经展开了共享弹出按钮的一部分,以便您可以看到我为数据包指定的属性的效果,以及它们是如何显示给用户来描述数据的。

图中有三个 app 可以从ShareHelper app 接收数据:MailPeopleSkyDrive。如果你在系统上安装了其他应用,你可能会看到其他条目,但请注意,列出的所有应用都是 Windows 应用商店应用,共享契约仅适用于应用,不包括传统的 Windows 桌面程序。

了解常见的共享问题

Windows 根据创建共享数据包时调用的set <XXX> 方法选择处理共享数据的应用。我调用了setStorageItemssetUri,所以 Windows 正在寻找可以处理文件或 URL 的应用。

关键词是——应用只需要声明支持包中任何一种类型的数据,就可以成为合适的共享目标。应用可以自由地从共享包中获取它们支持的数据,而忽略其他任何东西。

在内置 Windows 应用的情况下,Mail应用(一个电子邮件客户端)将使用文件作为新电子邮件消息的附件,People应用(一个社交媒体工具)将与我的社交媒体联系人共享 URL,SkyDrive应用将图像文件保存到云存储中。

这是我在清单 25-6 中制造的问题。我使用setUri向包中添加了一个 URL,它向用户提供了一些额外的信息,但与用户试图共享的内容没有直接关系。然而,如果用户选择PeopleMail应用作为分享目标,那么图像文件——真正的内容——将被丢弃,链接将被给予比它应有的更重要的对待。用户的联系人将被发送一个 URL,而没有用户试图共享的图像所提供的任何上下文——这对用户和消息的接收者来说是一个非常混乱的结果,可悲的是,这是共享数据时常见的错误。

如果您从“共享”弹出菜单中选择Mail应用,您就可以发现问题。Mail应用将显示在一个小的弹出菜单中,允许你通过选择收件人和添加一些文本来完成电子邮件,如图图 25-4 所示。

images 提示您需要在本地机器上执行这个测试,因为Mail应用无法在 Visual Studio 模拟器中正常启动。

images

图 25-4。与邮件应用分享数据

用户试图分享的图片文件被忽略了,更糟糕的是,邮件应用找到了网址,找到了其中包含的图片,并以 FlipView 的形式呈现给用户。这只是简单的混淆,因为用户选择了图像文件,但现在提供了完全不同的图像选择,这些图像是从我的示例应用悄悄添加到数据包的 URL 获得的。

回避问题

为了避免这个问题,确保添加到包中的每种类型的数据都是独立的,并且对用户有价值,这一点很重要。我的偏好是,仅当多种类型的数据等效时,才将它们添加到包中,例如,如果我正在共享本章的文本,我可能会添加 RTF 格式的手稿、纯文本等效内容和 SkyDrive 存储上内容的 URL。

在解决如何共享数据的问题时,我的基本想法是问自己,用户期望会发生什么?如果您不能立即将数据包中的项目与用户的合理预期相关联,您应该重新查看您的数据包内容。

如果您确实需要向数据包添加补充信息以支持用户选择的数据,您应该定义一个自定义属性。自定义属性不用于选择合适的共享目标应用,任何不知道其重要性的应用都可以忽略它们。您可以通过使用DataPackagePropertySet.insert方法向数据包添加一个自定义属性,传递您想要赋予该属性的名称及其值。在清单 25-8 的中,您可以看到我是如何用一个名为referenceURL的自定义属性替换了setUri方法的。

清单 25-8 。使用插入方法添加自定义属性

`...
var datapackage = e.request.data;

var files = [];
items.forEach(function (item) {
    files.push(item.data.file);
});
datapackage.setStorageItems(files);

// This statement is now commented out
//datapackage.setUri(new Windows.Foundation.Uri("http://apress.com"));

datapackage.properties.title = "Share Images";
datapackage.properties.description = "Images from the Pictures Library";
datapackage.properties.applicationName = "ShareHelper";
datapackage.properties.insert("referenceURL", "http://apress.com");
...`

如果你重启ShareHelper应用,在ListView中选择一些图像,并再次激活共享魔咒,你将在目标列表中只看到MailSkyDrive应用——这是因为People应用不能处理共享数据包中的文件,因此不会被 Windows 选为数据的目标。如果您选择Mail应用,您会看到用户选择的图像文件已经作为附件添加到消息中,如图图 25-5 所示。

images

图 25-5。用邮件应用分享文件

创建共享目标

在这一部分,我将通过把我在第二十四章的中创建的PhotoAlbum应用变成一个共享目标并允许它接收包含文件的数据包来展示共享契约的另一面。

提醒一下,PhotoAlbum应用的基本功能允许用户选择要在简单相册中显示的图像文件,该相册显示为在WinJS.UI.ListView控件中显示的一系列缩略图。

在前一章中,我在此基础上实现了文件激活、保存选取器和打开选取器合约,展示了应用集成到操作系统的不同方式——这是我将在本章分享合约中继续讨论的主题。提醒一下,图 25-6 显示了PhotoAlbum显示图像时的样子。

images

图 25-6。相册示例 app

我不打算重新列出示例应用的代码和标记,因为你可以在第二十四章中找到它们,或者从Apress.com下载该项目作为源代码包的一部分。我知道不得不前后翻页到另一章会令人沮丧,但另一种选择是花 10 页列出你已经看过的代码,我宁愿用这些空间向你展示新的契约和特性。

更新清单

与共享源不同,共享目标必须在清单中声明它们的功能。这是有意义的,因为共享源需要自由地共享用户正在处理的任何数据,而共享目标将有预先确定的方式来处理一组数据类型。

对于这一章,我将增强PhotoAlbum应用,使其能够处理包含.jpg.png文件的数据包。为此,从 Visual Studio 的Solution Explorer窗口打开package.appxmanifest文件,并导航到Declarations部分。从Available Declarations列表中选择Share Target并点击Add按钮。与其他契约一样,显示附加细节的表格,如图图 25-7 所示。显示红色警告是因为我还没有填充表单。

images

图 25-7。将份额目标申报添加到清单中

声明数据格式

你可以用两种方式之一来表达你对共享目标的支持。第一种是使用数据格式,这可以通过单击清单表单的Data formats部分中的Add New按钮来完成。当你点击这个按钮时,你会看到一个Data format字段,你可以在其中输入你的应用支持的数据格式。

Data format字段接受与DataPackage对象中的方法相对应的值,我在本章前面已经描述过了。表 25-6 提供了共享源可以使用的DataPackage对象中的方法和相应的Data format值之间的快速参考,共享目标必须在清单中声明这些值才能接收那种包。

images 提示请记住,如果正在共享的数据包包含至少一种您声明的类型,Windows 会将您的应用包括在呈现给用户的共享目标列表中。您不需要声明Data format值的精确组合来接收包含多种类型的包。同样,你不应该声明支持任何你的应用不能使用的数据类型。

Images

如果您想支持多种类型的数据,比如文件和 URL,再次点击Add New创建额外的Data format字段,并从表格中输入适当的值。完成后,键入Control+S保存清单更改。

声明文件类型支持

您还可以通过使用清单的Supported file types section指定单个文件类型来声明对共享目标联系人的支持。这允许您支持包含您的应用可以处理的文件类型的数据包,而不是任何文件(这是StorageItems数据格式的效果)。

区别很重要。例如,Mail应用想要处理文件,但它并不关心它们是什么,因为每个文件都适合作为电子邮件的附件。我需要对PhotoAlbum应用更有选择性,因为我只支持图像文件——例如,我无法在相册应用中使用 Excel 电子表格,因此我需要确保我的应用只是 PNG 和 JPG 文件的共享目标。

images 提示我将向您展示清单声明的两个部分是如何分别工作的,但是您可以在同一个声明中使用这两个部分来支持数据类型和文件类型的混合。您必须为此联系人指定至少一种文件类型或数据格式,否则您可以随意组合。

要为PhotoAlbum应用设置清单声明,请单击Add New按钮创建一个新的File type字段,并输入.png文件扩展名(包括句点)。再次点击Add New按钮,在File type字段输入.jpg。清单应该类似于图 25-8 中所示的清单。当您添加了第一种文件类型后,红色警告标记将会消失,告诉您清单声明是有效的。

images

图 25-1。在相册清单中增加对特定文件类型的支持

如果数据包包含至少一个与您指定的类型相匹配的文件,Windows 会将您的应用添加到共享目标列表中。这意味着您可能会收到包含一些您无法处理的文件的包——您有责任在包中找到您可以处理的文件,并忽略其余的文件。(你可以在本章后面看到我是如何做到这一点的。)

响应激活事件

当您的应用被选为数据包的共享目标时,Windows 会使用activated事件通知您。激活事件的detail.kind属性设置为ActivationKind.shareTarget。在清单 25-9 中,您可以看到我在PhotoAlbum项目中对/js/default.js文件的添加,以响应这一事件。

清单 25-9 。响应被激活的事件

... switch (args.detail.kind) { **    case activation.ActivationKind.shareTarget:** **        WinJS.Navigation.navigate("/pages/shareTargetView.html",** **            args.detail.shareOperation);** **        break;**         // *... statements for other activation types removed for brevity...*     default:         WinJS.Navigation.navigate("/pages/albumView.html");         break; } ...

args.detail.shareOperation属性返回一个Windows.ApplicationModel.DataTransfer.ShareTarget.ShareOperation对象,该对象提供对数据包的访问,并提供一些方法,通过这些方法,我可以在处理数据包时向 Windows 发送进度信号。我将ShareOperation对象传递给WinJS.Navigation.navigate方法,以便它可以在我创建的用于处理shareTarget激活事件的内容文件中使用,该文件名为shareTargetView.html,我将它添加到了PhotoAlbum Visual Studio 项目的pages文件夹中。在接下来的小节中,我将向您展示这个文件的内容。

images 注意需要注意的重要一点是,你的应用的一个新实例被启动来处理shareTarget事件。这意味着如果您的应用已经在运行,那么将会创建第二个实例。您将需要提供某种协调,以便用户启动的应用实例反映 Windows 启动的实例所做的更新,以处理共享数据包。对于这个例子,我将把用户共享的文件复制到本地应用数据文件夹中,PhotoAlbum应用将监视这个文件夹的变化。

处理共享操作

在清单 25-10 中,您可以看到shareTargetView.html文件的内容,我创建这个文件是为了给PhotoAlbum应用添加对共享目标契约的支持。script元素中的两个关键函数是占位符,我将在本节稍后完成。

清单 25-10 。shareTargetView.html 文件的最初内容

`

              

    
    
Shared by:
    
(For info: )
    
    
    
                      
`

即使有不完整的函数,这个文件中仍然有一些重要的事情在进行。对我来说,解释它们的最好方式是向你展示完成后的页面是什么样子,然后再返回。图 25-9 显示了 Windows 用来处理共享数据包的shareTargetView.html文件。

images 注意此时您将无法重新创建这个图,因为 default.js 文件中的关键函数尚未实现。

images

图 25-9。相册 app 收到分享数据包

Windows 在 650 像素的窗格中显示共享目标应用(其中 645 像素可供应用使用)。对于我的示例应用,我使用的布局包含一些标题信息(我将使用共享数据包中的属性填充)、一个ListView(我将使用数据包中的StorageFile对象填充)和两个button元素。

这些按钮允许用户进一步细化他们对包中文件的选择。点击Add All按钮会将数据包中的所有文件复制到本地 app data 文件夹中,并添加到相册中。点击Add Selected按钮将仅对用户在ListView中选择的图像进行操作。

我喜欢为用户提供进一步过滤数据包内容的选项,因为我对数据来自的应用一无所知。这在分享过程中创造了一个额外的步骤,这是一件坏事,但许多应用似乎不会让用户在分享前微调他们的选择,这更糟糕。我通过支持快速链接特性来证明向流程中添加额外步骤是正确的,我将在本章的后面对此进行描述。

报告进度

ShareOperation对象定义了许多方法,当你处理数据包时,这些方法用来通知窗口,如表 25-7 中所述。

你可以看到我是如何在清单 25-11 中的shareTargetView.html页面的ready函数中使用这些方法的,在这里我重复了代码并突出显示了关键语句。

清单 25-11 。处理共享包时使用 ShareOperation 方法

`...
ready: function (element, shareOperation) {

processPackage(shareOperation.data).then(function (list) {
        if (list.length == 0) {
**            shareOperation.reportError("No images files were shared");**
            return;
        }

shareListView.winControl.itemDataSource = list.dataSource;
        WinJS.Utilities.query("button.addButton").listen("click", function (e) {
**            shareOperation.reportStarted();**
            if (this.id == "addAll") {
**                shareListView.winControl.selection.selectAll();**
            }
            var filesToProcess = [];
            shareListView.winControl.selection.getItems().then(function (items) {
                items.forEach(function (item) {
                    filesToProcess.push(item.data.file);
                });
            });

copySelectedFiles(filesToProcess).then(function () {
**                shareOperation.reportDataRetrieved();**
**                shareOperation.reportCompleted();**
            });;
        });
    });
}
...`

只有当用户不再需要与你的应用交互时,你才应该调用reportStarted方法——这是因为 Windows 可能会关闭你的应用,并允许共享操作在后台继续,从而允许用户继续使用共享源应用。

对于我的例子来说,这意味着我不能调用reportStarted,直到用户点击其中一个按钮,表明他们想要导入哪些文件。我一解析完包中的内容就调用reportStarted方法,并在将内容复制到本地 app data 文件夹后调用reportDataRetrievedreportCompleted方法。

如果我从processPackage函数得到的WinJS.Binding.List对象不包含任何我可以操作的文件,我就调用reportError方法。在理想情况下,我不需要这样做,因为 Windows 已经将包的内容与清单声明中的文件类型进行了匹配,但我将此检查添加到我的共享目标应用中,以处理编写得不好的应用。一些共享源应用错误地设置了数据包中fileTypes属性的值(如本章前面所述)——该值会覆盖真正正在使用的文件类型,这会导致 Windows 发送不包含任何有用文件的我的应用包。

images 注意错误呈现给用户的方式有点奇怪。调用reportError方法时,共享目标 app 立即关闭。然后向用户显示一条通知消息,告诉他们出现了一个问题。只有当他们点击通知时,他们才能看到您传递给reportError方法的消息。

处理数据包

ShareOperation.data属性返回一个DataPackageView对象,它是数据包的只读版本。您使用这个对象来了解发送给您的包,并获取其中包含的数据。表 25-8 显示了DataPackageView对象定义的方法。

Images

从包中检索数据的方法都是异步的,并返回一个WinJS.Promise对象,该对象在完成时产生适当类型的数据。

contains方法让您检查包是否包含给定的数据类型。你可以将表 25-6 中的一个字符串值传递给这个方法,或者使用StandardDataFormats对象中的一个值,我已经在表 25-9 中列出了。

你可以在清单 25-12 中看到我是如何使用containsgetStorageItemsAsync方法的,它展示了我是如何在shareTargetView.html文件中实现processPackage函数的。

清单 25-12 。完成 processPackage 方法

... function processPackage(data) {     if (**data.contains(share.StandardDataFormats.storageItems)**) {         return **data.getStorageItemsAsync()**.then(function (files) {             var fileList = new WinJS.Binding.List();             files.forEach(function (file) {                  if (file.fileType == ".jpg" || file.fileType == ".png") {                      fileList.unshift({                           img: URL.createObjectURL(file),                           title: file.displayName,                           file: file                      });                  }             });             appName.innerText = data.properties.applicationName;             shareTitle.innerText = data.properties.title;             var refLink = data.properties["referenceURL"]             infoAnchor.innerText = infoAnchor.href = refLink == null ? "N/A" : refLink;             return fileList;         });     }; } ...

我首先使用contains方法来确保数据包中有文件或文件夹——这是我在示例应用中支持的唯一一种数据,否则没有必要进一步处理数据包。

我调用getStorageItemsAsync方法并检查传递给then方法的每个对象的fileType属性,这允许我过滤掉错误类型的文件夹和文件。我为我找到的每个图像文件添加一个对象到一个WinJS.Binding.List中,该对象的属性意味着我可以使用 default.html 文件中的 HTML 模板在ListView UI 控件中显示它(这是我在第二十四章的中用于所有PhotoAlbum示例的相同模板)。

DataPackageView.properties属性返回一个对象,您可以使用该对象获取由共享源应用添加到数据包中的属性。我读取了applicationNametitle属性的值,并检查了ShareHelper应用添加到包中的自定义属性是否存在。我使用这些值来设置布局中一些 HTML 元素的内容。

images 提示在告诉 Windows 你已经完成了共享操作之前,你必须小心确保你的异步方法调用已经完成。在这个例子中,我通过从processPackage函数返回一个Promise来完成这个任务,只有当我接收并处理了来自getStorageItemsAsync方法的结果时,这个任务才会完成。

复制数据

剩下的工作就是将数据从共享包复制到本地应用数据文件夹,我通过实现copySelectedFiles函数来完成,如清单 25-13 所示。

清单 25-13 。实现 copySelectedFiles 函数

... **function copySelectedFiles(files) {** **    var promises = []**; **    files.forEach(function (file) {;** **        var promise = localFolder.createFileAsync(file.name,** **            storage.CreationCollisionOption.replaceExisting)** **            .then(function (newfile) {** **                return file.copyAndReplaceAsync(newfile).then(function () {** **                    App.processFile(newfile);** **                });** **            });** **        promises.push(promise);** **    });** **    return WinJS.Promise.join(promises)** **}** ...

这个函数中没有新的技术——你可以在第二十二章的中了解基本的文件操作,并在第九章的中了解如何使用Promise.join方法。completed copySelectedFiles函数将用户从数据包中选择的所有文件复制到本地 app data 文件夹中,并返回一个Promise,只有当所有复制操作都完成时,该函数才会完成(我这样做是为了确保在我知道已经完成了数据包的内容之前不会调用ShareOperation方法)。

我复制文件而不是使用我在数据包中收到的位置有两个原因。首先,如果有另一个示例应用正在运行,我可以更容易地确保相册中的新内容得到反映。这是我在第二十四章中实现应用到应用选取器契约时遇到的相同问题,我已经用相同的方式解决了它——通过将我正在处理的文件复制到一个位置,该位置由PhotoAlbum应用监控新文件。我承认这是一个小技巧,但是 Windows 同时创建你的应用的两个实例的情况很少,尽管我可能会尝试,但我无法找到更好的方法在它们之间进行通信。我复制文件的第二个原因是,当共享操作结束时,我不知道共享源应用打算如何处理它们。我需要制作一个副本,以确保用户可以使用这些图像。

测试共享目标实现情况

共享目标契约的实施已经完成,现在您可以与PhotoAlbum应用共享图像文件。你已经知道如何用ShareHelper应用来做这件事,但是分享契约的一个好处是你可以从任何应用接收兼容的数据包。为此,转到桌面并使用File Explorer找到一个在Pictures库之外的图像文件。

右键单击该文件,从弹出菜单中选择Open with,并从列表中选择Photos(这是 Windows 8 附带的默认图像查看器应用,从桌面选择它意味着您不必担心图像文件类型的默认应用,如果您遵循第二十四章中的中的示例,它很可能是PhotoAlbum应用)。

激活分享魔咒,从目标应用列表中选择Photo Album。你会看到来自shareTargetView.html文件的布局,如图图 25-10 所示。点击Add All按钮,启动PhotoAlbum应用——你会看到你分享的图片显示出来。

images

图 25-10。分享来自 Windows Photos 应用的图片文件

在我继续之前,还有最后一点需要注意。我让您在Pictures库之外定位一个图像文件的原因是,我想演示打包StorageFile对象的方式将访问这些文件的隐含权限转移到目标应用。一切都按照它应该的方式运行——例如,您不必担心确保目标应用已被授予读取包含该文件的文件夹的权限。

当然,有了这种隐含的许可,就隐含了对您的信任,即您不会对数据做一些意想不到的事情。您应该确保在没有获得用户明确许可的情况下,不要删除或修改原始文件。

创建快速链接

快速链接是一个预先配置的动作,允许用户使用他们之前做出的细节或决定来执行简化的共享动作。在我的PhotoAlbum应用中,我让用户选择他们想要复制和导入的数据包中的图像。每次用户与PhotoAlbum共享文件时,强迫用户做出相同的决定是重复和令人讨厌的,尤其是因为他们很有可能已经花时间在 share source 应用中选择了他们想要的图像。

为了简化我的应用,我将创建一个快速链接,允许用户导入所有文件,而无需与我的应用布局进行任何交互。使用快速链接有两个阶段——创建它们和接收它们——我将在接下来的小节中向您展示这两个阶段。

创建快速链接

您可以通过向ShareOperation.reportCompleted方法传递一个QuickLink对象来创建一个快速链接,这个对象可以在Windows.ApplicationModel.DataTransfer.ShareTarget名称空间中找到。配置QuickLink对象,使其包含应用重复用户刚刚执行的共享操作所需的所有信息。QuickLink对象定义了表 25-10 中所示的属性。

Images

你可以在清单 25-14 中看到我如何为PhotoAlbum应用创建了一个QuickLink,它显示了我对shareTargetView.html文件中的ready函数所做的更改。

清单 25-14 。在共享操作结束时创建快速链接

`...
WinJS.Utilities.query("button.addButton").listen("click", function (e) {

if (this.id == "addAll") {
        shareListView.winControl.selection.selectAll();
    }
    var filesToProcess = [];
    shareListView.winControl.selection.getItems()
        .then(function (items) {
            items.forEach(function (item) {
                filesToProcess.push(item.data.file);
            });
        });     copySelectedFiles(filesToProcess).then(function () {
        shareOperation.reportDataRetrieved();

if (e.target.id == "addAll") {
**            var qlink = new share.ShareTarget.QuickLink();**
**            qlink.id = "all";**
**            qlink.supportedFileTypes.replaceAll([".png", ".jpg"]);**
**            qlink.title = "Add all files";**
**            qlink.thumbnail = storage.Streams.RandomAccessStreamReference.**
**                createFromUri(Windows.Foundation.Uri("ms-appx:img/logo.png"));**
**            shareOperation.reportCompleted(qlink);**

} else {
            shareOperation.reportCompleted();
        }
    });
});
...`

我想创建一个快速链接,只有当用户从数据包中选择了所有的图像,因为没有办法,我可以在未来有效地重复选择单个图像。其他种类的应用可以明智地提供一系列快速链接——例如,如果你正在编写一个电子邮件应用,你可能会创建QuickLink对象,以便用户可以快速发送电子邮件给以前的收件人。

您可以看到这些变化的效果,但这需要一段时间。启动PhotoAlbum应用(确保运行最新版本)。然后启动ShareHelper应用,使用ListView控制键选择图像,激活Share Charm,从列表中选择PhotoAlbum。点击Add All按钮。

保持在ShareHelper应用内,再次激活分享图标,你会在分享弹出菜单上看到一个新项目,如图图 25-11 所示。这是一个快速链接,在本例中,它允许用户在一个步骤中添加他们选择共享的所有文件。我还没有在PhotoAlbum应用中添加代码来处理快速链接——我将在下一节中做这件事——但是图中显示了快速链接是如何呈现给用户的。

images

图 25-11。分享弹出菜单中增加了一个快速链接

简单回顾一下,快速链接用于允许用户重复经常执行的共享操作。这就是为什么我让你激活两次分享魔咒:第一次定义了操作,第二次显示为快速链接。

选择快速链接目前只是加载标准的共享目标布局,但在下一节中,我将向您展示如何识别快速链接何时被选择,以便您可以简化共享操作。

接收快速链接

当你的应用被激活时,你可以通过读取ShareOperation.quickLinkId属性来确定用户是否选择了你的一个快速链接。该属性返回的值是您分配给QuickLink.id属性的值,允许您确定用户想要重复哪个共享操作。我的示例应用只有一个快速链接 id——all——你可以在清单 25-15 的中看到我是如何响应用户选择它的。

清单 25-15 。检测用户何时选择了快速链接

`...
ready: function (element, shareOperation) {

processPackage(shareOperation.data).then(function (list) {
        if (list.length == 0) {
            shareOperation.reportError("No images files were shared");
            return;
**        } else if (shareOperation.quickLinkId == "all") {**
**            shareOperation.reportStarted();**
**            var files = [];**
**            list.forEach(function (listItem) {**
**                files.push(listItem.file);**
**            });**
**            copySelectedFiles(files).then(function () {**
**                shareOperation.reportDataRetrieved();**
**                shareOperation.reportCompleted();**
**            });**
**        } else {**

shareOperation.reportStarted();

shareListView.winControl.itemDataSource = list.dataSource;
            WinJS.Utilities.query("button.addButton").listen("click", function (e) {
               // ...statements removed for brevity...
            });
        }
    });
}
...`

当用户选择快速链接时,我处理所有文件,不提示用户输入任何内容。结果是一个简化的共享操作,甚至不向用户呈现界面——文件只是无缝地添加到相册中。

总结

在这一章中,我向你展示了如何实现共享契约的两个部分,让你能够让数据从一个应用平稳地流向另一个应用。共享源应用负责打包数据并使其可供 Windows 使用,Windows 充当一个代理来查找可以处理共享数据的合适的共享目标应用。共享是关键的 Windows 交互之一,我鼓励你将它添加到你的应用中,并以支持最广泛的数据格式和文件类型的方式进行。你给用户分享你的应用的机会越多,你的应用就越能深入他们的工作流程。

在下一章,我将展示 Windows 8 支持的其他一些契约。

二十六、自动播放、协议激活和打印契约

在这一章中,我将向你展示如何实现另外三个契约来更紧密地将你的应用集成到 Windows 中:自动播放契约、协议激活契约和打印契约。表 26-1 提供了本章的总结。

Images

重温示例应用

对于这一章,我将继续使用我在第二十四章中创建并在第二十五章中扩展的PhotoAlbum应用。作为一个提醒,这个应用的基本功能让用户选择要在一个简单的相册中显示的图像文件(这只是一系列显示在WinJS.UI.ListView控件中的缩略图)。

我在此基础上实现了文件激活、保存选取器和打开选取器,以及共享合约,展示了应用集成到操作系统的不同方式——这是我将在本章中通过添加对更多合约的支持来继续的主题。提醒一下,图 26-1 展示了PhotoAlbum显示图像时的样子。

images

图 26-1。相册示例 app

Images 注意再次声明,我不打算重新列出示例的代码和标记,因为你可以在第二十四章中找到它们,或者从Apress.com下载该项目作为源代码包的一部分。

实施自动播放契约

自动播放契约允许您的应用在新存储连接到 Windows 8 设备时自动做出响应。AutoPlay 近年来已经失宠,因为它一直被用作让个人电脑感染恶意软件的手段,但针对 Windows 8 进行了改进,并有可能被更广泛地使用,特别是在用户喜欢简单而不是安全的平板电脑设备上(一般来说,我发现如果有机会,用户会更喜欢任何东西而不是安全)。

像所有的契约一样,实现自动播放是可选的,但却是值得的,尤其是当你的应用以任何方式处理媒体文件的时候。正如你将看到的,自动播放契约建立在我在第二十四章中展示的文件激活契约的基础上,一旦你实现了文件激活,只需要一点额外的工作。

更新清单

自动播放契约需要进行两项清单更改。首先,我需要声明我的应用想要访问可移动存储。为此,从 Visual Studio 的Solution Explorer窗口中打开package.appxmanifest文件,点击Capabilities选项卡,在功能列表中勾选Removable Storage项,如图图 26-2 所示。

images

图 26-1。在应用清单中启用移动存储功能

此外,我还必须告诉 Windows 我的应用希望如何集成到自动播放功能中,这需要移动到清单的Declarations选项卡。

Available Declarations列表中选择AutoPlay Content,点击Add按钮。填写属性部分以匹配图 26-3 ,点击Add New按钮创建新的Launch action部分。

images

图 26-3。宣布支持自动播放契约

很难阅读图像中的文本,所以我在表 26-2 中列出了清单表单字段的必需值。

Images

Content event值是您希望应用得到通知的 Windows 事件的名称。对于我的例子,我对ShowPicturesOnArrivalMixedContentOnArrivalStorageOnArrival事件感兴趣。这些是而不是 JavaScript 事件,声明中的条目作为 Windows 内部和你的应用之间的映射——我将很快向你展示这些事件是如何呈现给你的应用的。确保如我所展示的那样将事件名称大写——如果您采用 JavaScript 全小写约定,您的契约实现将会失败。表 26-3 列出了最常见的 Windows 自动播放事件。

Images

要弄清楚每种事件被触发的环境,需要对 Windows 进行一些研究。我发现在广泛的事件中注册兴趣更容易,这就是为什么如果用户连接包含图像的存储设备,我会为我可以预期的每个事件创建声明。

verb值是一个特定于应用的字符串,您将使用它来标识 Windows 事件。我的示例应用只能将文件添加到相册中,这就是为什么我的动词是addpicturesaddmixedaddstorage,以及为什么Action Display Name,它是给用户的应用将通过自动播放执行的动作的描述,在每种情况下都被设置为Add to Photo Album。一个更复杂的应用可能会为每个事件提供多个操作,例如,播放、复制、打印或压缩存储设备上的文件。

响应激活事件

自动播放契约的激活事件的detail.kind属性被设置为ActivationKind.file。这与用于文件关联契约的值相同。通过读取detail.verb属性的值来区分事件相关的契约,对于文件关联,该属性将被设置为open,对于自动播放,该属性将被设置为您分配给清单声明的verb字段的值。对于我的例子,这意味着我可以预期文件关联的verb属性是open,自动播放契约的属性是addpicturesaddmixedaddstorage,这取决于哪个 Windows 事件被触发。

为了响应自动播放的verb值,我对清单 26-1 中的PhotoAlbum default.js文件进行了修改。

清单 26-1 。更新 default.js 文件以支持自动播放

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;

var query = storage.ApplicationData.current.localFolder.createFolderQuery();
    query.addEventListener("contentschanged", function () {
        App.loadFilesFromCache();
    });
    query.getFoldersAsync();

app.onactivated = function (args) {
        if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {

if (ViewModel.fileList.length == 0) {
                    App.loadFilesFromCache();
                }

switch (args.detail.kind) {
                    case activation.ActivationKind.fileOpenPicker:
                        var pickerUI = args.detail.fileOpenPickerUI;
                        WinJS.Navigation.navigate("/pages/openPickerView.html",
                            pickerUI);
                        break;
                    case activation.ActivationKind.fileSavePicker:
                        var pickerUI = args.detail.fileSavePickerUI;
                        WinJS.Navigation.navigate("/pages/savePickerView.html",
                            pickerUI);
                        break;
**                    case activation.ActivationKind.file:**
**                        switch (args.detail.verb) {**
**                            case 'addpictures'😗*
**                            case 'addmixed'😗*
**                            case 'addstorage'😗*
**                                WinJS.Navigation.navigate(“/pages/autoplayView.html”,                                     args.detail.files);**
**                                break;**
**                            case 'open'😗*
**                                args.detail.files.forEach(function (file) {**
**                                    App.processFile(file);**
**                                });**
**                                WinJS.Navigation.navigate(“/pages/albumView.html”);**
**                                break;**
**                        }**
**                        break;**
                    default:
                        WinJS.Navigation.navigate("/pages/albumView.html");
                        break;
                }
            }));
        }
    };
    app.start();
})();`

当处理一个file激活事件时,我现在查看verb事件属性的值。如果值是open,我知道我正在处理文件关联契约,我从detail.files属性为数组中包含的每个文件调用App.processFile函数,就像我在上一章所做的一样。

当我得到一个其他的verb值时,我知道我正在处理自动播放契约,并且我通过导航到一个新的内容页面来响应,这个页面是我添加到项目pages文件夹中的,名为autoPlayView.html。当调用WinJS.Navigation.navigate方法时,我传递detail.files属性的值,这样我就可以在内容文件中使用它(你可以在第七章中了解更多关于这种技术的内容)。您可以在清单 26-2 中看到autoPlayView.html文件的内容。

清单 26-2 。autoPlayView.html 文件的内容

`

              

    
Select images to add to the album
    
    
     `

该文件呈现的布局基于一个ListView控件,在该控件中,我显示我在存储设备上找到的图像,允许用户选择应该导入到相册中的图像。还有一个button,用户点击它表示他们已经选择了想要的文件。

Images 注意我可以使用一个标准的打开文件选择器,让用户选择他们想要从存储设备导入的文件,但是没有办法禁用选择器导航控件。这意味着用户可以离开存储设备,从任何地方挑选文件。这并不总是所有应用的问题,但我想在这个例子中限制用户的选择,这就是我使用自定义布局的原因。

这个文件中的代码是我在前面章节中使用的技术的复述,并做了一些调整以适应自动播放契约。来自激活事件的detail.files属性的值作为folders参数传递给ready函数——我更改了名称,因为当自动播放契约的事件被触发时,detail.files属性返回的数组实际上包含了StorageFolder对象。通常只有一个文件夹,它是可移动存储设备的根目录。我处理数组中的所有项目,只是为了预防可移动设备的不同行为(微软对于是否会有多个文件夹被发送到应用非常含糊。)

为了安全起见,我查询了数组中的每个文件夹,并构建了我在一个WinJS.Binding.List对象中找到的文件的详细信息,我将它用作ListView控件的数据源。此时,我不想将存储设备的内容与应用中的其余图像混合在一起,这就是为什么我在本地将List定义到这个文件中,而不是使用视图模型中的那个。

Images 提示 Windows 将过滤查询返回的文件,因此只有那些匹配自动播放清单声明的文件才会包含在结果中。

当用户点击Add Selected Images按钮时,我获取所选的ListView项,并将文件复制到本地 app data 文件夹中。通过复制文件,我可以将图像作为相册的一部分显示,即使自动播放存储设备已断开连接或被移除。

一旦文件操作开始,我就导航到/pages/albumView.html文件,这样用户就可以看到他们添加的效果。

测试契约执行情况

现在,您已经看到了我是如何实现该契约的,是时候看看它是如何运行的了。首先,启动应用,以便在 Windows 设备上安装最新版本。应用不需要运行来处理自动播放事件,因此如果您愿意,您可以在此时停止或终止应用。

接下来就是准备 Windows 8 的机器了。这不是用户会采取的步骤,但我想展示一个特殊的效果。打开自动播放控制面板,确保勾选了Use AutoPlay for all media and devices选项,并将Removable drive选项设置为Ask me every time,如图 26-4 中所示。

images

图 26-4。配置自动播放测试契约执行

接下来,插入一些包含 JPG 或 PNG 文件的可移动存储设备,如 u 盘或相机存储卡。我在测试中使用了 u 盘,但几乎任何可移动存储设备都可以。你会看到一个弹出的提示信息,就像图 26-5 中的所示。

images

图 26-5。自动播放祝酒词

单击 toast,告诉 Windows 您要在存储设备上执行什么操作。窗口将显示一组选项,如图 26-6 所示。根据您安装的应用,您可能会看到不同的选项。我突出显示了您应该选择的操作,这显示了示例应用和我在前面的清单中指定的消息。

images

图 26-6。选择可移动硬盘的自动播放动作

点击Add to Photo Album项,Windows 将启动示例应用。激活事件将加载autoPlayView.html文件作为布局,查询存储设备的文件内容,找到的图像文件在ListView控件中呈现给用户,如图图 26-7 所示。

images

图 26-7。通过呈现可移动存储设备上的图像来响应自动播放事件

选择图像并点击Add Selected Images按钮会将图像复制到本地应用数据文件夹,并将布局切换到pages/albumView.html文件。

测试提示

当您测试 AutoPlay 契约的实现时,插拔存储设备可能会变成一个乏味的过程。你可以做一些事情来简化这个过程。第一个是在自动播放控制面板中为您正在使用的设备类型更改设置,如图图 26-8 所示。您可以看到示例应用包含在应用列表中,当连接新的存储设备时,可以选择该列表作为默认应用。

images

图 26-8。将示例应用设置为可移动驱动器的默认自动播放动作

您可以通过插入硬件一次,然后在每次想要测试时使用文件资源管理器触发自动播放操作,来避免完全处理硬件。在文件浏览器窗口中右键单击设备,在弹出菜单中选择Open AutoPlay,如图 26-9 所示。

images

图 26-3。无需移除和连接存储设备即可触发自动播放操作

这些技术使测试自动播放契约实现成为一种更加愉快的体验,尤其是当您处理大量不同的事件和动词时。

实现协议激活契约

协议激活契约让你的应用处理标准的 URL 协议,比如mailto,它用于启动创建和发送电子邮件的过程。该合约还可用于处理自定义协议,这些协议可用于执行应用之间的基本通信,或在您的网站和 Windows 应用之间移交任务(您在网页中嵌入了具有特定协议的链接,当用户单击该链接时,该链接会激活应用)。

在本节中,我将演示如何处理自定义协议,并使用它将数据从一个应用传递到另一个应用。

创建助手应用

首先,我需要创建助手应用,它将向用户呈现使用我的自定义协议的链接。我创建了一个名为ProtocolHelper的新应用,这个应用非常简单,所有的东西 HTML、CSS 和 JavaScript——都包含在default.html文件中,如清单 26-3 所示。

清单 26-3 。来自 ProtocolHelper 应用的 default.html 文件

`

         ProtocolHelper                          ` `          Protocol Link `

这个应用执行对Pictures库的深度查询,并显示它找到的第一张图像的缩略图。该布局还包含一个a元素,其href属性包含一个带有自定义协议的 URL,如下所示:

<a id="linkElem" **href=”photoalbum:C:\Users\adam\Pictures\flowers\astor.jpg”**>Protocol Link</a>

协议设置为photoalbum,网址的其余部分包含助手应用找到并显示的文件路径。在图 26-10 中可以看到助手应用运行时的样子。

Images 注意为了确保助手应用可以找到文件,您需要检查清单中的Pictures Library功能。

images

图 26-3。protocol helper app 的布局

点击Protocol Link会让 Windows 寻找一个可以处理自定义协议的应用。当然,目前还没有这样的应用,所以你会看到一个类似于图 26-11 所示的消息。在下一节中,我将向您展示如何在PhotoAlbum应用中添加协议激活支持。

images

图 26-11。没有应用处理 URL 协议时显示的消息

增加协议激活支持

现在我有了一个包含与photoalbum协议的链接的应用,我可以返回到PhotoAlbum应用并添加对处理该协议的支持,这意味着当你单击助手应用中的链接时,PhotoAlbum将被激活并被赋予计算出该做什么的任务。

添加清单声明

第一步是更新清单。从 Visual Studio Solution Explorer窗口打开package.appxmanifest文件,并导航到Declarations选项卡。从Available Declarations列表中选择Protocol,点击Add。将显示文本字段,供您输入想要支持的协议的详细信息。在Name字段中输入photoalbum并键入Control+S保存对清单的更改。(在本例中,您可以忽略LogoDisplay name字段,它们用于区分支持相同协议的应用,这种情况下不会发生。)你可以在图 26-12 中看到完整的清单部分。

images

图 26-12。宣布支持协议激活契约

响应激活事件

协议激活的激活事件的detail.kind属性设置为ActivationKind.protocol。你可以在清单 26-4 中的PhotoAlbum default.js文件中看到我是如何回应这个事件的。

清单 26-4 。响应协议激活事件

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var appstate = activation.ApplicationExecutionState;
    var storage = Windows.Storage;

var query = storage.ApplicationData.current.localFolder.createFolderQuery();
    query.addEventListener("contentschanged", function () {
        App.loadFilesFromCache();
    });
    query.getFoldersAsync();

app.onactivated = function (args) {
        if (args.detail.previousExecutionState != appstate.suspended) {
            args.setPromise(WinJS.UI.processAll().then(function () {

if (ViewModel.fileList.length == 0) {
                    App.loadFilesFromCache();
                }

switch (args.detail.kind) {
**                    case activation.ActivationKind.protocol:**
**                        if (args.detail.uri.schemeName == “photoalbum”) {**
**                            var path = args.detail.uri.path;**
**                            storage.StorageFile.getFileFromPathAsync(path)**
**                                .then(function (file) {**
**                                    App.processFile(file);**
**                            });**
**                        }**
**                        break;**
                    // ...statements removed for brevity...
                    default:
                        WinJS.Navigation.navigate("/pages/albumView.html");
                        break;
                }
            }));
        }
    };
    app.start();
})();`

激活事件的detail.uri属性返回一个Windows.Foundation.Uri对象。我使用schemeName属性来确定我的应用是为哪个协议激活的,并使用path属性来获取 URL 的路径部分,在本例中是ProtocolHelper应用在Pictures库中找到的文件的路径。除了schemeNamepath之外,Windows.Fountation.Uri对象还有许多有用的属性,我已经在表 26-4 中列出了它们,以及如果使用的 URL 是[www.apress.com/books/index.html](http://www.apress.com/books/index.html)时它们将返回的值(一个虚构但有用的 URL 来演示)。

Images

综上所述,您可以看到我通过调用StorageFile.getFileFromPathAsync方法,使用 URL 的path组件获得一个StorageFile对象,从而对photoalbum协议的激活做出响应。然后我将StorageFile传递给App.processFile方法,该方法将文件添加到照片库中。

声明能力

还有一个步骤:在PhotoAlbum清单中声明应用需要访问Pictures库。

这不是协议激活的一个要求,但它是让我的应用按照我想要的方式工作所必需的。协议激活的优点是实现起来快速简单,并且已经定义了很多有用的协议,比如mailto

缺点是应用之间没有信任转移或清单声明,这意味着协议激活最适合简单数据,或者当您确信两个应用对设备具有相同的访问权限时。对于我的例子,这意味着PhotoAlbum应用需要能够访问作为协议激活过程的一部分发送的文件,这意味着需要访问Pictures库(因为,正如您所记得的,激活事件的原因ProtocolHelper应用在Pictures库位置找到并显示第一个图像)。

在 Visual Studio 中打开package.appxmanfest文件,导航到Capabilities选项卡,并检查Pictures Library Access功能。对于一个更复杂的方法,你需要使用共享契约,我在第二十五章中描述过。

测试契约实现

要测试对协议激活契约的支持,请启动PhotoAlbum应用,以便在您的设备上安装最新的更改。与许多其他契约一样,应用不一定要运行才能使契约生效,但是在 Visual Studio 中启动应用的过程会强制 Windows 处理清单并注册应用对契约的支持。如果您愿意,可以在此时终止PhotoAlbum应用,测试仍将继续。

接下来,启动ProtocolHelper应用并点击Protocol Link锚元素。Windows 将向PhotoAlbum应用发送协议激活事件,这将具有将文件路径从一个应用传递到另一个应用的效果,从而将图像添加到相册中。

执行印刷契约

从应用中打印内容的能力非常重要,尽管与几年前相比,用户打印的次数减少了,在屏幕上查看和阅读的内容增加了。假设你的用户不需要打印并跳过这个契约是很诱人的,但是在做这个决定的时候,你是在强迫用户以一种特殊的方式消费你的内容,并且忽略了他们的偏好。

我发现使用打印机是一件痛苦的事情——尽管在 Windows 应用中相对容易处理——但每当我想跳过对打印的支持时,我都会提醒自己我的图书销售数字。有超过 50%的机会,你正在阅读这本书的印刷本。你可能更喜欢印刷本的原因有很多——也许你喜欢在通勤时在火车上阅读,你喜欢打开书,这样你就可以在屏幕上编写代码时跟随示例,或者你喜欢买一本并与你的团队分享。不管是什么原因,Apress 不会因为让你购买电子书而忽视你的偏好,同样,你也不应该因为忽略打印功能而忽视用户的偏好。在这一节中,我将通过添加对打印来自PhotoAlbum示例应用的图像的支持来解释 Windows 对打印的支持是如何工作的。

从 Windows 应用打印是基于将应用的当前布局发送到打印机,这意味着很容易获得基本的打印工作,但需要更多的努力才能产生有用的东西。我将首先向您展示基本的打印机制,然后向您展示如何创建仅用于打印的内容。

Images 正如你所料,你需要一台打印机来跟踪本章的这一节。您不需要实际打印任何东西,但是您需要有一个 Windows 识别为打印机的设备,以便它出现在 Devices Charm 中。

实现基本打印

印刷契约不需要清单声明。相反,您注册一个函数来处理由可以在Windows.Graphics.Printing名称空间中找到的PrintManager对象发出的printtaskrequested事件。此事件表示用户已发起打印请求,您可以相应地准备您的应用。

为了演示契约是如何操作的,我在albumView.html文件中添加了打印支持,这是PhotoAlbum应用用来显示它所编目的图像的内容。您可以在清单 26-5 中看到我所做的更改,我将在接下来的章节中介绍我所使用的对象。

Images 注意我在本章这一部分介绍的新对象都在Windows.Graphics.Printing名称空间中,除非另有说明。在代码示例中,我将这个名称空间别名化为print

清单 26-5 。给 albumView.html 文件添加基本的打印支持

`

         

    
    
    
                      
`

printtaskrequested事件被触发时,处理函数被传递一个对象,该对象的request属性返回一个PrintTaskRequest。您的目标是创建并配置一个PrintTask对象,设置将要打印的内容并配置打印过程。

创建打印任务

通过调用PrintTaskRequest.createPrintTask方法创建一个PrintTask对象。参数是任务的标题和一个处理程序,如果用户选择打印任务,这个处理程序将被调用。这似乎是重复的,但是当您将支持打印契约所需的步骤联系起来时,这是有意义的。为了理解我的意思,启动PhotoAlbum应用,调出魅力栏并激活设备的魅力(如果你愿意,你可以使用Win+K直接激活魅力)。

当你激活 Devices Charm 时,Windows 会向你发送printtaskrequested事件,这是你决定你的应用此刻是否能够打印的机会。你可以在清单 26-6 中看到我是如何处理的,这里我重复了来自printtaskrequested事件处理程序的部分代码。

清单 26-6 。当接收到 printtaskrequested 事件时,决定是否打印

... print.PrintManager.getForCurrentView().addEventListener("printtaskrequested",     function (e) { **        if (ViewModel.fileList.length > 0) {**             var printTask = e.request.createPrintTask("PrintAlbum",                 function (printEvent) {                 printEvent.setSource(MSApp.getHtmlPrintDocumentSource(document));             });             printTask.options.orientation = print.PrintOrientation.landscape;         };     }); ...

如果相册中没有图像,那么我就没有东西可以打印。因此,当我得到printtaskrequested时,我检查在WinJS.Binding.List中是否有我用来跟踪应用内容的对象,并且只有当有图像时才调用createPrintTask方法。

当你激活设备魅力时,你看到的将取决于你之前添加到应用中的图像数量。你可以在图 26-13 中看到两种不同的结果。图中左侧的屏幕向用户显示了可用打印机的列表,并且在应用调用createPrintTask时显示。图右边的屏幕显示了没有调用createPrintTask时呈现给用户的消息,表示此时没有要打印的内容。

images

图 26-13。接收到 printtaskrequested 事件时调用 createPrintTask 方法的效果

如图所示,我有几台旧的惠普打印机可供选择,当然,您在列表中看到的会有所不同。

配置打印任务

一旦创建了PrintTask对象,就可以对其进行配置,为用户提供打印任务的合理初始值。您可以通过返回一个PrintTaskOptions对象的PrintTask.options属性来实现。您可以为打印任务配置许多选项,但我不打算在此列出。首先,它们中的许多是大多数应用不会关心的小众设置,其次,用户可以在打印过程的下一阶段设置它们(我将很快谈到)。

相反,我在表 26-5 中列出了四个配置选项的示例。其中两个在很多情况下都很有用,另外两个说明了您对打印任务的控制级别。

Images

许多配置选项采用由Windows.Graphics.Printing名称空间中的对象定义的值。因此,例如,orientation属性被设置为由PrintOrientation对象定义的值之一,这可以在表 26-6 中看到。

Images

我不想让你觉得我没有必要跳过这一部分。我想谈谈打印契约的核心内容,即准备您的应用内容,以便您获得良好的打印结果。你可以在打印任务上设置太多的细节,如果我把所有的细节都列出来,我会没有空间,而且这些设置中的大部分从来没有被使用过。相反,你可以看到我是如何应用清单 26-7 中一个真正有用的设置选项的,在这里我设置了打印任务的方向。

清单 26-7 。设置打印任务的方向

... print.PrintManager.getForCurrentView().addEventListener("printtaskrequested",     function (e) {         if (ViewModel.fileList.length > 0) {             var printTask = e.request.createPrintTask("PrintAlbum",                 function (printEvent) {                     printEvent.setSource(MSApp.getHtmlPrintDocumentSource(document));                 }             ); **            printTask.options.orientation = print.PrintOrientation.landscape;**         };     }); ...

Images 注意我在这里走了一条捷径——这是我在真正的应用中不推荐的。如果你查看表 26-6 中的值,你会发现它们对应于我在第六章中向你展示的方向。如您所见,Windows 应用打印通过打印应用的布局来工作,这意味着您通常需要设置打印任务的方向以匹配设备的方向。

指定要打印的内容

当您创建一个PrintTask时,您指定了一个当用户从 Device Charm 中选择一个设备时将被调用的函数。这个函数被传递了一个定义了setSource方法的PrintTaskSourceRequestedArgs对象。

setSource方法用于指定将要打印的内容,对于 JavaScript Windows 应用,这意味着你必须使用MSApp.getHtmlPrintDocumentSource方法,如清单 26-8 中突出显示的,我在这里重复了PhotoAlbum应用的语句。

清单 26-8 。设置打印源

... print.PrintManager.getForCurrentView().addEventListener("printtaskrequested",     function (e) {         if (ViewModel.fileList.length > 0) {             var printTask = e.request.createPrintTask("PrintAlbum",                 function (printEvent) { **                    printEvent.setSource(MSApp.getHtmlPrintDocumentSource(document));**                 }             );             printTask.options.orientation = print.PrintOrientation.landscape;         };     }); ...

getHtmlPrintDocumentSource方法只接受一个 DOM Document对象,这意味着你只能打印应用的当前内容或者一个iframe元素的内容。这意味着您必须有创造性地为文档打印一些有用的东西,这是我稍后将返回的主题。

但是,首先让我们用应用中的默认内容完成打印过程。启动应用,确保相册中有一些图像,并激活设备的魅力。单击列表中的一台打印机将触发我传递给createPrintTask方法的函数,该函数将设置打印任务的源——Windows 获取这些信息并呈现给用户,如我在图 26-14 中所示。

images

图 26-14。完成打印任务

Windows 向用户显示了内容的预览,以及更改打印任务设置的机会(More settings链接允许用户查看和更改我前面提到的更神秘的配置选项)。如果你继续打印这个文档,你会得到如图图 26-15 所示的结果。(我使用保存图像的打印机驱动程序捕捉到了这一点,这使我可以向您显示结果,而不必打印到纸上,然后再次扫描页面。)

images

图 26-15。打印输出

我添加了此图所示的边框,因为 Windows 所做的更改之一是更改打印任务中的背景颜色,以便打印图像时不会消耗掉用户所有的墨水/碳粉。

这是我所做的唯一更改,否则,打印输出将与创建打印任务时应用的布局相匹配。这是好的,因为这意味着在应用中支持基本的打印很简单,这也是坏的,因为这意味着布局中的所有东西——包括按钮和滚动条——都被发送到打印机。在接下来的部分中,我将向您展示控制发送到打印机的内容的不同技术。

操作应用布局进行打印

改善打印效果的第一种方法是临时操作专门用于打印作业的应用布局。你可以使用 CSS,当然也可以使用 JavaScript,我会在本章的这一节向你展示这两种方法。

使用 CSS 操作应用布局

CSS 的一个鲜为人知且不常使用的功能是能够创建仅适用于特定类型媒体的样式。这意味着我可以轻松地向我的albumView.html文档添加一个style元素,在打印时改变内容的布局。你可以在清单 26-9 中看到一个简单的例子,在这里我创建了一个样式,隐藏了OpenClear按钮,并为打印执行了一些其他小的布局调整。

清单 26-9 。打印时使用 CSS 样式化元素

`...

<head> **    <style media=”print”>** **        #buttonContainer { visibility: hidden }** **        .listTitle { text-align: center; max-width: none;** **            border: thin solid black; margin: 0px }** **        .listImg { height: 180px; width: 270px }** **    </style>**     <title></title>     <script>         // *...JavaScript statements removed for brevity...*     </script> </head> ...`

这项技术的关键是将media属性添加到style元素中,并将值设置为print。这可确保仅在打印布局时应用样式,允许您调整应用布局以改善打印结果。你可以在图 26-16 中看到的变化。(我又一次给这个图加了边框。)

images

图 26-16。使用 CSS 改变打印布局的样式

正如您在图中看到的,我隐藏了按钮,并更改了为每个图像显示的标签的格式。我在这个例子中定义的样式应用了相对较小的变化,但是效果可以像你喜欢的那样广泛。

使用 JavaScript 操作应用布局

通过使用 JavaScript 来改变应用的布局,您可以做出更深刻的改变,尽管这比使用 CSS 需要更多的努力。特别是,您需要考虑一些 UI 控件的方式,包括我在示例应用中使用的ListView,依赖于使用事件向 UI 控件传递更改的数据源。作为示范,我修改了PhotoAlbum应用,这样每行最多打印两幅图像,避免了你在图 26-16 中看到的问题,每行的第三幅图像只是部分可见。首先,我在js/app.js文件中创建了一个名为adaptLayout的新函数,它切换作为ListView UI 控件数据源的WinJS.Binding.List对象中的元素数量。你可以在清单 26-10 中看到这个函数。

清单 26-10 。app.js 文件中的 adaptLayout 函数

`(function () {

var storage = Windows.Storage;
var access = storage.AccessCache;
var cache = access.StorageApplicationPermissions.futureAccessList;
var pickers = storage.Pickers;

var dataCache = [];

WinJS.Namespace.define("App", {

**    adaptLayout: function (prepareForPrint) {**
**        var flist = ViewModel.fileList;         if (prepareForPrint == true) {**
**            dataCache = flist.splice(4, flist.length -4);**
**        } else {**
**            dataCache.forEach(function (item) {**
**                flist.push(item);**
**            });**
**            dataCache.length = 0;**
**        }**
**    },**
    // ...other functions removed for brevity...
});

})();`

如果使用true作为参数调用该函数,则List中的项目数量将减少到 4 个。当使用参数false再次调用该函数时,这些项将被删除,从而将布局恢复到之前的状态。

我还更新了albumView.html文件中的script元素来使用这个函数进行打印,如清单 26-11 所示。

清单 26-11 。使用 JavaScript 改变打印布局

`...

...`

当我的PrintTask被用户激活时,我调用App.adaptLayout函数。我遇到的问题是,adaptLayout函数对List对象进行了更改,该对象使用事件将这些更改与ListView UI 控件进行了通信。那些事件将在我的函数被执行后被执行,这意味着我需要延迟用setSource方法将我的应用布局传递到窗口,直到那些事件被处理后,这就是我使用setImmediate方法的原因(它推迟了工作的执行,我在第九章中对此进行了详细描述):

... var printTask = e.request.createPrintTask("PrintAlbum", function (printEvent) {     var deferral = printEvent.getDeferral();     App.adaptLayout(true); **    setImmediate(function() {** **        printEvent.setSource(MSApp.getHtmlPrintDocumentSource(document));**         deferral.complete(); **    })** }) ...

因为我推迟了对setSource方法的调用,所以在我的函数执行完成之前,我不能给 Windows 打印的内容。幸运的是,PrintTaskSourceRequestedArgs对象定义了一个getDeferral方法,该方法返回一个对象,当我异步设置内容时,我可以调用该对象的complete方法。(你可以在第十九章中了解更多关于延期的内容,在那里我解释了它们在生命周期事件中的用法。)结果是我修改了List的内容,然后推迟了对setSource方法的调用,直到这些更改反映在ListView控件中。

与 CSS 技术不同,使用 JavaScript 会以用户可以看到的方式影响应用的布局,这意味着当打印任务完成或被用户取消时,将布局恢复到原始状态非常重要。PrintTask对象定义了一些有用的事件,我已经在表 26-7 中描述过,这些事件可以用来跟踪打印进度。

Images

为了恢复布局,我对completed事件感兴趣,我通过调用App.adaptLayout函数来响应该事件。我已经提供了这个函数作为addEventListener方法的一个参数,这意味着它将被传递给 event 对象,该对象具有恢复布局的效果(因为除了 Boolean true 之外的任何值都被作为将图像恢复到List的请求):

... printTask.addEventListener("**completed**", App.adaptLayout); ...

您可以在图 26-17 中的打印结果上看到这些变化的结果。

images

图 26-17。使用 JavaScript 改变打印应用的布局

创建特定打印内容

您可以通过调整应用的现有布局来改善打印效果,但要完全控制打印过程,您需要创建仅用于打印的内容。为了演示这种方法,我在albumView.html文件的script元素中添加了一个新特性,当用户在激活设备的魅力之前选择了ListView控件中的一个项目时,该特性会显示特定于打印的内容。你可以在清单 26-12 中看到支持这个特性的变化。

清单 26-12 。添加对特定打印内容的支持

`...

...`

如果在ListView中选择了一个项目,那么我调用导航 API 来显示我添加到pages文件夹中的名为printView.html的新页面。我将一个对象传递给navigate方法,该方法包含对PrintTaskSourceRequestedArgs对象的引用、由getDeferral方法返回的对象以及用户选择的来自ListView数据源的项目。这些细节将可用于我在printView.html文件的script元素中定义的ready函数,你可以在清单 26-13 中看到。

Images 注意我为PrintTask.completed事件注册了一个处理程序,当打印任务完成或取消时,它将应用导航回 albumView.html 页面。这样做的一个副作用是,我不得不改变对由PrintManager对象发出的printtaskrequested事件的兴趣注册方式。如果您试图使用addEventListener方法为printtaskrequested事件添加第二个侦听器,则会抛出异常,并且由于当应用导航回albumView.html文件时会执行ready函数中的代码,因此我需要通过向onprinttaskrequested属性分配一个新函数来替换现有的侦听器——这确保了最多只有一个侦听器,并且不会遇到异常。

清单 26-13 。printView.html 文件的内容

`

              

    
                      
`

这个文件包含一个非常简单的布局,显示选中的图像及其名称。script元素中的代码使用从ListView数据源中选择的项目来配置布局中的元素,然后调用setSource方法来为 Windows 提供要打印的内容。最后,调用延迟对象的complete方法,向 Windows 指示异步任务已经完成,可以向用户显示内容预览。你可以在图 26-18 中看到该文件产生的打印结果。

images

图 26-18。使用专用内容进行打印

从图中可以看出,只需稍加努力,您就可以创建专门用于打印的内容,并且不会受到试图调整主要用于在屏幕上显示的布局的限制。

总结

在这一章中,我已经向您展示了如何实现另外三个契约:自动播放、协议激活和打印。这三者在提供一流的应用体验方面都有自己的位置,你应该考虑实现它们,以便将你的应用更紧密地集成到更广泛的 Windows 体验中。在下一章中,我将向您展示如何控制在 Windows 开始屏幕上为您的应用创建的磁贴。

二十七、使用应用切片

在第四章中,我向你展示了如何设置一个应用在 Windows 开始屏幕上使用的图片。这是磁贴的基本操作,但应用可以更进一步,创建实时磁贴,即使在应用不运行时,也可以通过开始屏幕显示有用的信息。在本节中,我将向您展示如何创建动态切片,以及通过应用更新动态切片的不同方式。

使用动态磁贴有两个基本原因:因为您希望用户更频繁地运行您的应用,或者因为您希望用户不那么频繁地运行您的应用。如果你有一个旨在吸引用户注意力的应用,那么你希望将用户的目光吸引到你的应用磁贴上,并提醒他们你在那里。这对于游戏来说是真实的,例如,你想吸引用户并提醒他们你的游戏比他们通过开始菜单想要做的更有趣或更令人兴奋。在这些情况下,你有责任创建吸引人但不分散注意力的磁贴——这符合你的利益,因为 Windows 8 允许用户禁用令人讨厌的应用的动态磁贴。Windows Store 中的一些早期应用有动态磁贴设计,它们的风格非常激进,以至于我发现自己在移动磁贴,这样我就看不到它们了。

如果你有一个旨在提高用户生产力或工作生活的应用,那么你希望使用磁贴为用户提供关键信息的及时摘要,以便他们可以轻松获得关键事实,而无需启动你的应用,等待初始化,导航到正确的部分,等等。简而言之,你在不启动你的应用的情况下,通过给用户提供他们需要的信息来帮助他们。这需要仔细考虑用户关心什么,并为用户提供改变显示在磁贴上的信息种类的方法。

对于这类 app 来说,你的责任就是创建一个内容及时、明显、准确的磁贴,也就是说当 app 状态发生变化的时候更新磁贴。

我对实时应用磁贴的看法和我对 UI 动画的看法是一样的:如果少用的话,它们是个好东西,但是很快就会变得烦人和分散注意力(你应该总是提供一种方法来禁用它们)。不要误判你的应用对用户的重要性,不要把他们的开始屏幕变成维加斯老丨虎丨机。表 1 提供了本章的总结。

Images

为本章创建示例

我为这一章创建了一个名为LiveTiles的新 Visual Studio 项目。起点是非常基本的,因为我只需要布局包含按钮,将执行不同种类的瓷砖更新,从一个基本的实时瓷砖开始。清单 1 显示了应用的default.html文件,其中包含一个按钮。我将在本章中添加新的元素,并演示不同的技术。

清单 1。来自 LiveTiles 项目的 default.html 文件

`

<html> <head>     <meta charset="utf-8" />     <title>LiveTiles</title>


    
    
    


    
    

    
**        **` `    
`

这个示例应用的 CSS 同样简单,您可以在清单 2 的中看到/css/default.css文件的内容。这个 CSS 中没有新的技术,只是将 HTML 中的按钮放在屏幕的中央。

清单 2。/css/default.css 文件的内容

`body { display: -ms-flexbox;-ms-flex-direction: row;-ms-flex-align: stretch;
    -ms-flex-pack: center;}

container { display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: stretch;

-ms-flex-pack: center;}

container button {font-size: 30pt; width: 400px; margin: 10px;}`

你可以在图 1 中看到 app 的初始布局。正如承诺的那样,它非常简单,我将在本章的后面添加额外的按钮来演示其他特性。

images

图 1。示例 app 的初始布局

定义 JavaScript

如清单 3 所示,我从一个简单的default.js文件开始。这个 app 全是磁贴,我没有任何后台工作或者 app 状态要担心。我甚至不必像往常一样调用WinJS.Binding.processAll方法,因为我没有视图模型或任何要处理的数据绑定。

清单 3。一个简单的 Default.js 文件

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var $ = WinJS.Utilities.query;
    WinJS.strictProcessing();

var textMessages = ["Today: Pick up groceries",
                        "Tomorrow: Oil change",
                        "Wed: Book vacation",
                        "Thu: Renew insurance"];

app.onactivated = function (args) {         if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                $("#container > button").listen("click", function (e) {
**                    switch (this.id) {**
**                        case "basicTile"😗*
**                            // TODO - code for tile goes here**
**                            break;**
**                    }**
**                });**
            }));
        }
    };
    app.start();
})();`

我在文档中定位button元素并监听click事件,使用被点击按钮的id值计算出在每种情况下我需要做什么。目前布局中只有一个按钮,但我会添加更多。

设置平铺图像

因为这是关于 tiles 的一章,所以我在 Visual Studio 项目的images文件夹中添加了文件,这样我就可以创建一个基本的静态 tile。这些文件被称为tile30.pngtile150.pngtile310.png,你可以在图 2 的清单的Application UI部分看到我是如何应用这些图像的。

images

图二。为应用磁贴设置图像

images 提示当您使用磁贴并更改应用使用的设置时,您可能会发现磁贴图标不会显示在开始屏幕上。我发现右键点击应用,点击应用栏上的Uninstall,重新启动应用,往往就能解决问题。有时应用磁贴根本不显示——在这种情况下,键入应用名称的前几个字母来执行搜索,然后按 escape 键返回主开始屏幕;瓷砖通常会出现。如果所有这些都失败了,从模拟器和开发机器上卸载应用,重新启动,并在不启动模拟器的情况下从 Visual Studio 启动应用。

测试示例应用

目前没有太多的功能,但是如果您从 Visual Studio 启动应用,然后切换到开始屏幕,您将能够看到示例应用的静态磁贴。您可以使用LargerSmaller AppBar 命令在正常和宽按钮配置之间切换,您可以在图 3 中看到。

images

图三。示例 app 的方形宽静态瓷砖

如图所示,我添加到项目中并在清单中应用的图像显示了一个警铃;我为示例应用选择了这张图片,因为它将像一个提醒程序一样创建磁贴更新。我不打算创建提醒逻辑,但我需要一个更新和提醒是理想的主题。

创建动态磁贴

创建动态切片的基本原则包括三个步骤:

  1. 选择一个 XML 模板。
  2. 用您的数据填充模板。
  3. 将填充的 XML 传递给 Windows 以更新图块。

完成所有这些工作的 API 相当笨拙,但是这种笨拙可以相当简单地用助手函数来包装,一旦您启动并运行,这个过程就变得相对容易了。首先,我将创建基本类型的 live tile,它只包含文本信息,然后再创建更复杂的替代方案。清单 4 展示了当点击应用布局中的button时对default.js文件的修改,它创建了一个动态磁贴。

清单 4。创建实时互动程序

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
**    var wnote = Windows.UI.Notifications;**
    WinJS.strictProcessing();

var textMessages = ["Today: Pick up groceries",
                        "Tomorrow: Oil change",
                        "Wed: Book vacation",                         "Thu: Renew insurance"];

**    function getTemplateContent(template) {**
**        return wnote.TileUpdateManager.getTemplateContent(template);**
**    }**

**    function populateTemplateText(xml, values) {**
**        var textNodes = xml.getElementsByTagName("text");**
**        var count = Math.min(textNodes.length, values.length);**
**        for (var i = 0; i < count; i++) {**
**            textNodes[i].innerText = values[i];**
**        }**
**        return xml;**
**    }**

**    function updateTile(xml) {**
**        var notification = new wnote.TileNotification(xml);**
**        var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();**
**        updater.update(notification);**
**    }**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "basicTile":
**                                var template = wnote.TileTemplateType.tileSquareText03;**
**                                var xml = getTemplateContent(template);**
**                                updateTile(populateTemplateText(xml, textMessages));**
                                break;
                        }
                    });
            }));
        }
    };
    app.start();
})();`

我将工作分成了三个助手函数,可以在后面的例子中使用,而不必直接使用Windows.UI.Notifications名称空间,在那里可以找到与 tile 相关的功能。我将在接下来的部分中分解这个过程。

获取模板内容

可用于实时图块的模板集合由Windows.UI.Notifications.TileTemplateType枚举定义。有 45 种不同类型的模板可用,它们提供不同的大小(用于正方形和宽瓷砖)和不同数量的文本,有或没有图像。如果你看一下TileTemplateType(在[msdn.microsoft.com/en-us/library/windows/apps/windows.ui.notifications.tiletemplatetype.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.notifications.tiletemplatetype.aspx)),的 API 文档,你会看到每个不同模板的例子。我将从TileTemplateType.tileSquareText03模板开始,这是一个显示四行文本的纯文本模板。您可以在清单 5 中看到这个模板的 XML。

清单 5。tileSquareText03 模板的 XML

<tile>     <visual>         <binding template="TileSquareText03">             <text id="1"></text>             <text id="2"></text>             <text id="3"></text>             <text id="4"></text>         </binding>     </visual> </tile>

目标是设置每个text元素的内容——我将使用我在/js/default.js文件的textMessages数组中定义的四个字符串,它们代表我的假提醒应用即将发出的提醒。

获取 XML 模板的内容需要将一个值从TileTemplateType枚举传递给由Windows.UI.Notifications.TileUpdateManager定义的getTemplateContent方法。在这个区域中有一些相当长的名称空间和对象名,所以我为Windows.UI.Notifications名称空间定义了一个别名,并将获取 XML 内容的调用放入getTemplateContent助手函数中。

getTemplateContent方法返回一个Windows.Data.Xml.Dom.XmlDocument对象,为 XML 内容提供 DOM 操作。操作 XML 与处理 HTML 内容非常相似,尽管您将在下一节中看到,在处理动态切片时,您不必进行太多的操作。

images 提示除了Windows.Data.Xml.Dom名称空间,你会发现应用也可以使用Windows.Data.HtmlWindows.Data.Json名称空间。它们提供了用于处理 HTML 和 JSON 内容的对象,如果您想要在当前 DOM 之外处理 HTML,或者想要超越 Internet Explorer 10 中可用的基本 JSON 支持,它们会非常有用。

推广 xml 文本元素的内容

获得 XML 模板后,我现在需要填充text元素的内容。我已经定义了populateTemplateText函数,它接受一个模板和一个字符串值数组,并使用innerText属性设置文本元素的内容。

尽管模板中的text元素有id属性,但是XmlDocument.getElementById方法不能正常工作,所以下一个最好的选择是使用getElementsByTagName方法定位所有的text元素。这给了我一组按照它们在 XML 文档中出现的顺序排列的text元素,我依靠这种顺序使用innerText属性设置text元素的内容,就像我处理 HTML 元素一样。结果是一个填充的 XML 模板,如清单 6 所示。

清单 6。填充的 XML 模板

<tile>     <visual>         <binding template="TileSquareText03">             <text id="1">**Today: Pick up groceries**</text>             <text id="2">**Tomorrow: Oil change**</text>             <text id="3">**Wed: Book vacation**</text>             <text id="4">**Thu: Renew insurance**</text>         </binding>     </visual> </tile>

更新磁贴

最后一步是通过将填充的 XML 传递给系统来更新图块,这是通过一系列笨拙的 API 调用来完成的,如清单 7 中的所示,它重复了几页前的updateTile助手函数。

清单 7。用填充的 XML 更新图块

... function updateTile(xml) {     var notification = new wnote.TileNotification(xml);     var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();     updater.update(notification); } ...

首先,我需要创建一个TileNotification对象,将填充的 XML 作为构造函数参数传入。对于一个简单的磁贴,你只需要创建对象,但是我将在本章的后面向你展示如何为不同的场景配置它。接下来,我通过调用TileUpdateManager.createTileUpdaterForApplication方法创建一个TileUpdater对象。TileUpdater对象提供了一系列更新图块的不同方法。目前,我只是使用了最基本的方法,即调用update方法,传入上一步创建的TileNotification对象。稍后我将向您展示更复杂的安排。结果是当你点击布局中的button时,静态磁贴变成活动的,显示我的假约会数据,如图图 4 所示。

Images 警告Visual Studio 模拟器不支持实时图块。要测试 live tiles,你必须使用真正的 Windows 8 设备。

images

图 4。一个基本活瓦

您可以看到,小图标用于实时磁贴更新,以帮助用户识别与磁贴相关的应用。你还可以看到,我所做的更新并不是特别有用——在一个正方形的磁贴中没有太多的空间,当依赖文本时,很难向用户提供有意义的信息。

images 提示你不能指望用户看到你的磁贴更新。首先,您的应用磁贴可能不在最初显示的开始屏幕上,用户可能不会滚动它以使它变得可见。其次,用户可以使用开始屏幕应用栏禁用实时磁贴。这意味着您应该使用实时磁贴来显示应用本身也提供的信息,而不是将磁贴视为核心应用功能的一部分。

创建更有用的实时互动程序

我的基础磁贴更新还有一个问题,就是不影响宽磁贴。为了解决缺乏实用性的问题并支持所有的图块格式,我将采用不同的方法,如清单 8 所示,它强调了我对default.js文件所做的添加,以一起更新窄和宽图块格式。

清单 8。创建更有用的磁贴更新

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var textMessages = ["Today: Pick up groceries",
                        "Tomorrow: Oil change","Wed: Book vacation",
                        "Thu: Renew insurance", "Sat: BBQ"];

function getTemplateContent(template) {
        return wnote.TileUpdateManager.getTemplateContent(template);
    }

function populateTemplateText(xml, values) {
        var textNodes = xml.getElementsByTagName("text"); var count = Math.min(textNodes.length, values.length);
        for (var i = 0; i < count; i++) {
            textNodes[i].innerText = values[i];
        }
        return xml;
    }

function updateTile(xml) {
        var notification = new wnote.TileNotification(xml);
        var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();
        updater.update(notification);
    }

**    function combineXML(firstXml, secondXML) {**
**        var wideBindingElement = secondXML.getElementsByTagName("binding")[0];**
**        var importedNode = firstXml.importNode(wideBindingElement, true);**
**        var squareVisualElement = firstXml.getElementsByTagName("visual")[0];**
**        squareVisualElement.appendChild(importedNode);**
**        return firstXml;**
**    }**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "basicTile":
                                var squareTemplate =
**                                    wnote.TileTemplateType.tileSquareBlock;**
**                                var squareXML = populateTemplateText(**
**                                    getTemplateContent (squareTemplate),**
**                                        [textMessages.length, "Reminders"]);**

**                                var wideTemplate =**
**                                    wnote.TileTemplateType.tileWideBlockAndText01;**
**                                var wideData = textMessages.slice(0, 4)**
**                                wideData.push(textMessages.length, "Reminders");**
**                                var wideXml = populateTemplateText(**
**                                    getTemplateContent(wideTemplate),wideData);**

**                                updateTile(combineXML(squareXML, wideXml));**
                                break;
                        }
                    });
            }));
        }
    };
    app.start();
})();`

要执行影响方形和宽瓷砖的瓷砖更新,您需要选择并填充两个不同的模板,并组合它们的内容。为了使 square 更新更具可读性,我使用了一个不同的模板tileSquareBlock,它有两个文本元素——一个大的显示在一个小的上面(我通过查看用于TileTemplateType枚举的 API 文档选择了该模板,其中包含每个模板将如何出现的图片)。图 5 显示了微软为该模板提供的描述和图片。

images

图 5。tileSquareBlock 模板的描述

我不会显示单个提醒的详细信息,因为正如上一个演示所示,在一个狭窄的磁贴上没有空间,所以我调用带有摘要信息的populateTemplateText方法,如下所示:

... var squareXML = populateTemplateText(getTemplateContent (squareTemplate),     **[textMessages.length, "Reminders"]**); ....

模板中text元素的顺序与它们在模板中出现的顺序相匹配,这使得为模板设置内容非常简单。对于宽磁贴,我选择了tileWideBlockAndText01模板,你可以在图 6 中看到微软是如何描述的。

images

图六。tilewideblockandtext 01 模板的描述

您可以在清单 9 中看到这个模板的 XML。同样,文本元素的顺序与它们的显示顺序一致,因此前四个text元素对应于平铺左侧的文本,后两个对应于右侧的文本。

清单 9。宽图块模板的 XML

<tile>     <visual>         <binding template="TileWideBlockAndText01">             <text id="1"></text>             <text id="2"></text>             <text id="3"></text>             <text id="4"></text>             <text id="5"></text>             <text id="6"></text>          </binding>     </visual> </tile>

我通过从数据数组中复制前四项并将两个新项推入数组来填充模板。在一个真实的项目中,你需要考虑数据项比text元素少,但是为了简单起见,我跳过了这个细节。下一步是将两个模板组合在一起,形成单个更新的基础。这需要对 XML 进行一些操作——我在 wide 模板的 XML 中找到了binding元素,并将其插入到 square 模板的 XML 中,产生了如清单 10 所示的 XML 组合片段。

清单 10。组合 XML 片段以更新不同的图块大小

<tile>     <visual>         <binding template="TileSquareBlock">             <text id="1">5</text>             <text id="2">Reminders</text>         </binding>         <binding template="TileWideBlockAndText01">             <text id="1">Today: Pick up groceries</text>             <text id="2">Tomorrow: Oil change</text>             <text id="3">Wed: Book vacation</text>             <text id="4">Thu: Renew insurance</text>             <text id="5">5</text>             <text id="6">Reminders</text>         </binding>     </visual> </tile>

将 XML 传递给 Windows 也是以同样的方式完成的,并且更新两种大小的图块。你可以在图 7 的中看到结果。要查看实时磁贴,您需要重启示例应用,点击应用布局中的基本磁贴按钮,然后切换到Start屏幕。您可以通过选择图块并从开始屏幕应用栏中选择LargerSmaller按钮来切换图块尺寸。

images

图 7。示例应用的方形和宽瓷砖更新

为你的应用选择正确的模板至关重要。你需要找到一种向用户传达信息的方式,这种方式是有帮助的,并且(可选地)会鼓励他们打开和使用你的应用。

使用图像模板

并非所有的应用都受益于在磁贴中只显示文本。为此,有显示图像或图像和文本混合的模板。还有 peek 模板,在两种显示之间交替,通常是所有图像,然后是文本或文本和图像的混合。清单 11 显示了tileSquarePeekImageAndText02模板的 XML,它包含了textimage元素的混合。

清单 11。混合文本和图像模板的 XML

<tile>     <visual>         <binding template="TileSquarePeekImageAndText02"> **            <image id="1" src=""/>** **            <text id="1"></text>**         </binding>     </visual> </tile>

你可以看到我是如何构建一个小的帮助函数库来处理瓷砖的,我需要添加对处理image元素的支持。您可以在清单 12 中看到我对default.js文件所做的更改。

清单 12。添加对填充图像元素的支持

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var textMessages = ["Today: Pick up groceries", "Tomorrow: Oil change",
                        "Wed: Book vacation", "Thu: Renew insurance", "Sat: BBQ"];

**    var images = img/lily.png",img/astor.png",img/carnation.png",**
**       img/daffodil.png",img/snowdrop.png"];**

function getTemplateContent(template) {
        return wnote.TileUpdateManager.getTemplateContent(template);
    }

**    function populateTemplate(xml, textValues, imgValues) {**
**        if (textValues) {**
**            var textNodes = xml.getElementsByTagName("text");**
**            var count = Math.min(textNodes.length, textValues.length);**
**            for (var i = 0; i < count; i++) {**
**                textNodes[i].innerText = textValues[i];             }**
**        }**
**        if (imgValues) {**
**            var imgNodes = xml.getElementsByTagName("image");**
**            var count = Math.min(imgNodes.length, imgValues.length);**
**            for (var i = 0; i < count; i++) {**
**                imgNodes[i].attributes.getNamedItem("src").innerText = imgValues[i]**
**            }**
**        }**
**        return xml;**
**    }**

function updateTile(xml) {
        var notification = new wnote.TileNotification(xml);
        var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();
        updater.update(notification);
    }

function combineXML(firstXml, secondXML) {
        var wideBindingElement = secondXML.getElementsByTagName("binding")[0];
        var importedNode = firstXml.importNode(wideBindingElement, true);
        var squareVisualElement = firstXml.getElementsByTagName("visual")[0];
        squareVisualElement.appendChild(importedNode);
        return firstXml;
    }

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "basicTile":
                                var squareTemplate =
**                                    wnote.TileTemplateType.tileSquarePeekImageAndText02;**
**                                var squareXML =**
**                                    populateTemplate(getTemplateContent(squareTemplate),**
**                                        [textMessages.length, "Reminders"], images);**
**                                var wideTemplate =**
**                                    wnote.TileTemplateType.tileWidePeekImageCollection02;**
**                                var wideData = textMessages.slice(0, 4)**
**                                wideData.unshift(textMessages.length + " Reminders");**
**                                var wideXml =**
**                                    populateTemplate(getTemplateContent(wideTemplate),**
**                                    wideData, images);**
**                                updateTile(combineXML(squareXML, wideXml));**
                                break;
                        }
                    });
            }));         }
    };
    app.start();
})();`

我已经用populateTemplate替换了populateTemplateText函数,它既处理text又处理image元素。为了演示图像的使用,我在 Visual Studio 项目的images文件夹中添加了一些文件。我使用了前几章的花卉图片,你可以在图 8 的解决方案浏览器中看到我添加的内容。您可以在apress.com从本书附带的源代码下载中获得这些图像,或者使用您自己的图像(在这种情况下,您需要更改/js/default.js文件中的文件名)。

images

图 8。向 Visual Studio 项目添加图像文件

images 提示注意,我通过attributes属性设置了image元素中src属性的值,而不是使用由XmlElement对象定义的setAttribute方法。这是因为 Windows 8 的 DOM 支持不一致,调用getElementsByNameTag有时会返回一个XmlElement对象的集合,有时反而会返回一个IXmlNode对象的集合。IXmlNode对象没有定义setAttribute方法,所以我必须找到src属性并使用innerText属性设置其内容。

更新后,平铺最初显示图像,然后切换到带有动画的文本显示。几秒钟后,这个过程重复进行,如此继续下去。我觉得这种 live tile 有点烦人,但它可能正是你的应用所需要的。您可以在图 9 中看到图块的不同状态。请注意,我用于宽尺寸的模板有五个图像。我不需要调整图像的大小来显示它们;这是作为磁贴更新的一部分自动完成的。

images

图九。使用包含图像的图块模板

清除瓷砖

有时您需要清除图块的内容,通常是因为显示的信息现在已经过时,并且没有新的或值得注意的内容来代替它。为了演示如何清除文件,我向default.html页面添加了一个新的按钮元素,如清单 13 所示。

清单 13。向布局添加一个按钮来清除磁贴

`...

<body>     <div id="container">         <button id="basicTile">Basic Live Tile</button> **        <button id="clearTile">Clear Tile</button>**     </div> </body> ...`

清除磁贴很简单,如清单 14 所示,它详细说明了我对default.js文件所做的更改。

清单 14。添加清除磁贴的支持

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var textMessages = ["Today: Pick up groceries", "Tomorrow: Oil change",                         "Wed: Book vacation", "Thu: Renew insurance", "Sat: BBQ"];
    var images = img/lily.png",img/astor.png",img/carnation.png",
       img/daffodil.png",img/snowdrop.png"];

// ...helper functions removed for brevity...

**    function clearTile() {**
**        wnote.TileUpdateManager.createTileUpdaterForApplication().clear();**
**    }**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "basicTile":
                                // ...statements removed for brevity...
                                break;
**                            case "clearTile"😗*
**                                clearTile();**
**                                break;**
                        }
                    });
            }));
        }
    };
    app.start();
})();`

要清除图块,可以通过调用TileUpdateManager.createTileUpdaterForApplication方法创建一个TileUpdater对象,并对其调用clear方法。清除磁贴会将其返回到静态,显示清单中定义的应用图像。

使用徽章

徽章是显示在图块上的一个小指示器,可以作为我在前面几节中展示的完整实时图块更新的替代或补充。添加工卡所需的步骤类似于定期更新所需的步骤,因为您选择一个 XML 模板,填充内容并将其作为更新传递给 Windows。

徽章出现在应用磁贴的右下角,可以是 1 到 99 之间的数字,也可以是 Windows 定义的少数图标之一(称为徽章 字形)。这是一种非常有限的表达信息的方式,但在某些情况下却很有用。我发现显示数字的能力很有用,但是还没有找到一个真正的项目,因为选择是如此有限(并且你不能定义你自己的)。

我在示例项目的default.html文件中添加了两个新的button元素,这样我就可以演示徽章的使用。新增内容如清单 15 所示。

清单 15。向 default.html 文件添加一个新的按钮元素

`...

<body>     <div id="container">         <button id="basicTile">Basic Live Tile</button> **        <button id="numericBadge">Numeric Badge</button>** **        <button id="glyphBadge">Glyph Badge</button>**         <button id="clearTile">Clear Tile</button>     </div> </body> ...`

您可以看到我对default.js文件所做的添加,以支持清单 16 中的徽章。我列出了这个文件的完整代码,因为我想强调徽章的技术和瓷砖的技术是多么相似。

清单 16。向 default.js 文件添加对徽章的支持

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var textMessages = ["Today: Pick up groceries", "Tomorrow: Oil change",
                        "Wed: Book vacation", "Thu: Renew insurance", "Sat: BBQ"];

var images = img/lily.png",img/astor.png",img/carnation.png",
       img/daffodil.png",img/snowdrop.png"];

function getTemplateContent(template) {
        return wnote.TileUpdateManager.getTemplateContent(template);
    }

**    function getBadgeTemplateContent(template) {**
**        return wnote.BadgeUpdateManager.getTemplateContent(template);**
**    }**

function populateTemplate(xml, textValues, imgValues) {
        if (textValues) {
            var textNodes = xml.getElementsByTagName("text");
            var count = Math.min(textNodes.length, textValues.length);
            for (var i = 0; i < count; i++) {
                textNodes[i].innerText = textValues[i];
            }
        }
        if (imgValues) {
            var imgNodes = xml.getElementsByTagName("image");
            var count = Math.min(imgNodes.length, imgValues.length); for (var i = 0; i < count; i++) {
                imgNodes[i].attributes.getNamedItem("src").innerText = imgValues[i]
            }
        }
        return xml;
    }

**    function populateBadgeTemplate(xml, value) {**
**        var badgeNode = xml.getElementsByTagName("badge")[0];**
**        badgeNode.attributes.getNamedItem("value").innerText = value;**
**        return xml;**
**    }**

function updateTile(xml) {
        var notification = new wnote.TileNotification(xml);
        var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();
        updater.update(notification);
    }

**    function updateBadge(xml) {**
**        var notification = new wnote.BadgeNotification(xml);**
**        var updater = wnote.BadgeUpdateManager.createBadgeUpdaterForApplication();**
**        updater.update(notification);**
**    }**

function combineXML(firstXml, secondXML) {
        var wideBindingElement = secondXML.getElementsByTagName("binding")[0];
        var importedNode = firstXml.importNode(wideBindingElement, true);
        var squareVisualElement = firstXml.getElementsByTagName("visual")[0];
        squareVisualElement.appendChild(importedNode);
        return firstXml;
    }

function clearTile() {
        wnote.TileUpdateManager.createTileUpdaterForApplication().clear();
**        wnote.BadgeUpdateManager.createBadgeUpdaterForApplication().clear();**
    }

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "basicTile":
                                var squareTemplate =
                                    wnote.TileTemplateType.tileSquarePeekImageAndText02;
                                var squareXML =
                                    populateTemplate(getTemplateContent(squareTemplate),
                                        [textMessages.length, "Reminders"], images);
                                var wideTemplate =                                     wnote.TileTemplateType.tileWidePeekImageCollection02;
                                var wideData = textMessages.slice(0, 4)
                                wideData.unshift(textMessages.length + " Reminders");
                                var wideXml =
                                    populateTemplate(getTemplateContent(wideTemplate),
                                    wideData, images);
                                updateTile(combineXML(squareXML, wideXml));
                                break;
                            case "clearTile":
                                clearTile();
                                break;
**                            case "numericBadge"😗*
**                                var template = getBadgeTemplateContent(**
**                                    wnote.BadgeTemplateType.badgeNumber);**
**                                var badgeXml = populateBadgeTemplate(template,**
**                                    textMessages.length);**
**                                updateBadge(badgeXml);**
**                                break;**
**                            case "glyphBadge"😗*
**                                var template = getBadgeTemplateContent(**
**                                    wnote.BadgeTemplateType.badgeGlyph);**
**                                var badgeXml = populateBadgeTemplate(template, "alert");**
**                                updateBadge(badgeXml);**
**                                break;**
                        }
                    });
            }));
        }
    };
    app.start();
})();`

徽章的所有对象都在Windows.UI.Notifications名称空间中,旁边是用于图块的对象。通过将一个值从BadgeTemplateType枚举传递给BadgeUpdateManager.getTemplateContent方法,可以获得想要使用的模板。我已经在表 2 中展示了两种不同的模板类型。

Images

两个模板的内容是相同的,尽管使用正确的模板很重要,以防它们在未来的版本中发生变化。您可以在清单 17 的中看到由getTemplateContent方法返回的 XML。

清单 17。徽章模板的 XML 内容

<badge value=""/>

这是一个非常简单的模板。如果你想显示一个数字徽章,那么你设置value为你想显示的数字,在 1 到 99 之间。如果您设置的值超出了此范围,该值将显示为99+(数字 99 后跟一个加号)。

如果您想要显示一个字形徽章,那么您可以将value属性设置为您想要的字形的名称。在这个例子中,我使用了alert值。没有枚举这些值的 JavaScript 对象,但是您可以在[msdn.microsoft.com/en-us/library/windows/apps/hh761458.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/hh761458.aspx)看到它们的列表。共有 11 种字形,它们涵盖了应用可能想要传达给用户的一些常见信息。

一旦您填充了模板,您就创建了一个通知并将其传递给一个徽章更新程序,如示例中的updateBadge函数所示。使用徽章时,您不必担心不同的图块大小,一次徽章更新会影响方形和宽图块。在图 10 的中,您可以看到应用于示例应用磁贴的数字和字形徽章。我已经展示了应用于静态和动态瓷砖的徽章。

images

图 10。将徽章应用于静态和动态应用磁贴

最后,清除徽章更新需要调用徽章更新程序上的clear方法。您可以在示例中的clearTile函数中看到一个演示,它现在删除了动态磁贴更新和徽章,将磁贴返回到其初始静态。

高级磁贴功能

我在前面几节中展示的磁贴技术将满足大多数应用的需求。对于更特殊的情况,您可以使用一些高级功能来更好地控制您的应用切片。在接下来的小节中,我将向您展示如何使用这些特性。为了演示这些特性,我创建了一个名为AdvancedTiles的新 Visual Studio 项目。default.html中的初始布局如清单 18 中的所示,由三个简单的button元素组成。

清单 18。default.html 文件的初始内容

`

<html> <head>     <meta charset="utf-8" />     <title>AdvancedTiles</title>


    
    
         
    
**    **
    

    
**        ** **        ** **        **     
`

button元素是为本章这一部分的第一个例子设置的。您可以在清单 19 的文件中看到这个项目的/css/default.css的内容。

清单 19。default.css 文件的内容

`body { display: -ms-flexbox; -ms-flex-direction: row; -ms-flex-align: stretch;
    -ms-flex-pack: center;}

container {display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: stretch;

-ms-flex-pack: center;}

container button {font-size: 30pt; width: 500px; margin: 10px;}`

你可以在图 11 中看到这个 app 的初始布局。我将在本章的后面添加额外的按钮。

images

图 11。示例 app 的布局

我已经将本章前面的 tile helper 函数放到一个名为/js/tiles.js的文件中,并通过Tiles名称空间使它们可用。您可以在清单 20 中看到 tiles.js 文件的内容。

清单 20。/js/tiles.js 文件的内容

`(function () {

var wnote = Windows.UI.Notifications;

WinJS.Namespace.define("Tiles", {         getTemplateContent: function (template) {
            return wnote.TileUpdateManager.getTemplateContent(template);
        },
        getBadgeTemplateContent: function (template) {
            return wnote.BadgeUpdateManager.getTemplateContent(template);
        },
        populateTemplate: function (xml, textValues, imgValues) {
            if (textValues) {
                var textNodes = xml.getElementsByTagName("text");
                var count = Math.min(textNodes.length, textValues.length);
                for (var i = 0; i < count; i++) {
                    textNodes[i].innerText = textValues[i];
                }
            }
            if (imgValues) {
                var imgNodes = xml.getElementsByTagName("image");
                var count = Math.min(imgNodes.length, imgValues.length);
                for (var i = 0; i < count; i++) {
                    imgNodes[i].attributes.getNamedItem("src").innerText = imgValues[i]
                }
            }
            return xml;
        },
        populateBadgeTemplate: function (xml, value) {
            var badgeNode = xml.getElementsByTagName("badge")[0];
            badgeNode.attributes.getNamedItem("value").innerText = value;
            return xml;
        },
        updateTile: function (xml) {
            var notification = new wnote.TileNotification(xml);
            var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();
            updater.update(notification);
        },
        updateBadge: function (xml) {
            var notification = new wnote.BadgeNotification(xml);
            var updater = wnote.BadgeUpdateManager.createBadgeUpdaterForApplication();
            updater.update(notification);
        },
        combineXML: function (firstXml, secondXML) {
            var wideBindingElement = secondXML.getElementsByTagName("binding")[0];
            var importedNode = firstXml.importNode(wideBindingElement, true);
            var squareVisualElement = firstXml.getElementsByTagName("visual")[0];
            squareVisualElement.appendChild(importedNode);
            return firstXml;
        },
        clearTile: function () {
            wnote.TileUpdateManager.createTileUpdaterForApplication().clear();
            wnote.BadgeUpdateManager.createBadgeUpdaterForApplication().clear();
        }
    });
})();`

当我想在前面的例子基础上构建并列出不同之处时,我将调用这些函数。您可以在清单 21 中看到/js/default.js文件的初始内容。

清单 21。default.js 文件的初始内容为

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var dataObjects = [
        { name: "Projects", quant: 6, key: "projects" },
        { name: "Clients", quant: 2, key: "clients" },
        { name: "Milestones", quant: 4, key: "milestones" }
    ];

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {

WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "clear":
                                Tiles.clearTile();
                                break;
                        }
                    });
            }));
        }
    };
    app.start();
})();`

Clear Tile按钮已经连接好,并调用了Tile.clearTile方法来将磁贴重置为静态,并移除任何徽章。在接下来的部分中,我将添加其他按钮的代码。

最后,我在项目中添加了我在之前的示例应用中使用的相同的图块图像,并更新了应用清单,如图 12 所示。

images

图 12。设置清单图像文件

使用通知队列

我要描述的第一个高级特性是通知队列,它允许您使用磁贴轮流显示多达五个更新。当你的应用需要向用户显示一系列相关的消息或图像时,这可能会很有用——尽管,由于单个消息会显示大约 5 秒钟,你不能指望用户在访问开始屏幕时看到队列中的所有消息。这意味着你应该仔细选择你要展示的内容,这样每条信息都是独立的、有意义的,并且是有帮助的或有吸引力的。在示例应用中,我定义了一些可能总结用户项目承诺的数据:

... var dataObjects = [     { name: "Projects", quant: 6, key: "projects" },     { name: "Clients", quant: 2, key: "clients" },     { name: "Milestones", quant: 4, key: "milestones" }]; ...

每个数据对象都有三个属性:名称、数量和键。前两个是将向用户显示的数据项,key属性将让我区分队列中的消息——当您想要刷新通知时,这变得很重要。您可以在清单 22 中看到我对default.js文件所做的更改,以使用通知队列。

清单 22。使用通知队列

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var dataObjects = [
        { name: "Projects", quant: 6, key: "projects" },
        { name: "Clients", quant: 2, key: "clients" },
        { name: "Milestones", quant: 4, key: "milestones" }];

**    function updateTileQueue(xml, tag) {         var notification = new wnote.TileNotification(xml);**
**        notification.tag = tag;**
**        var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();**
**        updater.update(notification);**
**    }**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {

**                wnote.TileUpdateManager.createTileUpdaterForApplication()**
**                        .enableNotificationQueue(true);**

WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "clear":
                                Tiles.clearTile();
                                break;
                            case "multiple":
**                                dataObjects.forEach(function (item) {**
**                                    var xml = Tiles.getTemplateContent(**
**                                        wnote.TileTemplateType.tileSquareBlock);**
**                                    Tiles.populateTemplate(xml, [item.quant, item.name]);**
**                                    updateTileQueue(xml, item.key);**
**                                });**
                                break;
                        }
                    });
            }));
        }
    };
    app.start();
})();`

您必须显式地启用通知队列,这是通过创建一个TileUpdater对象并调用enableNotificationQueue方法,传递true作为方法参数来完成的。你只需要这样做一次,这就是为什么我在应用的初始化阶段执行这项任务。

当点击Multiple Notifications按钮时,我调用forEach方法来枚举数据对象。对于每个对象,我获取并填充模板 XML,就像我在本章前面所做的那样。(我将只为方块大小创建通知,但是组合模板的过程与本章的第一部分相同)。

images 提示通知队列最多可容纳五个通知;如果您添加的项目超过五个,最新添加的项目将会推出旧的项目。

当我来更新磁贴时,差异就出现了。我使用填充的 XML 创建了TileNotification对象,然后给tag属性赋值。所有这些都发生在updateTileQueue函数中,它允许系统区分它必须显示的不同通知。您将很快了解到,您可以通过重用标记值来替换单个通知。结果是,我将三个通知放入队列,磁贴将在它们之间旋转,大约每五秒钟从一个通知切换到另一个通知。您可以在图 13 中看到显示的三个通知。

images

图十三。在一个磁贴中显示多个通知

images 注意同样,您需要在本地机器上运行这个示例应用,因为 Visual Studio 模拟器不支持动态磁贴或通知。

我真的无法通过显示单个通知来捕捉这种工作方式,我建议您运行示例应用并查看开始屏幕,看看通知队列中的项目是如何显示的。

更新通知

您可以通过发出重用标记名的更新来更新通知。例如,如果我接受了一个新客户,那么我希望更新客户通知,以正确反映额外的业务。清单 23 展示了如何响应被点击的Update Notification按钮。

清单 23。通过重用标签更新通知

... switch (this.id) {     case "clear":         Tiles.clearTile();         break;     case "multiple":         dataObjects.forEach(function (item) {             var xml = Tiles.getTemplateContent(                 wnote.TileTemplateType.tileSquareBlock);             Tiles.populateTemplate(xml, [item.quant, item.name]);             updateTileQueue(xml, item.key);         });         break; **    case "update":** **        var dob = dataObjects[1];** **        dob.quant++;** **        var xml = Tiles.getTemplateContent(** **            wnote.TileTemplateType.tileSquareBlock);** **        Tiles.populateTemplate(xml, [dob.quant, dob.name]);** **        updateTileQueue(xml, dob.key);** **        break;** } ...

我增加数值属性,并通过updateTileQueue函数发布一个更新。务必注意使用正确的tag值。如果重用标签,更新将替换现有项目,保持队列的顺序。如果您使用的标记值不在队列中,Windows 会将其视为新的通知,并将其附加到队列的末尾,这意味着您可能会得到两个显示冲突数据的通知。

调度通知

您可以安排通知出现在磁贴上的时间,以及从队列中删除通知的时间。未来安排通知的能力对于确保在应用可能被暂停或被终止时向用户呈现不是立即有用的信息是有用的。然而,当这种情况发生时,你不能指望用户查看你的应用磁贴,所以对于重要信息,你应该使用更直接的方法,比如 toast 通知,我在第二十八章中描述了这一点。

计划从队列中删除通知的时间点的能力更有用,如果用户有一段时间没有运行您的应用,您可以避免向用户显示过时的数据。我在default.html文件中添加了一个新的button来发送预定的通知,如清单 24 所示。

清单 24。添加一个按钮元素来支持预定通知

`...

<body>     <div id="container">         <button id="multiple">Multiple Notifications</button>         <button id="update">Update Notification</button> **        <button id="schedule">Schedule Notification</button>**         <button id="clear">Clear Tile</button>     </div> </body> ...`

清单 25 显示了我对default.js文件所做的更改,以响应这个button并安排一个通知。

清单 25。安排图块通知

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var dataObjects = [ ,
        { name: "Clients", quant: 2, key: "clients" },
        { name: "Milestones", quant: 4, key: "milestones" }];

function updateTileQueue(xml, tag) {
        var notification = new wnote.TileNotification(xml);
        notification.tag = tag;
        var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();
        updater.update(notification);
    }

**    function scheduleTileQueue(xml, tag, start, end) {**
**        var notification = new wnote.ScheduledTileNotification(xml, start);**
**        notification.tag = tag;**
**        notification.expirationTime = end;**
**        var updater = wnote.TileUpdateManager.createTileUpdaterForApplication();**
**        updater.addToSchedule(notification);**
**    }**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {

wnote.TileUpdateManager.createTileUpdaterForApplication()
                        .enableNotificationQueue(true);

WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "clear":
                                Tiles.clearTile();
                                break;
                            case "multiple":
                                // ...statements removed for brevity...
                                break;
                            case "update":
                                // ...statements removed for brevity...
                                break;
**                            case "schedule"😗*
**                                var xml = Tiles.getTemplateContent(**
**                                    wnote.TileTemplateType.tileSquareBlock);**
**                                Tiles.populateTemplate(xml, [10, "Days Left"]);**
**                                var start = new Date(new Date().getTime() + (20 * 1000));**
**                                var end = new Date(start.getTime() + (30 * 1000));**
**                                scheduleTileQueue(xml, "daysleft", start, end);**
**                                break;**
                        }
                    });
            }));
        }
    };     app.start();
})();`

创建通知与其他技术完全相同:您像往常一样获取并填充 XML 内容。当您将更新应用到 tile 时,差异就出现了,这是我通过清单中所示的scheduleTileQueue函数完成的。

这项技术的关键是ScheduledTileNotification对象,通过将填充的 XML 和 tile 应该开始显示更新的时间传递给构造函数来创建这个对象。时间被表示为一个Date对象,在这个例子中,我已经指定更新应该在未来 20 秒后开始显示(对于一个真正的应用来说,这是一个非常短的时间,但是对于一个例子来说是理想的,因为你在点击按钮后不久就可以看到变化)。ScheduledTileNotification对象定义了一组用于配置通知的属性,如表 3 所示。

Images

您可以从清单中看到,我已经将到期时间设置为首次显示通知后的 30 秒。ScheduledTileNotification对象的时间值是绝对的,这意味着您的Date对象应该用日、月和年来定义(而不是仅仅用一段时间来表示)。如果您启动示例应用,单击Schedule Notification按钮,并切换到开始屏幕,您可以看到这些更改的效果。

您看到的确切效果将取决于通知开始显示时磁贴的状态。如果队列中已经有通知,则计划的通知将作为定期轮换的一部分显示。到达到期时间后,计划通知将从队列中删除,仅显示原始通知。

如果图块是静态的(即队列中没有通知),情况会略有不同。通知被显示为磁贴的唯一内容,并且当到期时间到达时,磁贴重置为其静态。您可以在图 14 中看到该示例对通知的影响。

images

图十四。向磁贴添加预定通知

确定通知是否启用

在本章中,我将向您展示的最后一项技术是确定应用磁贴是否会显示实时更新。应用磁贴的设置由Windows.UI.Notifications.NotificationSetting对象中的值表示,我已经在表 4 中列出了这些值。

Images

我已经在default.html文件中添加了一个最终的button来确定应用的磁贴设置,如清单 26 所示。

清单 26。添加一个按钮来检查实时磁贴更新的状态

`...

<body>     <div id="container">         <button id="multiple">Multiple Notifications</button>         <button id="update">Update Notification</button>         <button id="schedule">Schedule Notification</button> **        <button id="check">Check Status</button>**         <button id="clear">Clear Tile</button>     </div> </body> ...`

清单 27 显示了我在default.js文件中添加的内容,以确定状态。为了简单起见,我将结果写入 Visual Studio JavaScript Console窗口,这意味着您需要使用调试器启动应用来查看消息。

清单 27。检查实时更新是否对用户可见

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var dataObjects = [
        { name: "Projects", quant: 6, key: "projects" },
        { name: "Clients", quant: 2, key: "clients" },         { name: "Milestones", quant: 4, key: "milestones" }];

// ...functions removed for brevity...

**    function getValueFromEnum(val) {**
**        for (var prop in wnote.NotificationSetting) {**
**            if (wnote.NotificationSetting[prop] == val) {**
**                return prop;**
**            }**
**        }**
**    }**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {

wnote.TileUpdateManager.createTileUpdaterForApplication()
                        .enableNotificationQueue(true);

WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "clear":
                                Tiles.clearTile();
                                break;
                            case "multiple":
                                // ...statements removed for brevity...
                                break;
                            case "update":
                                // ...statements removed for brevity...
                                break;
                            case "schedule":
                                // ...statements removed for brevity...
                                break;
**                            case "check"😗*
**                                var setting =**
**                                    wnote.TileUpdateManager.**
**                                    createTileUpdaterForApplication().setting;**
**                                console.log("Live tile updates are " +**
**                                    getValueFromEnum(setting));**
                                break;
                        }
                    });
            }));
        }
    };
    app.start();
})();`

您可以通过创建一个TileUpdater对象(通过调用TileUpdateManager.createTileUpdaterForApplication方法)并读取setting 属性的值来查看这些值中的哪一个适用。您可以通过选择应用磁贴并单击应用栏中的按钮来更改设置。两个选项如图图 15 所示。他们将在enableddisabledForApplication设置之间切换。

images

图 15。启用和禁用单个应用的磁贴通知

如果没有为切片启用通知,则生成通知不会导致任何问题-通知会被直接丢弃。这意味着,如果您想提醒用户图块通知可用,只需检查设置值。对设置值的重要性保持敏感——如果用户只禁用了你的应用的通知(disabledForApplication值),那么你的通知可能很烦人或者没有为用户提供价值。如果用户已经禁用了所有应用的动态磁贴(值为disabledForUser),那么他们不太可能为你的应用破例。如果你遇到了disabledByGroupPolicy值,那就继续——提醒用户通知可用是没有意义的,因为他们不太可能覆盖设置。这在大型企业部署中很常见,在这些部署中,以安全和易于管理的名义禁用了许多系统功能。

总结

如果使用得当,动态磁贴可以为应用的核心功能增添强大的功能,让用户不必启动应用,或者吸引用户来启动应用。在本章中,我向您展示了如何使用 live app 磁贴,从选择和填充单个模板的基本技巧开始,然后继续演示如何组合多个模板以及如何使用徽章。

在很大程度上,这些基本技术将是您所需要的,但是我也向您展示了一些用于要求更高的应用的高级特性。其中包括使用通知队列显示消息的循环序列,更新队列中各个通知的内容,以及控制通知的计划时间。我已经向你展示了如何判断是否显示实时更新,从而结束了这一章。将这项技术放在最后可能有点奇怪,但在大多数情况下并不需要,因为如果动态磁贴被禁用,通知就会被丢弃。磁贴并不是 Windows 应用唯一可用的通知机制——在下一章,当我向您展示如何使用 toast 以及介绍系统启动器功能时,您将会了解到这一点。

二十八、使用Toast和系统启动器

在这一章中,我将向你展示如何使用 toast 通知,这是一种吸引用户注意力的更直接、更具侵入性的机制。Toast 通知是对我在上一章向您展示的更微妙的磁贴通知和徽章的补充,用于更重要或紧急的信息。像任何一种通知一样,祝酒词应该谨慎使用——太少的通知会让你的用户得不到他们想要的提示和提醒,太多的会让用户疲劳和烦恼。当有疑问时,允许用户使用我在第二十七章中描述的设置特性和技术来配置发出 toast 通知的环境。

我在本章中还描述了系统启动器功能,它允许你启动用户选择的 app 来处理你的 app 无法处理的文件和 URL。这是一个简单但有用的特性,值得了解如何使用它。表 1 提供了本章的总结。

Images

使用 Toast 通知

Toast 通知是一种更直接、更吸引用户注意力的通知方式。屏幕上弹出一个祝酒词,通常伴随着声音效果,并具有打断用户注意力和工作流程的效果。因此,应该谨慎使用 toast 通知,只在重要且需要立即采取措施的情况下才显示它们。在接下来的小节中,我将带您完成创建和显示 toast 通知的过程,并向您展示当用户与它们交互时如何响应。

创建示例应用

我为本章创建的示例应用名为Toast,我遵循了与上一章中的示例相同的基本格式——一个显示按钮的单页应用,当按钮被按下时,将触发通知。您可以在清单 1 中看到我对default.html文件所做的初始添加,它包括本章中 toast 通知示例的按钮元素。

清单 1。Toast 示例应用的初始 default.html

`

<html> <head>     <meta charset="utf-8" />     <title>Toast</title>


    
    
    


    
    

    
**        ** **        ** **        **     
`

您可以在清单 2 的中看到css/default.css文件,它显示了我用来创建应用布局的样式。

清单 2。来自 Toast 项目的 css/default.css 文件

`body, #container { display: -ms-flexbox; -ms-flex-direction: row;
    -ms-flex-align: stretch; -ms-flex-pack: center;}

container

container button {font-size: 30pt;width: 400px; margin: 10px;}`

标记和 CSS 结合在一起为应用创建了一个简单的布局,你可以在图 1 中看到。布局非常简单,因为本例中的所有工作都将用于生成和管理 toast 通知。

images

图 1。吐司示例 app 的布局

对于这一章,我不需要担心保存应用状态或处理暂停或终止,所以我从一个简单的default.js文件开始,如清单 3 所示。

清单 3。Toast 示例应用的初始 default.js 文件

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
**    var wnote = Windows.UI.Notifications;**
    WinJS.strictProcessing();

**    var toastMessages = ["7pm Leave Office", "8pm: Meet Jacqui at Lucca's Bar",**
**        "9pm: Dinner at Joe's"];**
**    var toastImage =img/reminder.png";**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
**                            case "toast"😗*
**                                // code will go here**
**                                break;**
                        }
                    });
            }));
        }
    };
    app.start();
})();`

这与我在 live tiles 章节中使用的起点非常相似。代码中的switch语句被设置为当Show Toast按钮被按下时做出响应(它的id属性值为toast),我将在本章后面为剩余的按钮添加其他的case语句。

images 注意提供 toast 通知功能的对象是Windows.UI.Notifications名称空间的一部分,为了简洁起见,我在示例中将其别名为wnote。除非我另有说明,本章中我提到的所有对象都是这个名称空间的一部分。

我已经为这个例子定义了不同的提醒数据,并向名为img/reminder.png的项目添加了一个图像,如图 2 中的所示。

images

图二。添加到项目中的 reminder.png 图像,用于 toast 通知

当我在本章后面显示 toast 通知时,您将再次看到该图像。除了reminder.png文件,我还在images文件夹中添加了我在上一节中使用的相同的闹钟图标集,并将这些文件应用到应用清单的应用 UI 部分,如图图 3 所示。

images 提示你可以从apress.com开始获得这本书的免费源代码下载中的所有图片文件。

images

图三。在清单中应用图标图片

使用 toast 通知的一个重要区别是,您必须明确声明您将在应用清单中使用它们。为此,在 manifest Application UI选项卡上查找Notifications部分。将Toast设置为Yes,如图图 4 所示。

images

图 4。启用吐司通知

Images 警告如果您忘记在清单中声明您将使用 toast 通知,Windows 将自动丢弃您的 toast 通知。

创建基本的祝酒通知

创建 toast 通知的过程与创建 live tile 通知的过程非常相似:选择一个 XML 模板,填充内容,然后将其传递给系统,以便向用户显示。您可以在清单 4 的中看到我添加到default.js文件中的代码,它创建了一个基本的 toast 通知。

清单 4。创建基本的祝酒通知

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var wnote = Windows.UI.Notifications;
    WinJS.strictProcessing();

var toastMessages = ["7pm Leave Office", "8pm: Meet Jacqui at Lucca's Bar",
        "9pm: Dinner at Joe's"];
    var toastImage =img/reminder.png";

**    function getTemplateContent(templateName) {**
**        var template = wnote.ToastTemplateType[templateName];**
**        return wnote.ToastNotificationManager.getTemplateContent(template);**
**    }**

**    function populateTemplate(xml, textValues, imgValues) {**
**        if (textValues) {**
**            var textNodes = xml.getElementsByTagName("text");             var count = Math.min(textNodes.length, textValues.length);**
**            for (var i = 0; i < count; i++) {**
**                textNodes[i].innerText = textValues[i];**
**            }**
**        }**
**        if (imgValues) {**
**            var imgNodes = xml.getElementsByTagName("image");**
**            var count = Math.min(imgNodes.length, imgValues.length);**
**            for (var i = 0; i < count; i++) {**
**                imgNodes[i].attributes.getNamedItem("src").innerText = imgValues[i]**
**            }**
**        }**
**        return xml;**
**    }**

**    function showToast(xml) {**
**        var notification = wnote.ToastNotification(xml);**
**        var notifier = wnote.ToastNotificationManager.createToastNotifier();**
**        notifier.show(notification);**
**        return notification;**
**    }**

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {
                WinJS.Utilities.query("#container > button").listen("click",
                    function (e) {
                        switch (this.id) {
                            case "toast":
**                                var xml = getTemplateContent("toastImageAndText04");**
**                                populateTemplate(xml, toastMessages, [toastImage]);**
**                                showToast(xml);**
                                break;
                        }
                    });
            }));
        }
    };
    app.start();
})();`

如果你运行应用并点击Show Toast按钮,你会看到屏幕右上角弹出了吐司,你可以在图 5 中看到示例制作的吐司是什么样子。

images 注意Visual Studio 模拟器不支持 toast 通知,就像它不支持 live tiles 一样。为了测试这个例子和本章中的所有其他例子,你必须使用一个真实的 Windows 8 设备,比如你的开发机器。

images

图五。敬酒通知

我选择了一个包含图像和一些文本行的 toast 通知模板,但系统添加了 bell 徽标,我再次在清单中为我的应用配置使用了该徽标,就像我在第二十七章中所做的一样。默认情况下,当显示提示信息时,您会听到一声蜂鸣声,几秒钟后提示信息会再次消失。

一次最多可以显示三个通知,它们可以来自多个应用或来自同一个应用。第一个 toast 通知与屏幕右侧对齐,与屏幕顶部有一个小间隙,后续通知堆叠在第一个通知的下方。如果要显示三个以上的通知,则使用一个队列,当旧的通知逐渐消失时,显示新的通知。在接下来的几节中,我将解释示例中的代码是如何工作的,以及它如何导致图中所示的 toast。

获取并填充 Toast 模板

创建 toast 通知的第一步是选择模板。八个受支持的模板由Windows.UI.Notifications.ToastTemplateType对象枚举,包含纯文本和文本-图像混合选项。你可以在[msdn.microsoft.com/en-us/library/windows/apps/windows.ui.notifications.toasttemplatetype.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.notifications.toasttemplatetype.aspx)的文档中看到ToastTemplateType对象的模板样本。通过将来自ToastTemplateType对象的值作为参数传递给Windows.UI.Notifications.ToastNotficationManager.getTemplateContent方法,可以获得 toast 模板的 XML 内容。在示例中,我添加了一个助手函数来完成这项工作,我也称之为getTemplateContent。作为我在上一章中使用的方法的变体,这个函数将包含模板名称的字符串值作为它的参数,并返回适当的 XML,如清单 5 中的所示。

清单 5。来自示例应用的 getTemplateContent 函数

... function getTemplateContent(templateName) {    var template = wnote.ToastTemplateType[templateName];    return wnote.ToastNotificationManager.getTemplateContent(template); } ...

在这个例子中,我选择了toastImageAndText04模板,它包含一个图像和三行非换行文本。在该模板中,第一行以粗体显示,如果文本行太长而无法显示,Windows 将缩短它们并应用省略号(...)。您可以在清单 6 中看到这个模板的 XML。

清单 6。toastImageAndText04 模板的 XML

<toast>     <visual>         <binding template="ToastImageAndText04">             <image id="1" src=""/>             <text id="1"></text>             <text id="2"></text>             <text id="3"></text>         </binding>     </visual> </toast>

填充 XML 模板的过程与 tile 的过程相同——事实上,我从上一章复制了这个例子的populateTemplate函数。toast 模板只有一种大小,这意味着您不必担心 XML 片段的组合。在这个例子中,我使用了三个简单的文本字符串和我前面提到的图像来填充模板,您可以在清单 7 中看到结果。

清单 7。示例应用的填充 Toast 模板

<toast>     <visual>        <binding template="ToastImageAndText04">           <image id="1" src="img/reminder.png**"/>           <text id="1">**7pm Leave Office**</text>           <text id="2">**8pm: Meet Jacqui at Lucca's Bar**</text>           <text id="3">**9pm: Dinner at Joe's**</text>         </binding>     </visual> </toast>

显示 Toast 通知

一旦填充了 XML 模板,就可以向用户显示 toast 通知。在示例中,我创建了showToast函数来处理这个问题。

第一步是创建一个ToastNotification对象,将填充的 XML 作为构造函数参数传入。然后创建一个负责实际工作的ToastNotifier对象——通过调用ToastNotificationManager.createToastNotifier方法得到这个对象。最后一步是在ToastNotifier对象上调用show方法,传入ToastNotification对象向用户显示吐司。我已经重复了清单 8 中的showToast函数。

清单 8。来自示例应用的 showToast 函数

... function showToast(xml) {     var notification = wnote.ToastNotification(xml);     var notifier = wnote.ToastNotificationManager.createToastNotifier();     notifier.show(notification);     return notification; } ...

配置 Toast 通知

您可以通过向模板 XML 添加属性来更改 toast 通知的行为。在接下来的小节中,我将解释这些选项,并演示应用它们所需的代码。

配置持续时间

有两个设置可用于控制向用户显示 toast 通知的时间段:shortlong。默认是short,这意味着祝酒词显示 7 秒钟。long持续时间显示 25 秒的通知。

您可以通过向模板 XML 中的 toast 元素添加一个duration属性并将其设置为longshort(这是仅有的两个受支持的值),来指定将使用哪个设置。为了支持设置持续时间,我向示例中的default.js文件添加了一个setToastDuration函数,如清单 9 所示。

清单 9。将 setToastDuration 函数添加到 default.js 文件

`...
function getTemplateContent(templateName) {
    var template = wnote.ToastTemplateType[templateName];
    return wnote.ToastNotificationManager.getTemplateContent(template);
}

function setToastDuration(xml, duration) {
**    var attribute = xml.createAttribute("duration");**
**    attribute.innerText = duration;**
**    xml.getElementsByTagName("toast")[0].attributes.setNamedItem(attribute);**
}

// ...other functions omitted for brevity...
...`

然后我从onactivated函数中的button元素的click事件处理函数中调用这个函数。您可以看到我是如何将这一更改应用到清单 10 中的default.js文件的。

清单 10。调用 setToastDuration 函数

... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) {         args.setPromise(WinJS.UI.processAll().then(function () {             WinJS.Utilities.query("#container > button").listen("click",                 function (e) {                     switch (this.id) {                         case "toast":                             var xml = getTemplateContent("toastImageAndText04");                             populateTemplate(xml, toastMessages, [toastImage]); **                            setToastDuration(xml, "long");**                             showToast(xml);                             break;                     }                 });         }));     } }; ...

在清单 11 的中,您可以看到向模板 XML 添加了duration属性。

清单 11。向 XML 中的 Toast 元素添加 Duration 属性

<toast **duration="long"**>     <visual>         <binding template="ToastImageAndText04">             <image id="1" srcimg/reminder.png"/>             <text id="1">7pm Leave Office</text>             <text id="2">8pm: Meet Jacqui at Lucca's Bar</text>             <text id="3">9pm: Dinner at Joe's</text>         </binding>     </visual> </toast>

只有当通知的内容特别重要并且错过通知的影响很严重时,才应该使用long设置。例如,微软引用未接电话和即将到来的约会提醒作为适合long选项的例子。我对这种方法的问题是,它让我决定什么对用户来说是重要的,所以我倾向于让这成为我的应用的可配置选项,使用我在第二十章中描述的设置功能。

配置音频警报

可以通过 XML 属性配置的另一个行为是当显示 toast 通知时播放的音频警报。您可以通过添加一个audio属性来实现,这个属性有一个src属性来指定播放的音频,还有一个loop属性来指定如何播放。这两个属性的选项都非常有限,大概是为了加强某种一致性,并确保用户将小范围的音频选项与 toast 通知相关联。有九个不同的值可以用于src属性,如表 2 所示。正如你将看到的,微软一直非常禁止何时使用它们。

Images

第一次显示通知时,DefaultIMMailReminderSMS音很短,播放一次。AlarmAlarm2CallCall2声音更长,并且只要通知可见就重复。

使用表中的值来指定 toast 通知的声音效果时需要小心。如果您想要使用AlarmAlarm2CallCall2声音,那么您需要确保 audio 元素上的loop属性被设置为true,并且toast元素上的duration属性被设置为long。如果没有正确设置这两个属性,那么用户将会听到默认的声音(相当于指定了Notification.Default值)。

但是,如果您想在duration属性设置为long时使用DefaultIMMailReminderSMS声音,那么您必须确保loop设置为false。如果looptrue,那么用户将会听到Alarm的声音,与您指定的值无关。

images 提示如果你不想让任何声音伴随你的祝酒通知,使用一个silent属性设置为trueaudio元素。

对于我的示例应用,我想使用Reminder声音,这意味着我想生成如清单 12 所示的 XML,确保loop属性的值为false

清单 12。向 Toast 通知 XML 添加音频元素

<toast duration="long">     <visual>         <binding template="ToastImageAndText04">             <image id="1" srcimg/reminder.png"/>             <text id="1">7pm Leave Office</text>             <text id="2">8pm: Meet Jacqui at Lucca's Bar</text>             <text id="3">9pm: Dinner at Joe's</text>         </binding>      </visual> **     <audio src="ms-winsoundevent:Notification.Reminder" loop="false"/>** </toast>

为了将audio元素添加到示例中的 XML,我定义了setToastAudio函数,您可以在清单 13 中看到。如果silent参数为true,该函数将禁用 toast 通知的声音,否则将使用提供的值设置srcloop属性。

清单 13。示例应用的 setToastAudio 函数

`...
function getTemplateContent(templateName) {
    var template = wnote.ToastTemplateType[templateName];
    return wnote.ToastNotificationManager.getTemplateContent(template);
}

function setToastDuration(xml, duration) {
    var attribute = xml.createAttribute("duration");
    attribute.innerText = duration;
    xml.getElementsByTagName("toast")[0].attributes.setNamedItem(attribute);
} function setToastAudio(xml, silent, sound, loop) {
**    var audioElem = xml.createElement("audio");**
**    if (silent) {**
**        audioElem.setAttribute("silent", "true");**
**    } else {**
**        audioElem.setAttribute("src", sound);**
**        audioElem.setAttribute("loop", loop);**
**    }**
**    xml.getElementsByTagName("toast")[0].appendChild(audioElem);**
}

// ...other functions omitted for brevity...
...`

您可以看到我是如何从清单 14 中的default.js按钮click处理函数调用这个函数的,它创建了我在清单 12 中展示的 XML。

清单 14。调用 setToastAudio 函数

... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) {         args.setPromise(WinJS.UI.processAll().then(function () {             WinJS.Utilities.query("#container > button").listen("click",                 function (e) {                     switch (this.id) {                         case "toast":                             var xml = getTemplateContent("toastImageAndText04");                             populateTemplate(xml, toastMessages, [toastImage]);                             setToastDuration(xml, "long"); **                            setToastAudio(xml, false,** **                                "ms-winsoundevent:Notification.Reminder", false);**                             showToast(xml);                             break;                     }                 });         }));     } }; ...

这些变化的效果是,我的祝酒词伴随着与提醒相关的标准声音。

处理吐司激活和解除

我还没有把属性添加到 XML 中。还有一个属性可以使用,我将在这一节解释它是什么以及它是如何工作的。在此之前,我需要解释一下 toast 通知的生命周期。

一旦将填充的 XML 传递给ToastNotifier.show方法,通知就会呈现给用户,有三种可能的结果。第一个结果是用户通过点击或触摸通知来激活。第二种结果是用户取消通知,或者通过向屏幕的右边缘滑动通知,或者单击当鼠标在通知窗口上时出现的X图标,我已经在图 6 中显示了这一点(此图中的红色突出显示是我添加的,没有向用户显示)。

images

图六。出现在祝酒通知上的解雇图标

第三个结果是用户忽略了通知。在由duration属性指定的时间段之后,系统将通过使弹出窗口慢慢消失来代表用户消除通知。您可以通过监听ToastNotification对象发出的一组事件来响应这些结果。我在表 3 中总结了三个事件。

Images

在接下来的小节中,我将向您展示如何处理activateddismissed事件。当 Windows 不能向用户显示 toast 通知时,触发failed事件。据我所知,只发现了两个导致failed事件的原因:试图从一个应用发出超过 4096 个通知,以及试图为过去的日期安排一个 toast 通知。我将在本章的后面向你展示如何安排 toast 通知,但是我很少遇到 toast 通知的问题,所以我不打算演示如何处理failed事件。

处理激活

当用户点击或触摸正在显示的通知时,激活的事件被触发。传递给处理函数的对象的target属性返回用户激活的ToastNotification对象,如果您在生成多个通知的应用中使用单个函数,这将非常有用。ToastNotification.content属性返回用于创建通知的 XML,您需要将这个值与您的应用数据关联起来,以确定哪个通知被激活了。为此,您可以将launch属性应用于 XML 中的toast元素——该属性可以设置为任何字符串值,以便于识别通知。我在示例中添加了两个函数来设置和读取launch属性,如清单 15 所示。

清单 15。用于设置和读取 Toast 元素的 Launch 属性的函数

... function getTemplateContent(templateName) {     var template = wnote.ToastTemplateType[templateName]; `    return wnote.ToastNotificationManager.getTemplateContent(template);
}

function setToastLaunchValue(xml, val) {
**    var attribute = xml.createAttribute("launch");**
**    attribute.innerText = val;**
**    xml.getElementsByTagName("toast")[0].attributes.setNamedItem(attribute);**
}

function getToastLaunchValue(xml) {
**    var attribute =**
**       xml.getElementsByTagName("toast")[0].attributes.getNamedItem("launch");**
**    return attribute ? attribute.value : null;**
}

function setToastDuration(xml, duration) {
    var attribute = xml.createAttribute("duration");
    attribute.innerText = duration;
    xml.getElementsByTagName("toast")[0].attributes.setNamedItem(attribute);
}
...`

这些函数使用标准的 XML DOM 操作来设置或读取属性值。如果我在显示通知之前在 XML 中设置值,那么我可以更容易地识别哪个通知被激活了,如清单 16 中的所示。

清单 16。使用启动属性识别通知

`...
app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        args.setPromise(WinJS.UI.processAll().then(function () {

**            var notificationId = 0;**

WinJS.Utilities.query("#container > button").listen("click",
                function (e) {
                    switch (this.id) {
                        case "toast":
                            var xml = getTemplateContent("toastImageAndText04");
                            populateTemplate(xml, toastMessages, [toastImage]);
                            setToastDuration(xml, "long");
                            setToastAudio(xml, false,
                                "ms-winsoundevent:Notification.Reminder", false);
**                            setToastLaunchValue(xml, "notification" + notificationId++);**
                            var notification = showToast(xml);
                            notification.addEventListener("activated", function (e) {
**                                var id = getToastLaunchValue(e.target.content)**
**                                console.log("Toast notification " + id**
**                                    + " was activated");**                             });
                            break;
                    }
                });
        }));
    }
};
...`

在这个清单中,我使用setToastLaunchValue函数为每个通知分配一个launch值,并使用getToastLaunchValue函数读取该值。如果您启动应用,单击Show Toast按钮,然后单击 toast 通知弹出窗口,您将在 Visual Studio JavaScript Console窗口上看到类似于此的消息,表明通知已激活:
Toast notification notification0 was activated

处理 Toast 激活导致的应用激活

当您的应用运行或暂停时,您只能使用ToastNotification activated事件来响应 toast 激活。如果您的应用在通知显示后被用户终止或关闭,那么 Windows 将激活您的应用,并使用传递给onactivated处理函数的对象的detail.arguments属性,将您分配给通知 XML 的launch属性的值传递给您的应用。在清单 17 中,我向您展示了如何回应这些信息。

清单 17。通过 onactivated 功能接收 Toast 激活详情

`...
app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        args.setPromise(WinJS.UI.processAll().then(function () {

**            if (typeof args.detail.arguments == "string"**
**                    && args.detail.arguments.indexOf("notification") == 0) {**
**                // respond to notification being activated**
**            }**

var notificationId = 0;

WinJS.Utilities.query("#container > button").listen("click",
                function (e) {
                    switch (this.id) {
                        case "toast":
                            var xml = getTemplateContent("toastImageAndText04");
                            populateTemplate(xml, toastMessages, [toastImage]);
                            setToastDuration(xml, "long");
                            setToastAudio(xml, false,
                                "ms-winsoundevent:Notification.Reminder", false);
                            setToastLaunchValue(xml, "notification" + notificationId++);
                            var notification = showToast(xml);
                            notification.addEventListener("activated", function (e) {
                                var id = getToastLaunchValue(e.target.content)
                                console.log("Toast notification " + id
                                    + " was activated");                             });
                            break;
                    }
                });
        }));
    }
};
...`

处理解雇

当用户显式关闭弹出窗口,或者当用户完全忽略它并且超时时,ToastNotification对象触发dismissed事件。您可以通过读取传递给处理函数的事件的reason属性的值来找出发生了什么。该属性将返回由ToastDismissalReason对象枚举的值之一,我在表 4 中总结了这些值。

Images

清单 18 显示了对default.js文件的添加,以处理dismissed事件。

清单 18。处理通知驳回事件

... switch (this.id) {     case "toast":         var xml = getTemplateContent("toastImageAndText04");         populateTemplate(xml, toastMessages, [toastImage]);         setToastDuration(xml, "long");         setToastAudio(xml, false, "ms-winsoundevent:Notification.Reminder", false);         setToastLaunchValue(xml, "notification" + notificationId++);         var notification = showToast(xml);         notification.addEventListener("activated", function (e) {             var id = getToastLaunchValue(e.target.content);             console.log("Toast notification " + id + " was activated");         }); **        notification.addEventListener("dismissed", function (e) {** **            var id = getToastLaunchValue(e.target.content);** **            if (e.reason == wnote.ToastDismissalReason.userCanceled) {** **                console.log("The user dismissed toast notification " + id);** **            } else if (e.reason == wnote.ToastDismissalReason.timedOut) {** **                console.log("Toast notification " + id + " timed out");** **            }** **        });**         break; } ...

Images 提示如果你的应用正在运行,你只能知道通知何时被取消。系统不会启动您的应用来告诉您用户没有回复或取消了通知。

安排祝酒通知

您可以计划在未来某个时间向用户显示 toast 通知,可能是在您的应用不再运行或暂停时。这对于特定时间的通知很有用,比如日历提醒,你想确保用户收到通知,但你不能确定他们会在关键时刻使用你的应用。

您通过创建一个ScheduledToastNotification对象来调度 toast 通知,如清单 19 中的所示,它显示了我添加到default.js文件中的scheduleToast函数来帮助调度通知。

清单 19。添加到示例应用的 default.js 文件中的 scheduleToast 函数

`...
function getToastLaunchValue(xml) {
    var attribute =
       xml.getElementsByTagName("toast")[0].attributes.getNamedItem("launch");
    return attribute ? attribute.value : null;
}

function scheduleToast(xml, time, interval, count) {
**    var notification = wnote.ScheduledToastNotification(xml, time, interval, count);**
**    var notifier = wnote.ToastNotificationManager.createToastNotifier();**
**    notifier.addToSchedule(notification);**
}

function setToastDuration(xml, duration) {
    var attribute = xml.createAttribute("duration");
    attribute.innerText = duration;
    xml.getElementsByTagName("toast")[0].attributes.setNamedItem(attribute);
}
...`

传递给函数来创建一个ScheduledToastNotification对象的四个参数是填充的 XML 模板、一个指定通知将显示的未来时间的Date对象、暂停持续时间以及通知将暂停的次数。

当用户在由duration属性指定的时间内没有明确激活或关闭时,一个预定的 toast 通知就会暂停(我在本章前面已经描述过了)。Windows 将在暂停间隔(以毫秒为单位)结束后显示通知,这给了用户另一个响应的机会。这个过程会重复最后一个参数指定的次数。您可以看到我如何安排一个通知来响应在清单 20 中按下的Schedule Toast按钮。

清单 20。调度敬酒通知

`...
app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        args.setPromise(WinJS.UI.processAll().then(function () {

if (typeof args.detail.arguments == "string"
                    && args.detail.arguments.indexOf("notification") == 0) {
                // respond to notification being activated
            }

var notificationId = 0;

WinJS.Utilities.query("#container > button").listen("click",
                function (e) {
                    switch (this.id) {
                        case "toast":
                            // ...statements removed for brevity...
                            break;
**                        case "schedule"😗*
**                            var xml = getTemplateContent("toastImageAndText04");**
**                            populateTemplate(xml, toastMessages, [toastImage]);**
**                            var scheduleDate = new Date(new Date().getTime()**
**                                + (10 * 1000));**
**                            scheduleToast(xml, scheduleDate, 60000, 2);**
**                            break;**
                    }
                });
        }));
    }
};
...`

在这个例子中,我将通知安排在未来 10 秒钟(这样您在试验这个例子时就不必等待太久)。我告诉 Windows 暂停通知 60 秒,并允许通知暂停两次(这意味着用户将总共看到通知三次——一次在预定时间到达时,两次在通知暂停后)。

Images 警告 Windows 对暂停间隔和允许的暂停次数进行限制。间隔必须至少为 1 分钟,并且不能超过 1 小时。snoozes 的数量必须介于 1 和 5 之间(含 1 和 5)。

确定是否启用 Toast 通知

您可以通过创建一个ToastNotifier对象(通过ToastNotificationManager)并读取setting属性来决定是否显示 toast 通知。在清单 21 中,您可以看到我添加到default.js文件中的最后一条case语句,以响应被按下的Check Status按钮。

清单 21。检查 Toast 通知的状态

... switch (this.id) {     case "toast":         // *...statements omitted for brevity...*         break;     case "schedule":         // *...statements omitted for brevity...*         break; **    case "check":** **        var notifier = wnote.ToastNotificationManager.createToastNotifier();** **        var value = getToastSettingValueFromEnum(notifier.setting);** **        console.log("Toast setting: " + value);** **        break;** } ...

setting属性将返回由NotificationSetting对象枚举的一个值,为了将该值呈现为可读的形式,我在示例中添加了getToastSettingValueFromEnum函数,如清单 22 所示。

清单 22。getToastSettingValueFromEnum 函数

`...
function setToastDuration(xml, duration) {
    var attribute = xml.createAttribute("duration");
    attribute.innerText = duration;
    xml.getElementsByTagName("toast")[0].attributes.setNamedItem(attribute);
}

function getToastSettingValueFromEnum(val) {
**    for (var prop in wnote.NotificationSetting) {**
**        if (wnote.NotificationSetting[prop] == val) {**
**            return prop;**
**        }**
**    }**
}

// ...other functions omitted for brevity...
...`

您可以在表 5 中看到NotificationSetting对象定义的值。这些值与我在第二十七章中描述的值相同,当时我向你展示了如何决定是否向用户显示实时磁贴通知。

Images

使用应用启动器

有些时候,你想通知用户一些你的应用不能直接处理的重要数据。例如,我可能有一个监控新文件的Pictures库的应用,但是它只能显示有限范围的格式。我想在有新文件时通知用户,即使我不能直接在应用中显示它们。

这是一个人为的例子,因为 Internet Explorer 将很乐意显示大多数类型的图像文件-但它让我构建一个示例应用来演示应用启动器,您可以使用它来调用其他应用来为您处理您的数据。虽然图像格式可能不是问题,但是我在本节中描述的技术可以在您处理无法直接处理的数据时应用。

虽然这个示例应用很简单,但是考虑到我将要演示的特性的简单性,它是相当长的。我不会为此道歉:它允许我演示不同的功能如何一起使用——在这种情况下,在Windows.StorageWindows.Storage.Search名称空间中的对象(在第二十一章到 23 章中描述)、文件激活契约(在第二十四章中描述)、toast 通知(本章)以及新添加的应用启动器。我在这本书里给你看的例子越多,将来你有问题要解决的时候,找到你需要的东西的机会就越大。

创建示例应用

本节的示例应用名为FileWatcher,它出现了我之前描述的问题——它监控Pictures库,并在添加新文件时通知用户。我写的应用,以便它可以只显示 JPG 文件。您可以在清单 23 的中看到这个项目的default.html文件的内容。

清单 23。default.html 文件的内容

`

<html> <head>     <meta charset="utf-8" />     <title>FileWatcher</title>


    
    
    


         
    

    
             
     `

这个应用的布局基于一个img元素和一个button。当按钮被单击时,我将开始监视Pictures库,并在用户激活我的 toast 通知时使用img元素显示 JPG 图像。您可以在清单 24 中看到我用来设计布局样式的 CSS,它显示了css/default.css文件。

清单 24。default.css 文件的内容

`body { display: -ms-flexbox; -ms-flex-direction: column;
    -ms-flex-align: center; -ms-flex-pack: center;}

imgContainer { border: medium solid white;

width: 512px; height: 382px;
    margin: 20px; text-align: center;}

img {height: 382px; max-width: 512px;}
button {font-size: 24pt; margin: 10px;}`

添加图像

我已经向 images 目录添加了一个名为logo.png的文件,您可以看到我在default.html文件中的img元素的src属性中使用了这个文件。该图像在透明背景上显示一个白色图标,您可以在图 7 中看到该图像,它显示了应用的布局。

images

图 7。file watcher app 的初始布局

我还用包含相同图标的文件替换了清单中使用的图像文件,但这些文件的大小符合要求。您可以从免费的源代码下载中获得所有这些图像文件,可以从apress.com获得。

定义 JavaScript 代码

这个应用将生成 toast 通知,所以我从本章的第一部分中提取了我需要的函数,并将它们放在一个名为js/toast.js的文件中。我没有对函数做任何修改,只是将它们放在一个名为Toast的名称空间中。您可以在清单 25 的中看到toast.js文件的内容。

清单 25。toast.js 文件

`(function () {
    "use strict";

var wnote = Windows.UI.Notifications;

WinJS.Namespace.define("Toast", {

getTemplateContent: function(templateName) {
            var template = wnote.ToastTemplateType[templateName];
            return wnote.ToastNotificationManager.getTemplateContent(template);
        },

populateTemplate: function (xml, textValues, imgValues) {
            if (textValues) {
                var textNodes = xml.getElementsByTagName("text");
                var count = Math.min(textNodes.length, textValues.length);
                for (var i = 0; i < count; i++) {
                    textNodes[i].innerText = textValues[i];
                }
            }
            if (imgValues) {
                var imgNodes = xml.getElementsByTagName("image");
                var count = Math.min(imgNodes.length, imgValues.length);
                for (var i = 0; i < count; i++) {
                    imgNodes[i].attributes.getNamedItem("src").innerText = imgValues[i]
                }
            }
            return xml;
        },

showToast: function(xml) {
            var notification = wnote.ToastNotification(xml);
            var notifier = wnote.ToastNotificationManager.createToastNotifier();
            notifier.show(notification);
            return notification;
        }

});
})();`

我只需要几个功能,因为这个应用只会创建基本的吐司通知。这个应用的主要代码包含在default.js文件中,如清单 26 所示,它使用文件查询来监控Pictures库,并在添加新文件时显示 toast 通知。当用户激活通知时,布局中的img元素将显示新文件——也就是说,只要它是 JPG 文件。出于这个例子的目的,我假设我的应用不能以同样的方式处理其他文件格式。在清单中,您将看到一个注释,它充当处理其他文件类型的代码的占位符。

清单 26。js/default.js 文件的初始内容

`(function () {
    "use strict";

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var storage = Windows.Storage;

var fileList = [];

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            args.setPromise(WinJS.UI.processAll().then(function () {

startButton.addEventListener("click", function (e) {
                    startButton.disabled = true;
                    var query = storage.KnownFolders.picturesLibrary.createFileQuery(
                            storage.Search.CommonFileQuery.orderByName);

query.addEventListener("contentschanged", function (e) {

query.getFilesAsync().then(function (files) {
                            files.forEach(function (file) {
                                if (fileList.indexOf(file.path) == -1) {
                                    fileList.push(file.path);
                                    displayToastForFile(file);
                                }
                            });
                        });
                    });

setTimeout(function () {
                        query.getFilesAsync().then(function (files) {
                            files.forEach(function (file) {
                                fileList.push(file.path);
                            });
                        });
                    }, 1000);
                });
            }));
        }
    };     function displayToastForFile(file) {
        var messages = ["Found new file", file.displayName];
        var xml = Toast.getTemplateContent("toastImageAndText04");
        Toast.populateTemplate(xml, messages, ["ms-appx:img/logo.png"]);
        var notification = Toast.showToast(xml);
        notification.addEventListener("activated", function (e) {
            if (file.fileType == ".jpg") {
                imgElem.src = URL.createObjectURL(file);
            } else {
**                // ... code to process other file types will go here...**
            }
        });
    }

app.start();
})();`

当一个新文件被添加到Pictures库中时,我接收到的contentschanged事件并没有向我提供新添加文件的详细信息,所以我保存了一个文件路径数组,并使用它来确定库中哪些文件是新的。如果您的Pictures库中有大量文件,您可能需要监视不同的位置,因为数组会变得非常大。

更新清单

此应用需要进行两项清单更改才能工作。打开package.appxmanifest文件并导航至Capabilities选项卡。勾选Pictures Library选项,如图图 8 所示。这是允许应用监控新文件的Pictures库所必需的。

images

图 8。启用对图片库的访问

第二个变化是声明应用将生成 toast 通知。导航到清单的Application UI部分,将Toast capable选项设置为Yes,如图图 9 所示。(您也可以为应用设置徽标,但这不是必需的。我在第二十四章的中重复使用了我为PhotoAlbum应用创建的标志。)

images

图九。为示例应用启用 Toast 通知

测试示例应用

启动示例应用并单击Start按钮,开始监控Pictures库中的新文件。如果你拷贝一个新文件到Pictures库,你会看到一个 toast 通知,如图图 10 所示。(新文件可能需要几秒钟才能报告给应用。)

images 注意你需要在本地开发机器等真实的 Windows 8 设备上运行这个应用,因为通知不会显示在 Visual Studio 模拟器中。

images

图 10。敬酒通知报告一份新发现的文件

如果您通过点击或触摸通知来激活它,那么应用将使用标记中的img元素来显示图像,但前提是它是 JPG 文件。如果不是,那么什么都不会发生。我将在下一节中解决这个问题。

使用应用启动器

启动文件的默认应用是通过Windows.System.Launcher对象完成的,它定义了表 6 所示的方法。

Images

images 提示从这些方法中可以看出,这个对象也可以用于启动 URL 的默认应用。在这一章中,我将着重于文件激活,但是 URL 激活的过程遵循相同的模式。

使用Windows.System.LauncherOptions对象指定启动器的选项。这让你可以对启动过程进行一些细粒度的控制,包括指示 Windows 如果没有安装可以处理该文件类型的应用应该发生什么。您可以在表 7 中看到由LauncherOptions对象定义的属性。

Images

您可以在清单 27 的中看到我如何在我的示例应用中使用LauncherLauncherOptions对象,它显示了我对default.js文件中的displayToastForFile函数所做的更改。我使用带有LauncherOptions对象的launchFileAsyc方法,配置为向用户显示可以处理图像文件的应用列表。

清单 27。添加启动默认应用的支持

... function displayToastForFile(file) {     var messages = ["Found new file", file.displayName];     var xml = Toast.getTemplateContent("toastImageAndText04");     Toast.populateTemplate(xml, messages, ["ms-appx:img/logo.png"]);     var notification = Toast.showToast(xml);     notification.addEventListener("activated", function (e) {         if (file.fileType == ".jpg") {             imgElem.src = URL.createObjectURL(file);         } else { **            var options = new Windows.System.LauncherOptions();** **            options.displayApplicationPicker = true;** **            Windows.System.Launcher.launchFileAsync(file, options);**         }     }); } app.start(); ...

如果你启动这个应用,点击Start按钮,将任何不是 JPG 文件的文件复制到Pictures库,你会看到和我在上一节给你看的一样的 toast 通知。不同之处在于,当您激活 toast 时,您将看到一个已实现文件激活契约的 Windows 应用商店应用列表(以及已实现等效桌面功能的桌面应用)。选择其中一个应用,您添加到库中的文件将会打开。

此功能无需您知道对于给定的文件类型有哪些应用可用,或者用户选择了哪个应用作为默认应用。要了解这是如何工作的,复制一个完全不同类型的文件——比如一个 Word DOCX文件——你会看到 Windows 会处理细节。更好的是,微软简化了在没有安装应用的情况下查找合适应用的流程。要了解这是如何工作的,取一个现有文件,将文件扩展名改为.xxxxx(或者系统没有合适应用的任何扩展名)。当您在示例应用中激活 toast 通知时,您会看到一个有用的警告,并邀请您找到您需要的软件,如图图 11 所示。

images

图 11。帮助用户定位处理文件类型的 app

这是一个简单的功能,但它允许你构建应用,这些应用可以操作它们不直接支持的格式的文件。该特性还利用了文件和协议激活契约,允许您构建补充应用,使能够处理特定的文件格式。

总结

在这一章中,我向你展示了如何使用 toast 通知来吸引用户的注意。谨慎而周到地使用,这是一个重要的特性,可以提高你的应用对用户的价值。过度使用,你会创建一个令人讨厌的、侵扰性的应用,它会打断用户对他们不重视的通知的注意力。我还向您展示了如何使用 app launcher,它允许您启动文件或 URL 的默认应用,而不知道哪个应用被配置为默认应用,甚至不知道是否安装了合适的应用。在下一章中,我将向您展示如何使用 Windows 对传感器的支持,将真实世界的数据引入您的应用。

二十九、使用传感器

Windows 8 支持一个传感器框架,您可以使用它来获取有关设备运行条件的信息。在本章中,我描述了最常遇到的传感器,从位置传感器开始,通过它您可以确定设备正在世界上的哪个地方使用。

即使设备中没有特殊的位置感应硬件,如 GPS 接收器,Windows 也会尝试确定设备位置。我在本章中描述的其他传感器确实需要特殊的硬件,如光和加速度传感器,尽管这种设备越来越常见,特别是在笔记本电脑和平板电脑中。

在本章中,我将向您展示如何读取来自不同传感器的数据,但您如何使用这些数据为用户带来好处则取决于具体的应用。因此,示例通常只是将传感器数据显示为文本。

当你阅读这一章的时候,你会遇到一些地方,我已经注意到,在我的测试硬件中,某些特性对传感器不起作用。这并不罕见,因为传感器及其设备驱动程序的质量有很大的可变性,因此需要仔细测试。

使用传感器时,注意不要根据你收到的数据对用户在做什么做出大胆的推断。例如,当使用光线水平传感器时,不要仅仅因为光线水平低就认为现在是夜间——这可能是因为设备被放在包中,或者只是因为传感器被遮挡。始终允许用户覆盖基于传感器数据对应用行为所做的更改,并在可能的情况下,在对当前条件做出假设之前,尝试从不同来源收集数据——例如,通过将光线水平与当前时间和位置相结合,您可以避免假设工作日是晚上。表 1 提供了本章的总结。

Images

创建示例应用

我将创建一个示例应用,它将允许我演示我在本章中介绍的每个传感器。我将按照我在本书其他地方使用的模式来实现这一点,即创建一个使用导航条导航到内容页面的应用,每个页面包含一个主要功能。我创建了一个名为Sensors的新 Visual Studio 项目,您可以在清单 1 中看到default.html文件的内容。这个文件包含目标元素,我将在其中加载内容页面和导航栏,以及每个页面的命令。

清单 1。default.html 文件的内容

`

         Sensors                              
        

Select a page from the NavBar

    
    

`

HTML 文件包含了导航我在本章中使用的所有内容页面的按钮,但是我不会在每一节开始之前添加这些文件。您可以在清单 2 的示例中看到我定义的管理元素布局的样式,它显示了/css/default.css文件的内容。

清单 2。/css/default.css 文件的内容

`body {display: -ms-flexbox; -ms-flex-direction: column;
    -ms-flex-align: stretch; -ms-flex-pack: center;    }

contentTarget {display: -ms-flexbox; -ms-flex-direction: row;

-ms-flex-align: stretch; -ms-flex-pack: center;}
.container { border: medium solid white; margin: 10px; padding: 10px;}
.containerHeader {text-align: center;}

buttonsContainer button

*.imgElem {height: 500px;}
*.imgTitle { color: white; background-color: black;font-size: 30pt; padding-left: 10px;}

.messageItem {display: block; font-size: 20pt; width: 100%; margin-top: 10px;}

messageContainer

.messageList {height: 80vh;}

.label { margin: 20px; width: 600px;}
.label span { display: inline-block; width: 250px; text-align: right;}
h1.warning { display: none; text-align: center;}`

最后,我已经定义了清单 3 中的代码,它显示了/js/default.js文件的内容。在这一章中,我关注的是设备,所以我不担心管理不同的生命周期事件——因此,JavaScript 代码执行一个非常基本的应用初始化来设置内容页面的导航,我将把它添加到 Visual Studio 项目的pages文件夹中。

清单 3。/js/default.js 文件的内容

`(function () {

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;

WinJS.Navigation.addEventListener("navigating", function (e) {
        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {
            WinJS.Utilities.empty(contentTarget);
            WinJS.UI.Pages.render(e.detail.location,
                contentTarget, WinJS.Navigation.state)
                .then(function () {
                    return WinJS.UI.Animation.enterPage(contentTarget.children)
                });
        });
    });

app.onactivated = function (args) {
        if (args.detail.previousExecutionState
                != activation.ApplicationExecutionState.suspended) {             args.setPromise(WinJS.UI.processAll().then(function () {
                navbar.addEventListener("click", function (e) {
                    var navTarget = "pages/" + e.target.winControl.id + ".html";
                    WinJS.Navigation.navigate(navTarget);
                    navbar.winControl.hide();
                });
            }));
        }
    };

app.start();
})();`

如果您启动应用并显示 NavBar,您将看到如图图 1 所示的布局——尽管单击 NavBar 命令栏会产生错误,因为我还没有添加激活时加载的文件。

images

图 1。示例 app 的基本布局

images 注意为了让这个例子尽可能简单,我编写了每个内容页面,就好像它是唯一将被显示的页面一样。这意味着在显示来自不同传感器的数据之前,您必须重新启动示例应用。

使用地理定位

地理定位对于应用开发来说是一个越来越重要的功能,因为它为根据用户所处的位置定制用户体验提供了基础。许多 Windows 8 设备将配备 GPS 硬件,但 Windows 也可以尝试通过结合从一系列替代来源获取的信息来确定没有 GPS 的设备的当前位置——例如,包括设备 IP 地址和附近无线网络的名称——这种技术可能会惊人地准确。在接下来的小节中,我将向您展示如何使用地理定位特性,以及如何使用 Visual Studio simulator 测试地理定位。

images 注意地理定位是一个功能的例子,您可以使用特定于 Windows 的 API 或通过 HTML5 API 来访问。我发现 Windows APIs 倾向于提供与 Windows 设备的功能更一致的细粒度功能,这是意料之中的,因为 HTML5 APIs 的用途非常广泛。我将在本章中使用 Windows 地理定位 API。

准备地理定位示例

为了演示地理定位特性,我在 Visual Studio 项目的pages文件夹中添加了一个名为geolocation.html的文件。您可以在清单 4 中看到这个文件的内容。

清单 4。geolocation.html 文件的内容

`

              
        
    


        
        
        
    


        

Messages


        

        

        

    

`

该文件定义了一个布局,在左侧面板中有三个按钮,可以激活不同的地理定位功能,在右侧有一个大面板,可以显示更新消息。我已经将这个例子的 JavaScript 放到了一个名为/pages/geolocation.js的单独文件中,您可以在清单 5 中看到这个文件。目前,该文件包含一个处理程序函数,用于在点击button元素时进行监听,以及在收到事件时执行的占位符函数——我将在接下来的小节中填充这些函数,向您展示地理定位功能的不同方面。我还添加了代码,让我可以轻松地在应用布局中显示消息。

清单 5。/pages/geolocation.js 文件的初始内容

`(function () {
    var geo = Windows.Devices.Geolocation;

var messages = new WinJS.Binding.List();
    function writeMessage(msg) {
        messages.push({ message: msg });
    }

function getFromCode(src, code) {
        for (var propName in src) {
            if (code == src[propName]) { return propName; }
        }
    }

WinJS.UI.Pages.define("/pages/geolocation.html", {
        ready: function () {
            messageList.winControl.itemDataSource = messages.dataSource;

WinJS.Utilities.query("#buttonsContainer button").listen("click",
                function (e) {
                switch (e.target.innerText) {
                    case "Get Location":
                        getLocation();
                        break;
                    case "Start Tracking":
                        startTracking();
                        break;

case "Stop Tracking":
                        stopTracking();
                        break;
                }
            });
        }
    });

function getLocation() {
        // ...code will be added here...
    }

function startTracking() {
        // ...code will be added here...
    }

function stopTracking() {         // ...code will be added here...
    }
})();`

在清单中启用位置访问

必须在清单中启用对设备位置的访问,方法是打开package.appxmanifest文件,导航到Capabilities部分,并选中Location选项,如图图 2 所示。

images

图二。启用清单中的位置访问

Images 警告如果您试图在未启用清单中的Location功能的情况下访问设备位置,将会产生错误。

获取位置快照

获取设备当前位置最简单的方法就是拍快照,也就是说你向系统询问当前位置,系统提供,然后你就完事了。另一种方法是跟踪当前位置,这意味着当位置发生变化时,系统会为您的应用提供更新。我将在本章的后面向您展示位置跟踪是如何工作的,但是拍摄位置快照相对简单,让我介绍支持地理定位特性的对象,这些对象在Windows.Devices.Geolocation名称空间中定义。

images 注意除非我另有说明,否则我在本节中引用的所有新对象都可以在Windows.Devices.Geolocation名称空间中找到,在示例中我将它别名化为变量geo

为了添加对拍摄位置快照的支持,我在/pages/geolocation.js文件中实现了getLocation函数,如清单 6 所示。

清单 6。实现 getLocation 函数来拍摄位置快照

`(function () {
    var geo = Windows.Devices.Geolocation;
**    var geoloc;**

var messages = new WinJS.Binding.List();
    function writeMessage(msg) {
        messages.push({ message: msg });
    }

**    function getStatus(code) {**
**        for (var propName in geo.PositionStatus) {**
**            if (code == geo.PositionStatus[propName]) { return propName; }**
**        }**
**    }**

WinJS.UI.Pages.define("/pages/geolocation.html", {
        ready: function () {
            messageList.winControl.itemDataSource = messages.dataSource;

WinJS.Utilities.query("#buttonsContainer button").listen("click",
                function (e) {
                switch (e.target.innerText) {
                    case "Get Location":
                        getLocation();
                        break;
                    case "Start Tracking":
                        startTracking();
                        break;
                    case "Stop Tracking":
                        stopTracking();
                        break;
                }
            });

**            geoloc = new geo.Geolocator();**
**            writeMessage("Status: " + getStatus(geoloc.locationStatus));**
**            geoloc.addEventListener("statuschanged", function (e) {**
**                writeMessage("Status: " + getStatus(geoloc.locationStatus));**
**            });**
        }
    });

**    function getLocation() {**
**        geoloc.desiredAccuracy = geo.PositionAccuracy.high;**
**        geoloc.getGeopositionAsync().then(function (pos) {**
**            writeMessage("Snapshot - Lat: " + pos.coordinate.latitude**
**                + " Lon: " + pos.coordinate.longitude**
**                + " (" + pos.coordinate.timestamp.toTimeString() + ")");**
**        });**
**    }**     function startTracking() { /* ...code will go here... /}
    function stopTracking() { /
...code will go here... */}
})();`

虽然只有少量的添加,但是这段代码中有很多内容,所以我将在接下来的部分中一步一步地进行分解。

创建和配置地理定位器对象

获取位置的第一步是创建并配置一个Geolocator对象。一个对象可以用于多次获取位置,所以我在加载内容页面时执行的ready函数中创建了一个Geolocation对象——这将允许我在整个示例中使用一个Geolocator对象。当你创建一个新的Geolocator对象时,没有使用构造函数参数,如清单 7 所示,这里我重复了来自geolocation.js文件的语句。

清单 7。创建新的地理定位器对象

... geoloc = new geo.Geolocator(); ...

下一步是配置Geolocator对象。拍摄位置快照时,只有一个设置,通过desiredAccuracy属性访问。这个属性必须设置为由PositionAccuracy枚举定义的值之一,我已经在表 2 中描述过了。

Images

defaulthigh值并没有指定位置应该被确定的精确程度。相反,它们指定设备应该如何获取位置。default值将倾向于不太准确的信息,这些信息可以快速、轻松、免费地获得。high值将获得设备可以产生的最精确的位置,并且它将使用它所拥有的所有资源来获得该精确度,即使这意味着消耗大量的电池电量或使用可能花费用户金钱的服务(例如来自蜂窝服务提供商的定位服务)。

images 提示请注意,您无法指定用于获取位置的技术,甚至无法找出设备获取位置的不同选项。你所能做的就是设置你想要的精确度,剩下的就交给 Windows 了。

关于为属性desiredAccurcy使用哪个值的决定通常最好由用户来做,尤其是出于财务方面的考虑。发现一个写得很糟糕的应用耗尽了你所有的电池电量已经够糟糕了,但发现它在这样做的同时一直在耗费你的钱,这是无法忍受的。您可以看到我是如何在清单 8 的示例中指定高精度选项的,在这里我重复了来自geolocation.js文件的语句。

清单 8。配置地理定位器对象的精确度

... geoloc.desiredAccuracy = **geo.PositionAccuracy.high**; ...

我不担心获取位置的成本,因为我将使用 Visual Studio 模拟器向应用提供位置数据。

监控地理位置状态

Geolocator对象定义了statuschanged事件,它提供了关于地理定位特性状态的通知。一个Geoloctor对象的状态是通过locationStatus属性获得的,并且将是由PositionStatus枚举定义的值之一,我已经在表 3 中列出了这些值。

Images

我通过读取locationStatus属性的值并在应用布局右侧面板的ListView控件中显示一条消息来处理statuschanged事件。如果启动 app,会提示允许 app 访问位置数据,如图图 3 所示。

images

图三。请求位置访问

如果点击Block按钮,Windows 会拒绝你的 app 访问位置数据,并且会触发statuschanged事件,表示locationStatus属性已经更改为disabled,如图图 4 所示。

images

图 4。禁止访问位置数据的影响

如果您在应用中获得了disabled值,那么您将知道您将无法访问位置数据。在这一点上,根据你的应用,你可能能够继续并向用户提供某种减少的功能,或者你可能需要显示一条消息,鼓励用户授予你的应用所需的访问权限。你应该而不是做的是忽略disabled值,只是尝试读取位置数据——你不会得到任何数据,用户也不会明显看出缺乏位置访问是你的应用产生奇怪结果的原因。

要访问位置数据,激活Settings图标,点击Permissions链接,改变Location拨动开关的位置。当你重新加载 app 时,你会看到状态显示为ready,表示有位置数据可用,如图图 5 。

images

图 5。允许访问位置数据的效果

获取位置

点击应用布局中的Get Location按钮,调用geolocation.js文件中的getLocation函数,就是这个函数生成位置的快照。在的清单 9 中,我重复了getLocation的实现。

清单 9。getLocation 函数的实现

... function getLocation() {     geoloc.desiredAccuracy = geo.PositionAccuracy.high;     geoloc.getGeopositionAsync().then(function (pos) {         writeMessage("Snapshot - Lat: " + pos.coordinate.latitude             + " Lon: " + pos.coordinate.longitude             + " (" + pos.coordinate.timestamp.toTimeString() + ")");     }); } ...

一旦我配置了Geolocator对象,我就调用getGeopositionAsync方法。这个方法返回一个Promise,当它被实现时,将产生当前位置的快照。快照作为一个Geoposition对象传递给then函数,它定义了我在表 4 中描述的两个属性。

Images

CivicAddress对象有点令人失望,因为 Windows 没有用地址的细节填充该对象。这个想法是 Windows 的第三方附加软件可以提供这项服务,但它们并没有被广泛使用,所以你不能指望在用户的设备上找到一个安装的。

超越获得位置

Windows 位置传感器非常善于给你设备的经度和纬度,但仅此而已,你甚至不能依赖于自动填充的CivicAddress对象。如果您想超越获取基本坐标的范围,那么您将需要使用众多可用的地图 web 服务之一。我喜欢openstreetmap.org,它有出色的反向地理编码服务(将坐标转化为街道地址)和精确的地图数据。最重要的是,这些服务可以免费使用,并且不需要您将 API 密钥嵌入到您的应用中(当密钥更改或过期时,这是一个管理难题)。

如果你比我更少受到 API 键的困扰,那么你可能会考虑 Bing 地图 AJAX 控件,它使获取地址和在 Windows 应用中显示地图变得非常容易。有使用限制,高请求量收费,但地图很好,有一些应用开发的代码示例。你可以在[bingmapsportal.com](http://bingmapsportal.com)找到更多细节。当然,谷歌也有一个类似的库,它的功能和 Bing 选项一样好,也需要 API 密钥和高使用率的费用。您可以在[developers.google.com/maps](https://developers.google.com/maps)获得 Google APIs 的详细信息。

我将把注意力集中在Geocoordinate对象上,它是正确填充的,并定义了我在表 5 中描述的属性。

Images

并非Geocoordinate对象中的所有属性都将被填充——例如,这可能是因为设备没有能够提供高度细节的硬件,或者因为某些属性仅在位置被跟踪时可用,而不是拍摄快照。在示例中,当由getGeopositionAsync方法返回的Promise被满足时,我使用latitudelongitudetimestamp属性在应用布局的右侧面板中显示一条消息。

测试位置快照

现在您已经看到了各种对象是如何组合在一起的,是时候测试示例应用拍摄位置数据快照的能力了。最简单的方法是使用 Visual Studio 模拟器,它能够模拟位置数据。启动 app 前,点击模拟器窗口的Set location按钮,勾选Use simulated location选项,输入纬度值38.89和经度值-77.03,如图图 6 (这里是白宫所在地)。

images

图六。输入模拟位置数据

点击Set Location按钮应用模拟位置,然后启动示例应用。点击Get Location按钮生成位置快照,并在应用布局中显示消息。您可以在图 7 中看到本次测试使用模拟数据的结果。

images

图 7。生成位置快照

不一定要用模拟数据。如果您没有在模拟器弹出窗口中选择Use simulated location选项,那么应用将从 Windows 中读取位置数据。我使用了模拟数据,因为我想创建一个可以持续重复的结果,但我建议也使用真实数据——尤其是如果你的设备不支持 GPS。虽然可变,但非 GPS 位置数据的准确性可能相当惊人——仅使用无线网络名称,我的电脑就可以确定其位置在我家 200 英尺以内。

追踪位置

下一个向您展示的功能是在位置变化时跟踪位置的能力。如果您想监控设备的位置,您可以定期调用getGeopositionAsync方法,但是这是一个很难实现的过程。您不希望过于频繁地拍摄位置快照,因为设备可能不会移动,而且您会不必要地消耗设备资源(可能还会消耗用户的钱)。如果您拍摄快照的频率太低,您将错过设备移动的时刻,并以部分数据磁道结束。

为了更容易跟踪位置,Geolocator对象定义了positionchanged事件,当设备的位置超出您定义的阈值时,就会触发该事件。您可以看到我是如何在清单 10 中的示例应用中添加位置跟踪支持的,它展示了我对startTrackingstopTracking函数的实现,以及一个编写显示位置信息的新函数。

清单 10。位置跟踪的实现

... **function startTracking() {** **    geoloc.movementThreshold = 100;** **    geoloc.addEventListener("positionchanged", displayLocation);** **    writeMessage("Tracking started");** **start.disabled = !(stop.disabled = false);** `}

function stopTracking() {
**    geoloc.removeEventListener("positionchanged", displayLocation);**
**    writeMessage("Tracking stopped");**
**    start.disabled = !(stop.disabled = true);**
}

function displayLocation(e) {
**    writeMessage("Track - Lat: " + e.position.coordinate.latitude**
**        + " Lon: " + e.position.coordinate.longitude**
**        + " (" + e.position.coordinate.timestamp.toTimeString() + ")");**

}
...`

您可以通过设置Geolocator.movementThreshold属性的值来设置移动的阈值。当设备移动的距离超过您指定的米数时,将触发positionchanged事件。

images 提示一米大约是 3 英尺 3 英寸。可以指定一个低于设备确定其位置精度的移动阈值,在这种情况下,只要位置改变,就会触发positionchanged事件。

startTracking函数中,我将阈值设置为 100 米(大约 110 码),然后使用addEventListener方法为positionchanged事件设置一个事件监听器,指定使用displayLocation函数(也在清单中定义)来处理该事件。为了停止跟踪,我简单地调用removeEventListener函数来取消displayLocation作为事件监听器的注册。

传递给处理函数的事件对象定义了一个返回一个Geoposition对象的position属性,我使用它的coordinate属性来显示新位置的细节。

要测试位置跟踪,启动应用,通过导航栏导航到geolocation.html页面,然后单击Start Tracking按钮。现在点击 Visual Studio 模拟器的Set location按钮,输入新的坐标,然后点击Set Location按钮。每次设置新坐标时,只要新位置距离旧位置至少 100 米,就会在应用布局中显示一条新消息。在图 8 中可以看到追踪位置的效果。

images

图 8。使用 positionchanged 事件跟踪位置

使用光线传感器

如今,光传感器越来越常见,用于改变照亮设备屏幕的电量,以最大限度地减少电池消耗(屏幕通常是设备功耗的最大消耗者,在光线较暗的情况下调暗屏幕可以节省大量电力,并使屏幕使用起来不那么累)。对于一个应用来说,光传感器最常见的用途是试图找出设备何时在户外或用户何时入睡,这两者都可以与其他信息(如时间或位置)相关联,以改变应用的行为。几年前,我有一台 PDA,它利用光传感器提供不同寻常的选项,例如黎明警报和提醒我是否在室内呆了太长时间——虽然不总是成功,但玩起来很有趣。

images 注意与即使设备没有专用的位置硬件也能产生位置的位置传感器不同,光传感器(以及我在本章中描述的其他传感器)需要实际的硬件。硬件相当常见,为了测试示例项目,我使用了我在第六章的中提到的戴尔 Inspiron Duo。这对组合有一个触摸屏和一系列硬件传感器,这使它成为测试应用的理想选择。我不想听起来像是戴尔的广告(我不太喜欢戴尔),但这对组合非常便宜,尤其是二手的,我发现它对于在部署前测试应用非常有用,特别是在确保我的触摸屏交互有意义和感觉自然的时候。

为了演示光传感器,我在名为light.html的项目的pages文件夹中添加了一个文件,其内容可以在清单 11 中看到。

清单 11。light.html 文件的内容

`

         

    
        

No Light Sensor Installed

        

Light level: (None)

        

Condition: (None)

    
`

光线传感器由Windows.Devices.Sensors.LightSensor对象表示,您通过getDefault方法获得对传感器的引用,如下所示:

... var sensor = Windows.Devices.Sensors.LightSensor.getDefault(); ...

如果getDefault方法返回的值是null,那么当前设备不包含光传感器。在这个例子中,我检查了null结果,如果传感器不存在,就显示一条消息。

拍摄光线水平的快照

一旦获得了传感器对象,就可以通过调用getCurrentReading方法来获取传感器值的快照,如清单 12 所示。

清单 12。拍摄亮度快照

... displaySensorReading(**sensor.getCurrentReading()**.illuminanceInLux); ...

getCurrentReading方法返回一个Windows.Devices.Sensors.LightSensorReading对象,它定义了我在表 6 中描述的两个属性。

Images

illuminanceInLux属性返回以勒克斯为单位的亮度。维基百科对勒克斯单位有很好的描述,并有一个表格描述了一些有用的勒克斯范围。可以在[en.wikipedia.org/wiki/Lux](http://en.wikipedia.org/wiki/Lux)看到文章。我使用这些勒克斯范围的简化集来猜测设备的工作条件,包括室外、室内和办公室内——你可以在light.html文件的displaySensorReading函数中看到我的映射。

Images 注意您会注意到对getCurrentReading方法的调用在清单 11 中被注释掉了,它显示了light.html文件的内容。如果我调用getCurrentReading方法来拍摄光照水平的快照,我发现跟踪光照水平(我将在下一节描述)不再有效。这可能只是我的戴尔 Duo 中的光线传感器的一个特征,但我无法使用任何其他传感器来确定。

跟踪光线水平

拍摄光线水平的快照可能很有用,但通常您会希望在光线水平变化时收到通知。LightSensor对象定义了readingchanged事件,当光线级别改变时触发该事件。传递给处理函数的事件定义了一个名为reading的属性,该属性返回一个包含光线级别细节的LightSensorReading对象。在清单 13 的中,你可以看到我是如何为这个事件添加一个处理函数的,以及我是如何通过调用我的displaySensorReading函数来显示亮度级别(以及我对设备运行条件的猜测)的。

清单 13。通过 readingchanged 事件跟踪光线水平

... sensor.addEventListener("**readingchanged**", function (e) {     displaySensorReading(e.reading.illuminanceInLux); }); ...

测试光传感器示例

你可以在图 9 的中看到light.html页面的布局和我测试示例应用时的光线水平。

images

图九。使用光传感器

我建议在根据光线水平决定应用行为时要谨慎,因为光线水平和设备运行条件之间没有明确的相关性。在图中,你可以看到我的照明水平是 8907 勒克斯,我将其归类为在办公室。这是一个不错的猜测,但我是在黄昏前设备在我家一个有玻璃墙的房间里时拍的这张快照。我的观点是,相同的光照水平可以存在于一系列不同的系统中,在响应光传感器的读数时保持灵活是有好处的——例如,如果一个应用将我的网络设置更改为 8900 勒克斯的办公室配置,这可能会有所帮助,但某种覆盖是必不可少的,因为我今天正好在家工作。我在本章中描述的所有传感器都是如此——传感器数据可能很有用,它可以使你的应用更有帮助和更灵活,但你应该适应你对读数的反应方式,并在数据导致你做出错误推断时,总是为用户提供一个覆盖。

使用倾斜仪

测斜仪测量设备倾斜的角度。倾斜仪通常用在游戏中,以允许该设备被用作控制器。您很快就会看到,使用测斜仪与使用光传感器非常相似,因为大多数代表传感器的物体都有大致相似的设计。为了演示测斜仪的使用,我在示例项目的pages文件夹中添加了一个名为tilt.html的文件,其内容可以在清单 14 中看到。

清单 14。tilt.html 文件的内容

`

              

` `     
        

No Inclinometer Installed

        

Pitch: (None)

        

Roll: (None)

        

Yaw: (None)

    
    
        
    
`

测斜仪由Windows.Devices.Sensors.Inclinometer物体表示;就像光传感器一样,您必须调用getDefault方法来获取一个您可以从中读取数据的对象,就像这样:

... var sensor = Windows.Devices.Sensors.Inclinometer.getDefault(); ...

如果getDefault方法返回的值为 null,那么设备没有安装测斜仪。

使设备倾斜

您可以通过调用getCurrentReading方法来获取当前设备倾斜的快照,该方法返回一个Windows.Devices.Sensors.InclinometerReading对象,该对象定义了表 7 中显示的属性。

Images

对象定义了一个事件,但是我不能让它在我的设备上触发。为了解决这个问题,我已经注释掉了事件处理代码,并用一个对setInterval的调用来代替它,我用它来重复轮询传感器的倾斜值。作为显示读数过程的一部分,我对布局中的一个div元素进行了变换,以便显示一个随着设备旋转而“自动调平”的正方形——这与我在第十八章中描述的变换类型相同。你可以在图 10 中看到该应用的布局(请记住,这张截图是在设备倾斜时拍摄的)。

images

图十。跟踪和显示设备倾斜

使用加速度计

加速度计测量加速度,通常与位置数据一起使用,以确定设备移动的方式和时间-例如,如果位置跟踪报告设备以每小时 6 英里的速度移动,而加速度计报告有规律的加速度脉冲,则与锻炼相关的应用可能会开始记录数据,以防用户忘记记录他们的日常跑步。加速度计也可以用于确定设备如何定向,因为当设备静止时,朝向地球的加速度将是 1g。

为了演示加速度计的使用,我在 Visual Studio 项目的pages文件夹中添加了一个名为acceleration.html的新文件。您可以在清单 15 中看到这个文件的内容。

清单 15。acceleration.html 文件的内容

`

         

` `     
        

No Accelerometer Installed

        

Accelerate X: (None)

        

Accelerate Y: (None)

        

Accelerate Z: (None)

    
`

你现在应该能认出这个模式了。加速度计设备由Windows.Devices.Sensors.Accelerometer对象表示,您必须调用getDefault方法来获取表示传感器的对象,如下所示:

... var sensor = Windows.Devices.Sensors.Accelerometer.getDefault(); ...

如果来自getDefault方法的结果为空,则设备中没有加速度计硬件。

获得设备加速度

您可以通过调用返回一个Windows.Devices.Sensors.AccelerometerReading对象的getCurrentReading方法来获得加速度计测量的力的快照。这个对象定义了我在表 8 中描述的属性。

Images

我在displaySensorReading函数中读取这些属性的值,这产生了如图图 11 所示的布局和数据。

images

图 11。测量加速度

您可以通过监听由Accelerometer对象发出的readingchanged事件来跟踪设备加速度。传递给处理函数的事件对象的reading属性返回一个AccelerometerReading对象,您可以在acceleration.html文件中看到我是如何处理这个事件的。

images Accelerometer也定义了shaken事件。一些加速度计硬件可以检测到设备何时被快速摇动,这个手势将触发shaken事件。我的测试设备中的加速度计硬件不支持摇动手势,通常解释设备方向的变化(我在第六章的中描述过)。依赖此事件时要小心,因为它可能不会被触发,并且要求用户摇动设备可能会导致意外的配置更改,正如我在戴尔 Duo 上的经历一样。

使用指南针

指南针允许您确定设备指向的方向。使用指南针需要磁力计硬件,它可以测量磁场的强度和方向。指南针在确定地图数据的方向以使其与现实世界相符时最有用——例如,我进行了大量的长距离行走和跑步,我的手持(非 Windows) GPS 设备使用其指南针来确保拓扑图与我所面对的方向相符,这使我更容易找到自己的位置。

为了演示指南针传感器,我在 Visual Studio 项目的pages文件夹中添加了一个名为direction.html的新文件,您可以在清单 16 中看到该文件的内容。

清单 16。direction.html 文件的内容

`

              

    
        
                     
    
    
        

No Compass Installed

        

Heading: (None)

    
`

指南针的工作原理和我在本章中描述的其他传感器一样。指南针由Windows.Devices.Sensors.Compass对象表示,您必须调用getDefault方法来获取对象,以便读取传感器数据,如下所示:

... var sensor = Windows.Devices.Sensors.Compass.getDefault(); ...

如果getDefault方法返回 null,那么设备没有指南针传感器硬件。

获取设备方向

您可以通过调用返回一个Windows.Devices.Sensors.CompassReading对象的getCurrentReading方法来获得设备朝向的快照。该对象定义了表 9 中所示的属性。

Images

并非所有的指南针硬件包都能够产生磁航向和真北航向,例如,我的测试设备中的传感器只产生磁方位。在本例中,我将CompassReading对象传递给displaySensorReading函数,该函数显示数字标题并对div元素进行旋转,以显示一个始终指向北方的箭头。您可以在图 12 的中看到布局和传感器数据。

images

图 12。显示指南针的数据

您可以通过监听readingchanged事件来跟踪航向的变化,当指南针传感器报告的方向发生变化时,Compass对象会触发该事件。我在示例中使用这个事件来保持示例布局最新。

总结

在本章中,我向您展示了您的应用如何利用 Windows 8 传感器框架来获取真实世界的数据。像所有高级功能一样,传感器数据需要谨慎使用,但您可以通过使用您收到的数据来创建灵活和创新的应用,以适应他们的环境。在本书的下一部分,我将向您展示如何准备您的应用并将其发布到 Windows 应用商店。

三十、创建要发布的应用

在本书的这一部分,我将向您展示如何在 Windows 应用商店中发布应用。我将从头开始,创建一个应用,从创建到提交给微软审查。在此过程中,我将向您展示如何应用驱动您的业务模式的功能,并确保应用符合 Windows 应用商店的政策。在这一章中,我将带您完成开始之前需要的步骤,然后创建一个应用,我将在接下来的章节中介绍它的发布过程。

决定你的应用

显然,应该从决定你的应用要做什么开始。对于这一章,我将创建一个照片浏览器应用。这不是任何真正的用户想要付费的东西(尤其是因为 Windows 8 中已经包含了这样的应用),但它是这本书这一部分的理想例子,因为功能简单且独立,让我专注于发布过程的不同部分。

决定你的商业模式

当创建一个应用时,首先要做的是决定你想要使用的商业模式。Windows 应用商店支持一系列不同的应用销售方式,包括免费赠送、试用版、收取固定费用、收取订阅费、应用内广告和销售应用内升级。

对于我的示例应用,我将提供一个免费的限时试用来吸引用户,然后向他们收取 5 美元的基本应用费用。但我的 Windows 8 财富将以应用内升级的形式出现,其中一些将获得永久许可,一些将在订阅的基础上出售。

images 注意我不打算演示应用内广告,因为它与 Windows 8 商店没有直接关系。微软有一个广告 SDK,可以很容易地在一个应用中包含广告,你可以从[advertising.microsoft.com/windowsadvertising/developer](http://advertising.microsoft.com/windowsadvertising/developer)获得。如果你想使用另一家广告提供商,你需要仔细检查广告内容和获取广告的机制是否违反了应用认证要求(我在第三十三章中向你展示了如何测试)。

在表 30-1 中,我已经列出了我的应用的不同可购买功能、它们的价格以及它们的用途。该应用的免费试用将支持 30 天的所有功能。

Images

让我再次强调,我不会真的卖这个应用,我只是需要一个工具来告诉你如何卖你的。因此,虽然我的应用及其升级既乏味又昂贵,但它们将允许我演示如何在一个应用中创建和组合一系列商业模式。

准备就绪

在开始创建我的应用之前,我需要做几件事情。首先是创建一个 Windows Store 开发者账户,允许你向商店发布应用,并从中获得任何收入。您可以以个人身份或代表公司创建一个帐户,Microsoft 会更改该帐户的年费(目前个人为 49 美元,公司为 99 美元)。您将需要一个 Microsoft 帐户来打开 Windows 应用商店开发人员帐户,但您在下载 Visual Studio 时应该已经有了一个。

images 提示微软将 Windows Store 账户作为其开发者产品的一部分,如 TechNet 和 MSDN。如果您已经购买了这些服务中的一项,您可能不必直接为帐户付费。

在 Visual Studio 的Store菜单中选择Open Developer Account,开始创建帐户的过程。获得帐户的过程需要填写一些表格并设置支付细节,这样你就可以从你的应用销售中获得利润。一旦你创建了一个账户,你会看到如图图 30-1 所示的仪表板。此仪表板提供了你的商店帐户的详细信息,包括你的应用和付款。

images

图 30-1。Windows Store 仪表盘

保留应用名称

开始开发应用之前,您可以在 Windows 应用商店中保留应用的名称。预约有效期为一年,在此期间,只有您可以发布该名称的应用。保留一个名字是一个明智的想法,这样你就可以继续创作艺术品、网站和营销宣传材料,而不用担心在你开发应用时别人会用这个名字。

你可以通过从 Visual Studio Store菜单中选择Reserve App Name项或者点击 Windows 应用商店仪表盘中的Submit an app链接来保留名称(反正Reserve App Name菜单会带你去那里)。仪表盘呈现了发布一个 app 所需的不同步骤,但我目前唯一关心的是第一步,也就是App name步骤,如图图 30-2 所示。

images

图 30-2。显示应用发布流程第一步的 Windows 仪表盘

点击App name项,输入您想要预订的姓名。对于我的 app,我保留了名称Simple Photo Album,如图图 30-3 所示。

images

图 30-3。选择应用的名称

您保留的名称是应用将在商店中列出的名称,与您的 Visual Studio 项目的名称不同。我将在第三十三章中向您展示如何将 Visual Studio 项目与仪表板关联起来,但我甚至还没有创建我的 Visual Studio 项目)。

images这意味着当你阅读这一章时,我对Simple Photo Album名称的保留可能已经失效,并且可能已经被其他人使用。

一旦您保留了您的应用名称,仪表板将更新发布过程的第一步以反映您的选择,如图 30-4 所示。

images

图 30-4。完成应用发布流程的第一步

这就是目前所需要的全部准备工作。保留名称后,我现在可以创建我的 Visual Studio 项目并开始构建我的应用。

创建 Visual Studio 项目

我已经使用Blank App模板创建了一个新的 Visual Studio 项目。我将项目命名为PhotoApp,只是为了演示您可以将项目的名称与应用在商店中的名称和用户名称分开。你可以在图 30-5 中看到成品 app 的外观。

images

图 30-5。示例 app 的布局

左侧面板包含一些按钮和ToggleSwitch控件,允许用户选择显示哪些图像以及是否显示缩略图。右侧面板包含一个大的FlipView控件,该控件始终可见,可用于浏览已加载的图像。在右边面板的底部是一个ListView控件,显示可用图像的缩略图。缩略图的可见性和图像的选择将由用户购买的功能来控制。在本章的后面,我将向你展示应用布局的各个组成部分。

创建商业模式代码

如果你正在创建一个完全免费的应用,那么你可以通过应用我在本书中向你展示的技术来开始构建你的功能。你不必担心收款或启用功能或任何其他类型的与商店的互动。但是如果你打算对你的应用或者它的一些功能收费,你需要仔细考虑你的应用的结构。我发现最好的方法是从一开始就将一些与核心商业模式相关的功能植入应用,这样我就可以构建应用的功能,然后回来实施我的商业计划。

为此,我添加到PhotoApp项目的第一个文件叫做store.js,我把它放到了js文件夹中。你可以在清单 30-1 中看到这个文件的内容。

清单 30-1 。/js/store.js 文件的内容

`(function() {

WinJS.Namespace.define("ViewModel.Store", {
        events: WinJS.Utilities.eventMixin,

checkCapability: function(name) {
            var available = true;
            setImmediate(function () {
                ViewModel.Store.events.dispatchEvent("capabilitycheck",
                    { capability: name, enabled: available });
            });
            return available;
        }
    });

})();`

这个文件目前非常简单,但当我添加将我的应用集成到 Windows 商店的支持时,它将成为我在第三十一章中的主要关注点。我创建了一个名为ViewModel.Store的新名称空间,并在其中添加了一个checkCapability函数。应用的其他部分将调用这个函数来查看用户是否已经购买了执行某些操作的权利——在第三十二章中,我将实现代码来检查我的产品层和升级,但目前每个请求都返回true,表明某个功能可用。这将允许我在实现我的业务模型代码之前构建出我的应用的功能。

当调用checkCapability函数时,它会发出一个capabilitycheck事件——我将在第三十二章中使用这个事件来响应用户试图使用他们尚未购买的功能。我已经使用WinJS.Utilties.eventMixin实现了事件,这是一个有用的对象,您可以使用它向常规 JavaScript 对象添加事件支持。它定义了在 DOM 元素对象上可以找到的标准的addEventListenerremoveEventListener方法,以及dispatchEvent方法,该方法允许您向注册的侦听器发送任意类型的事件。使用eventMixin 对象比编写自己的事件处理代码更简单,也更不容易出错,通过ViewModel.Store.event属性使对象可用,我提供了一个点,应用的其他部分可以在这里注册事件。

images 提示你不必创建一个eventMixin对象的新实例。一个实例在想要使用该对象的应用的任何部分之间共享,eventMixin代码将不同类型事件的侦听器分开。你可以通过查看 Visual Studio 项目参考中的base.js文件来了解微软是如何实现这个特性的。

创建视图模型状态

我的下一步是创建让我维护应用状态的代码,我已经通过添加一个名为viewmodel.js的新文件到js文件夹中完成了。您可以在清单 30-2 中看到该文件的内容。

清单 30-2 。viewmodel.js 文件的内容

`(function () {

WinJS.Namespace.define("ViewModel", {
        State: WinJS.Binding.as({
            pictureDataSource: new WinJS.Binding.List(),
            fileTypes: false,
            depth: false,
            thumbnails: false,
        }),
    });

WinJS.Namespace.define("Converters", {
        display: WinJS.Binding.converter(function(val) {
            return val ? "block" : "none";
        })
    });

})();`

名称空间ViewModel.State包含了WinJS.Binding.List对象,我将使用它作为数据源,通过 WinJS FlipViewListView UI 控件来显示图片。我还定义了一组可观察的属性,我将使用它们来跟踪用户当前在应用中启用了哪些功能——这不同于用户是否许可了它们。我需要跟踪用户使用某个特性的权限以及它当前是否开启,而ViewModel.State名称空间中的属性跟踪后者。

images 提示这种方法要求我在启用ViewModel.State名称空间中的相应属性之前,检查用户是否有权使用某个特性。这就是ViewModel.Store.checkCapability函数的用途,当我向您展示/js/default.js文件的内容时,您将很快看到我是如何处理它的。

定义布局

为了创建我在图 30-5 中展示的布局,我将清单 30-3 中显示的元素添加到default.html文件中。这个应用不需要任何导航或内容页面,因为它需要这样一个简单的布局。

清单 30-3 。在 default.html 文件中定义示例应用的标记

`

         PhotoApp                               **    ** **    **      **    
** **        
** **            ** **            
** **        
** **    
** **    
** **        
** **            ** **        
** **    
**

**    

**
**        <div id="fileTypes" data-win-control="WinJS.UI.ToggleSwitch"**
**            data-win-bind="winControl.checked: State.fileTypes"**
**            data-win-options="{title: 'Show JPG',  labelOn: 'Yes', labelOff: 'No'}">**
**        
**
**        <div id="depth" data-win-control="WinJS.UI.ToggleSwitch"**
**            data-win-bind="winControl.checked: State.depth"**
**            data-win-options="{title: 'All Folders',  labelOn: 'Yes', labelOff: 'No'}">**
**        **
**        <div id="thumbnails" data-win-control="WinJS.UI.ToggleSwitch"**
**            data-win-bind="winControl.checked: State.thumbnails"**
**            data-win-options="{title: 'Show Thumbnails',labelOn: 'Yes', labelOff: 'No'}">**
**        **
**        **
**        **
**         
**
**        <div id="flipView" data-win-control="WinJS.UI.FlipView"**
**            data-win-options="{ itemTemplate: flipTemplate,**
**                itemDataSource: ViewModel.State.pictureDataSource.dataSource}">**
**        
**
**        <div id="listView" data-win-control="WinJS.UI.ListView"**
**            data-win-bind="style.display: State.thumbnails Converters.display"**
**            data-win-options="{ itemTemplate: listTemplate,**
**                tapBehavior: invokeOnly,**
**                itemDataSource: ViewModel.State.pictureDataSource.dataSource}">**
**        **
**    **

`

标记可以分为三类。第一类是进入左侧面板的控制元素(由属性为buttonsContainerdiv元素表示)。除了标准的 HTML button元素,我还应用了WinJS.UI.ToggleSwitch控件,我在第十一章的中描述过。您可以在图 30-6 中看到详细的控制按钮,并且您会注意到这些按钮代表了我将作为升级出售给用户的功能。

images

图 30-6。控制元素

我需要某种方式让用户在试用期间购买基本功能或升级到全套功能,这就是我添加Buy/Upgrade按钮的原因。您可以看到我是如何将ToggleSwitch控件的 checked 属性链接到ViewModel.State名称空间中的可观察值的。

下一部分标记包含在div元素中,该元素的id属性是imageContainer。我使用一个FlipView控件作为主图像显示,一个ListView控件显示缩略图。我在第十四章和第十五章中介绍了这些 WinJS UI 控件,我在这个应用中对它们的使用非常简单和标准。

标记的最后一部分是用于ListViewFlipView控件的模板。这些使用了我在第八章的中描述的WinJS.Binding.Template功能。两个模板都有一个img元素,对于用于FlipView控件的模板,我使用一个div元素来显示当前显示文件的名称。在图 30-7 中,您可以看到控件和模板是如何组合成应用布局的右侧面板的。

images

图 30-7。示例应用的右侧面板

定义 CSS

这个例子的 CSS 非常简单——我非常依赖 flex box 布局来创建一个适应不同屏幕分辨率、设备方向和布局的应用。你可以在清单 30-4 的文件中看到我定义的样式。

清单 30-4 。/css/default.css 文件的内容

`body {display: -ms-flexbox; -ms-flex-direction: row;}

div.container { border: medium solid white; padding: 20px; margin: 20px;
    display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: stretch;
    -ms-flex-pack: center;}

div[data-win-control='WinJS.UI.ToggleSwitch'] { border: thin solid white;
    padding: 20px; margin: 10px 0;}

buttonContainer

buttonContainer button

imageContainer

flipView

listView { -ms-flex: 1; max-height: 200px; min-height: 200px; border: thin solid white;

display: none;}

.flipItem {display: -ms-flexbox;-ms-flex-direction: column;-ms-flex-align: center;
    -ms-flex-pack: center; width: 100%; height: 100%;}
.flipImg { position: relative; top: 0; left: 0; height: calc(100% - 70px); z-index: 15;}
.flipTitle { font-size: 30pt;}
.listItem img { width: 250px; height: 200px;}

@media print {
    #buttonContainer, #listView { display: none;}
}

@media screen and (-ms-view-state: snapped) {
    #buttonContainer { display: none;}
}

@media screen and (-ms-view-state: fullscreen-portrait) {
    body { -ms-flex-direction: column-reverse;}
    #buttonContainer { -ms-flex-direction: row; -ms-flex-pack: distribute;}
    #buttonContainer button {display: none;}
}`

在打印时,以及当应用处于纵向和快照布局时,我使用媒体查询来更改应用布局。不同的布局与 Windows 应用商店集成没有直接关系,但为了完整起见,我在布局中添加了一些变化。

定义 JavaScript 代码

/js/default.js文件包含将布局的不同部分联系在一起并显示图像的代码。你可以在清单 30-5 中看到这个文件的内容。

清单 30-5 。default.js 文件的内容

`(function () {
    "use strict";

WinJS.Binding.optimizeBindingReferences = true;

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var storage = Windows.Storage;
    var search = storage.Search;

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !==
                activation.ApplicationExecutionState.suspended) {

refresh.addEventListener("click", function (e) {
                    loadFiles();
                });

WinJS.Utilities.query("#buttonContainer > div").listen("change",
                    function (e) {
                    if (ViewModel.Store.checkCapability(e.target.id)) {
                        ViewModel.State[e.target.id] = e.target.winControl.checked;
                        if (e.target.id == "thumbnails") {
                            listView.winControl.itemDataSource
                                = ViewModel.State.pictureDataSource.dataSource;
                        } else {
                            setImmediate(loadFiles);
                        }

} else {
                        e.target.winControl.checked = false;
                    }
                });

listView.addEventListener("iteminvoked", function (e) {
                    flipView.winControl.currentPage = e.detail.itemIndex;
                });
                flipView.addEventListener("pageselected", function (e) {
                    var index = flipView.winControl.currentPage;
                    listView.winControl.ensureVisible(index);

});
            }
            args.setPromise(WinJS.UI.processAll().then(function() {
                return WinJS.Binding.processAll(document.body, ViewModel)
                    .then(function () {
                        setupPrinting();
                        loadFiles();                 });
            }));
        }
    };

function setupPrinting() {
        Windows.Graphics.Printing.PrintManager.getForCurrentView().onprinttaskrequested =
            function (e) {
                if (ViewModel.Store.checkCapability("print")
                    && ViewModel.State.pictureDataSource.length > 0) {
                    var printTask = e.request.createPrintTask("PrintAlbum",
                        function (printEvent) {
                            printEvent.setSource(
                                MSApp.getHtmlPrintDocumentSource(document));
                    });
                    printTask.options.orientation
                        = Windows.Graphics.Printing.PrintOrientation.landscape;
            };
        };
    }

function loadFiles() {
        var options = new search.QueryOptions();
        options.fileTypeFilter.push(".png");

if (ViewModel.State.fileTypes) {
            options.fileTypeFilter.push(".jpg", ".jpeg");
        }
        if (ViewModel.State.depth) {
            options.folderDepth = search.FolderDepth.deep;
        } else {
            options.folderDepth = search.FolderDepth.shallow;
        }
        storage.KnownFolders.picturesLibrary.createFileQueryWithOptions(options)
            .getFilesAsync().then(function (files) {
                var list = ViewModel.State.pictureDataSource;
                list.dataSource.beginEdits();
                list.length = 0;
                files.forEach(function (file) {
                    list.push({
                        image: URL.createObjectURL(file),
                        title: file.displayName
                    });
                });
                list.dataSource.endEdits();
        })
    };

app.start();
})();`

这个例子中需要注意的重要一点是,ViewModel.State名称空间中的属性控制应用行为的方式。例如,在loadFiles函数中,定位的文件类型和搜索文件的深度由ViewModel.State.fileTypesViewModel.State.depth属性驱动,如下所示:

... if (**ViewModel.State.fileTypes**) {     options.fileTypeFilter.push(".jpg", ".jpeg"); } if (**ViewModel.State.depth**) {     options.folderDepth = search.FolderDepth.deep; } else {     options.folderDepth = search.FolderDepth.shallow; } ...

这些属性由应用布局中的ToggleSwitch控件设置,但是只有在成功调用ViewModel.Store.checkCapability函数后,这些值才会更改,如下所示:

... if (**ViewModel.Store.checkCapability(e.target.id)**) {     ViewModel.State[e.target.id] = e.target.winControl.checked;     if (e.target.id == "thumbnails") {         listView.winControl.itemDataSource= ViewModel.State.pictureDataSource.dataSource;     } else {         setImmediate(loadFiles);     } } else {     e.target.winControl.checked = false; } ...

为了简单起见,我将每个ToggleSwitch控件的id设置为我想要检查的功能的名称,并在ViewModel.State名称空间中使用相同的属性名称。实现业务模型代码可能会变得复杂,在你的应用组件和你销售的产品和升级之间实现尽可能多的通用性会让生活变得容易得多。

也就是说,在很大程度上,这些代码使用了我在本书中向您展示的技术。我不想详细讨论代码,因为我已经描述了各种特性是如何工作的。万一有什么东西吸引了你的注意,而你又找不到它,我在表 30-2 中列出了代码的关键特性,你可以在本书的什么地方找到它们。

Images

更新清单

创建基本应用的最后一步是更新清单。为了访问Pictures库中的文件,我需要打开package.appxmanifest文件,导航到Capabilities部分,勾选Pictures Library选项,如图图 30-8 所示。

images

图 30-8。允许访问清单中的图片库

我还在images文件夹中添加了一些新文件,用于磁贴和闪屏。我添加的所有文件都显示相同的图标,在透明背景上绘制成白色。你可以在图 30-9 中看到该图标,它显示在黑色背景上,以便在页面上可见。

images

图 30-9。用于示例应用的图标

我添加的文件被命名为tile<size>.png,其中<size>是以像素为单位的图像宽度。你可以在图 30-10 中看到我是如何将图像应用到应用中的,图中显示了货单的Application UI部分以及我所做的更改。

images

图 30-10。更改应用使用的磁贴和闪屏图像

测试示例应用

本章剩下的工作就是测试示例应用。使用 Visual Studio Debug菜单中的Start Debugging项启动应用。默认情况下,ViewModel.State命名空间中的属性设置为false,这意味着您将看到基本的应用布局,如图图 30-11 所示。

images

图 30-11。基础 app 布局

当然,当你运行应用时,你会看到什么取决于你的Pictures Library的内容。当应用首次启动时,它将只显示根Pictures目录中的 PNG 文件,这将限制显示的内容。在基本模式下,您可以通过滑动FlipView或点击其导航按钮来浏览图像。

通过启用ToggleSwitch控件,您可以扩大显示图像的范围,以便包含 JPG 文件和Pictures Library中更深层次的文件。当然,还会显示ListView控件,显示可用图像的缩略图。

Refresh按钮将清除数据源的内容,并从磁盘重新加载文件。此时Buy/Upgrade按钮没有任何作用,但是我会在第三十二章中连接它,这样用户就可以进行购买。

总结

在本章中,我开始了创建和发布应用的过程,首先为我的应用保留名称,然后构建将提供给用户的基本功能。从一开始,我就添加了检查用户是否购买了应用关键功能的支持,我将在下一章实现这些检查背后的策略,我还将向您展示如何将您的应用集成到 Windows 应用商店中。

三十一、Windows 应用商店集成

在本章中,我将向您展示如何将您的应用与 Windows 应用商店集成,并实现您的应用商业模式。我将向您展示提供商店功能访问的对象,以及如何在您的应用发布到商店之前使用它们来模拟不同的许可场景。我将向您展示如何确定您的用户有权使用哪些功能,以及如何强制实施该模型来阻止在没有合适许可证的情况下使用应用。

您会发现,与 Windows 商店集成的技术相当简单,但是实施业务模型和模拟不同的场景可能相当复杂,需要大量的测试。

关于应用许可的一般建议

在我开始为我的示例应用实现和实施业务模型之前,我想提供一些一般性的建议。期望你的用户付费使用你的应用是完全合理和公平的,但你需要对你提供的功能的价值和你所处的世界持现实态度。

你必须接受你的应用会被广泛盗版。它会在发布后的几个小时内出现在意想不到的地方,任何许可执行和权限管理都会立即被破坏。用户(如果你有一个特别吸引人的应用,会有很多用户)会从你的辛勤工作中获益,而不用付给你一分钱。

如果这是你的第一个应用,你会经历一系列常见的情绪:震惊、沮丧、愤怒和不公平感。你可能会非常沮丧,以至于更新你的应用来添加更复杂的许可方案,或者要求定期检查一些额外的验证服务。这是你能做的最糟糕的事情。

每次你添加额外的权限管理和许可层,你就让你的应用更难使用。但你实际上并没有让复制变得更难,因为没有一个现存的方案能够抵挡住想要免费拷贝的人的关注。你唯一惩罚的人是你的合法用户,他们现在不得不千方百计让你的应用工作。在竞争激烈的市场中,你希望消除尽可能多的障碍来吸引用户使用你的应用——而你增加的每一个使用障碍都会让竞争对手的应用更有吸引力。

一个更明智的方法是接受人们总是会抄袭你的软件,并花些时间考虑为什么会这样。

你可能想知道是什么让我觉得我可以告诉你让所有那些罪犯敲诈你——但这是我花了很多时间思考的事情,因为它每天都影响着我。

书籍和应用有很多共同点

我的书在出版后几小时内就出现在文件共享网站上。这种情况已经发生了很多年,我开始明白,这并不是第一次出现的问题。

首先,从纯实践的角度来看,我没有办法阻止我的书被分享,即使我想这样做。第二,我开始意识到人们下载非法拷贝有各种原因,其中一些可能有利于我的长期销售数字。以下是我为自己的书考虑的几大类别:

  1. 收集者
  2. 快速修补程序
  3. 预算员
  4. 疼痛回避者

收藏者分享我的书是因为他们可以——他们喜欢收集大型图书馆的书籍、音乐、应用、游戏以及任何他们感兴趣的东西。对我来说,这些人并不代表销售的损失,因为他们从一开始就不会买一本,以后也不会。

速战速决者是那些有特定问题想要解决的人,他们会复制我的一本书,看看里面是否有解决方案。这些人也不代表销售失败,因为对他们来说,一个特定问题的解决方案并不代表 20-50 美元的价值。但是这些人确实代表了潜在的未来客户——他们可能记得他们发现我的书很有用,并为他们想更深入了解的主题购买了一本(或我的另一本著作)。

预算者是那些可能更喜欢买一本,但又买不起的人。书可能很贵,如果手头紧的话,书有多重要也没关系。这些人是也是潜在客户,因为他们现在可能破产了,但情况不会总是这样。当他们有更多的钱时,他们可能会开始买书,我希望他们在买书时对我的书有正面的感觉。

痛苦回避者是那些想要内容,但无法以适合他们的形式获得的读者。他们希望他们的内容以一种特定的方式或以一种特定的方式传递,而我和 Apress 不会给他们。所以他们转向文件共享,因为它以他们需要的方式给了他们所需要的东西。这些人是潜在的客户,无论是当我的作品以他们想要的方式出现时,还是当他们的需求发生变化时。

所以,总的来说,有一类人会抄袭我的书,因为他们可以也永远不会买一本,还有三类人今天会抄袭我的书,但将来可能会成为大客户。他们可能还会向其他人推荐我的书,这些人可能会付费购买一本(这种情况比你想象的更常见)。

收藏家对我来说是一个失败的事业,所以我不会因为他们而失眠。其他类型的文件复印机是我想培养的人,让他们体验我的内容,希望我将来能从他们身上赚些钱。我可以诚实地说,我从未把我的任何一本书放在文件共享网站上,但我很想这么做,因为我认为这有助于确保未来的收入。

同样重要的是,让我的书更难用并不会让任何一个复印机去买书。速战速决者会在别处找到解决方案,预算者不会有更多的闲钱,所以他们只会抄袭别人的书,而逃避痛苦者仍然无法以他们想要的方式获得内容。他们永远不会知道他们是否喜欢我的写作风格,因为他们永远也不会看到——我现在和将来都不会从他们那里得到任何销售。

关注重要的事情

我试着考虑他们想要什么,而不是试图惩罚复印机。需要快速修复吗?我在每章的开头添加了汇总表,以便更容易找到具体的解决方案。没有现金吗?我写了 Windows Revealed 的书,花几美元就能让你快速入门。需要一种特定格式的电子书?我与 Apress 合作,它提供了一系列不同的无 DRM 格式。

我对文件共享者的回应是努力让我的书更有吸引力、更有用,而不是更难使用。而且,如果我成功了,我会取悦我的付费读者,因为他们希望快速找到解决方案,获得一系列电子书格式,并获得新主题的廉价有效的快速入门书籍。

我没有浪费时间去烦恼,而是为我的书被如此广泛地分享而暗自自豪,并一直希望今天抄袭我的书的人明天会为这些书付高价。

我给你的建议是,对你的 Windows 应用商店应用采取类似的方法。想想为什么人们复制它们,并试图为那些未来可能购买的人增加价值,同时为现在已经购买的人增加价值。你不能停止文件复制,所以你可以很好地接受它,并认为它是真正的自由市场暴露。

我不希望人们抄袭我的书,但如果他们不打算买一本,我宁愿他们抄袭我的书,而不是竞争对手的书,对书来说是真的,对应用来说也是真的。

处理基本的商店场景

实现应用业务模型的过程有点奇怪,因为您创建了一系列代表不同许可场景的 XML 文件,然后在您的应用中实现处理它们的代码。这些文件被称为场景文件,代表当你的应用在商店中发布并被用户下载或购买时,你的应用可以访问的数据。在本节中,我将创建一个非常基本的场景文件来介绍该格式,向您展示如何访问该场景所代表的数据,并在应用中对其进行响应。

创建场景文件

我已经在项目中添加了一个名为store的新文件夹,我将在其中放置场景文件。我的第一个场景描述了用户下载了应用,但没有购买任何应用内升级或订阅的情况。对于我的业务模型,这意味着我对表示三种场景感兴趣:

  1. 用户购买了基本应用功能的永久许可证。
  2. 用户下载了尚未过期的免费试用版。
  3. 用户下载了一个已经过期的免费试用版。

为了表示和测试这些场景,我在 store 文件夹中创建了一个名为initial.xml的新文件,其内容可以在清单 1 中看到。

清单 1。/store/initial.xml 的内容

<?xml version="1.0" encoding="utf-16" ?> <CurrentApp>   <ListingInformation>     <App>       <AppId>e4bbe35f-0509-4cca-a27a-4ec43bed783c</AppId>       <LinkUri>http://apress.com</LinkUri>       <CurrentMarket>en-US</CurrentMarket>       <AgeRating>3</AgeRating>       <MarketData xml:lang="en-us">         <Name>Simple Photo Album</Name>         <Description>An app to display your photos</Description>         <Price>4.99</Price>         <CurrencySymbol>$</CurrencySymbol>         <CurrencyCode>USD</CurrencyCode>       </MarketData>     </App>   </ListingInformation>   <LicenseInformation>     <App>       <IsActive>true</IsActive>       <IsTrial>false</IsTrial>       <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate>     </App>   </LicenseInformation> </CurrentApp>

这个文件有两个部分,包含在CurrentApp元素中。ListingInformation元素包含 Windows Store 上应用列表的详细信息,而LicenseInformation元素包含用户获得该软件的许可证的详细信息。我将在接下来的小节中解释这两个元素。

Images 注意 Windows 对场景文件的内容非常挑剔。确保CurrentApp元素关闭后没有内容,包括空行。如果应用在启动时崩溃,请尝试从 Visual Studio 重新加载应用,如果这不起作用,请仔细查看您的场景文件,看看您是否添加了 Windows 不期望的内容。

了解清单部分

场景文件的ListingInformation部分包含了应用的描述,并为您提供了应用如何在商店中显示给用户的详细信息。我不太注意场景文件的这一部分,尽管当我为我想要提供的应用内升级和订阅创建定义时,我会在第三十二章中添加它。在表 1 中,我描述了商店整合和测试过程中的各种元素及其效果。

Images

虽然你需要在一个场景文件中为这些元素创建值,但是ListingInformation部分中的值取自你发布应用时提供的信息,我将在第三十三章中演示。使用什么值进行测试并不重要,它们是必需的,但这些值只是真实数据的占位符,您不必使用您计划为真实 Windows 应用商店部署提供的相同值。

images 注意【Windows 应用商店集成期间的一个常见问题是为AppId元素使用格式错误的值。我发现最简单的方法是从应用清单中获取值。打开清单,导航到Packaging选项卡,从Package name字段复制值。

了解许可部分

LicenseInformation元素包含用户为应用及其升级获得的所有许可证的详细信息。对于我最初的例子,我只为应用本身定义了一个测试许可证,我已经在清单 2 中重复了。正是这一部分,我将改变创造不同的场景,我想迎合。

清单 2。来自 initial.xml 场景文件的许可信息

... <LicenseInformation>   <App>     <IsActive>true</IsActive>     <IsTrial>false</IsTrial>     <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate>   </App> </LicenseInformation> ...

我在表 2 中描述了场景文件这一部分的元素。

Images

对于我的初始场景,我已经将IsActive元素设置为true,将IsTrial元素设置为false——这种组合代表了用户已经购买了应用的基本功能的情况。

使用许可信息

现在我已经定义了场景文件,我可以在我的应用中使用它了。我希望尽可能将许可证的管理与应用中的其他代码分开,这样我就可以在一个地方改变商业模式。在 Windows 商店中销售应用和应用内升级的方式有很大的灵活性,并且您可能不会第一次就获得可选功能的定价和组合,因此在您将进行更改的基础上进行编码是有意义的,并且您希望尽可能简化该过程。

首先,我对/js/store.js文件做了一些补充,如清单 3 所示。只有几行新的代码,但是有许多新的对象和技术要介绍,所以我将在接下来的小节中一步一步地分解这些新增内容。

清单 3。添加到/js/store.js 文件

`(function() {
**    var storage = Windows.Storage;**

**    var licensedCapabilities = {**
**        basicApp: false,**
**    }**

WinJS.Namespace.define("ViewModel.Store", {
        events: WinJS.Utilities.eventMixin,

checkCapability: function (name) {
**            var available = licensedCapabilities[name] != undefined**
**                ? licensedCapabilities[name] : true;**
            setImmediate(function () {
                ViewModel.Store.events.dispatchEvent("capabilitycheck",
                    { capability: name, enabled: available });
            });             return available;
        },

**        currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,**

**        loadLicenseData: function () {**
**            var url = new Windows.Foundation.Uri("ms-appx:///store/initial.xml");**
**            return storage.StorageFile.getFileFromApplicationUriAsync(url)**
**                .then(function (file) {**
**                    return ViewModel.Store.currentApp.reloadSimulatorAsync(file);**
**                });**
**        },**

});

**    ViewModel.Store.currentApp.licenseInformation.addEventListener("licensechanged",**
**        function () {**
**            var license = ViewModel.Store.currentApp.licenseInformation;**
**            licensedCapabilities.basicApp = license.isActive;**
**    });**

})();`

跟踪能力权利

我对store.js文件做的第一个添加是定义一个名为licensedCapabilities的对象,我将使用它来跟踪用户对应用中各个功能区域的权限——尽管最初我只跟踪应用的基本功能,如下所示:

... var licensedCapabilities = {     basicApp: false, } ...

即使你的应用的功能与你要出售的应用内升级完全匹配,也要跟踪用户使用你的应用的功能的权利,这一点很重要,我设计的示例应用就是这种情况。

这有两个原因:首先,您可能需要稍后进行更改,并且您希望使您的代码过于依赖您最初定义的许可证。第二,你很少会得到一个完美的匹配,所以,作为一个例子,我打算提供一个名为The Works(如第三十章中的所述)的升级,它将允许用户访问所有的应用功能,因此在这个升级的许可证和应用功能之间没有直接的映射——你可以在第三十二章中看到我如何处理这个轻微的不匹配。

添加了licensedCapabilities对象后,我可以更新Windows.Store.checkCapabilities方法,以便它开始反映用户的权限,如下所示:

... checkCapability: function (name) { **    var available = licensedCapabilities[name] != undefined ? licensedCapabilities[name]** **        : true;**     setImmediate(function () {         ViewModel.Store.events.dispatchEvent("capabilitycheck",             { capability: name, enabled: available });     });     return available; }, ...

我用粗体标出了关键语句。如果在licensedCapabilities中有一个属性对应于被检查的能力,那么我使用licensedCapabilities值来响应checkCapability调用。

请注意默认值的不同。在licensedCapabilities对象中,我将basicApp属性的值设置为false,这将默认拒绝用户访问该功能。然而,在checkCapability方法中,如果在licensedCapabilities对象中没有相应的值,我将返回true。我这样做是为了让我定义的功能总是需要一个许可,但是如果我忘记定义一个功能,我不会通过禁用一个即使他们愿意付费也不能激活的功能来破坏用户的体验。

images 提示这可能会让你觉得奇怪的谨慎,但是我已经写了一些这样的 Windows Store 实现,它们变得非常复杂——如果你忘记连接一个应用功能,在慷慨方面犯错误会带来更好的体验。

获取当前应用的数据

用于管理 Windows 应用商店集成的对象位于Windows.ApplicationModel.Store名称空间中。这个关键对象叫做CurrentApp,它提供了对从 Windows 商店获得的许可证信息的访问,并定义了允许您启动应用和升级购买流程的方法。表 3 显示了CurrentApp对象定义的方法和属性,我将在本章中使用它们来管理示例应用。

Images

我将在这一章中使用关键方法和属性,我将在使用它们时引入来自Windows.ApplicationModel.Store名称空间的其他对象。然而,CurrentApp对象有一个问题——它只有在应用发布后用户从商店下载了应用时才有效。

出于集成和测试的目的,您必须使用CurrentAppSimulator对象,它也包含在Windows.ApplicationModel.Store名称空间中。CurrentAppSimulator对象定义了CurrentApp定义的所有方法和属性,但是作用于场景文件。您使用CurrentAppSimulator对象,直到您准备好发布您的应用,此时您替换应用中的引用,以便您使用CurrentApp对象。

我想尽可能简单地完成从集成到发布的过渡,所以我定义了一个对CurrentAppSimulator类的引用,这样以后我只需要做一个修改。您可以看到我添加到ViewModel.Store名称空间的引用,如下所示:

`...
WinJS.Namespace.define("ViewModel.Store", {
    events: WinJS.Utilities.eventMixin,

checkCapability: function (name) {
        // ...code removed for brevity...
    },

**    currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,**

loadLicenseData: function () {
        // ...code removed for brevity...
    },

});
...`

我会在任何需要使用CurrentApp/CurrentAppSimulator功能的时候引用ViewModel.Store.currentApp属性,当我准备好发布时只做一个改变(我会在第三十三章中演示)。

除了那些由CurrentApp定义的方法之外,CurrentAppSimulator对象还定义了一个额外的方法。这个方法叫做getFileFromApplicationUriAsync,它将一个代表场景文件的StorageFile对象作为它的参数。该方法在文件中加载 XML 元素,并使用它们来模拟许可场景,因此您可以在将应用发布到应用商店之前实现您的业务模型。

为了加载场景文件,我在ViewModel.Store名称空间中定义了一个名为loadLicenseData的函数,如下所示:

`...
WinJS.Namespace.define("ViewModel.Store", {
    events: WinJS.Utilities.eventMixin,

checkCapability: function (name) {
        // ...code removed for brevity...
    },

currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,

**    loadLicenseData: function () {**
**        var url = new Windows.Foundation.Uri("ms-appx:///store/initial.xml");         return storage.StorageFile.getFileFromApplicationUriAsync(url)**
**            .then(function (file) {**
**                return ViewModel.Store.currentApp.reloadSimulatorAsync(file);**
**            });**
**    },**

});
...`

我使用 URL 获得一个StorageFile对象,然后调用reloadSimulatorAsync方法从我的/store/initial.xml文件中加载数据。

响应许可变更

我对/js/store.js文件做的最后一个添加是为licensechanged事件添加一个处理函数。当许可证信息发生变化时(例如当用户进行购买时)以及当使用CurrentAppSimulator对象加载一个场景文件时,该事件被触发。下面是我定义的处理函数:

... ViewModel.Store.currentApp.**licenseInformation**.addEventListener("**licensechanged**",     function () {         var license = ViewModel.Store.currentApp.licenseInformation;         licensedCapabilities.basicApp = license.isActive;     } ); ...

CurrentApp.licenseInformation属性返回一个LicenseInformation对象。该对象定义了licensechanged事件,我已经为其添加了处理程序。

当事件被触发时,我再次读取CurrentApp.licenseInformation属性的值以获得LicenseInformation对象,该对象定义了我在表 4 中描述的属性。

Images

从表中可以看出,场景文件中的元素直接对应于由LicenseInformation对象定义的属性,这使得创建和测试一系列不同的许可情况变得相对容易。在我的处理函数中,我将licenseCapabilities对象中basicApp属性的值设置为LicenseInformation.isActive属性的值。这意味着如果用户拥有有效的许可证,对basicApp功能的ViewModel.Store.checkCapability方法的调用将返回true

加载场景数据

为了加载我的场景数据,我从/js/default.js文件中添加了对loadLicenseData方法的调用,如清单 4 所示。

清单 4。确保应用启动时加载许可信息

... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) {         if (args.detail.previousExecutionState !==             activation.ApplicationExecutionState.suspended) {             // *...code removed for brevity...*         }         args.setPromise(WinJS.UI.processAll().then(function() {             return WinJS.Binding.processAll(document.body, ViewModel)                 .then(function () { **                    return ViewModel.Store.loadLicenseData().then(function () {**                         setupPrinting();                         loadFiles(); **                    });**             });         }));     } }; ...

我调用ViewModel.Store.loadLicenseData方法作为Promise对象链的一部分,这些对象将在应用启动时闪屏被移除之前完成。这可确保在向用户显示应用功能之前加载我的许可证数据。

实施许可政策

在这一点上,我已经建立了对 Windows Store 的支持,因此如果在场景文件中有应用本身的有效许可证,我的checkCapabilities方法将返回true,但是测试这个特性非常令人失望,因为我没有在应用的任何地方强制执行许可证。在这一部分,我将开始充实执行我的商业模式的不同方面的代码,通过只允许用户访问应用的基本功能,如果他们已经购买了许可证或如果他们正在使用免费试用。

触发能力检查

我需要做的第一件事是调用checkCapability方法来查看用户是否有权使用basicApp功能。我已经在/js/default.js文件中这样做了,如清单 5 所示。

清单 5。检查用户是否有权使用基本应用功能

... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) {         if (args.detail.previousExecutionState !==             activation.ApplicationExecutionState.suspended) {                 // *...code removed for brevity...*         }         args.setPromise(WinJS.UI.processAll().then(function() {             return WinJS.Binding.processAll(document.body, ViewModel)                 .then(function () {                     return ViewModel.Store.loadLicenseData().then(function () {                         setupPrinting();                         loadFiles(); **                        ViewModel.Store.checkCapability("basicApp");**                     });             });         }));     } }; ...

您会注意到,虽然我调用了checkCapability方法,但是我并没有对结果做任何事情。我依赖于事件capabilitycheck事件,每当调用checkCapability方法时就会触发该事件。我将在执行基本应用功能策略的代码中处理这个事件,我已经在一个名为/js/storeInteractions.js的新文件中定义了它。我为storeInteractions.js文件在default.html文件中添加了一个新的script元素,如清单 6 所示。

清单 6。向 default.html 文件添加新的脚本元素

`...

<head>     <meta charset="utf-8" />     <title>PhotoApp</title>


    
    
    


    
    
    
**    **
    

...`

我定义了一个新文件,因为执行这种策略的代码往往冗长且重复,我希望将它与store.js文件中的代码分开。您可以在清单 7 中看到storeInteractions.js文件的内容。

清单 7。storeInteractions.js 文件的初始内容

(function () {
`    var pops = Windows.UI.Popups;

ViewModel.Store.events.addEventListener("capabilitycheck", function (e) {
        if (e.detail.capability == "basicApp") {
            if (ViewModel.Store.currentApp.licenseInformation.isTrial &&
                e.detail.enabled) {
**                // user has a trial period which has not expired**
            } else if (!e.detail.enabled) {
**                // user has a trial period which has expired**
            }
        }
    });

function buyApp() {
**        // code to purchase the app will go here**
**        return WinJS.Promise.wrap(true);**
    }
})();`

作为调用我添加到default.js中的ViewModel.Store.checkCapability方法的结果,将调用capabilitycheck事件的处理函数。我对这个代码文件中的两个场景感兴趣。第一是用户正在使用尚未过期的 app 免费试用,第二是免费试用已经过期。您可以在清单中看到我是如何结合事件对象和LicenseInformation对象的细节来处理这些场景的。

images 注意我对基本应用功能的第三个设想是,用户已经购买了许可证。在这种情况下,我什么都不做,因为我想远离我的付费客户。嗯,至少在他们尝试使用需要升级的功能之前是这样,但是我会在第三十二章回到这个话题。

在接下来的小节中,我将填充事件处理函数中当前包含注释的三个部分。

处理有效试用期

我想借此机会提醒用户,他们正在试用我的应用,让他们有机会在启动应用时购买许可证。您可以在清单 8 的中看到我是如何做到这一点的,其中我展示了我对storeInteractions.js文件所做的添加。

清单 8。提示用户购买应用

`(function () {
    var pops = Windows.UI.Popups;

ViewModel.Store.events.addEventListener("capabilitycheck", function (e) {
        if (e.detail.capability == "basicApp") {
            if (ViewModel.Store.currentApp.licenseInformation.isTrial
                    && e.detail.enabled) {

**                var daysLeft = Math.ceil(                     (ViewModel.Store.currentApp.licenseInformation.expirationDate**
**                    - new Date()) / (24 * 60 * 60 * 1000));**
**                var md = new pops.MessageDialog("You have " + daysLeft**
**                    + " days left in your free trial");**
**                md.title = "Free Trial";**
**                md.commands.append(new pops.UICommand("OK"));**
**                md.commands.append(new pops.UICommand("Buy Now"));**
**                md.defaultCommandIndex = 0;**
**                md.showAsync().then(function (command) {**
**                    if (command.label == "Buy Now") {**
**                        buyApp();**
**                    }**
**                });**

} else if (!e.detail.enabled) {
                // user has a trial period which has expired
            }
        }
    });

function buyApp() {
        // code to purchase the app will go here
        return WinJS.Promise.wrap(true);
    }
})();`

我计算出用户的试用期还剩多少天,并使用Windows.UI.Popups.MessageDialog显示这些信息,我在第十三章的中对此进行了描述。MessageDialog有一个OK按钮可以关闭弹出窗口,还有一个Buy Now按钮可以调用buyApp功能(我很快就会实现)。

测试场景

我需要修改我的场景文件来测试这个新代码,创建一个用户有一个试用期的场景。在清单 9 的中,您可以看到我是如何修改/store/initial.xml文件来创建我想要的环境的。

清单 9。更改 initial.xml 文件中的场景

... <LicenseInformation>   <App>     <IsActive>true</IsActive>     <IsTrial>**true**</IsTrial>     <ExpirationDate>**2012-09-30T00:00:00.00Z**</ExpirationDate>   </App> </LicenseInformation> ...

我已经将IsTrial元素的值更改为true,并确保ExpirationDate元素包含一个未来几天的日期。我在 2012 年 9 月 22 日写这一章,所以我在清单中指定的日期是未来 8 天。当您测试这个示例时,您将需要更改数据。

当你启动 app 时,你会看到一条消息,告诉你试用还剩多少天,如图图 1 所示。

images

图 1。向用户显示试用期还剩多少时间

点击OK按钮关闭对话框,让用户继续使用应用。点击Buy Now按钮调用我在storeInteractions.js文件中定义的buyApp函数,当我在下一节实现它时,它将启动购买过程。有了这个新功能,我可以提醒用户,他们只有一个试用期,让他们有机会尽早购买应用。

处理过期的试用期

我想阻止用户使用该应用时,试用期已过,只提供他们购买的机会。我使用另一个MessageDialog来做这件事,如清单 10 中的所示。

清单 10。处理过期的试用期

`(function () {

var pops = Windows.UI.Popups;

ViewModel.Store.events.addEventListener("capabilitycheck", function (e) {
        if (e.detail.capability == "basicApp") {
            if (ViewModel.Store.currentApp.licenseInformation.isTrial
                    && e.detail.enabled) {
                var daysLeft = Math.ceil(
                    (ViewModel.Store.currentApp.licenseInformation.expirationDate
                    - new Date()) / (24 * 60 * 60 * 1000));
                var md = new pops.MessageDialog("You have " + daysLeft
                    + " days left in your free trial");                 md.title = "Free Trial";
                md.commands.append(new pops.UICommand("OK"));
                md.commands.append(new pops.UICommand("Buy Now"));
                md.defaultCommandIndex = 0;
                md.showAsync().then(function (command) {
                    if (command.label == "Buy Now") {
                        buyApp();
                    }
                });

} else if (!e.detail.enabled) {
**                var md = new pops.MessageDialog("Your free trial has expired");**
**                md.commands.append(new pops.UICommand("Buy Now"));**
**                md.showAsync().then(function () {**
**                    buyApp().then(function (purchaseResult) {**
**                        if (!purchaseResult) {**
**                            ViewModel.Store.checkCapability("basicApp");**
**                        }**
**                    });**
**                });**
            }
        }
    });

function buyApp() {
        // code to purchase the app will go here
        return WinJS.Promise.wrap(true);
    }
})();`

我向用户显示的对话框通知他们试用已经过期,并提供给他们一个Buy Now按钮。当按钮被点击时,我调用buyApp函数,它将负责启动购买过程。该函数返回一个Promise对象,该对象在流程完成时实现,如果购买成功,则产生true,如果没有购买,则产生false(这可能有多种原因,包括用户取消交易、未能提供有效的支付形式,或者因为无法访问 Windows Store,没有连接的设备无法进行购买)。

images 提示在开发免费试用的应用时,我喜欢加上一个宽限期。几年前,我想在一次长途飞行中使用一个试用应用,却发现它已经过期,并且我的连接能力不足意味着我无法升级,尽管我愿意这样做。相反,我对开发人员决定不再购买应用的硬性规定感到恼火。因此,对于我自己的项目,我通常会添加一个按钮,将试用期延长几天,以便他们有机会进行购买。我只允许一次扩展,之后应用停止工作。

如果购买成功,我就关闭对话框,这样用户就可以使用这个应用了。如果购买没有完成,我调用ViewModel.Store.checkCapability方法,再次评估许可证,导致相同的对话框显示,直到用户终止应用或能够完成购买。我使用了checkCapability方法,以便触发capabilitycheck事件,让我的应用的其他部分保持对正在发生的事情的了解。

测试场景

为了测试这个场景,我必须在/store/initial.xml场景文件中指定一个过去的日期,如清单 11 所示。为了得到我想要的效果,我必须将isActive元素设置为false,并确保将isTrial元素设置为true

清单 11。在 initial.xml 文件中创建过期的试用场景

... <LicenseInformation>   <App>     <IsActive>**false**</IsActive>     <IsTrial>**true**</IsTrial>     <ExpirationDate>**2011-09-30T00:00:00.00Z**</ExpirationDate>   </App> </LicenseInformation> ...

当你启动应用时,你会看到如图图 2 所示的对话框。

images

图二。处理过期的试用期

此时,buyApp函数返回的Promise返回true,表示从商店购买成功。点击Buy Now按钮,对话框将被关闭,您可以使用该应用。

为了模拟一次失败的购买,修改buyApp函数,使Promise产生false,如清单 12 所示。

清单 12。模拟一次失败的购买

... function buyApp() {     // code to purchase the app will go here     return WinJS.Promise.wrap(**false**); } ...

重启应用,你会看到同样的对话框。点击Buy Now按钮将暂时关闭对话框,但它会在一会儿后重新出现,阻止应用被使用。

增加购买应用的支持

购买应用的大部分工作由 Windows 和 Windows 商店负责。我所要做的就是表明我想要启动这个过程,这是通过CurrentApp对象来完成的。您可以看到我如何在清单 13 的storeInteractions.js文件中实现了buyApp函数,以使用CurrentApp功能的这一方面。

清单 13。实现 buyApp 功能

... function buyApp() { **    var md = new pops.MessageDialog("placholder");** **    return ViewModel.Store.currentApp.requestAppPurchaseAsync(false).then(function () {** **        if (ViewModel.Store.currentApp.licenseInformation.isActive) {** **            md.title = "Success"** **            md.content = "Your purchase was succesful. Thank you.";** **            return md.showAsync().then(function () {** **                return true;** **            });** **        } else {** **            return false;** **        }** **    }, function () {** **        md.title = "Error"** **        md.content = "Your purchase could not be completed. Please try again.";** **        return md.showAsync().then(function () {** **            return false;** **        });** **    });** } ...

关键部分是对requestAppPurchaseAsync方法的调用,它启动了购买过程。这个方法的参数是一个boolean值,指示是否应该为交易生成收据——我已经指定了false,这意味着不需要收据。

如果购买成功,requestAppPurchaseAsync方法返回一个正常完成的WinJS.Promise,否则调用错误函数(参见第九章,了解传递给Promise.then方法的不同函数的解释)。

成功和错误与流程本身有关,因此您需要检查 success 函数中的许可证状态,以确保购买成功。我通过使用另一个MessageDialog报告结果的来响应购买的结果。我的buyApp函数返回一个Promise,如果购买成功则返回true,否则返回false

当使用CurrentAppSimulator对象时,调用requestAppPurchaseAsync方法会显示一个对话框,允许您模拟购买请求的不同结果。要看到这个对话框,启动应用并点击Buy Now按钮。图 3 显示了对话框和你可以选择的选项来模拟结果。

images

图三。模拟 app 购买流程

S_OK选项将模拟一次成功的购买,而其他值模拟不同种类的错误。在我的例子中,我没有区分不同种类的错误,并以同样的方式对待所有失败的购买。

测试失败的购买

如果您选择一个错误条件并点击Continue按钮,您将看到如图图 4 所示的错误信息。点击Close按钮关闭对话框,因为应用仍未获得许可,导致应用再次显示过期警告。

images

图 31-4。通知用户购买失败

测试一次成功的购买

测试成功购买稍微复杂一些,因为检查许可证信息是否正确更新很重要。首先,启动应用,在执行任何其他操作之前,在 Visual Studio JavaScript 控制台中输入以下语句:

console.log("Active: " + ViewModel.Store.currentApp.licenseInformation.isActive); console.log("Trial: " + ViewModel.Store.currentApp.licenseInformation.isTrial);

这些语句产生以下输出,表明该应用没有有效的许可证,并且是作为免费试用获得的,如下所示:


Active: false Trial: true


点击应用对话框上的Buy Now按钮,通过从列表中选择S_OK值并点击Continue按钮来模拟一次成功的购买。您将看到如图图 5 所示的对话框,当您点击Close按钮时,您将能够使用该应用。

images

图 5。确认成功购买应用

现在,在 JavaScript 控制台窗口中重新输入这些命令,您将看到许可信息已经更改,表明用户已经许可了应用,如下所示:


Active: true Trial: false


当用户获得许可证时,licensechanged事件被自动触发,这意味着我在/js/store.js文件中的处理函数将重新评估可用的许可证,并更新用于响应对ViewModel.Store.checkCapability函数的调用的属性,确保应用状态与用户获得的许可证保持一致。

images 注意重要的是要理解场景文件不会改变——只会改变正在运行的应用中许可信息的状态。如果重启 app,会重新加载/store/initial.xml文件中描述的场景,重新创建用户没有许可证,免费试用期已过的情况。

总结

在本章中,我已经向您介绍了Windows.ApplicationModel.Store名称空间,并使用它包含的一些对象来实现我的示例业务模型的一部分。我已经为我的应用添加了对强制试用期和从商店购买应用以获得永久许可证的支持,并使用场景文件测试了该功能。在下一章,我将向你展示如何销售和管理应用内升级。

三十二、销售升级

在这一章中,我将向您展示如何使用 Windows 应用商店从您的应用内向您的用户销售升级。我演示了如何在模拟器文件中创建描述升级的条目,如何获取有关已购买的升级的信息,以及如何启动 Windows Store 过程来购买升级。

在场景文件中定义产品

创建应用内升级的技术从场景文件中的定义开始。对于这一章,我在store文件夹中创建了一个名为upgrades.xml的新文件,其内容可以在清单 1 中看到。

清单 1。upgrades.xml 文件的内容

<?xml version="1.0" encoding="utf-16" ?> <CurrentApp>   <ListingInformation>     <App>       <AppId>e4bbe35f-0509-4cca-a27a-4ec43bed783c</AppId>       <LinkUri>http://apress.com</LinkUri>       <CurrentMarket>en-US</CurrentMarket>       <AgeRating>3</AgeRating>       <MarketData xml:lang="en-us">         <Name>Simple Photo Album</Name>         <Description>An app to display your photos</Description>         <Price>4.99</Price>         <CurrencySymbol>$</CurrencySymbol>         <CurrencyCode>USD</CurrencyCode>       </MarketData>     </App> **    <Product ProductId="fileTypes">** **      <MarketData xml:lang="en-us">** **        <Name>JPG Files Upgrade</Name>** **        <Price>4.99</Price>** **        <CurrencySymbol>$</CurrencySymbol>** **        <CurrencyCode>USD</CurrencyCode>** **      </MarketData>** **    </Product>**   </ListingInformation>   <LicenseInformation>     <App>       <IsActive>true</IsActive>       <IsTrial>false</IsTrial>       <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate>     </App> **    <Product ProductId="fileTypes">** **      <IsActive>true</IsActive>** **    </Product>**   </LicenseInformation> </CurrentApp>

我只在 upgrades.xml 文件中添加了一个应用内升级。在 Windows Store 的说法中,应用内升级被称为产品,与定义基本功能的应用相对。要定义一个新产品,您必须在场景文件的ListingInformationLicenseInformation部分添加一个元素,就像我在示例中所做的那样。

images 注意对于这个场景文件,我希望应用的基本功能可以在试用期尚未到期。您可以看到我是如何在LicenseInformation.App元素中做到这一点的,但是您必须更改ExpirationDate元素中的日期,以指定一个未来的日期来获得正确的效果。

定义产品详细信息

您使用一个Product元素定义产品的细节,并使用ProductId属性指定产品的名称,如下所示:

... <Product **ProductId="fileTypes"**> ...

这是升级将被您的应用识别的名称,不会向用户显示。我已经指定了fileTypes名称,这与我在应用中构建的功能一致。包含在Product元素中的元素与应用在App元素中的元素具有相同的含义,它们描述升级并指定其成本和货币。在本例中,我将fileTypes产品描述为JPG Files Upgrade,并将其价格设为 4.99 美元。

定义产品许可

您可以通过向 XML 文件的LicenseInformation部分添加一个Product元素来设置该场景的许可状态。您必须确保ProductId属性的值与您用来描述升级的值相匹配,并且您必须包含一个IsActive元素,该元素被设置为true以指示用户拥有有效的许可证,否则为false

您还可以使用ExpirationDate元素来表示您打算在订阅的基础上销售的产品。您指定的日期将是订阅结束的时间点(或者,如果您指定了过去的日期,则为已经结束的时间点)。

定义剩余产品

既然我已经向您展示了如何定义单个产品,我将为我的应用将支持的其他升级向场景文件添加条目。您可以在清单 2 的中看到相当长的附加内容。

清单 2。为剩余产品定义场景条目

<?xml version="1.0" encoding="utf-16" ?> <CurrentApp>   <ListingInformation>     <App>       <AppId>e4bbe35f-0509-4cca-a27a-4ec43bed783c</AppId>       <LinkUri>http://apress.com</LinkUri>       <CurrentMarket>en-US</CurrentMarket>       <AgeRating>3</AgeRating>       <MarketData xml:lang="en-us">         <Name>Simple Photo Album</Name>         <Description>An app to display your photos</Description>         <Price>4.99</Price>         <CurrencySymbol>$</CurrencySymbol>         <CurrencyCode>USD</CurrencyCode>       </MarketData>     </App>     <Product ProductId="fileTypes">       <MarketData xml:lang="en-us">         <Name>JPG Files Upgrade</Name>         <Price>4.99</Price>         <CurrencySymbol>$</CurrencySymbol>         <CurrencyCode>USD</CurrencyCode>       </MarketData>     </Product> **    <Product ProductId="depth">** **      <MarketData xml:lang="en-us">** **        <Name>All Folders Upgrade</Name>** **        <Price>4.99</Price>** **        <CurrencySymbol>$</CurrencySymbol>** **        <CurrencyCode>USD</CurrencyCode>** **      </MarketData>** **    </Product>** **    <Product ProductId="thumbnails">** **      <MarketData xml:lang="en-us">** **        <Name>Thumbnails Upgrade</Name>** **        <Price>1.99</Price>** **        <CurrencySymbol>$</CurrencySymbol>** **        <CurrencyCode>USD</CurrencyCode>** **      </MarketData>** **    </Product>** **    <Product ProductId="theworks">** **      <MarketData xml:lang="en-us">** **        <Name>The Works Upgrade + Printing</Name>** **        <Price>9.99</Price>** **        <CurrencySymbol>$</CurrencySymbol>** **        <CurrencyCode>USD</CurrencyCode>** **      </MarketData>** **    </Product>**   </ListingInformation>   <LicenseInformation>     <App>       <IsActive>true</IsActive>       <IsTrial>false</IsTrial>       <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate>     </App>     <Product ProductId="fileTypes">       <IsActive>true</IsActive>     </Product> **    <Product ProductId="thumbnails">** **      <IsActive>false</IsActive>** **      <ExpirationDate>2011-09-30T00:00:00.00Z</ExpirationDate>** **    </Product>**   </LicenseInformation> </CurrentApp>

我的场景文件现在包含我销售的所有升级的列表信息和其中两个的许可信息。fileTypes升级得到了正确的许可,但是我在订阅基础上出售的缩略图升级的许可已经过期。

提醒一下,我已经在表 1 中列出了升级及其产品 id。

Images

从场景文件的LicenseInformation部分省略产品的细节相当于它们不是该产品的许可证——您也可以通过添加一个Product元素但将IsActive元素设置为false来实现这种效果。

切换到新的场景文件

为了使用我的新场景文件,我需要更新/js/store.js文件中的代码,如清单 3 所示。通常,您将构建一个不同场景文件的库来支持全面的测试。对于我自己的项目,我发现在我有一套完整的测试来覆盖所有的许可证排列之前,我可能会有多达 20 个不同的场景文件。幸运的是,对于我相对简单的示例应用,我不需要那么多文件。

清单 3。更改 store.js 文件以加载新场景

... loadLicenseData: function () {     var url = new Windows.Foundation.Uri("**ms-appx:///store/upgrades.xml**");     return storage.StorageFile.getFileFromApplicationUriAsync(url)         .then(function (file) {             return ViewModel.Store.currentApp.reloadSimulatorAsync(file);         }); }, ...

Images 提示我发现如果我得到了意想不到的结果,那通常是因为我忘记了加载正确的文件。

使用许可信息

我现在的目标是使用我定义的许可证信息来设置用户对应用中不同功能的权限。我通过CurrentApp对象(或者开发过程中的CurrentAppSimulator对象)来做这件事。您可以看到我对/js/store.js文件所做的更改,以利用清单 4 中的新许可信息。

清单 4。使用/js/store.js 文件中的产品许可信息

`(function() {
    var storage = Windows.Storage;

var licensedCapabilities = {
        basicApp: false,
**        fileTypes: false,**
**        depth: false,**
**        thumbnails: false,**
**        print: false,**
    }

WinJS.Namespace.define("ViewModel.Store", {
        events: WinJS.Utilities.eventMixin,

checkCapability: function (name) {
            var available = licensedCapabilities[name] != undefined
                ? licensedCapabilities[name] : true;
            setImmediate(function () {
                ViewModel.Store.events.dispatchEvent("capabilitycheck",
                    { capability: name, enabled: available });
            });
            return available;
       },

currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,         loadLicenseData: function () {
            var url
                = new Windows.Foundation.Uri("ms-appx:///store/upgrades.xml");
            return storage.StorageFile.getFileFromApplicationUriAsync(url)
                .then(function (file) {
                    return ViewModel.Store.currentApp.reloadSimulatorAsync(file);
                });
        },
    });

ViewModel.Store.currentApp.licenseInformation.addEventListener("licensechanged",
        function () {
            var license = ViewModel.Store.currentApp.licenseInformation;
            licensedCapabilities.basicApp = license.isActive;

**            var products = license.productLicenses;**
**            if (products.lookup("theworks").isActive) {**
**                licensedCapabilities.fileTypes = true;**
**                licensedCapabilities.depth = true;**
**                licensedCapabilities.thumbnails = true;**
**                licensedCapabilities.print = true;**
**            } else {**
**                licensedCapabilities.fileTypes = products.lookup("fileTypes").isActive;**
**                licensedCapabilities.depth = products.lookup("depth").isActive;**
**                licensedCapabilities.thumbnails = products.lookup("thumbnails").isActive;**
**            }**
        });
})();`

处理产品许可证

我在store.js文件中做的另一个更改是扩展了licensechanged事件的事件处理程序中的代码,这样它就可以处理升级产品的许可证并设置licensedCapabilities事件中的属性。

LicenseInformation对象定义了一个productLicenses属性(我从CurrentApp.licenseInformation属性中获得了LicenseInformation对象)。由productLicenses属性返回的对象允许您使用lookup方法查找单个产品,如下所示:

... products.lookup("theworks") ...

lookup 方法的参数是场景文件中Product元素的ProductId属性值。在上面的片段中,我已经请求了名为theworks的升级许可。

lookup 方法返回一个ProductLicense对象,它包含所请求产品的许可状态的详细信息。ProductLicense对象定义了表 2 中描述的属性。

lookup方法的好处在于,即使在场景文件中没有相应的Product元素,它也会返回一个ProductLicense对象——属性isActive将被设置为false,表示没有值许可。这意味着我可以安全地查找名为theworks的产品,并获得一个响应,我可以用它来决定应该启用哪些应用功能。我处理产品许可的方法是从查看用户是否购买了theworks的许可开始。如果是,那么我启用由licensedCapabilities对象定义的所有功能,如下所示:

... var products = license.productLicenses; if (products.lookup("theworks").isActive) { **    licensedCapabilities.fileTypes = true;** **    licensedCapabilities.depth = true;** **    licensedCapabilities.thumbnails = true;** **    licensedCapabilities.print = true;** } else {     licensedCapabilities.fileTypes = products.lookup("fileTypes").isActive;     licensedCapabilities.depth = products.lookup("depth").isActive;     licensedCapabilities.thumbnails = products.lookup("thumbnails").isActive; } ...

如果用户没有获得作品的有效许可,那么我会依次查找其他产品,并设置相应功能的状态。这意味着,例如,只有当用户拥有theworks时,才会启用print功能,提供了一个我如何将应用功能与升级产品分离的简单演示。

测试许可证信息

您可以通过启动应用并在 Visual Studio JavaScript 控制台窗口中输入以下语句来测试许可证信息:


["fileTypes", "depth", "thumbnails", "print"].forEach(function(cap) {     console.log(cap + ": " + ViewModel.Store.checkCapability(cap)); });


点击 Return,您应该会看到以下输出,它根据场景文件中的许可证信息指示用户有权使用哪些应用功能:


fileTypes: true depth: false thumbnails: false print: false


如您所料,fileTypes功能被启用,所有其他功能被禁用。您可以在应用中看到这些信息。关闭告诉您试用期还剩多少天的对话框,尝试更改ToggleSwitch控件的位置。您只能移动标有Show JPG的选项,因为其他选项与用户无权使用的功能相关。类似地,如果您激活 Devices Charm,您将看到如图图 1 所示的消息,因为print功能对用户不可用。

images

图 1。当打印功能未被许可时激活设备魅力

更正许可权利

既然我已经向您展示了如何检查单个产品的许可信息,我需要返回并纠正/js/store.js文件中的代码。当应用处于试用期时,我希望用户能够使用所有的应用功能,就像他们购买了基本应用并订阅了theworks升级一样。您可以在清单 5 的中看到我对store.js所做的修改。

清单 5。允许用户在试用期内使用所有功能

`...
ViewModel.Store.currentApp.licenseInformation.addEventListener("licensechanged",
    function () {
        var license = ViewModel.Store.currentApp.licenseInformation;
        licensedCapabilities.basicApp = license.isActive;

var products = license.productLicenses;
        if (products.lookup("theworks").isActive
**                || (license.isActive && license.isTrial)) {**

licensedCapabilities.fileTypes = true;
            licensedCapabilities.depth = true;
            licensedCapabilities.thumbnails = true;
            licensedCapabilities.print = true;
        } else {             licensedCapabilities.fileTypes = products.lookup("fileTypes").isActive;
            licensedCapabilities.depth = products.lookup("depth").isActive;
            licensedCapabilities.thumbnails = products.lookup("thumbnails").isActive;
        }
    });
...`

我已经使用了基本应用的许可证信息,在未到期的试用期和基本应用加theworks升级的许可证之间建立了等价关系。

您会记得,upgrade.xml场景文件指定应用处于试用期,因此如果您在更新了store.js文件后重启应用,您应该可以访问所有功能。

销售升级产品

现在,我的应用强制执行产品许可证,我可以提示用户购买升级。如何做到这一点取决于你的应用的性质。我建议你仔细考虑这一点,因为以有益和礼貌的方式提示用户和不断向他们索要金钱是有区别的。

对于这一章,我将使用一个简单的方法,即当用户试图使用一个未经许可的特性时,提示用户进行升级。

images 提示我打算提示用户升级,因为我想把重点放在升级机制上,但我不会在真正的应用中这样做,因为这让用户很烦。当我第一次提示用户升级时,我通常会提供一个选项来禁用对同一特性的任何进一步提示。我建议你考虑类似的方法。你可以使用应用数据功能永久记录用户的偏好,我在第二十章的中描述过。

您将回忆起当用户试图使用一项功能时,Windows.Store.checkCapability方法触发了capabilitycheck事件。我在第三十一章的/js/storeInteractions.js文件中对此事件做出了回应,以加强我对基本应用的商业模式,这样我就可以向用户出售应用并加强试用期。我将使用类似的技术来管理升级过程。在接下来的部分中,我将介绍我所做的更改和添加。

调度状态事件

我将从添加对从ViewModel.State名称空间调度事件的支持开始。我销售升级的方法是提示用户响应应用布局的变化,我想确保当用户成功购买升级时,我正确地更新了应用布局的状态。这意味着我需要某种方式来表明ViewModel.State名称空间中的数据已经更改,为此,我在/js/viewmodel.js文件中添加了一些内容,如清单 6 所示。

清单 6。添加对从 ViewModel 发出事件的支持。状态名称空间

`(function () {

WinJS.Namespace.define("ViewModel", {         State: WinJS.Binding.as({
            pictureDataSource: new WinJS.Binding.List(),
            fileTypes: false,
            depth: false,
            thumbnails: false,
**            events: WinJS.Utilities.eventMixin,**
**            reloadState: function () {**
**                ViewModel.State.events.dispatchEvent("reloadstate", {});**
**            }**
        }),
    });

WinJS.Namespace.define("Converters", {
        display: WinJS.Binding.converter(function(val) {
            return val ? "block" : "none";
        })
    });

})();`

名称空间中的单个属性是可以观察到的,但我需要某种方式来表明应用状态发生了根本变化,应用中的数据应该被刷新。为此,我添加了一个events属性,并为其分配了WinJS.Utilities.eventMixin对象和一个reloadState函数,当被调用时,该函数会触发一个名为reloadstate的事件。在接下来的小节中,您将看到我是如何使用该函数并响应事件的。

管理采购流程

当用户试图激活他们无权使用的功能时,我会启动升级购买流程。我通过处理capabilitycheck事件来检测这种情况,扩展我在第三十一章中添加的代码来处理应用购买过程。在清单 7 中,您可以看到我对/js/storeInteractions.js文件所做的修改,这些修改扩展了购买,包括了升级。

清单 7。增加销售应用内升级的支持

`(function () {

var pops = Windows.UI.Popups;

ViewModel.Store.events.addEventListener("capabilitycheck", function (e) {
        if (e.detail.capability == "basicApp") {
            // ...statements removed for brevity...

**        } else if (e.detail.capability == "print" && !e.detail.enabled) {**
**            var md = new pops.MessageDialog("Printing is only available to subscribers");**
**            md.commands.append(new pops.UICommand("Subscribe"));**
**            md.commands.append(new pops.UICommand("Cancel"));**
**            md.showAsync().then(function (command) {**
**                if (command.label != "Cancel") {**
**                    buyUpgrade("theworks");                 }**
**            });**

**        } else if (!e.detail.enabled) {**
**            var md = new pops.MessageDialog("You need to buy an upgrade to use this "**
**                + " feature or subscribe to unlock all features");**
**            md.commands.append(new pops.UICommand("Upgrade"));**
**            md.commands.append(new pops.UICommand("Subscribe"));**
**            md.commands.append(new pops.UICommand("Cancel"));**
**            md.showAsync().then(function (command) {**
**                if (command.label != "Cancel") {**
**                    var product = command.label**
**                        == "Upgrade" ? e.detail.capability : "theworks";**
**                    buyUpgrade(product).then(function (upgradeResult) {**
**                        if (upgradeResult) {**
**                            var val = ViewModel.State[e.detail.capability];**
**                            if (val != undefined) {**
**                                ViewModel.State[e.detail.capability] = !val;**
**                            }**
**                            ViewModel.State.reloadState();**
**                        }**
**                    });**
**                }**
**            });**
**        }**
    });

function buyApp() {
        // ...statements removed for brevity...
    }

**    function buyUpgrade(product) {**
**        var md = new pops.MessageDialog("");**
**        return ViewModel.Store.currentApp.requestProductPurchaseAsync(product, false)**
**        .then(function () {**
**            if (ViewModel.Store.currentApp.licenseInformation.productLicenses**
**                    .lookup(product).isActive) {**
**                md.title = "Success"**
**                md.content = "Your upgrade was succesful. Thank you.";**
**                return md.showAsync().then(function () {**
**                    return true;**
**                });**
**            } else {**
**                return false;**
**            }**
**        }, function () {**
**            md.title = "Error"**
**            md.content = "Your upgrade could not be completed. Please try again.";**
**            return md.showAsync().then(function () {**
**                return false;**
**            });**
**        });     }**
}})();`

您将回忆起当调用ViewModel.Store.checkCapability方法时会触发capabilitycheck事件。如果要求的功能不是basicApp,那么我知道它与我想出售的升级相关联。在接下来的章节中,我将解释如何销售不同类别的升级产品。

间接销售能力

我的应用包含了print功能(我只将其作为我的theworks产品的一部分出售),可以解锁应用中的所有内容。这是一个间接升级的例子,我销售的产品不只是激活用户现在想要的功能。我将print功能的请求与其他类型的升级分开处理,如下所示:

... } else if (**e.detail.capability == "print" && !e.detail.enabled**) {     var md = new pops.MessageDialog("Printing is only available to subscribers");     md.commands.append(new pops.UICommand("Subscribe"));     md.commands.append(new pops.UICommand("Cancel"));     md.showAsync().then(function (command) {         if (command.label != "Cancel") {             buyUpgrade("theworks");         }     }); } ...

我向用户显示一条简单的消息,解释他们请求的功能只对订户可用,并给他们购买订阅的机会,如图图 2 所示。

images

图二。请求打印时提示用户订阅

Windows 8 没有为我提供一种方法来阻止在激活设备魅力时显示设备窗格,所以我显示我的消息,以便用户在关闭窗格时可以看到它。如果用户点击Subscribe按钮,然后调用buyUpgrade函数,这将启动升级过程(我将很快对此进行描述)。我以theworks为参数调用buyUpgrade函数,表示用户想要购买订阅产品。

直接销售能力

对于应用中的其他功能,我想让用户选择是只购买他们尝试使用的功能,还是订阅并解锁所有功能,我的做法如下:

... } else if (!e.detail.enabled) {     var md = new pops.MessageDialog("You need to buy an upgrade to use this "         + " feature or subscribe to unlock all features"); **    md.commands.append(new pops.UICommand("Upgrade"));** **    md.commands.append(new pops.UICommand("Subscribe"));** **    md.commands.append(new pops.UICommand("Cancel"));** **    md.showAsync().then(function (command) {** **        if (command.label != "Cancel") {** **            var product = command.label** **                == "Upgrade" ? e.detail.capability : "theworks";**             buyUpgrade(product).then(function (upgradeResult) {                 if (upgradeResult) {                     var val = ViewModel.State[e.detail.capability];                     if (val != undefined) {                         ViewModel.State[e.detail.capability] = !val;                     }                     ViewModel.State.reloadState();                 }             });         }     }); } ...

我通过给MessageDialog添加一个Subscribe按钮并改变我传递给buyUpgrade函数的参数来达到我想要的效果。你可以在图 3 中看到我呈现给用户的对话框。

images

图三。向用户出售升级和订阅

如果用户点击Upgrade按钮,我就开始购买解锁该功能的产品。如果用户点击Subscribe按钮,我就开始购买theworks产品。

如果用户成功购买,那么我会尝试更新与该功能相关联的ViewModel.State属性,如果有的话。这就完成了用户通过激活一个ToggleSwitch开始的 UI 交互,意味着升级的结果是立竿见影的。用户可以许可的一些升级需要重新加载数据,所以我调用我在清单 6 的/js/viewmodel.js文件中定义的ViewMode.State.reloadState方法。

刷新应用状态

我需要执行的最后一步是确保应用状态反映了用户许可的功能。最简单的方法是从Pictures库中重新加载文件,以便应用显示用户有权查看的所有图像。您可以在清单 8 中看到我如何刷新应用状态以响应reloadstate事件,它显示了我对/js/default.js文件中的onactivated函数所做的更改。

清单 8。刷新应用状态

`...
args.setPromise(WinJS.UI.processAll().then(function() {
    return WinJS.Binding.processAll(document.body, ViewModel)
        .then(function () {
            return ViewModel.Store.loadLicenseData().then(function () {

**                ViewModel.State.events.addEventListener("reloadstate", function (e) {**
**                    loadFiles();**
**                    listView.winControl.itemDataSource**
**                        = ViewModel.State.pictureDataSource.dataSource;**
**                });**

setupPrinting();
                loadFiles();
                ViewModel.Store.checkCapability("basicApp");
            });
    });
}));
...`

为了处理该事件,我调用了loadFiles函数,该函数定位用户有权查看的文件。我还刷新了用于显示缩略图的ListView控件的数据源,如下所示:

... listView.winControl.itemDataSource = ViewModel.State.pictureDataSource.dataSource; ...

ListView控件的一个奇怪之处在于,如果它在被隐藏的时候被初始化,然后显示给用户,它就不能正确显示内容——你得到的只是一个看起来空空的控件。一个快速简单的修复方法是设置itemDataSource属性,它触发更新并生成新的内容元素。我没有改变数据源——我只是将它再次分配给 control 属性,以便当用户购买查看缩略图的功能时,内容能够正确显示。

购买升级

buyUpgrade函数负责发起购买过程并响应结果。currentApp对象定义了requestProductPurchaseAsync方法,该方法启动 Windows 商店升级过程(或者在使用CurrentAppSimulator对象时的购买模拟)。该方法的参数是与购买相关的产品的ProductId值和指示是否需要收据的boolean值:

... ViewModel.Store.currentApp.**requestProductPurchaseAsync**(product, false) ...

我收到了应该购买的产品作为buyUpgrade函数的参数,我指定了false,表示我不想要收据。requestProductPurchaseAsync方法返回一个Promise,当购买过程完成时,它被实现。当购买成功时,执行成功处理函数,否则执行错误处理函数。我向用户显示确认购买结果的消息,如下所示:

... function buyUpgrade(product) {     var md = new pops.MessageDialog("");     return ViewModel.Store.currentApp.requestProductPurchaseAsync(product, false)     .then(function () {         if (ViewModel.Store.currentApp.licenseInformation.productLicenses                 .lookup(product).isActive) { **            md.title = "Success"** **            md.content = "Your upgrade was succesful. Thank you.";** **            return md.showAsync().then(function () {** **                return true;**             });         } else { **            return false;**         }     }, function () { **        md.title = "Error"** **        md.content = "Your upgrade could not be completed. Please try again.";** **        return md.showAsync().then(function () {** **            return false;** **        });**     }); } ...

我的buyUpgrade函数返回一个Promise,当用户关闭消息对话框时该函数被满足,如果购买成功则产生true,否则产生false,允许我构建动作链,比如刷新应用状态。

测试场景

如果您启动应用并将Show Thumbnails切换开关滑动到Yes位置,系统会提示您升级或订阅。点击Upgrade按钮,会出现模拟购买对话框,如图图 4 所示。

images

图 4。模拟购买升级

这是模拟应用购买的同一个对话框,它有相同的结果选择。该对话框显示所购买产品的名称,我在图中突出显示了该名称。

确保选择了S_OK选项,并点击Continue按钮模拟成功购买。当您关闭确认购买成功的对话框时,您会看到应用布局更新为显示缩略图,并且ToggleSwitch已经移动到Yes位置,如图图 5 所示。

images

图 5。购买缩略图功能许可证的影响

其他功能仍然未经许可,这意味着如果你滑动其他ToggleSwitch控件或激活设备的魅力,你会再次得到提示。

测试订阅升级

重启应用,使许可信息重置为场景文件的内容,并再次滑动Show Thumbnails切换开关。这一次,当出现提示时,点击Subscribe按钮。您将再次看到购买模拟器对话框,但这次购买的产品被报告为theworks,如图图 6 所示。

images

图六。模拟购买解锁多种能力的升级

选择S_OK选项并点击Continue按钮,模拟一次成功的购买。缩略图将像以前一样显示,但这一次应用的其他功能也已解锁——例如,如果您将All Folders ToggleSwitch 滑动到Yes位置,将使用深度查询来定位您的Pictures库中的文件,如图图 7 所示。

images

图 7。购买适用于多种能力的升级的效果

创建应用内商店

我对示例应用的最后一个添加是连接Buy/Upgrade按钮,以便为我想卖给用户的各种产品创建一个店面。到目前为止,我的所有销售提示都是由用户试图执行特定操作而触发的,但我也想让用户有机会在任何时候出于任何原因进行购买。在接下来的部分中,我将对应用进行一系列的添加,以便用户可以点击按钮,看到可用的选项,并进行购买。

images 注意这种添加需要相对大量的代码,但它对许多应用来说是一种重要的添加,需要我在本书中向你展示的许多技术——包括数据绑定、模板和Flyout控件——以及一些基本的 DOM 操作。这一部分的清单有点长,但是我建议努力完成它们。

增强视图模型。存储命名空间

我不想将我的商业模式的细节泄露到应用的主要功能中,所以我将对ViewModel.Store名称空间进行一些添加,以表示产品并启动购买,而不直接公开Windows.ApplicationModel.Store名称空间。您可以在清单 9 的中看到我对/js/store.js文件所做的添加。

清单 9。扩展视图模型的功能。商店名称空间

`(function() {
    var storage = Windows.Storage;

var licensedCapabilities = {
        basicApp: false,
        fileTypes: false,
        depth: false,
        thumbnails: false,
        print: false,
    }

WinJS.Namespace.define("ViewModel.Store", {
        events: WinJS.Utilities.eventMixin,

checkCapability: function (name) {
            //   ...statements omitted for brevity...
        },

currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,

loadLicenseData: function () {
            //   ...statements omitted for brevity...
        },

**        isBasicAppPurchased: function () {**
**            var license = ViewModel.Store.currentApp.licenseInformation;             return license.isActive && !license.isTrial;**
**        },**

**        isFullyUpgraded: function() {**
**            return ViewModel.Store.currentApp.licenseInformation.productLicenses**
**                .lookup("theworks").isActive;**
**        },**

**        getProductInfo: function () {**
**            var products = [**
**                { id: "p1", name: "Product 1", price: "$4.99", purchased: true },**
**                { id: "p2", name: "Product 2", price: "$1.99", purchased: false },**
**                { id: "p3", name: "Product 3", price: "$10.99", purchased: false },**
**                { id: "p4", name: "Product 4", price: "$0.99", purchased: false }];**
**            return products;**
**        },**
**        requestAppPurchase: function() {**
**            ViewModel.Store.events.dispatchEvent("apppurchaserequested");**
**        },**

**        requestUpgradePurchase: function (productId) {**
**            ViewModel.Store.events.dispatchEvent("productpurchaserequested",**
**               { product: productId });**
**        }**
    });

ViewModel.Store.currentApp.licenseInformation.addEventListener("licensechanged",
        function () {
            //   ...statements omitted for brevity...
        });
})();`

我添加到ViewModel.Store名称空间的函数分为三类:两个提供管理button元素所需的信息,一个提供可用产品的目录,两个启动购买过程。我将在下面的章节中解释每一个。

为布局提供信息

与 Windows 商店(或任何其他商业平台,就此而言)集成的一个方面是应用可能处于的不同状态的数量,以及满足所有这些状态的需要。你可以在getStoreLabelisBasicAppPurchasedisFullyUpgraded函数中看到这一点的缩影,我使用所有这些函数来管理Buy/Upgrade按钮在应用布局中的呈现。

如果用户是通过免费试用期的一部分,那么我希望按钮提供他们购买应用的机会。Windows Store 不允许用户购买应用内升级,直到购买了基本应用,因此我需要确保我为用户提供正确的交易,以便提供合理而有用的用户体验。

isBasicAppPurchased允许我告诉什么时候我需要向用户出售基本应用,什么时候我需要出售升级。另一方面,当用户购买了theworks升级时,我想禁用button元素,因为没有什么可卖的了。为此,我创建了 isFullyUpgraded函数,当不再有任何产品留给用户时,该函数返回true

images 提示请注意,我没有透露任何定义基本应用功能的细节,也没有透露必须购买哪些产品才能创建完全升级的条件。我热衷于在应用的不同部分之间保持强烈的分离感,而不是在store.js文件之外的任何地方建立商业模式的知识,以便我在未来可以更容易地进行调整。

提供产品信息

getProductInfo功能提供了应用可用升级的详细信息。该函数返回一个对象数组,每个对象都包含表示商店产品id(对应于场景文件中的条目)、应该向用户显示的name、升级的price以及产品是否已经是purchased的属性,如下所示:

... getProductInfo: function () {     var products = [ **        { id: "p1", name: "Product 1", price: "$4.99", purchased: true },** **        { id: "p2", name: "Product 2", price: "$1.99", purchased: false },** **        { id: "p3", name: "Product 3", price: "$10.99", purchased: false },** **        { id: "p4", name: "Product 4", price: "$0.99", purchased: false }];**     return products; }, ...

我最初实现这个函数时使用了静态虚拟数据。当我使用 Windows Store 时,我经常这样做,因为这让我可以绝对确保我不会依赖于Windows.ApplicationModel.Store名称空间中的对象或应用中销售的实际产品。同样,我这样做是为了使长期维护尽可能容易。一旦我让应用内商店前台功能正常工作并添加对生成真实数据的支持,我将返回到这个函数。

为购买提供支持

我添加到ViewModel.Store名称空间的最后两个函数为应用的其他部分提供支持,以启动应用和升级购买。我在不久前创建的布局中使用这个特性向用户显示产品:

`...
requestAppPurchase: function() {
    ViewModel.Store.events.dispatchEvent("apppurchaserequested");
},

requestUpgradePurchase: function (productId) {
    ViewModel.Store.events.dispatchEvent("productpurchaserequested",
        { product: productId });
}
...`

storeInteractions.js文件包含处理购买过程的代码,我不想将该功能与store.js文件紧密耦合,所以我使用两个新事件发出购买请求:apppurchaserequestedproductpurchaserequested。我将在下一节处理这些事件。

定义商场互动

为了理清后端流程,我需要处理我在 store.js 文件的storeInteractions.js文件中创建的两个新事件,该文件已经包含了我需要卖给用户的所有代码。您可以在清单 10 中看到我对storeInteractions.js文件所做的添加。

清单 10。响应 storeInteractions.js 文件中的新购买事件

`(function () {

var pops = Windows.UI.Popups;

ViewModel.Store.events.addEventListener("capabilitycheck", function (e) {
        // ...statements omitted for brevity...
    });

**    ViewModel.Store.events.addEventListener("apppurchaserequested", function () {**
**        buyApp().then(function (result) {**
**            if (result) {**
**                ViewModel.State.reloadState();**
**            }**
**        });**
**    });**

**    ViewModel.Store.events.addEventListener("productpurchaserequested", function (e) {**
**        buyUpgrade(e.detail.product).then(function (result) {**
**            if (result) {**
**                ViewModel.State.reloadState();**
**            }**
**        });**
**    });**

function buyApp() {
        // ...statements omitted for brevity...
    }

function buyUpgrade(product) {
        // ...statements omitted for brevity...
    }

})();`

收到事件后,我调用现有的buyAppbuyUpgrade函数,并在成功购买后调用ViewModel.State.reloadState方法,以确保应用布局反映新获得的功能。

定义标记和样式

我将使用一个WinJS.UI.Flyout控件向用户展示应用内商店,我在第十二章中对此进行了描述。商店将包含由ViewModel.Store.getProductInfo方法返回的每个产品的详细信息,我将使用一个WinJS.Binding.Template对象生成我需要的 HTML 元素,我在第八章的中对此进行了描述。您可以看到我对清单 11 中的default.html文件所做的修改,添加了Flyout和模板。

清单 11。将弹出按钮和模板添加到 default.html 文件

`...

<body>     <div id="flipTemplate" data-win-control="WinJS.Binding.Template">         <div class="flipItem">             <img class="flipImg" data-win-bind="src: image" />             <div class="flipTitle" data-win-bind="innerText: title"></div>         </div>     </div>


        

            
        

    

**    

**
**        
**
**        
**
**    
**

**    <div id="storeFlyout" data-win-control="WinJS.UI.Flyout"**
**        data-win-options="{placement: 'right'}">**
**        

Upgrades
**
**        
**
**            
Name
**
**            
Price
**
**            
Buy
**
**        
**
**        
**
**            **
**        
**
**    **

...`

这个新标记非常简单。我将为每个产品生成productTemplate模板中的元素,并将它们添加到Flyout中的productContainer元素中(以及我将在代码中生成的一些补充元素)。我向名为/css/store.css的项目添加了一个新的样式表,以包含我用于Flyout和模板元素的样式,您可以在清单 12 中看到这个新文件的内容。

清单 12。定义弹出菜单和模板元素的样式

`.title {font-size: 20pt;text-align: center;}

productContainer

.pname, .pprice, .pbuy, .purchased {margin: 10px;}
.pname {-ms-grid-column: 1;}
.pprice {-ms-grid-column: 2; text-align: right;}
.pbuy, .purchased { text-align: center; -ms-grid-column: 3;}

cancelContainer {text-align: center}`

我依靠 CSS 网格布局来定位元素;一部分网格信息是通过样式表应用的,另一部分是当我从default.js文件中的模板生成元素时在代码中应用的,我将在下一节中描述。我向default.html文件的head部分添加了一个脚本元素,以便在应用中包含新文件,如清单 13 中的所示。

清单 13。将 store.css 文件的脚本元素添加到 default.html 文件

`...

<head>     <meta charset="utf-8" />     <title>PhotoApp</title>


    
    
    


    
**    **
    
    
    
    

...`

编写代码

我已经为我的应用内商店做好了所有的基础准备,剩下的就是将代码添加到/js/default.js文件中,将各个部分组合在一起。您可以在清单 14 中看到我添加的内容。

清单 14。实现应用商店所需的 default.js 文件的附加内容

`(function () {
    "use strict";

WinJS.Binding.optimizeBindingReferences = true;

var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation; var storage = Windows.Storage;
    var search = storage.Search;

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !==
                activation.ApplicationExecutionState.suspended) {

// ...statements removed for brevity...
            }

args.setPromise(WinJS.UI.processAll().then(function() {
                return WinJS.Binding.processAll(document.body, ViewModel)
                    .then(function () {
                        return ViewModel.Store.loadLicenseData().then(function () {

ViewModel.State.events.addEventListener("reloadstate",
                                    function (e) {
                                loadFiles();
                                listView.winControl.itemDataSource
                                    = ViewModel.State.pictureDataSource.dataSource;
**                                configureUpgradeButton();**
                            });

**                            upgrade.addEventListener("click", function () {**
**                                if (ViewModel.Store.isBasicAppPurchased()) {**
**                                    showStoreFront();**
**                                } else {**
**                                    ViewModel.Store.requestAppPurchase();**
**                                }**
**                            });**

setupPrinting();
                            loadFiles();
**                            configureUpgradeButton();**
                            ViewModel.Store.checkCapability("basicApp");
                        });
                });
            }));
        }
    };

**    function configureUpgradeButton() {**
**        if (ViewModel.Store.isFullyUpgraded()) {**
**            upgrade.disabled = "true"**
**        } else if (ViewModel.Store.isBasicAppPurchased()) {**
**            upgrade.innerText = "Upgrade";**
**        } else {**
**            upgrade.innerText = "Purchase";**
**        }**
**    }     function showStoreFront() {**
**        var products = ViewModel.Store.getProductInfo();**
**        var rowNum = 2;**
**        WinJS.Utilities.empty(productContainer);**
**        products.forEach(function (product) {**
**            productTemplate.winControl.render(product).then(function (newDiv) {**
**                if (!product.purchased) {**
**                    var button = document.createElement("button");**
**                    button.innerText = "Buy";**
**                    button.setAttribute("data-product", product.id);**
**                    WinJS.Utilities.addClass(button, "pbuy");**
**                    newDiv.appendChild(button);**
**                } else {**
**                    var div = document.createElement("div");**
**                    div.innerText = "Purchased";**
**                    WinJS.Utilities.addClass(div, "purchased");**
**                    newDiv.appendChild(div);**
**                }**
**                while (newDiv.children.length > 0) {**
**                    var celem = newDiv.children[0];**
**                    celem.style.msGridRow = rowNum;**
**                    productContainer.appendChild(celem);**
**                }**
**            });**
**            rowNum++;**
**        });**
**        WinJS.Utilities.query("button.pbuy", productContainer).listen("click",**
**                function(e) {**
**            var productId = e.target.getAttribute("data-product");**
**            ViewModel.Store.requestUpgradePurchase(productId);**
**        });**

**        WinJS.Utilities.query("#cancelContainer button").listen("click", function () {**
**            storeFlyout.winControl.hide();**
**        });**

**        storeFlyout.winControl.show(upgrade);**
**    }**

function setupPrinting() {
        // ...statements removed for brevity...
    }

function loadFiles() {
        // ...statements removed for brevity...
    };

app.start();
})();`

这里有很多新代码,但都很简单。为了便于理解,我将把它分成两个部分。

管理按钮元素

我的第一个任务是确保用户点击显示商店的button元素被正确显示,这是我通过添加configureUpgradeButton函数完成的,如下所示:

... function configureUpgradeButton() {     if (**ViewModel.Store.isFullyUpgraded**()) {         upgrade.disabled = "true"     } else if (**ViewModel.Store.isBasicAppPurchased**()) {         upgrade.innerText = "Upgrade";     } else {         upgrade.innerText = "Purchase";     } } ...

您可以看到我是如何使用在ViewModel.Store名称空间中添加的方法来处理按钮的。如果应用已经完全升级,没有什么可卖的,那么我禁用按钮。如果用户已经购买了应用,那么我将按钮中的文本设置为Upgrade,如果用户还没有购买基本功能,我将文本设置为Purchase。当我测试我的应用商店功能时,你可以在本章的后面看到这些不同的状态。

我还使用了isBasicAppPurchased方法来判断当用户点击按钮时该做什么。如果用户还没有购买基础 app,那么我调用ViewModel.Store.requestAppPurchase方法,这将导致 app 购买过程开始,如下所示:

... upgrade.addEventListener("click", function () {     if (**ViewModel.Store.isBasicAppPurchased()**) {         showStoreFront();     } else { **        ViewModel.Store.requestAppPurchase();**     } }); ...

如果用户已经让购买了这个应用,那么我调用showStoreFront函数,我将在下一节描述这个函数。

推广弹出型按钮

showStoreFront函数负责填充Flyout控件并对其进行配置,以便用户可以开始升级的购买过程。这个函数的代码很冗长,因为我用一个button来补充从模板生成的元素,以发起对产品的购买,或者用一个div元素来表示已经购买了升级。如果您忽略函数的这一部分,代码的其余部分将变得更容易理解,如下所示:

... function showStoreFront() {     var products = ViewModel.Store.getProductInfo(); `    var rowNum = 2;
    WinJS.Utilities.empty(productContainer);
    products.forEach(function (product) {
        productTemplate.winControl.render(product).then(function (newDiv) {
            // ...statements to supplement template elements omitted...
        });
        rowNum++;
    });
    WinJS.Utilities.query("button.pbuy", productContainer).listen("click",
            function(e) {
        var productId = e.target.getAttribute("data-product");
        ViewModel.Store.requestUpgradePurchase(productId);
     });
    WinJS.Utilities.query("#cancelContainer button").listen("click", function () {
        storeFlyout.winControl.hide();
    });

storeFlyout.winControl.show(upgrade);
}
...`

我为每个产品的Flyout添加元素,并为启动升级过程的按钮的click事件设置一个处理程序。一旦我填充并配置了Flyout控件,我就调用show方法将它显示给用户。当我测试新功能时,您可以在下一部分看到商店是如何出现的。

测试应用商店功能

我已经准备好测试我为应用商店添加的新功能,尽管你会注意到我仍然在使用我的静态虚拟产品数据。只有当我对应用内商店的工作方式感到满意时,我才会转向真实数据。我需要从配置我的场景文件开始。我仍然在使用我在本章前面创建的upgrades.xml文件,在清单 15 中,你可以看到LicenseInformation.App部分的初始设置,这是我将为这些测试更改的部分。

清单 15。基本应用购买测试的许可证配置

... <LicenseInformation>   <App> **    <IsActive>true</IsActive>** **    <IsTrial>true</IsTrial>** **    <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate>**   </App>   <Product ProductId="fileTypes">     <IsActive>true</IsActive>   </Product>   <Product ProductId="thumbnails">     <IsActive>false</IsActive>     <ExpirationDate>2011-09-30T00:00:00.00Z</ExpirationDate>   </Product> </LicenseInformation> </CurrentApp> ...

我想模拟一个还没有到期的试用期,所以我将isActiveisTrial元素设置为true,并在ExpirationDate元素中指定一个日期,对我来说是将来的某一天。你必须使用不同的日期来获得正确的效果。

测试基本应用购买

启动该应用,在您取消关于试用期还剩多少天的提醒后,您会看到布局中的按钮标记为Purchase。如果您点击此按钮,您将看到购买模拟器对话框,其中将显示正在购买基本应用。

images

图 8。模拟购买基本应用功能

确保选择了S_OK选项并点击Continue按钮,模拟一次成功的购买。当您关闭确认您购买的消息时,您会看到按钮标签已更改为Upgrade

测试升级采购

不用重启 app,再次点击按钮就会看到应用内商店,如图图 9 所示。我使用了一个非常基本的布局,但是您可以看到我是如何显示虚拟产品数据来为用户提供升级的。

单击其中一个Buy按钮,您将看到购买模拟器对话框,尽管是针对一个在场景文件中不存在的产品。

images

图九。显示虚拟产品数据的应用内商店

添加真实产品数据

现在我知道我的应用内商店工作了,我可以添加真正的产品数据,这是通过修改/js/store.js文件中的代码来完成的。您可以在清单 16 中看到我所做的修改。

清单 16。使用真实产品数据

`...
getProductInfo: function () {
**    return ViewModel.Store.currentApp.loadListingInformationAsync()**
**    .then(function (info) {**

**        var products = [];**
**        var cursor = info.productListings.first();**
**        do {**
**            var prodInfo = cursor.current;**

**            products.push({**
**                id: prodInfo.value.productId,**
**                name: prodInfo.value.name,**
**                price: prodInfo.value.formattedPrice,**
**                purchased: ViewModel.Store.currentApp.licenseInformation.productLicenses**
**                    .lookup(prodInfo.value.productId).isActive**
**            });**
**        } while (cursor.moveNext());**
**        return products;**
**    });** },
...`

CurrentAppCurrentAppSimjulator对象定义了loadListingInformationAsync。该方法返回一个Promise,完成后会产生一个Windows.ApplicationModel.Store.ListingInformation对象,其中包含 Windows 应用商店中关于您的应用及其升级的列表信息——该信息对应于场景文件的ListingInformation部分。ListingInformation对象定义了我在表 3 中描述的属性。

Images

我对应用内商店感兴趣的是productListings属性,因为它返回了一个ProductListing对象的列表,每个对象描述了我的应用的一个升级。ProductListing对象定义了我在表 4 中描述的属性。

Images

属性返回的对象将对象呈现为一个列表,这就是为什么我使用了一个 ?? 循环。对于每个产品列表,我创建一个具有我的应用内商店Flyout所需属性的对象,并查找每个产品的许可信息,以查看是否已经购买。

我修改过的getProductInfo方法的结果是一个Promise,当它被实现时,产生一个描述性对象的数组;这意味着我需要更新default.js文件中的showStoreFront函数来期待Promise,如清单 17 所示。

清单 17。修改 Default.js 文件中的 showStoreFront 函数

function showStoreFront() { **    ViewModel.Store.getProductInfo().then(function (products) {**         var rowNum = 2;         WinJS.Utilities.empty(productContainer);         products.forEach(function (product) {             productTemplate.winControl.render(product).then(function (newDiv) {                 if (!product.purchased) {                     var button = document.createElement("button");                     button.innerText = "Buy"; `button.setAttribute("data-product", product.id);
                    WinJS.Utilities.addClass(button, "pbuy");
                    newDiv.appendChild(button);
                } else {
                    var div = document.createElement("div");
                    div.innerText = "Purchased";
                    WinJS.Utilities.addClass(div, "purchased");
                    newDiv.appendChild(div);
                }
                while (newDiv.children.length > 0) {
                    var celem = newDiv.children[0];
                    celem.style.msGridRow = rowNum;
                    productContainer.appendChild(celem);
                }
            });
            rowNum++;
        });
        WinJS.Utilities.query("button.pbuy", productContainer).listen("click",
                function (e) {
            var productId = e.target.getAttribute("data-product");
            ViewModel.Store.requestUpgradePurchase(productId);
        });
        WinJS.Utilities.query("#cancelContainer button").listen("click", function () {
            storeFlyout.winControl.hide();
        });

storeFlyout.winControl.show(upgrade);    });}`

通过这一更改,我的应用内商店功能将显示真实的产品数据,这些数据在使用CurrentAppSimulator对象时从场景文件中获得,在使用CurrentApp对象时从 Windows 商店数据中获得。

用真实数据测试应用内商店

最终测试是检查真实产品数据是否正确显示,以及当用户购买theworks升级时布局中的按钮是否被禁用。首先更新upgrades.xml场景文件,这样应用启动时就有了基本应用的许可证。将IsActive元素设置为true,将IsTrial元素设置为false,如清单 18 所示。

清单 18。更新最终测试的场景文件

... <LicenseInformation>   <App> **    <IsActive>true</IsActive>** **    <IsTrial>false</IsTrial>**     <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate>   </App>   <Product ProductId="fileTypes">     <IsActive>true</IsActive>   </Product>   <Product ProductId="thumbnails">     <IsActive>false</IsActive>     <ExpirationDate>2011-09-30T00:00:00.00Z</ExpirationDate>   </Product> </LicenseInformation> ...

启动应用,点击标有Upgrade的按钮;你会看到应用商店显示真实的产品数据,如图 10 所示。

images

图 10。显示真实产品数据的应用内商店

您可以看到,JPG Files Upgrade显示为 purchased,这与场景文件的LicenseInformation部分中的数据相匹配。点击The Works UpgradeBuy按钮,您将看到购买模拟器对话框。如果您模拟一次成功的购买并关闭确认对话框,您将会看到Upgrade按钮现在已被禁用,表示没有进一步的升级可用。

总结

在本章中,我向您展示了如何利用 Windows 应用商店支持销售应用内升级。我演示了如何销售单个和多个应用功能的升级,以及如何确定用户购买了哪些功能。我还演示了如何创建一个应用商店,它允许用户随时购买升级。在下一章,也是本书的最后一章,我将向你展示如何准备并发布你的应用到 Windows 应用商店。

三十三、发布到 Windows 应用商店

在这一章中,我完成了我在第三十章中开始的流程,并准备好我的应用,以便它可以提交到 Windows Store 进行认证和上市。我将向您展示如何准备 Windows 应用商店仪表板上的应用列表,如何测试您的应用以发现问题,以及如何将您的应用上传到 Microsoft 以便进行测试。

准备应用列表

现在,我已经有了一个可以运行并经过测试的应用,我需要返回到 Windows 应用商店仪表板,并完成发布过程中的更多步骤。您可以通过从 Visual Studio Store菜单中选择Open Developer Account来打开仪表板。

在第三十章中,我通过为 app 保留一个名称,完成了发布过程的第一步;你会看到这一步在 Windows Store 中显示为完成,如图图 1 所示。

images

图 1。显示已完成第一步的 Windows Store 仪表盘

在接下来的几节中,我将完成更多的步骤,以便为应用的最终检查、测试和更改做好准备。

完成销售详情部分

点击Selling Details链接,设置基础应用的定价信息。我将对我的示例应用收取 4.99 美元,并提供 30 天的试用期(你可以在图 2 中看到我是如何配置的)。

images

图二。设置 app 的销售详情

检查您想要销售应用的市场。我使用Select all链接选择了 Windows Store 运营的所有市场,以便尽可能广泛地销售我的应用。微软会自动为我的应用定价,价格相当于我在当地的美元价格。点击Save按钮,保存销售详情;您将返回到步骤列表。

完成高级功能部分

点击Advanced Features链接,设置应用内升级的列表信息。(这一部分也可用于配置 notification services,我没有在本书中介绍,因为它们需要服务器功能才能工作)。

使用 web 表单创建您想要提供的一组升级。确保Product ID字段对应于您已经用场景文件测试的产品之一是很重要的;否则,您将创建用户无法许可的应用功能。你可以在图 3 中看到我为我的示例应用创建的升级集。点击Save按钮保存这些设置并返回主列表。

images

图三。创建应用内升级

完成年龄评定部分

点击Age rating and rating certificates链接,选择你的应用适合的年龄段。Windows 应用商店仪表板提供了每个年龄组的目标受众和应用功能的相关信息。选择最合适的群体,请记住,面向年轻用户的应用将无法访问设备和传感器。我选择了12+类别,如图图 4 所示。点击Save按钮保存这些设置并返回主列表。

images

图 4。选择应用的年龄等级

完成加密部分

点击Cryptography链接,指定您的应用是否使用加密技术。包括美国在内的相当多的国家限制使用或出口加密技术,因此对这一部分做出准确的声明尤为重要。我选中了 No 选项,如图 5 所示,因为我的示例应用没有使用任何形式的加密技术。

images

图 5。宣布使用加密技术

这是最后一个最需要的改变,所以当你点击Save按钮时,你应该看到前五个步骤已经完成,如图图 6 所示。稍后我会回来完成剩下的步骤。

images

图六。正在进行发布流程

将应用与商店关联

下一步是更新 Visual Studio 项目,使其与 Windows 应用商店列表相关联。为此,从 Visual Studio Store菜单中选择Associate App with the Store菜单项。一旦你提供了你的开发者证书,你就可以从你创建的应用列表中选择,如图图 7 所示。

images

图 7。为应用选择列表

选择一个列表并点击Next按钮。我已经选择了我的Simple Photo Album列表,但是因为你不能用完全相同的名称创建列表,你将看到你创建的列表。

您将看到从 Windows 应用商店列表中获取并在应用清单中使用的值的详细信息,如图 8 所示。

images

图 8。将在清单中使用的商店值列表

单击Associate按钮,将 Visual Studio 项目与 Windows 应用商店列表相关联。您的清单将被更新,以便项目中的关键字段与您在清单中提供的信息相匹配。

提供商店标志

此时,您需要添加一个徽标,该徽标将显示在 Windows 应用商店中您的应用旁边,它是以 50 × 50 像素文件的形式提供的。我使用了与磁贴和闪屏相同的标志,我将它添加到项目的images文件夹中的一个名为store50.png的文件中。

要应用商店徽标,打开package.appxmanifest文件,导航到Packaging选项卡,并更改Logo字段的值,如图图 9 所示。

images

图九。更换店标

删除商店模拟代码

在发布之前,从应用中移除CurrentAppSimulator对象并用CurrentApp替换它是很重要的。您可以看到我在/js/store.js文件中所做的更改,以准备在清单 1 中发布应用。我发现能够轻松地返回到处理错误报告的测试代码很有帮助,所以我注释掉了模拟代码,而不是删除它。

清单 1。从/js/store.js 文件中删除商店模拟代码

`...
WinJS.Namespace.define("ViewModel.Store", {
    events: WinJS.Utilities.eventMixin,

checkCapability: function (name) {
        // ...statements omitted for brevity...
    },

**    //currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,**
**    currentApp: Windows.ApplicationModel.Store.CurrentApp,**
**    loadLicenseData: function () {         //var url**
        //    = new Windows.Foundation.Uri("ms-appx:///store/upgrades.xml");
        //return storage.StorageFile.getFileFromApplicationUriAsync(url)
        //    .then(function (file) {
        //        return ViewModel.Store.currentApp.reloadSimulatorAsync(file);
        //    });
**        return WinJS.Promise.wrap("true");**
    },

isBasicAppPurchased: function () {
        var license = ViewModel.Store.currentApp.licenseInformation;
        return license.isActive && !license.isTrial;
    },

isFullyUpgraded: function() {
        return ViewModel.Store.currentApp.licenseInformation.productLicenses
            .lookup("theworks").isActive;
    },

getProductInfo: function () {
        // ...statements omitted for brevity...
    },

requestAppPurchase: function() {
        ViewModel.Store.events.dispatchEvent("apppurchaserequested");
    },

requestUpgradePurchase: function (productId) {
        ViewModel.Store.events.dispatchEvent("productpurchaserequested",
           { product: productId });
    }
});
...`

因为我没有移除loadLicenseData函数,所以我需要返回一个结果,该结果将允许来自/js.default.js文件的调用工作。为此,我使用了Promise.wrap方法,我在第九章中描述过。

images 提示此时你也可以移除场景文件,因为它们不会被发布的应用使用。

构建应用包

下一步是构建将上传到 Windows 应用商店的包。首先,从 Visual Studio Store菜单中选择Create App Packages。将显示Create App Packages向导,第一个问题询问您是否要为 Windows 商店创建一个包,如图图 10 所示。

images

图十。创建应用包向导

选择Yes并点击Sign In按钮。提供您的开发者账户凭证,然后从列表中选择您希望应用包关联的列表,如图图 11 所示。

images

图 11。选择与商店关联的列表

这可确保您的列表中的最新信息用于构建应用包。单击Next按钮,您将能够设置创建包的位置,设置版本信息,并指定您的应用将在哪些处理器架构上运行。

我已经接受了默认值,如图图 12 所示。这意味着这个包将在我的项目文件夹中的AppPackages文件夹中创建,版本将是1.0.0,我的应用将能够在任何平台上运行。

images

图 12。设置版本和架构选项

点击Create按钮,生成 app 包。创建包后,您将有机会运行 Windows 应用认证包,如图图 13 所示。微软在批准其应用在 Windows Store 中销售之前,会对其进行一系列测试,你可以通过运行认证工具包来防止潜在的问题,该工具包将检查你的应用是否存在一些与微软相同的问题。

images 提示如果您尚未安装 Windows 8 SDK,您可以下载其中的认证套件。SDK 在[msdn.microsoft.com/en-us/windows/hardware/hh852363.aspx](http://msdn.microsoft.com/en-us/windows/hardware/hh852363.aspx)可用。

images

图十三。开始认证检查

点击Launch Windows App Certification Kit按钮开始测试过程。该套件将运行您的应用,并对其进行一系列测试,这大约需要 5 分钟。在此期间,你会看到应用暂时运行,然后消失-不要在测试时尝试与应用交互。

测试完成后,您将看到结果,并有机会看到详细的报告。该报告包含所发现的任何问题的详细信息,并就如何解决这些问题提供了一些基本建议。

我的示例应用没有任何问题,所以几分钟后我会看到如图 14 所示的摘要屏幕。

images

图十四。显示认证测试成功的摘要

完成应用列表

现在,应用包已经完成并经过测试,是时候返回 Windows Store 仪表板并完成应用列表了(您可以通过从 Visual Studio Store 菜单中选择Open Developer Account返回到Dashboard)。

完成包装部分

点击Package链接,将应用包上传到 Windows 应用商店。如果您在创建包时接受了默认位置,那么您将能够在 Visual Studio 项目的AppPackages文件夹中找到您需要的文件。该包文件的扩展名为appxupload,你可以将它拖拽到网页上,如图图 15 所示。上传软件包文件后,点击Save按钮返回主列表页面。

images

图 15。上传包到商店

完成描述部分

点击Description链接,输入您的应用的详细信息。当用户浏览或搜索 Windows 商店时,这些信息会呈现给用户,经过深思熟虑的信息可以使您的应用更容易找到——鉴于应用市场的竞争如此激烈,这一点非常重要。特别是,我建议您注意提供高质量的屏幕截图(可以用 Visual Studio 模拟器拍摄),并仔细考虑您指定的关键字。

在所需信息列表的最后是您在应用中提供的升级描述,如图 16 所示。确保你提供的描述对用户有一定的意义。

images

图十六。提供升级描述

填写完所有必需的详细信息并上传图片后,单击Save按钮返回主列表页面。

完成测试人员注意事项部分

单击Notes for Testers部分,提供微软测试人员验证您的应用所需的任何信息。这是需要包含的重要信息,因为测试人员对你的应用的关注和兴趣是有限的,你需要确保他们能让你的应用快速而轻松地工作。输入测试提示后,单击Save按钮返回主列表页面。

提交您的应用

主列表页面应该显示所有步骤现在都已完成。当你准备好提交你的应用时,点击Submit for certification按钮,如图图 17 所示。

images

图 17。提交 app 认证

您的应用将被提交进行认证,您将在测试过程中看到其进度的细节,如图 18 所示。一个应用通过认证需要几天时间,但微软会在你的应用通过该过程的每个阶段时向你发送电子邮件。

images

图十八。通过认证流程跟踪您的应用

如果您的应用顺利通过所有测试和审查,它将在 Windows 应用商店中列出,可供用户购买和下载。

总结

在本章中,我向您展示了准备和提交您的应用以获得认证并在 Windows 应用商店中列出的流程。在将应用提交给 Microsoft 之前对其进行彻底测试非常重要,因为可能需要几天时间才能获得任何问题的详细信息。

这本书到此结束。我首先向您展示了如何使用您现有的 web 应用知识来构建一个简单的 Windows 应用,然后在此基础上向您展示更复杂和复杂的功能和技术,最后是与 Windows 应用商店集成并提交一个应用进行认证和发布。我希望你的应用取得成功,并希望你喜欢读这本书,就像我喜欢写这本书一样。

第一部分:入门指南

我在这本书的开头设置了内容和风格的场景,将 Windows 8 开发放在上下文中,并向您展示如何轻松地基于现有的 web 应用开发知识来创建一个简单的 Windows 8 应用。

第二部分:核心开发

在本书的这一部分,我将向您介绍支撑 Windows 8 开发的核心开发特性和技术。这包括创建适应 Windows 设备使用方式的布局,允许用户在应用中导航,以及使用数据绑定和模板来创建动态应用布局。我还介绍了 Promise 对象,它是异步编程模型中的一个关键构建块,在整个 Windows 应用开发中使用。

第三部分:UI 开发

在本书的这一部分,我将向您展示可以用来构建更丰富的 Windows 8 用户体验的 UI 控件。这些控件与标准 HTML 元素结合使用,提供流畅的触摸和鼠标交互,并支持创建数据驱动的应用布局。

第四部分:平台集成

在本书的这一部分,我将向您展示如何将您的应用集成到 Windows 8 平台中,从而让您为用户提供更流畅的用户体验,可以利用操作系统和其他 Windows 8 应用的功能。

第五部分:销售应用

posted @ 2024-08-19 15:41  绝不原创的飞龙  阅读(69)  评论(0)    收藏  举报