HTML5-和-JavaScript-的-Windows8-开发高级教程-全-
HTML5 和 JavaScript 的 Windows8 开发高级教程(全)
一、将 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 应用开发中。
注微软使用了 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 的良好理解将为您提供足够的基础,让您了解自己不知道的内容。
提示我在本书中最常使用的 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 元素,如button
和input
,为应用创建 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 规格的电脑都可以。在开发中,更快的机器让开发变得更愉快,但是你不需要任何强大的东西。
提示如果你正在购买一台用于 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 帐户可以免费创建,如果您还没有帐户,可以按照说明进行操作。
提示如果你要从早期版本的 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 模拟这种效果,但没有什么可以替代真实的设备。同样,一个带触摸屏的设备将让你开发和测试触摸屏交互——另一个关键的应用功能。这是你可以模拟的其他东西,但是模拟不能代替真实的东西。
提示我用的是戴尔 Latitude Duo,它是平板电脑和笔记本电脑的奇怪混合体。你可以买到便宜的二手货,它们有一个像样的触摸屏和一个方向传感器(尽管这种驱动程序必须从传感器制造商那里找到,而不是从戴尔那里)。我不建议在 Duo 上编码,这对于舒适来说有点太慢了,而且缺少 RAM,但它是一个相当不错的测试机器,我不会没有它。
开始使用
一旦你安装了 Windows 8 和 Visual Studio,你就可以开始了。在本节中,我将向您展示如何为一个应用创建一个 Visual Studio 项目,并简要介绍您将使用的工具和功能。如果您使用过另一种集成开发工具,您将会认识到 Visual Studio 的许多关键特性。
创建 Visual Studio 项目
要创建一个新的应用项目,您可以单击 Visual Studio 起始页上的New Project
链接(当您第一次启动 Visual Studio 2012 时显示),或者从File
菜单中选择New Project
。
注意实际上是
FILE
菜单,因为微软已经决定在 Visual Studio 中以大写字母显示菜单,尽管这给人的感觉是你的开发工具在对你大喊大叫。我将只是参考正常情况下的菜单。
你会看到New Project
对话框,如图图 2-1 所示。Visual Studio 包括一些模板,可以帮助您开始不同种类的项目,这些模板显示在对话框的左侧。可用的模板集因您使用的 Visual Studio 版本而异。该图显示了可用于 Visual Studio Express 2012 for Windows 的模板,该模板支持四种用于创建应用的编程语言。对于每种语言,都有一些预先填充的模板,用于创建不同的项目。
图 2-1。Visual Studio 新建项目对话框窗口
我明白为什么微软包括这些模板,但它们是相当无用的。对于新程序员来说,面对一个空项目和一个闪烁的光标可能有点令人担忧,但是他们放入这些模板的代码并不是很好,并且除了最简单和最琐碎的项目之外,很少是你想要的那种东西。
提示 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 所示。
图 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 需要几秒钟来打包应用并将其传输到远程设备,过一会儿这就变得令人厌倦了。
提示你不能像使用传统的 Windows 桌面应用那样,将一个应用复制到另一个设备上。微软非常热衷于使用 Windows Store 来部署应用,在你准备好发布之前,最简单的设备测试方法是使用远程工具。
这就剩下第三个选项:Visual Studio 模拟器。这是我在开发过程中测试应用的方式,我建议你也这样做。该模拟器是真实 Windows 8 设备的忠实再现,您可以使用它来模拟一些重要的设备特征,包括设备方向、GPS 位置、触摸和手势输入以及不同的屏幕大小和分辨率。然而,它并不完美,而且有些应用功能你无法在模拟器中正确测试。我会在适当的章节中指出这些特性,这样你就知道如何使用其他方式来运行应用。
您可以使用 Visual Studio 菜单栏选择您希望的应用运行方式。默认情况下,Visual Studio 会在本地机器上运行一个新的项目,但是你可以通过点击图 2-3 中按钮旁边的箭头来改变。该按钮显示的文本反映了您所做的选择,因此它可能与图形不完全匹配,但您不会错过该按钮,因为它旁边有一个大的绿色箭头。(您需要单击以更改设置的箭头是按钮文本右侧的小向下箭头,而不是绿色箭头)。
图 2-3。选择 Visual Studio 运行应用的方式
从下拉菜单中选择Simulator
选项,如图所示。做出选择后,单击绿色箭头或按钮文本启动应用。Visual Studio 将启动模拟器,安装您的应用,并开始运行它。你用来创建项目的Blank App
Visual Studio 模板确实只生成了一个应用的基本结构,所以目前没有太多东西可看——只有一个黑色的屏幕,左上角有一小行文字写着Content goes here
。暂时忽略这个应用,看看模拟器窗口右边的按钮,我已经在图 2-4 中显示了。你也可以通过从 Visual Studio Debug
菜单中选择Start Debugging
来启动应用——效果是一样的。我将在本章的后面回到 Visual Studio 调试器。
注意当我告诉你启动应用时,我的意思是你应该选择
Start Debugging
菜单项或点击工具栏上的按钮,以便使用调试器。有几个特性需要在没有调试器的情况下进行测试,但是我会在您使用它们的时候说明这一点。在所有其他情况下,您应该确保调试器正在运行。
图 2-4。Visual Studio 模拟器
我在模拟器旁边放大显示了按钮。这些按钮允许您使用模拟器以不同的方式与应用交互,并更改模拟设备的方向。
模拟器按钮按相关功能分组。第一组改变输入法,允许你用鼠标模拟触摸输入。当我向你展示如何处理触摸手势时,你会在第十七章中看到这些按钮是如何工作的。
第二组允许您模拟顺时针和逆时针旋转设备。我在第六章中使用这些按钮向你展示如何创建在设备方向改变时适应的布局。
第三组中唯一的按钮改变屏幕分辨率和像素密度,我在第六章中也探讨了这一点。第四组中也只有一个按钮,用于模拟 GPS 数据。在《??》第二十九章中,当我向你展示如何创建具有位置感知的应用时,你会看到这是如何使用的。最后一个按钮组可以让你截屏模拟器显示的任何内容。我在这本书里不用这些按钮。
模拟器使用提示
模拟器的工作方式是在您的开发机器上创建第二个桌面会话,并将其显示在一个窗口中。这是一个巧妙的技巧,但是它有一些值得了解的副作用。如果你有任何问题,有些是有用的,有些是值得注意的。
首先,当您使用 Visual Studio 启动一个应用时,应用包会在本地计算机上安装和执行,并显示在模拟器中。这意味着,如果您愿意,您可以导航到模拟器的开始屏幕或本地计算机上的真正开始屏幕,并在没有 Visual Studio 的情况下启动应用。如果你在运行应用时遇到问题——这种情况时有发生——你通常可以通过进入开始屏幕,找到你正在开发的应用,然后卸载它(右击应用的磁贴并选择Uninstall
)来解决问题。
第二,一些提供与 Windows 功能集成的应用功能最容易从桌面测试,例如文件激活,我在第二十四章中描述了它。因为 Visual Studio 模拟器正在运行常规的 Windows 会话,所以当您第一次在模拟器中激活桌面时,您设置为自动运行的所有应用都会启动。我发现这导致了一些问题,特别是对于那些希望独占存储位置或管理硬件的应用。我最讨厌的例子是让我重新映射鼠标按钮的软件——当我在模拟器中切换到桌面时,该软件的第二个实例会自动启动,并使我的鼠标无法使用,直到我杀死其中一个进程(由于缺少可用的鼠标,这项工作变得更加复杂)。这些通常不是终端问题,但是如果您在使用模拟器时开始看到奇怪的问题,请记住它运行一个完整的 Windows 会话,包括所有好的和坏的方面。
最后,模拟器创建和显示 Windows 会话的方式意味着,如果在模拟器启动时连接到 VPN,它将不起作用。您可以在模拟器启动后激活 VPN ,一切都会好的。
控制应用执行
启动应用后,Visual Studio 界面会发生变化,为您提供控制其执行的选项。工具栏上出现一排按钮,如图图 2-5 所示。
图 2-5。在 Visual Studio 中控制应用执行的按钮
这些按钮可以暂停、停止和重启应用。如果您对项目进行了任何更改,重启按钮将确保在开始执行之前更新应用。第四个按钮(其图标是一个带有两个形成圆圈的箭头的闪电)是重新加载按钮。此按钮可快速重新加载任何已更改的文件,而无需完全停止和重新启动应用。这个特性在开发过程中很有用,例如,当我微调 CSS 布局时,我发现它最有用。如果您在项目中添加或移除文件,或者进行需要重新安装应用的更改(例如更改清单,我将很快介绍),则无法重新加载—在这些情况下,Visual Studio 将提示您执行重新加载。
提示按钮由 Visual Studio
Debug
菜单上执行相同功能的项目补充。我倾向于使用工具栏按钮,但它们之间没有区别。
探索项目
默认情况下,Solution Explorer
位于 Visual Studio 窗口的右上角,尽管您可以移动它,甚至完全分离它。按照 Visual Studio 的说法,一个解决方案是一个或多个项目的包装。本书中的所有示例应用都包含在单个项目中,并且在很大程度上,解决方案是 Windows 8 应用之前的开发实践的延续。
Solution Explorer
提供了对项目中所有文件的访问。Solution Explorer
显示的大部分条目是 Visual Studio 在您创建项目时生成的文件夹和文件,其他条目是对您的应用所依赖的文件的引用。你可以在图 2-6 中看到所有文件的更多细节,我将在接下来的章节中解释每个文件的作用和它们的初始内容。
图 2-6。解决方案浏览器,显示 Visual Studio 为空白应用模板生成的文件
要编辑项目中的文件,只需在解决方案资源管理器窗口中双击其条目。您还可以使用图标行正下方的搜索栏在解决方案资源管理器中按名称搜索文件,这在大型复杂的项目中非常有用。(图标本身允许您导航和配置解决方案资源管理器)。
提示
Solution Explorer
窗口下面的是Properties
窗口。这个窗口在其他种类的开发项目中很重要,但是它对于 JavaScript 应用开发没有任何价值,您可以安全地关闭它,为Solution Explorer
窗口腾出更多空间,这对大型复杂的项目很有用。您可以通过View
菜单重新打开任何已关闭的窗口(通过Other Windows
菜单项可以打开一些不常用的窗口)。
探索项目参考
Solution Explorer
中显示的第一个条目是项目参考。共有六个参考文件,在图 2-7 中的Solution Explorer
中可以看到完全展开的References
项。
图 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.js
和ui.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.js
和ui.strings.js
,包含显示给用户的文本,针对不同的语言和地区进行了本地化。您不需要直接处理这些文件,因为它们的内容是通过base.js
和ui.js
文件自动消费的。
探索默认文件
当您使用Blank App
模板创建 Visual Studio 项目时,会生成三个默认文件,它们构成了应用的基本结构和初始内容。这些文件是default.html
(在项目的根文件夹中)default.css
(在css
文件夹中)default.js
(在js
文件夹中)。这些文件也描述了一个 JavaScript Windows 8 应用项目的基本组织——根文件夹中的 HTML 文件、css
文件夹中的 CSS 文件和js
文件夹中的 JavaScript 文件。这种结构是一种建议,并不是强制的,你可以自由地以你喜欢的任何方式组织你的文件——你将在本书中看到我这样做,以使示例应用更容易理解。在接下来的几节中,我将向您展示每个文件的初始内容,并解释关键特征,指出我将在本书后面详细讨论的章节。在本章中我不打算修改这些文件,但是在第三章中,你将使用它们来构建你的第一个真正的应用。
注意你可以重命名或替换这些文件,但我在本书中使用它们作为示例应用,这样你就知道哪些文件是由 Visual Studio 创建的,哪些文件是我添加的。
了解默认 HTML 文件
default.html
文件在应用启动时加载,负责导入default.css
和default.js
文件,这是使用标准的 HTML link
和script
元素完成的。这些元素也用于导入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 文件,并使用link
和script
元素将它们纳入范围,就像在 web 应用中一样。这些文件是按照它们被指定的顺序加载的,这意味着你不能在一个不同的 JavaScript 文件中调用一个函数,除非那个文件的script
元素已经被处理,并且在一个 CSS 文件中定义的样式可以被随后导入的文件中的样式覆盖。如果你想改变你的应用的主题,你可以改变文档中的第一个link
元素,这样它就会导入ui-light.css
文件。
提示对于大多数应用来说,
default.html
文件通常是一个占位符,其他内容将被导入其中,这意味着该文件往往会保持非常简短。我将在第五章中解释内容是如何导入的,以及这种方法如何适应应用开发。
了解默认 CSS 文件
当第一次创建时,default.css
文件包含一些占位符,你可以在清单 2-2 中看到。这个文件中没有定义实际的样式,但是请记住,ui-dark.css
或ui-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
。
图 2-8。Visual Studio 清单编辑器的应用 UI 标签
提示
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 所示。
图 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 中高亮显示了这个按钮。
图 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 所示。
图 2-11。激活用调试器关键字创建的断点
您使用标准的Step Into
、Step Over
和Step Out
命令来控制调试器的执行。Visual Studio 将这些作为工具栏上的按钮和Debug
菜单中的项目提供。您可以通过键入F5
或点击工具栏上的Continue
按钮来恢复应用的正常执行。
在其他代码中设置断点
使用debugger
关键字要求您能够修改文件,这与显示在Solution Explorer
窗口的References
部分的 JavaScript 文件不同。对于这种文件,您必须使用 Visual Studio 对断点的支持。通过右键单击希望调试器中断的语句,并从弹出的Breakpoint
菜单中选择Insert Breakpoint
,可以创建一个断点。断点在文件的空白处显示为红色圆圈。当您想要移除断点时,右键单击该语句并从断点菜单中选择Delete Breakpoint
。
Visual Studio 断点非常复杂,您可以创建仅在特定情况下触发的条件断点。尽管这个特性很好,但当我试图弄清楚微软是如何在base.js
或ui.js
文件中实现一个特性时,我发现自己尽可能地使用debugger
关键字并依赖 Visual Studio 断点。
监控变量
我将for
循环放入清单 2-5 的代码中的原因是为了演示 Visual Studio 让您看到变量如何变化的方式——当您使用debugger
关键字或 Visual Studio 断点时,这一功能的工作方式完全相同。
启动应用,当调试器取得控制权时,将鼠标悬停在源代码中的变量i
上。您将看到显示的当前值。值的右边是一个小图钉图标——单击这个, Visual Studio 将创建一个显示当前值的小窗口。键入F5
继续执行,等待下一次循环迭代再次触发调试器。你会看到值窗口被更新,显示变量的当前值,如图 2-12 所示。
图 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
显示对象的方式。
图 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 所示。选项将在左侧音符、右侧音符或两者上进行测试。
图 3-1。笔记选择页面
用户点击图中所示的三个按钮中的一个,显示第二页,如图图 3-2 所示。每个音符都显示在五线谱上,用户按下按钮来识别音符。在图 3-2 中显示的音符是一个C
,例如,用户可以按下C
按钮。
图 3-2。闪存卡页面
当用户看到所有的注释时,页面布局会改变。笔记名称按钮被几个导航按钮取代,如图图 3-3 所示。Again
按钮测试用户对同一音符的选择,Back
按钮返回选择页面。
图 3-3。向用户呈现导航控件
正如我所说,这是一个简单的应用。在接下来的小节中,我将带您完成我用来创建它的过程。同时,我将介绍一些关键的 Windows 8 概念,如导航、异常处理和数据绑定。我会在第四章给你演示如何完成 app。
重温示例应用项目
我将使用的 Visual Studio 项目是我在第二章中创建的项目。这个项目是使用项目名NoteFlash
从Blank 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
。这个元素将是我在单页模型中导入内容的容器,允许我向用户显示不同的内容。
提示应用的初始页面不必如此简单——你可以将本地元素与其他页面的元素以任何适合你的应用的组合混合在一起。我倾向于将这个页面用于整个应用中的通用内容,当我添加一个名为 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 应用来说都是如此。有意义的变量名只有这么多,定义两个类似于data
或user
的变量并出现问题只是时间问题,尤其是当一个应用依赖于不同程序员的库时。代码的不同部分对有争议的变量的值和意义有不同的期望,结果可能是从稍微奇怪的行为到数据损坏。
使用自动执行功能
在清单 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,但是对于大多数简单的任务来说已经足够接近了,我将在本书中通篇使用它。
提示你可以继续为你的 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 和应该传递给页面的任何状态信息。
提示在第七章中我介绍了导航特性,它简化了这一技术。
在这个函数内部,可以看到一些 WinJS API 调用。我想替换而不是添加pageFrame
元素中的任何内容。我调用了WinJS.Utilities.empty
方法,它移除了作为参数传递的元素的所有子元素。我将pageFrame
元素称为一个全局变量,这是我在第二章讨论使用浏览器特有特性时描述的 Internet Explorer 特性。
一旦我删除了任何现有的内容,我就调用WinJS.UI.Pages.render
方法。这个方法是更大的页面特性的一部分,例如,除了使用iframe
元素之外,它还提供了一些有用的行为。然而,在其核心,render
方法是 web 应用 Ajax 请求中常用的XMLHttpRequest
对象的包装器。
提示我将在本章稍后向应用添加页面时向您展示
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
条目,并从弹出菜单中选择Add
New 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
类使用了新的字体,这允许我将任何元素中的文本显示为一系列注释。musicSmall
和musicDisabled
类是我在整个应用中使用的常见样式。我会把它们和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
New Item
,并使用Page Control
项目模板。
Page Control
模板是一个方便的 Visual Studio 特性,它可以在一个步骤中创建一个 HTML 文件、一个 JavaScript 文件和一个 CSS 文件。HTML 文件包含一个用于 CSS 文件的link
元素和一个用于 JavaScript 文件的script
元素,以便在使用 HTML 时自动加载代码和样式。
JavaScript 和 CSS 文件创建在与 HTML 文件相同的文件夹中,并自动命名,这意味着我的项目包含三个新文件:pages/selectorPage.html
、pages/selectorPage.css
和pages/selectorPage.js
。你可以在图 3-4 中看到这些文件是如何在解决方案浏览器中显示的。
图 3-4。使用页面控制模板创建一组链接的 HTML、CSS 和 JavaScript 文件
新的 CSS 和 JavaScript 文件允许我区分特定页面的样式和代码,以及适用于整个应用的样式和代码(在default.css
和default.js
文件中定义)。清单 3-4 显示了我的selectorPage.html
文件的内容,它包含了将要呈现给用户的 HTML 标记。
清单 3-4 。selectorPage.html 文件的内容
`
<html> <head> <meta charset="utf-8" /> <title>selectorPage</title>
** **
** **
Select Notes
****
**
**
**
Left Hand
****
**
**
**
**
Both Hands
****
**
**
**
**
Right Hand
****
**
我突出显示了将 CSS 和 JavaScript 文件带入上下文的link
和script
元素,以及我对文件所做的更改。Windows 应用真的很像 web 应用,并且没有将 Visual Studio 创建的文件与 HTML 页面相关联的魔法——您负责确保您需要的一切都链接到 HTML,尽管 Visual Studio 在从其模板创建文件时会有很大帮助。
所有三个selectorPage
文件的内容将被导入到应用的主导航结构中,这意味着由default.js
文件定义的功能和属性可以在特定于页面的脚本中调用,而在default.css
中定义的 CSS 类将被应用到各个 HTML 文件中的元素。
我的更改删除了 Visual Studio 放入body
元素中的默认内容,代之以一个简单的结构,该结构将向用户提供要测试的笔记的选择。我已经应用了我在css/default.css
文件中定义的样式来应用音符字体。奇怪的字符串在字体中显示为有意义的音符,但它们本身看起来非常奇怪。
提示在第五章我解释了为什么default . CSS 和 default.js 文件的内容总是可用,当我解释 Windows 应用如何处理导入的 HTML 内容时。
HTML5 语义元素和划分
HTML5 有趣的一点是增加了新元素类型,比如section
和article
。当您希望一致地将语义结构应用于内容时,这些元素非常有用,这样内容区域的重要性就可以从包含它的元素类型中明显看出。这是对 HTML 4 方法的一大改进,在 HTML 4 方法中,语义通常是应用于div
元素的表达类,这很难一致地应用,并且使得与采用不同类本体的第三方共享内容变得特别困难。这很好,尤其是当你处理大量内容的时候。
另外,新的 HTML 开发人员倾向于一种被称为 divitus 或 div-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
文件的内容。
提示 Visual Studio 默认缩进 CSS,不适合我的开发风格,对书籍页面布局没有帮助。我已经禁用了层次缩进功能(使用
Tools
Options
Text Editor
CSS
Formatting
),我在本书中展示的所有 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 列找到了具有prompt
的id
值的元素,并指定它应该跨越 3 列。
另一个感兴趣的网格属性是-ms-grid-column-align
,我在这个例子中没有使用它。该属性指定网格正方形内元素的对齐方式,可以设置为start
、end
、center
或stretch
。如果您使用的是从左到右的语言,比如英语,那么start
和end
值将元素左右对齐。center
值使元素居中,stretch
值调整元素的大小,使其完全填充分配给它的空间。您可以使用网格属性创建一些非常复杂的布局。
提示详见
[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 中文件的内容,你会发现一些元素的内容是一系列字符引用,就像这样:
`...
这就是我如何表达音符和它周围的符号。您可以看到片段中的div
元素具有music
和musicSmall
样式,这意味着浏览器将应用我在本章前面的default.css
文件中定义的自定义font face
。并不是所有我想从字体中得到的符号都被映射成方便的字符,所以我混合使用了常规字符和 HTML 转义码来获得我需要的内容。你可以在图 3-1 中看到音乐字体和相关样式的效果,图中显示了selectorPage.html
文件的最终外观。
定义选择器页面的 JavaScript 代码
selectorPage.html
标记中的元素看起来像按钮,但它们只是div
元素。为了让它们表现得像按钮,我需要使用 CSS 和 JavaScript 代码的组合。你已经看到了 CSS——在清单 3-5 中,我定义了div.musicButton
和div.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 应用中使用的两个加载器是YepNope
和RequireJS
。如果小心使用,一个好的 JavaScript 加载器确实可以减少用户盯着加载屏幕的时间,并且在使用像YepNope
这样的条件加载器的情况下,避免下载特定用户、浏览器或平台不需要的脚本文件。
你可以将这些相同的技术应用到你的 JavaScript Windows 应用中,但是这样做没有任何好处——更糟糕的是,你只会让你的应用更难测试和调试。
Windows 8 应用部署在本地,这意味着您的脚本文件加载速度非常快,快到您不太可能需要推迟执行来解决性能问题。同样,因为 JavaScript 文件是本地加载的,所以不需要缩减代码来减少带宽消耗。
有一些重要的 WinJS 特性和约定隐含地依赖于同步脚本执行,包括我在上一节中向您展示的define
方法。这并不是说你不能解决这些情况——但是在这样做之前,你应该停下来问问自己你正在试图解决什么问题。自从 Windows 8 的最早发布版本以来,我一直在编写 Windows 应用,我还没有遇到任何可以通过添加 JavaScript 加载程序来解决的问题。可能有一些非常特殊的情况,加载器非常有用,但我还没有遇到过,但这些情况很可能很少发生,因此您不应该仅仅因为它是 web 应用开发工作流的一部分就自动将加载器添加到您的 Windows 应用项目中。
处理按钮点击
在我的ready
事件处理程序中,我将handleMusicButtonEvents
函数注册为click
、mousedown
和mouseup
事件的处理程序。我没有在标记中使用button
元素,所以我通过应用和移除背景颜色来响应mousedown
和mouseup
事件。这些是您在 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
特性,并在第十八章中详细讨论名称空间。)
提示您会注意到我使用标准的 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
元素整齐地显示在布局网格上,并对基本的鼠标交互做出响应(尽管单击时它们加载的页面尚不存在)。
图 3-5。允许用户选择一组音符
目前看起来不太像,但这一章的大部分内容都是关于 Windows 应用开发的上下文和背景。到目前为止,该应用本身仅由几个短文件和一些基本的 JavaScript 组成。下一章会加快速度,你会看到应用的其余功能很快到位。
总结
示例应用目前没有做太多工作,但是到目前为止,您已经了解了 Windows 应用开发中一些最基本的概念。您还了解了 Windows 为帮助您管理全局命名空间而提供的约定和功能。Windows 应用开发不是 web 应用开发,但是您现有的技能为您创建丰富流畅的 Windows 应用打下了良好的基础。在本章中,你看到了我如何使用标准 HTML 和 CSS 来创建应用的结构和布局,包括 CSS3 web 字体和grid
布局(尽管使用了特定于供应商的前缀)以及标准 DOM 事件,如click
和mouseover
。在下一章中,我将添加到示例应用中并构建功能。
四、完成应用
在这一章中,我将完成我在第三章中开始的基础版本NoteFlash
示例应用。我继续使用与常规 web 应用开发有很多共同之处的方法和技术,但我也开始融入更多特定于 Windows 的功能,这些功能可通过 WinJS API 获得。我简要概述了我使用的每个 Windows 应用的功能,并解释了在本书中可以获得更多详细信息的地方。
重温示例应用
在这一章中,我将直接从第三章的开始构建NoteFlash
项目。正如您所记得的,我将应用的基本结构放在适当的位置,定义了导航功能,并定义了将在整个应用中应用的样式。我还使用了Page Control
条目模板来生成一组相关的 HTML、CSS 和 JavaScript 文件,这些文件用于创建允许用户选择要测试的笔记的内容。你可以在图 4-1 中看到结果。在这一章中,我将创建额外的内容来根据用户的选择执行测试。
图 4-1。便签选择页面
定义 Notes 数据
构建示例应用的下一步是定义用户将要测试的笔记。为此,我在名为notes.js
的js
文件夹中添加了一个新的 JavaScript 文件,其内容可以在清单 4-1 中看到。(右击Solution Explorer
中的js
文件夹,选择Add
New Item
,使用JavaScript File
项模板。)
提示在
Solution Explorer
窗口中右键点击js
文件夹,从弹出菜单中选择Add
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
方法允许您通过从现有的类派生来创建新的类。
提示微软曾表示,基于类的继承比标准的基于 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.Utilities
和WinJS.UI.Pages
。如果您想使用query
方法在 HTML 中搜索元素,您可以调用WinJS.Utilities.query
。查询方法是WinJS.Utilities
名称空间的一部分,该名称空间包含许多其他有用的函数。
名称空间可以是分层的。Utilities
名称空间是WinJS
名称空间的一部分。WinJS
包含许多子名称空间,Utilities
只是其中之一。在上一节中,我使用了WinJS.Class.define
方法——该方法位于WinJS.Class
名称空间中,而Class
是Utilities
的对等体。通过使用名称空间,微软将大量的功能打包到两个全局名称空间对象中:WinJS
和Windows
。
所有的名称空间都是:全局对象。例如,清单 4-2 展示了如何使用常规的 JavaScript 对象来重新创建WinJS.Utilities.query
方法。
清单 4-2 。作为对象层次结构的名称空间
... var WinJS = { Utilities: { query: function (someArguments) { // ...implementation goes here... } } }; ...
名称空间的层次性意味着您可以在名称空间层次结构的不同位置重用变量和方法的名称。例如,一个(假设的)WinJS.Database.query
方法完全独立于WinJS.Utilities.query
,尽管这两个方法都被称为query
。这是名称空间的好处之一。如果所有的方法都是全局的,我会以像queryHTMLById
和queryHTMLByTagName
这样的名字结束,这和你在 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
方法创建新的名称空间。第一个参数是要创建的命名空间的名称,第二个参数是其成员将被添加到命名空间的对象。
提示您可以通过将带点的名称作为第一个参数传递给 define 方法来创建名称空间的层次结构,比如
MyData.Music.Notes
。将自动创建层次结构中的每个级别。
在清单中,我创建了一个名为Notes
的新名称空间,它包含两个数组:leftHand
和rightHand
。每个数组包含一组Note
对象,代表与那只手相关的音符序列,这些对象是使用我在本章前面描述的Note
类创建的。
使用define
方法创建名称空间在几个方面让生活变得更简单。首先,名称空间被自动添加到全局名称空间中。考虑到创建全局变量是多么容易(真的太容易了),这没什么大不了的,但这确实意味着我将能够在我的应用中的任何地方引用Notes.leftHand
和Notes.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"),**
** ],**
** });**
})();`
结果是一个包含leftHand
和rightHand
注释的名称空间。当然,您可以自己执行这些检查,但是使用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.js
和notes.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 应用中很重要,因为用户可以重新定位设备或改变分配给应用的屏幕大小(我会讨论这两个功能,并在第六章的中告诉你如何适应它们)。
提示当我需要精确划分屏幕空间时,我倾向于使用网格布局,当我更关心流动性和居中元素时,我倾向于使用 flexbox 布局。
您可以通过将display
属性设置为–ms-flexbox
来启用 flexbox 布局,就像我在清单中所做的那样。最重要的属性是–ms-flex-direction
,它指定了子元素的布局方式。我已经在表 4-1 中列出了该属性支持的值。
在清单中,我指定了列值,这意味着我的元素将按照它们在 HTML 中定义的顺序从上到下排列。
–ms-flex-pack
属性指定元素如何沿–ms-flex-direction
属性指定的轴对齐(沿垂直轴为column
值,沿水平轴为row
值)。我已经在表 4-2 中列出了该房产的价值。
在清单中,我使用了justify
属性,这意味着flashContainer
元素中的元素将被隔开,这样它们就占据了元素的整个高度。
–ms-flex-align
属性指定元素沿轴的对齐方式,该轴未被–ms-flex-direction
属性使用——也就是说,与元素布局所沿的轴成 90 度,称为正交轴。该属性值如表 4-3 所示。
使用这三个属性,你可以在布局中构建很多流动性,但是还有一些我在本书中没有用到的属性(并且经常发现没有用)。你可以在[
msdn.microsoft.com/en-us/library/windows/apps/hh453474.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/hh453474.aspx)
看到完整的列表。
只需将
–ms-flex-align
属性设置为justify
并将–ms-flex-pack
属性设置为center
,子元素将被放置在元素的中心。
在清单中,我指定了center
值,这意味着我的元素将与flashContainer
元素的中心对齐。结合我指定的其他值,flashContainer
元素的子元素将垂直布局,分布在元素的整个高度,并位于元素的中心。您可以在图 4-2 中看到页面的布局。
图 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
属性。
提示
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.css
和ui-light.css
文件中定义,我在第二章中介绍过,它们定义了 Windows 应用的所有基本样式。在图 4-3 中,您可以看到按钮是如何显示的。
图 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 中可以看到这些按钮。
图 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
元素。
图 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 中看到完整的应用,显示了正在向用户显示的笔记,以及关于正在测试哪组笔记的信息,用户的进度,以及指示正确答案的颜色提示。
图 4-6。完成的 NoteFlash 应用
正如我之前提到的,这不是NoteFlash
应用的最终版本,但基本功能已经完成,用户可以测试他的视奏能力。在后面的章节中,我将回到这个应用并添加更多的功能。
更新应用清单
虽然我已经完成了应用的 HTML、CSS 和 JavaScript 组件,但还有一点工作要做。如果你转到Start
屏幕并找到NoteFlash
应用的磁贴,你会发现应用呈现给用户的方式相当简单,如图图 4-7 所示。
图 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 中的所示。
图 4-8。设置磁贴的图像
我的图像文件的名称以谱号开头,然后详细说明分辨率,例如clef30x30.png
。您可以看到我是如何为清单中的各个字段设置图像名称的。图像没有缩放,如果图像大小不合适,您将无法使用它。
还要注意,我已经为Background color
选项设置了一个值。这用于设置瓷砖的颜色——对于这个应用,我选择了蓝色的阴影,由十六进制代码#528FC8
指定。
提示虽然图中没有显示,但是我也设置了闪屏的图像。
设置应用名称
我还想更改显示在磁贴上的名称,该名称取自清单中的Display name
字段。我已经把这个改成了Note Flash
,如图图 4-9 所示。
图 4-9。更改应用的显示名称
我还更改了Description
字段,它为用户提供了应用的摘要。更改显示名称和应用图像的结果是如图 4-10 所示的磁贴,为用户呈现一个更加完美的应用磁贴。
提示这是一个静态磁贴,只显示图片和应用名称。在第二十七章的中,我将向你展示如何创建动态磁贴,向用户显示有用的信息。
图 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 对本章进行了总结。
创建单页项目
为了演示支撑单页模型的技术,我使用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
Bottom Right
定义该应用布局结构的三个元素是 id 为left
、topRight
和bottomRight
的div
元素。left
元素包含一些button
元素,我将在本章后面使用它们来导入内容以响应用户输入。topRight
和bottomRight
元素提供了显示导入内容的结构。
定义初始 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
元素横跨两行,由于我没有明确指定位置,所以它会在第一列和第一行。topRight
和bottomRight
元素在第二列,每行一个。你可以在图 5-1 中看到网格布局的效果。
图 5-1。样板工程的初始布局
定义初始 JavaScript
我在js/default.js
文件中的初始 JavaScript 代码如清单 5-3 所示。除了为WinJS.Utilities.query
方法创建我喜欢的$
别名之外,代码还在名为left
的div
元素中找到了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
WinJS API 的主要部分是一组 UI 控件。这些是应用于标准 HTML 元素的增强,以提供特定于应用的功能。为了以声明方式将内容导入到我的应用中,我使用了一个非常基本的控件HtmlControl
。
我将HtmlControl
应用到div
元素中我想要插入导入内容的地方。应用控件就是将data-win-control
属性设置为 WinJS API 中控件对象的全名,也就是WinJS.UI.HtmlControl
。
我必须配置控件,以指定我想要导入的文件的名称。格式是包含一个 JSON 片段,其中的uri
属性定义了文件名,在本例中是contentBasic.html
。我不喜欢像这样将 JSON 嵌入到 HTML 中,但是声明式导入需要它。
提示在这一章中,我不会深入探究 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
文件的内容。
提示我创建了 contentBasic.html 文件,方法是在解决方案浏览器窗口中右键单击项目条目,从弹出菜单中选择
Add
New Item
,并使用HTML Page
项模板。这个模板创建一个 HTML 页面。这不同于我之前使用的Page Control
项目模板,它创建了 HTML 文件、CSS 文件和 JavaScript 文件(并在 HTML 文件中添加了link
和script
元素)。Page Control
模板只是为了方便,并没有赋予它创建的文件任何特殊属性。事实上,我更喜欢在需要时单独创建文件,很少在自己的项目中使用Page Control
模板。
清单 5-6 。contentBasic.html 档案
`
这是一个简单的 HTML 文件,包含一个内嵌的style
元素和一些基本的 HTML。如果您启动示例应用,您将看到声明式导入的效果,如图图 5-2 所示。我在图中突出显示了导入的内容。
注意,已经在div
元素中的内容仍然存在。导入内容不会替换任何现有的子元素——如果您只想要导入的内容,就必须显式删除目标元素的内容(我很快就会这么做)。
图 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
元素,而不仅仅是那些已经导入的元素。
提示 CSS 优先考虑属性定义的顺序。这意味着导入内容中的属性会覆盖主文件中定义的属性。
确保您的样式只影响一个文档中的元素的最简单方法是确保您缩小 CSS 样式的范围,使它们只影响导入的元素。你可以在清单 5-7 中看到我是如何为contentBasic.html
做这些的。
清单 5-7 。缩小对导入内容中 CSS 样式的关注
`
我添加了一个div
元素,作为将要导入到文档中的元素的父元素。div
不会改变导入内容的外观或结构,但它允许我缩小样式的关注范围,这样它们就不会泄露到布局的其他部分。你可以在图 5-3 的中看到这种变化的效果。注意来自default.html
文件的内容不再受右对齐的影响。
图 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
我现在可以在我的 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
变量。
注意这是内容导航的基本形式,因为我为用户提供了一种改变布局组成的方式。在 Windows 应用中使用常规的 HTML 元素和事件进行导航是完全允许的,但是也有一些特定于应用的 UI 控件和 API 专用于导航,我在第七章的中对此进行了描述。
我使用WinJS.Utilities.empty
方法删除目标元素中的现有内容,然后创建一个新的HtmlControl
对象来导入contentBasic.html
文件。一个HtmlControl
对象的两个构造函数参数是目标 HTML 元素和一个包含配置信息的对象,配置信息的格式与我在上一节中声明的格式相同。您可以使用标准的 DOM API 方法(比如document.getElementById
),使用将id
属性值视为变量的 IE 特性,或者像我在本例中所做的那样,使用WinJS.Utilities.query
方法来获得目标元素。这是我别名为$
的方法,它返回一个匹配 CSS 查询字符串的元素数组,即使只有一个匹配元素。这就是为什么我必须通过将[0]
附加到方法调用来提取我想要的元素。
提示以编程方式创建的
HtmlControl
的配置信息是一个对象,而不是一个 JSON 字符串。你必须记住不要把论点用引号括起来。
这些更改的结果是,在单击左侧面板中的某个按钮之前,不会导入任何内容。两个按钮导入相同的内容——contextBasic.html
文件——但是内容导入到的元素不同。这些动作相互独立工作,也就是说如果你同时点击两个按钮,contentBasic.html
文件的内容将被导入到文档中的两个位置,如图图 5-4 所示。
图 5-4。以编程方式将相同的内容导入两个位置
使用HtmlControl
以编程方式解决了声明性使用的一些不足。首先,我获得了对何时导入内容的控制权——在本例中,我在响应按钮点击时导入内容。其次,我可以通过删除目标元素中存在的任何内容并创建另一个HtmlControl
对象来更改显示的内容。这是一个很大的进步,它让我可以将我的应用分解成可管理的块,我可以按需组合并显示给用户,根据应用的变化状态重用部分布局来导入和显示内容。
进口互动内容的风险
即使以编程方式使用,HtmlControl
也不能以有效的方式处理script
元素——它们仍然被添加到主文档的head
元素中,并在导入常规 HTML 元素之前执行。然而,HtmlControl
对象构造函数接受一个回调函数的可选参数,该函数将在内容导入后执行。这提供了我以前没有的定时信号,允许我创建可以对新添加的元素进行操作的代码。至少,最初看起来是这样——但这是一个陷阱,需要谨慎。我将介绍回调函数的使用,并向您展示它所产生的问题。首先,我需要一些需要 JavaScript 才能有用的内容。清单 5-10 显示了我添加到根项目文件夹中的一个新文件的内容,名为contentButton.html
。
清单 5-10 。contentButton.html 档案
`
这是一个包含一个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
元素。点击新导入的button
,contentBasic.html
文件的内容被导入到右下面板。
这种方法有两个问题。一个是可见的,并且相当容易分类。另一个是无形的,但更重要,需要时间和注意力来理解和解决。
了解 CSS 问题
可见的问题是我前面描述的 CSS 范围问题的变体。我想重温一下,因为它强调了在导入内容时缩小 CSS 选择器范围的重要性。
如果您运行示例应用并单击Button One
,则会加载contentButton.html
文件。出现的Press Me
按钮与其父容器的左边缘对齐。如果点击Press Me
按钮,contentBasic.html
文件被导入到布局中,Press Me
按钮的对齐被移动到父项的右侧。你可以在图 5-5 中看到效果,图中显示了点击button
前后的 app。
图 5-5。由进口 CSS 驱动的按钮延时动作
这种变化的原因是在contentBasic.html
文件中为message
样式定义的附加属性。contentButton.html
文件中的button
包含在message
类的div
元素中,两个文件共享相同的元素结构和命名模式。当导入contentBasic.html
文件时,我定义的 CSS 样式比我预期的应用得更广泛。
事实上,问题是由一系列交互触发的,这使得在开发和测试过程中很难发现问题(这种性质的大多数问题比按钮突然移动位置更加微妙)。
理解紧耦合问题
更严重的问题是,我在default.js
和contentButton.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。注册回调函数的方法
`
define
方法有两个参数。第一个是您希望得到通知的文件的名称——在这个场景中,这总是当前文件的名称,因为我希望在 HTML 元素导入后得到通知。
第二个参数更复杂—它是一个对象,其属性指定回调函数,这些函数将在响应内容生命周期的不同部分时执行。属性集由WinJS.UI.Pages.IPageControlMembers
接口定义,涵盖了生命周期中的各个阶段。
注意 JavaScript 不支持接口,但其他 Windows app 编程语言支持。不必拘泥于细节,对于 JavaScript Windows 应用,接口定义了一组方法和属性,对象必须定义这些方法和属性才能在特定情况下使用,或者在本例中,定义了一组受支持的值或属性名。接口的概念不太适合 JavaScript,但它是让这种语言被视为一等 Windows 应用公民并访问 Windows API 的成本的一部分。
支持的属性名集合有error
、init
、load
、processed
和ready
,它们可以用于在导入特定文件时对其进行监控。
当您在导入的文件中使用define
方法时,最有用的属性是ready
,当内容已经加载并插入到主布局中时,您分配给该属性的函数将被执行。ready
属性是我支持 JavaScript 代码所需的定时信号,该代码对导入文件的 HTML 元素进行操作。
执行分配给 ready 属性的函数时,会传递两个参数。第一个参数是内容将要插入的元素。第二个参数是传递给render
方法的数据对象。
在这个例子中,我使用数据对象的target
和content
属性来配置来自Press Me
按钮的click
事件的处理方式。通过以这种方式传递信息,我确保了contentButton.html
中的代码不依赖于default.html
文件中的元素结构。
提示这种技术并不完美,因为导入的文件需要知道自己在 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 中,我已经展示了将多个内容项导入单个元素的两种方法中的第一种。当点击id
为button1
的按钮时,我调用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.html
、contentBasic.html
,然后再一次为contentButton.html
。但是由于我已经忽略了WinJS.Promise
对象,我实际上是让顺序在运行时确定。如果运行该示例并单击Button One
,您将看到如图图 5-6 所示的结果。
图 5-6。不受管理的内容顺序
如图所示,内容导入的顺序与我调用render
方法的顺序不匹配。发生这种情况的原因有很多,但是对排序最重要的影响来自于render
方法缓存内容这一事实。加载contentButton.html
文件的第二次调用完成得非常快,因为第一次调用的结果被缓存了。
提示尽管缓存在这个例子中起了很大的作用,但是当你丢弃由
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
方法的顺序导入。
图 5-7。强制输入内容的顺序
这种方法的好处是内容按照我想要的顺序排列。缺点是我拒绝了运行时一次加载和处理多个项目的机会,这是异步编程的主要好处之一。当涉及到对布局中的内容进行排序时,为了得到您需要的结果,牺牲性能通常是值得的。
使用导航 API
在我的示例应用中,我仍然有一个问题:用户可以导航到哪里的细节必须包含在每个页面中。当您希望为用户提供不同的路线来导航到相同的内容时,这可能会有所限制。例如,想象一个以特定方式显示数据并可以通过另外两个页面访问的页面——您想为用户提供一种返回到他来自的页面的方法,但是没有办法知道那是哪个页面。
您可以使用包含在WinJS.Navigation
名称空间中的 WinJS 导航 API 来解决这个问题。导航 API 帮助您跟踪您在应用中所做的导航更改,并使用这些数据来创建更灵活的布局。在接下来的小节中,我将向您展示如何使用这个 API。导航 API 实际上不做任何导航,它只是在需要导航服务的应用部分和实现您的首选导航策略的代码之间充当代理。随着导航请求的创建和完成,导航 API 会维护一个导航历史,您可以使用它在内容中创建更灵活的导航。
处理导航事件
使用导航 API 时,您必须做的第一件事是为至少一个导航事件注册回调函数。这些事件是在发出导航请求时触发的(我将在下一节演示)。对于每个导航请求,三个导航事件依次发生——我在表 5-2 中总结了这些事件。这些事件没有内置的处理程序,所以你可以在你的应用中把它们解释成最有意义的。我倾向于处理navigating
事件,而忽略其他事件。
您可以用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 文件中的导航方法
`
现在我有了一些内容,它们可以请求导航到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
`
** **
`
本文档中的重要元素是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
元素的内容。
提示通过
Event
对象的detail.state
属性,导航事件处理函数可以使用状态对象。
在图 5-8 中点击Button One
可以看到导航到contentBasic.html
的效果。如果你在应用刚刚启动的时候以这种方式导航,没有导航历史,因此canGoBack
属性返回false
,这意味着Back
按钮将被禁用。虽然没有历史记录,但是有一个可用的状态对象,您可以在布局中看到显示的消息。
图 5-8。点击按钮一导航至 contentBasic.html
作为对比,你可以看到在图 5-9 中点击Button Two
然后点击Press Me
按钮导航到contentBasic.html
的效果。有可用的导航历史,所以Back
按钮被激活,点击它将导航回contentButton.html
(当然,这里的要点是在contentBasic.html
文件的代码或标记中没有对contentButton.html
的引用)。在这个导航序列中,我没有向navigate
方法传递状态对象参数,这在消息中有所反映。
图 5-9。点击按钮一导航到 contentBasic.html,然后按我
避开浏览器导航功能
既然你已经看到了导航 API 是如何工作的,你可能想知道为什么不直接使用浏览器内置的history
和location
对象。问题是这些对象不允许你根据应用的单页内容模型来定制导航。一个 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 对本章进行了总结。
创建示例项目
我已经创建了一个名为AppViews
的示例项目,这样我可以在本章中演示不同的特性。我再次使用了 Visual Studio Blank App
项目模板。你可以在清单 6-1 的中看到我对default.html
所做的添加,我将把它用作我的 HTML 母版页。
清单 6-1 。AppViews 项目中 default.html 文件的内容
`
<html> <head> <meta charset="utf-8"> <title>AppViews</title>
我使用 CSS grid
特性创建了一个 2 乘 2 网格的主布局。网格将包含在 id 为gridContainer
的div
元素中,每个子元素包含一个标签来指示其位置。你可以在清单 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;
}`
提示我保留了 Visual Studio 创建的
js/default.js
文件。我将回到这个文件,并在本章的后面向您展示它的内容。
这些文件产生了一个简单的应用,它有四个彩色象限,如图 6-1 所示。在本章的剩余部分,我将使用这个应用来解释应用可以显示的不同视图,以及如何适应它们。
图 6-1。默认视图中显示的示例应用
了解地铁景观
Metro 应用可以以四种视图之一显示。图 6-1 显示了全屏横向视图中的应用,其中整个显示屏专用于示例应用,屏幕的最长边位于设备的顶部和底部。还有另外三个视图需要你处理:全屏人像、抓拍、填充。
在全屏纵向视图中,你的应用占据了整个屏幕,但是最长的边在设备的左右两边。在截图中,应用以 320 像素宽的条状显示在屏幕的左边缘或右边缘。在填充视图中,除了被抓拍的应用占据的 320 像素条之外,应用显示在整个屏幕上。仅当显示器的水平分辨率为 1366 像素或更高时,才支持对齐和填充视图。对于当今的大多数设备来说,这意味着只有当设备处于横向方向时,填充和对齐视图才可用,但这不是必需的,并且在屏幕足够大的情况下,设备将能够在横向和纵向方向上对齐和填充。你可以在图 6-2 中的截图和填充视图中看到示例应用。
提示在填充视图和捕捉视图之间切换的最简单方法是按下
Win
+ .
(句点键)。
图 6-2。截图和填充视图中的示例应用
如您所见,Metro 应用的默认行为只是适应任何可用的空间。在我的示例应用中,这意味着可用空间在我的网格中的列间平均分配。当你的应用在填充视图中时,这通常不是那么糟糕,因为 320 像素并不是屏幕空间的巨大损失。当你的应用在快照视图中时,它会有更大的影响,因为 320 像素根本不是很大的空间。如图所示,我的示例被压缩到可用空间中,没有完全显示文本。
注意图中另一个 app 报告其当前视图。我在这本书的源代码下载中包含了这个应用,以防你会觉得它有用——这个应用叫做
PlaceHolder
,在本章的文件夹中。它使用的特性和功能与我在本章中描述的相同,这也是我没有列出代码的原因。
用户决定他想要哪个视图以及何时想要。你不能创建一个只在特定视图下工作的应用,所以你需要花时间让你的应用布局以一种有意义的方式适应每个视图。有不同的方法来适应这些视图,我将在接下来的小节中带您了解它们。
注意理解这一章的最好方法是跟随并像我一样构建应用。这将让你看到应用响应视图变化的方式,这是静态截图无法正确捕捉的。
使用 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 中看到效果。
图 6-3。使用 CSS 网格调整布局以适应填充的视图
适应快照视图
快照视图通常需要更多的思考。你需要在那个 320 像素的长条中放一些有用的东西,但是整个应用的布局通常放不下。我的首选方法是在应用处于快照视图时切换到仅显示信息的视图,并在用户与我的应用交互后立即脱离该视图。在本章的后面,我将向你展示如何改变你的应用的视图。
不管你用什么方法,你都必须面对这样一个事实:与整个屏幕相比,你的空间相对较小。在我的示例应用中,我通过改变我的 CSS 网格来做出响应,这样它只有一列——这具有在网格的其余部分隐藏内容的效果,使用了清单 6-4 中所示的属性。
清单 6-4 。适应快照视图
... @media screen and (-ms-view-state: snapped) { ** #gridContainer {** ** -ms-grid-columns: 1fr;** ** }** } ...
你可以在图 6-4 中看到结果。
图 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 所示。
提示 Metro 枚举有点像名称空间和接口。它们在 JavaScript 中实际上没有多大意义,但是它们使得像 C#等其他 Metro 语言一样使用 Windows API 中的对象成为可能。在 JavaScript 中,它们被表示为对象,这些对象的属性定义了一组预期或支持的值。
我已经在default.js
文件中添加了一些代码,这样其中一个网格元素就会显示当前的方向,如清单 6-5 所示。
提示我不想在我的代码中一直输入
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 内容的良好结合对我来说是最好的。
图 6-5。通过使用 JavaScript 改变元素内容来适应视图
适应导入内容的视图变化
没有特殊的机制将视图信息传播到您导入到布局中的内容,但是您可以使用 CSS media
规则并响应resize
事件,就像处理母版页一样。清单 6-7 显示了我添加到项目content.html
中的一体化页面的简单例子。(一体化页面将脚本和样式包含在与标记相同的文件中——我在整本书中使用它们来为示例应用添加自包含的演示。)
清单 6-7 。响应导入内容的视图更改
`
`
我已经使用default.js
文件中的WinJS.UI.Pages.render
方法导入了这些内容,如清单 6-8 所示。有关该方法的更多详细信息,请参见第 XXX 章。内容被导入到具有bottomRight
的id
的元素中,占据了网格布局的右下部分。
清单 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 的中可以看到按钮元件的两种状态。
图 6-6。在导入的内容中使用视图更改事件和 CSS 媒体规则
这与我用于主内容的技术完全相同,但我想强调的是,你需要在整个应用中应用它们,包括你导入的任何内容。如果你不严格地应用这些技术,当某些应用状态和视图组合在一起时,你最终会得到一个对用户来说很奇怪的应用。
提示您可能会看到对一个
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 添加元素以显示视图和方向
`...
清单 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
对象提供对器件方向的访问。currentOrientation
和nativeOrientation
值返回对应于DisplayOrientations
枚举值的整数。我已经在表 6-3 中列出了这些值。
nativeOrientation
属性将只返回landscape
或portrait
值,并设置基线与其他值是相对的。在清单中,我读取了currentOrientation
和nativeOrientation
属性的值,并将它们显示在布局中。我还为由DisplayProperties
对象发出的orientationchanged
事件创建了一个处理函数。当当前方向或自然方向改变时,触发此事件。传递给 handler 函数的Event
对象不包含关于哪个值已更改的信息,这意味着您必须读取属性值,并确定您需要在应用的上下文中做什么。图 6-7 显示了方向信息是如何在示例应用中显示的(当然,您看到的值将取决于设备方向)。我留下了显示当前视图的代码,以强调您需要管理方向和视图的组合,以创建一个具有完全响应布局的应用。
图 6-7。显示当前和本地方向
您可以使用 Visual Studio 模拟器来测试方向,并且可以使用图中突出显示的两个按钮来更改方向。模拟器的自然方向是横向,在图中,我将模拟器旋转了 180 度(您可能会看到小的 Microsoft 徽标位于模拟器窗口的顶部)。
使用 CSS 适应设备方向
您还可以使用 CSS media
规则来响应设备方向,但仅限于设备是横向还是纵向——标准视图和翻转视图之间的差异无法通过 CSS 来表达。关键规则属性为orientation
,支持的值为landscape
和portrait
。清单 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 中看到效果(如果你正在阅读这本书的印刷版本,有黑白图像,你将需要运行这个例子)。
图 6-8。根据方向变化改变元素的颜色
看起来这种orientation
媒体规则属性重复了我在本章前面向您展示的–ms-view-state
的功能,但实际上它们可以很好地协同工作。当我想在设备处于横向时应用样式时,我发现orientation
属性很有用,例如,不管它是在全屏、快照还是填充视图。
表达您的设备方向偏好
并非所有应用都能够在所有方向上提供完整的用户体验,因此当设备在不同方向之间移动时,您可以要求应用不要旋转。你可以使用应用清单来声明你的长期偏好。当我向NoteFlash
应用添加磁贴图标时,您在第 XXX 章看到了清单,它包含了运行时执行您的应用所需的配置信息。要打开清单,双击Solution Explorer
窗口中的package.appxmanifest
文件。默认情况下,Visual Studio 为清单打开一个漂亮的编辑器,但是清单只是一个 XML 文件,如果愿意,您可以直接编辑文本。我很喜欢这个编辑器,因为它用一个漂亮的 UI 覆盖了所有的配置选项。如图 6-9 所示,要更改方向设置,点击Application UI
选项卡并在Supported Rotations
部分检查您想要支持的方向。
图 6-9。使用清单编辑器为应用选择支持的方向
当您设置您想要支持的方向时,您只是表达了一种偏好,仅此而已。在图中,我已经说明了我的示例应用在纵向和纵向翻转方向上效果最好,如果可能的话,Windows 应该只在这些方向上显示我的应用。
提示不勾选任何一个选项表明你的应用很乐意在所有选项中显示。
不选中某个选项并不会阻止您的应用以该方向显示。Windows 将尝试满足您的愿望,但前提是它在运行您的应用的设备上有意义。作为一个例子,我的只有纵向的偏好在只有横向的桌面设备上没有意义,所以 Windows 会以横向显示我的应用,因为否则对用户来说毫无用处。
您不能使用方向首选项来避免实现对捕捉和填充视图的处理。如果你的应用真的不能在特定的方向和视图组合中工作,那么你需要通过监听视图和方向变化事件来处理这个问题,并向用户显示一条消息来解释这个问题,并鼓励他将你的应用切换到首选的排列。
对于首选项有意义的设备,Windows 将尊重您的首选方向。对于我的示例应用,这意味着如果你以横向方向启动应用,应用将以纵向模式启动,即使这意味着布局将呈 90 度——这将鼓励用户重新调整设备以适应你的应用。它工作得很好,对于平板设备来说,这是一个自然和无缝的反应。
注意截图并不能很好地说明效果,因为它只是显示了与常规方向成 90 度的布局。你真的需要对这个例子进行实验来理解它的效果。不幸的是,您将需要一个带有加速度计的设备来进行测试,因为 Visual Studio 模拟器忽略了方向首选项。我使用戴尔 Inspiron Duo 进行这种测试-它有点动力不足,但价格合理,而且它有方向变化所需的硬件。
覆盖清单方向首选项
您可以在应用运行时更改应用的方向首选项,这将临时覆盖清单中的设置。我说暂时,因为下一次你的应用启动时,清单首选项将再次生效。如果你的应用有独特的模式,其中一些模式在某些方向上无法有意义地表达,那么能够覆盖方向偏好是有用的。当用户在应用内容和布局中导航时,你可以让窗口知道你在任何给定时刻想要支持哪些方向。
注意你必须向用户提供一个视觉提示,来解释你的应用的当前状态和你当前支持的一组方向之间的关系。如果你不提供这个提示,你将会迷惑用户,创建一个应用,它将进入一个方向,然后,没有明显的原因,陷入其中。用户不会将应用的状态已经改变联系起来。我建议您不要动态更改方向首选项,而是支持所有方向,并调整您的布局,以解释为什么某些应用状态在某些方向上不起作用。
可以通过Windows.Graphics.Display.DisplayProperties
对象的autoRotationPreferences
属性动态更改方向首选项。您使用 JavaScript 按位 OR 操作符(使用|
字符表示)来组合来自DisplayOrientations
枚举的值,以指定您想要支持的方向。为了演示这个特性,我在default.html
文件中添加了一个标记为Lock
的button
元素,如清单 6-13 所示。该按钮覆盖orientation
首选项,防止方向改变。
清单 6-13 。给 default.html 文件添加一个按钮
`...
清单 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
属性设置为零,表示我没有方向偏好。
注意如果你更改了首选项,Windows 会立即旋转你的应用,使当前方向不是你的首选选项之一。您应该小心使用这种行为,除非是为了明确响应明确描述的用户交互,否则不要强制改变方向。在没有用户指导的情况下触发方向改变是令人讨厌和困惑的,它会破坏用户对你的应用的信心。
适应像素密度
有一种趋势是显示器每英寸具有更大数量的像素。这最初是由苹果及其“视网膜”显示器流行起来的,但这种硬件已经变得更加普遍,也用于 Windows 8 设备。更高的像素密度的效果是打破显示器中的像素数量和显示器尺寸之间的联系,创建物理上更小的高分辨率显示器。
传统上,当显示器的分辨率增加时,屏幕的尺寸也会增加。目标是能够在屏幕上显示更多内容——更多 UI 控件、更多窗口、更多表格行等等。
对于高像素密度显示器,目标是显示与相同尺寸的低像素密度显示器相同的数量,并使用额外的像素使图像更清晰和锐利。为了实现这一点,Windows 扩展了 Metro 应用——如果不这样做,你最终会得到太小而无法阅读的文本和太小而无法触摸或点击的 UI 控件。表 6-4 显示了 Windows 8 基于显示屏像素密度应用于 Metro 应用的三个缩放级别。
Windows 会自动缩放 Metro 应用,并将始终缩放到表中的某个级别。你不必担心放大你的布局,即使它们包含绝对尺寸——例如,Windows 会将你指定的 CSS 像素数量转换成场景背后放大的显示像素数量。当您在 DOM 中查询元素的大小时,您将得到 CSS 像素而不是缩放值。所有这些使得创建在高和低像素密度下看起来都不错的 Metro 应用变得非常容易。
在这种排列中,有一点不太好,那就是位图图像,随着像素密度的增加,显示质量会下降。高密度显示的效果是位图图像看起来模糊不清,边缘参差不齐,如图图 6-10 所示。左边的图像显示了放大图像时会发生什么,右边的图像显示了您应该瞄准的清晰边缘。
图 6-10。位图图像在高像素密度显示器上放大时产生的模糊效果
解决这个问题的最好方法是使用矢量图像格式,比如 SVG。这说起来容易做起来难,因为许多设计包缺乏良好的 SVG 支持。更实用的解决方案是为每个 Windows 8 缩放级别创建一个位图,并确保使用最合适的图像来匹配显示器的像素密度。为了演示这种方法,我创建了三个图像文件。你可以在图 6-11 中看到它们。
图 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
元素。
注意仅仅将图像文件复制到磁盘上的图像文件夹是不够的。您还需要在 Visual Studio
Solution Explorer
窗口中右键单击 images 文件夹,并从弹出菜单中选择Add
Existing
项。选择图像文件并点击Add
按钮。
清单 6-15 。使用一组缩放文件中的图像
`...

测试图像选择
如果没有一系列配备不同像素密度显示屏的设备,测试这项功能是很棘手的。Visual Studio simulator 支持在不同的屏幕分辨率和密度之间切换,但它不能正确处理图像,而是以原始大小显示图像,这就违背了模拟的目的。然而,模拟器确实加载了正确的图像来反映模拟的密度,所以我可以通过限制img
元素的大小来得到我想要的效果,我已经使用了我在default.css
文件中定义的样式之一,如清单 6-16 所示。对于真实的设备和真实的项目,这不是你需要做的事情。
清单 6-16 。固定图像元素的大小以说明像素密度支持
`...
testImg {
width: 100px;
height: 100px;
}
...`
模拟器窗口边缘的一个按钮改变屏幕尺寸和密度,如图图 6-12 所示。最有用的设置是 10.6 英寸显示屏,可以用四种不同的分辨率进行模拟,覆盖 Windows 8 支持的不同比例级别。
图 6-12。在 Visual Studio 模拟器中更改屏幕特性
为了测试这种技术,启动应用并在可用的像素密度之间切换。您将看到自动显示针对像素密度的图像。
提示每次更改后,你都必须在调试器中重新加载应用才能看到正确的图像——请求的图像名称(
img.png
)和缩放版本(img.scaled-XXX.png
)之间的映射似乎被缓存了。
使用 JavaScript 适应像素密度
您可以通过Windows.Graphics.Display.DisplayProperties
对象获得像素密度和缩放工厂的详细信息。为了演示这是如何工作的,我使用名称img100.png
、img140.png
和img180.png
将图像文件的副本添加到项目images
文件夹中。我已经创建了这些副本,因此它们不受我在上一节中描述的缩放命名方案的约束。此外,我已经从default.html
文件的img
元素中移除了src
属性,如清单 6-17 所示。
清单 6-17 。从 default.html 文件的 img 元素中删除 src 属性
`...
我想要的信息可以通过DisplayProperties.resolutionScale
属性获得,该属性返回一个对应于Windows.Graphics.Display.ResolutionScale
枚举的值。这个枚举中的值是scale100Percent
、scale140Percent
和scale180Percent
。在清单 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();
})();`
注意你也可以使用 CSS
media
规则来适应像素密度,但是没有方便的缩放贴图,你必须直接处理像素密度值。这是 Visual Studio 模拟器的问题,因为它以错误的方式舍入了像素密度数字,所以模拟的分辨率不属于正确的类别。此外,由于您只需要使位图图像适应像素密度,而使用 CSS 无法做到这一点,因此这种技术没有什么价值。如果你发现自己在调整布局的任何其他部分以适应像素密度,那你的方法就有问题了。请记住:Windows 将为您缩放应用中的所有其他内容。
总结
在这一章中,我向你展示了让你的应用布局适应设备的不同方法——适应视图、适应方向和适应像素密度。确保您的应用以有意义的方式适应,对于创建流畅、高质量的用户体验非常重要,这种体验可以与设备和操作系统功能完美融合。如果你不仔细考虑你的适应方法,或者更糟的是,完全跳过它们,你将创建一个行为怪异的应用,并且不能真正与 Metro 体验相融合。我的建议是花时间考虑你的应用能做出什么样的最佳反应,特别是在快照视图和纵向视图方向上。
在下一章中,我将介绍一些 UI 控件,它们是 Metro 的独特部分,您可以使用它们来提供一致且简单的应用功能导航。
七、命令和导航
在第五章中,我向你展示了如何使用单页布局来创建一个 Windows 应用的基础。当我这样做的时候,我使用了button
和锚(a
)元素来导航内容,就像我在普通的 web 应用中所做的一样。在这一章中,我将向你展示应用特有的控件,这些控件专用于提供在应用中导航的命令,并操纵它所呈现的数据或对象:应用工具栏和导航工具栏。这些控件提供了 Windows 应用独特的视觉风格和交互模型的很大一部分,并且存在于大多数应用中(游戏似乎是个例外,在游戏中非标准界面很常见)。在这一章中,我将向你展示如何创建和应用这些控件,并且在这样做的时候,提供更多关于 WinJS 控件模型的细节,我在第五章中使用HtmlControl
导入内容的时候提到过。表 7-1 对本章进行了总结。**
*
创建示例项目
我使用Blank App
模板创建的本章示例项目名为AppBars
,它遵循单页面导航模型。这一章是关于导航的,所以我已经创建了一个项目,它有一个母版页和两个内容文件,稍后我将向您展示。当应用启动时,我导入其中一个内容文件,然后,在本章的后面,我将添加一些 Windows 应用 UI 控件,以便在其他内容之间导航。default.html
文件将作为母版页,如清单 7-1 中的所示。
清单 7-1 。AppBars 项目的 default.html 主页
`
<html> <head> <meta charset="utf-8"> <title>AppBars</title>
这将是我的母版页,我将把内容导入到属性为contentTarget
的div
元素中。在所有其他方面,我保留了这个文件,就像我使用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 中看到的,它显示了示例应用的初始外观。
图 7-1。app bars 示例 app 的初始外观
创建应用命令
应用栏出现在屏幕底部,为用户提供对命令的访问。命令对当前范围内的数据或对象执行一些功能,这通常意味着您的命令应该与您当前呈现给用户的内容相关。
AppBar 是 Windows 应用用户体验的重要组成部分。它为关键交互提供了一致和熟悉的锚点,并允许您将 UI 控件从应用的主布局中移出,以便您使用屏幕空间为用户提供更多数据。在这一节中,我将详细介绍 AppBar,向您展示如何创建、填充和管理 UI 控件以及如何响应用户命令,并告诉您在应用中充分利用 app bar 所需要知道的一切。很自然,可以从创建 AppBar 开始,这是通过WinJS.UI.AppBar
对象完成的。创建 AppBar 最简单的方法是通过向标准 HTML 元素添加数据属性,以声明的方式完成。应用栏通常被添加到母版页,这样你就不必在每次导入新内容时重新设置它们。考虑到这一点,清单 7-5 展示了在default.html
文件中添加一个 AppBar。
注意
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 的中间部分切掉了,这样更容易看到各个按钮。
提示如果你现在运行这个例子,AppBar 不会出现。您需要首先应用清单 7-6 中的所示的更改。
图 7-2。清单 7-5 中的元素创建的 app bar
声明 AppBar 控件
AppBar 的起点是创建一个WinJS.UI.AppBar
对象,我使用div
元素上的data-win-control
属性来完成,如下所示:
`...
这是我在前面章节中使用的 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
对象。
提示注意,我不必亲自处理滑动或右键单击来显示 AppBar 控件。这在应用中是默认连接的,因此应用栏(和导航条,我将在本章后面介绍)会自动出现。
我已经把对WinJS.Navigation.navigate
方法的调用放在一个函数中,我把它传递给由processAll
方法返回的Promise
的then
方法。这很重要。虽然我在母版页中设置了 AppBar,但我想从内容页中管理和控制它(我将在本章的后面解释为什么并向您展示如何做到这一点)。我无法通知导入的内容母版页中的 HTML 元素已经被处理并转换为 WinJS UI 控件,这一点很重要,因为我不希望该内容中的代码在准备好之前就开始访问控件功能(这将导致引发异常)。解决方案是确保processAll
方法在导入内容之前已经完成了它的工作,这意味着使用 WinJS Promise
,如代码所示。
提示我说 WinJS 控件是从 HTML 元素创建的,或者应用于 HTML 元素,但实际发生的是 WinJS 用一些 CSS 格式化 HTML 元素,并且在某些情况下,添加一些新元素。该技术非常类似于基于 JavaScript 的 UI 库,如 jQuery UI 和 jQuery Mobile。我不想给你留下正在进行某种魔术的印象——在大多数情况下,WinJS 控件是标准的 HTML 和 CSS。在某些情况下,JavaScript 代码会使用一些 Windows API 调用,但这种情况非常少见,而且这些也是您可以在代码中使用的 Windows API(这也是我在本书中描述的)。
向 AppBar 添加按钮
通过向 AppBar 添加具有data-win-control
值WinJS.UI.AppBarCommand
的button
元素,向用户显示命令,如下所示:
... <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 的关键。
设置命令 ID
id
属性标识与按钮相关的命令。没有预定义的命令,因此您可以自由分配在应用环境中有意义的id
值。该属性有两种用途。首先,在响应用户交互时,使用id
属性来确定请求了哪个命令。其次,您使用id
告诉 AppBar 在导入内容时显示哪些命令。我将在本章后面的响应命令部分演示这两种用法。
配置命令按钮的外观
icon
、label
和tooltip
属性定义了按钮的外观。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
)。
提示如果找不到适合自己需要的图标,可以将
icon
属性设置为包含自定义图标的 PNG 文件的名称。
label
和tooltip
属性指定显示在按钮图标下的字符串,以及当鼠标悬停在按钮上或者当用户在按钮上滑动手指而没有松开时显示的字符串。这些属性的值不是根据icon
值自动推断出来的,你可以在你的应用中自由使用任何有意义的文本。然而,重要的是不要给众所周知的图标赋予新的含义,因为你赋予label
和tooltip
属性的值不会总是显示给用户。在我向您展示的代码片段中的button
中,我将icon
设置为bold
并将label
和tooltip
都设置为Bold
,这产生了如图图 7-3 所示的按钮。
图 7-3。配置应用栏中命令按钮的外观
图的左侧显示了全屏横向视图中显示的 AppBar 的一部分,其中显示了label
文本(我只显示了 AppBar 的一侧,其他按钮都在旁边)。图的右侧显示了快照视图中的 AppBar。在这个视图中没有显示label
文本,因为 AppBar 已经通过省略label
和将图标打包堆叠在一起来适应缩小的屏幕空间。你也不能依靠tooltip
的值来帮助用户理解一个按钮会做什么,因为它们不会显示在只支持触摸的设备上。你可以依靠图标的清晰度来传达按钮的含义——这使得做出适当的选择并尊重与众所周知的图标相关的惯例变得很重要。
提示如果你发现自己很难通过图标传达命令,你可能想停下来想想你的应用的设计。Windows 应用的用户体验是关于即时和明显的交互,这可能是因为你试图在一个命令中包含太多的含义。通过将动作分解成一系列命令或者使用上下文菜单(我在本书的第三部分中描述了这一点),你可以为你的用户创造更好的体验。
分组和分隔按钮
AppBar 中有两个部分:选择部分位于 AppBar 的左侧,包含应用于用户选择的数据或对象的命令。全局部分位于应用栏的右侧,包含始终可用且不受单个选项影响的命令。section
属性决定了一个按钮被分配到哪个部分,如你所料,支持的值是selection
和global
。
当您创建 AppBar 时,您用您的应用在所有情况下支持的所有命令填充每个部分。然后,您可以更改命令集和单个命令的状态,以匹配应用的状态——稍后我将向您展示如何做到这一点。
选择全局命令的位置
微软已经为一些全局命令在应用栏上的位置定义了一些规则。如果你的应用有一个New
或Add
命令,那么它应该被放在最右边的全局命令,并且应该用add
图标显示(这个图标不应该用于任何其他命令)。
放置在New
或Add
左侧的命令应为对应命令。如果你的应用处理的数据或对象具有应用之外的生命(比如照片,它们驻留在设备存储上,可以被其他应用访问),那么你必须使用一个Delete
命令(一个Delete
的label
值和一个delete
的icon
值)。如果你的应用只处理自己的数据,那么你应该使用一个Remove
命令(一个label
值为Remove
,一个图标值为remove
)。如果该操作将删除多个项目,那么您应该使用一个Clear
命令(一个标签值为Clear
,一个图标值为clear
)。
设置命令类型
有四种类型的命令可以添加到 AppBar,由type
属性指定——这些类型对应于值button
、toggle
、separator
和flyout
。如果您没有为type
属性显式设置值,那么将使用button
值。
button
和toggle
类型创建常规按钮,当它们被点击时触发事件。它们之间的区别在于toggle
类型创建了一个具有开和关状态的切换按钮。我之前关注的Bold
按钮是一个toggle
命令,你可以在图 7-4 中看到关闭和打开状态是如何显示的。我将很快向您展示如何以编程方式检查和更改切换状态。
图 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 控件的特性和功能。
提示注意,我已经使用了
e.target
来定位点击事件所源自的元素。该事件来自命令,而不是 AppBar,因此您需要确保处理正确的元素。
由winControl
属性返回的对象是由data-win-control
属性指定的对象。对于 AppBar 命令,winControl
是一个WinJS.UI.AppBarCommand
对象,我可以访问这个对象定义的所有属性和方法。在本例中,我已经读取了label
属性来获取显示在命令按钮下的文本字符串。
AppBarCommand
对象非常简单,它定义的大多数成员对应于我在本章前面描述的配置选项(id
、icon
、section
、type
等等)。还有一些额外的属性,我已经在表 7-3 中描述了它们,尽管我在本章后面才详细解释其中的几个。
使 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
值的数组,并隐藏数组中没有指定的所有命令。我用这个方法来隐藏除了cmdBold
、cmdFont
和cmdAdd
命令之外的所有命令。
getCommandById
接受一个id
值并返回相应的AppBarCommand
对象。在清单中,我为cmdBold
命令找到了AppBarCommand
对象,并将disabled
属性设置为true
(正如我在上一节中所描述的,它将按钮保留在 AppBar 上,但阻止它被使用)。你可以在图 7-5 中看到这些方法在 AppBar 上的效果。
图 7-5。为导入的内容定制 app bar
我喜欢使用showOnlyCommands
方法,因为这意味着我提供了我想要显示的命令的明确列表,但是在AppBar
对象中还有其他方法可以用来为您的内容准备 AppBar。在表 7-4 中,我描述了你可以用来显示、隐藏和定位命令的一整套方法。
使用弹出按钮
基本命令可以用button
或toggle
命令类型处理,但是对于复杂的命令,你需要使用flyout
类型,它将命令与一个被称为弹出按钮的弹出窗口链接起来。使用WinJS.UI.Flyout
UI 控件创建弹出按钮,在这一节中,我将向您展示如何创建和使用弹出按钮,以及如何将它们与您的 AppBar 命令相关联。
提示在这一节中,我将解释如何使用 AppBars 的
Flyout
控件,但是您也可以使用Flyout
来创建通用的弹出窗口。更多细节和示例见第十二章。
声明弹出型按钮
声明弹出按钮的最佳位置是在母版页中,这样整个应用中都可以使用相同的元素集。通过将data-win-control
属性设置为WinJS.UI.Flyout
,弹出按钮被应用于div
元素。您可以设置div
元素的内容,以适应您的应用的需求——Flyout
UI 控件的作用是处理弹出窗口,对您向用户呈现的内容没有任何限制。清单 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 所示。飞出式按钮被轻轻关闭,这意味着如果用户点击飞出式按钮占据的屏幕区域之外,它们将再次隐藏。这意味着您不必添加任何种类的取消按钮来删除弹出按钮。
图 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.html
和page3.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.html
和page3.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 都会显示。
图 7-7。向示例应用添加导航栏
图中的细节可能很难辨认,但是你可以看到我在图 7-8 的中特写的两个命令按钮。数字没有预定义的图标,所以我将命令的icon
值设置为来自Segoe UI Symbol
字体的字符代码(\u0032
和\u0033
)。点击命令将导航到相应的页面。
图 7-8。标准导航条按钮上的命令
使用自定义导航条
在导航条上使用一系列按钮来表示命令并不总是有意义的。在这些情况下,您可以为WinJS.UI.AppBar
控件定义一个自定义布局。清单 7-17 显示了customNavBar.html
文件的内容,这是我使用 Visual Studio HTML Page
项模板添加到项目中的。
清单 7-17 。customNavBar.html 文件的内容
`
这是另一个自包含的 NavBar,HTML 元素和响应它们的代码定义在同一个文件中。不同的是,我在data-win-options
属性中将layout
属性设置为custom
。custom
值告诉WinJS.UI.AppBar
控件,你不想要标准的基于命令的布局,你将自己提供和管理导航条中的元素。
注意你可以在应用栏和导航条上使用自定义布局——但这通常不是个好主意。应用的导航结构可能会有所不同,用户希望导航条能够反映出你的应用的独特特征。另一方面,命令应该是一致的,并使用我前面描述的约定来应用。用户将期待在应用栏中的命令,它们的布局和响应方式与所有其他 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
文件中处理的。
提示我也在项目中添加了
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();
})();`
在导航到请求的内容之前,我移除了id
为navbar
的元素,并通过winControl
隐藏了appbar
元素。这给了我所需的应用状态,以便新内容可以加载自己的导航栏,并且不会被应用栏遮挡。你可以在图 7-9 中看到导航条产生的效果。
图 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();
})();`
我在这个清单中使用的两个动画方法是exitPage
和enterPage
。名称解释了每个动画的用途,两种方法的参数都是一个或多个需要动画的元素。为了简单起见,我传递了母版页中承载导入内容的元素的children
属性的结果。
请注意,没有为这些动画配置样式或持续时间的选项。通过使用这些方法,您可以应用标准的页面过渡动画,这对于所有 Windows 应用都是一样的。这是一个很好的方法,因为很多非常明智和理性的开发人员在得到一个动画库时会变得有点疯狂,并且往往会忘记动画是为了帮助用户。
我不能给你看截图中的动画,所以你应该运行示例应用来看看效果。由exitPage
和enterPage
方法触发的动画简洁、简单且有效。它们足够简单和快速,当用户第 100 次看到动画时不会感到厌烦,但它们足够丰富,足以引发用户意识到应用中的内容已经发生了变化。
总结
在这一章中,我已经向你展示了如何使用WinJS.UI.AppBar
控件来创建应用栏和导航条。这些是 Windows 用户体验中的基本元素,它们为用户与你的应用的交互带来了一致性和清晰性。这并不是说使用 AppBars 和 NavBars 应该受到限制——即使有微软制定的惯例,也有许多不同的方式来组织您呈现给用户的命令,并且您可以选择使用自定义 NavBar 布局来定制您提供的导航体验。
在下一章,我将介绍视图模型和数据绑定的主题。现在我们已经了解了 Windows 应用布局和导航的基本结构,是时候考虑如何采用同样的方法来组织和显示应用的数据了。*
八、查看模型和数据绑定
在本章中,我将向您介绍视图模型和数据绑定。这是两个基本的技术,可以让您创建可伸缩性好、易于开发和维护、能够流畅地响应数据变化的应用。
您可能已经从设计模式中熟悉了模型和视图模型,如模型-视图-控制器(MVC),模型-视图-视图模型(MVVM 和模型-视图-视图控制器(MVVC))。我不打算在本书中详细讨论这些模式。有很多关于 MVC、MVVM 和 MVVC 的好信息,从维基百科开始,它有一些非常平衡和深刻的描述。
我发现使用视图模型的好处是巨大的,除了最简单的应用项目,其他项目都值得考虑,我建议你认真考虑遵循同样的道路。我不是一个模式狂热者,我坚信应该采用解决实际问题的部分模式和技术,并将它们应用到具体的项目中。最后,你会发现我对如何使用视图模型持相当开放的观点。
我在本章中描述的 WinJS 特性支撑了 Windows 应用支持的一些基本交互模型。为了确保我为更高级的特性打下坚实的基础,我慢慢地开始这一章,并逐步介绍关键概念。理解这些特性是充分利用高级 UI 控件和概念的前提,比如语义缩放,我在第十六章中对此进行了描述。表 8-1 对本章进行了总结。
重温示例应用
在这一章中,我继续构建我在前一章中创建的AppBars
项目。提醒一下,这个应用引入了 NavBars 和 AppBars,并包含了一些简单的内容页面。我将在此基础上展示新的应用特性。
分离应用组件
我将首先应用一个视图模型来修复第七章中的示例应用的一些缺点。在此过程中,我将向您展示我首选的视图模型对象结构,并演示视图模型可以有多简单,同时还能使开发人员的工作更加轻松。
注意如我之前所说,我对视图模型的构成持非常开放的态度,它包括不直接呈现给用户的数据。
定义视图模型
视图模型最重要的特征是全局可用性和一致性。在 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
名称空间中的后台数据。最初这个名称空间中没有属性,但是我会在本章的后面添加一些。
提示 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
文件中反映这些更改,而不必搜寻并找到所有使用了appbar
和navBarContainer
值的实例。在包含在内容文件中的代码中,我可以将我想要的导航条导入到视图模型中列出的元素中,如清单 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,
中的导航事件处理函数,这意味着导航条控件不必应用于id
为navbar
的元素。最后,我对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。
图 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,这意味着该值可以被读取,但不能被修改。
提示在视图模型中使用计算值的好处是生成值的逻辑保存在一个地方,而不是嵌入到需要显示值的每个
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
的函数中,该函数使用视图模型来获取word
和wordLength
数据值。应用的功能是相同的,但现在视图模型充当数据存储库以及更新属性的代码和响应这些更新的代码之间的中介。
使用数据绑定
在这一点上,我已经达到了我的目标,但不是以一种有益的方式。例如,尽管我已经将更新代码分离到它自己的函数中,并使用视图模型数据进行更新,但我仍然必须直接从与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.html
的script
元素,它显示了正在为word
和wordLength
属性处理的变更事件。
清单 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
方法,我能够确保在更新完这两个值之前不发送word
和wordLength
属性的变更事件。
让我重申一下,你只需要在非常特殊的情况下走这么远。我已经向您展示了这种技术,因为这些情况经常令人惊讶地出现,并且因为它允许您获得关于 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();
})();`
注意当你使用声明式绑定时,你应该总是将
WinJS.Binding.optimizeBindingReferences
属性的值设置为true
,就像我在例子中所做的那样。虽然我没有遇到任何问题,但微软警告说,省略这一步会产生绑定操作导致内存泄漏的风险。我没有在本书的一些例子中应用这个属性,因为我想让他们尽可能专注于手头的功能,但是对于真正的项目,你应该采纳微软的建议。
您需要将对WinJS.Binding.processAll
方法的调用放入到Promise.then
调用链中,以便在用WinJS.UI.Pages.render
方法导入内容之后、向用户显示之前处理绑定。processAll
方法返回一个Promise
对象,所以很容易得到正确的顺序,尽管嵌套的then
调用链可能会变得相当深。
提示我在第九章中深入覆盖了
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> ...
冒号(:
)将属性名称与数据属性名称分开。如果与声明性绑定相关的数据项是可观察的,则声明性绑定会自动保持最新。清单中的两个声明性绑定都与可观察的数据项相关,因此我的布局与视图模型保持同步。当您只需要向用户显示值时,这是使用编程绑定的一个很好的替代方法。
提示通过用分号(
;
)分隔绑定,您可以在单个**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
对象添加到名称空间,如清单所示。
提示注意,我已经返回到清单中视图模型的更简单形式。我在本章其余部分描述的技术可以应用于任何一种方法,但是这个更简单的视图模型使我更容易说明我所做的更改。
List
对象不是 JavaScript 数组的直接替代。它实现了数组中的许多方法和属性,包括我在清单中使用的在word
属性更新时向List
添加一个项目的push
方法。主要的区别是没有数组索引器(像myArray[0]
),你必须使用getAt
和setAt
方法(myArray.getAt(0)
)来代替。您很快就会习惯它,从积极的方面来看,List
对象支持一些很好的特性来排序和过滤它的内容。表 8-2 总结了由List
对象定义的最重要的方法。
这些只是基本的方法,我建议您花些时间看看 API 文档,更详细地探索一下List
对象的功能。
提示
List
对象支持的许多功能是它作为一些 WinJS UI 控件的数据源所必需的。你可以在第三部分中了解更多关于数据源和使用它们的控件的信息。
在清单 8-18 中,我创建了一个空的List
对象,但是如果你将一个 JavaScript 数组传递给构造函数,你可以预先填充一个List
。你也可以传递一个可选的配置对象,它支持表 8-3 所示的属性。
您可以按如下方式应用这些选项:
... var myArray = ["Apple", "Orange", "Cherry"]; var myList = new WinJS.Binding.List(myArray, **{**
** proxy: true,** ** binding: false** **}**); ...
我还没有在实际项目中使用过这些选项。proxy
属性是危险的,并且binding
选项需要在由List
发出的事件的处理函数中非常小心地编码,以防止观察到不再是集合的一部分的对象(并且在有用的情况下,我更喜欢自己注意使对象可被观察到)。
观察列表
正如您所料,List
对象会在集合内容发生变化时发出事件。有几个事件,最有用的是iteminserted
、itemchanged
、itemremoved
和itemmoved
。每个事件的重要性从它的名字就可以看出,通过查看传递给事件处理函数的事件的detail
属性,您可以获得插入、更改、删除或移动内容的详细信息。
更改列表中对象的属性值不会触发事件。
为了演示如何观察一个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
按钮时都会添加一个新元素,创建一个简单的历史。
图 8-2。观察列表对象以创建简单的历史显示
您会注意到,我只展示了List
对象的编程绑定。有一些可用的声明性绑定,但是它们被包装在一些 WinJS UI 控件中。我将在本书的第三部分中介绍这些控件并解释它们如何与List
对象一起使用。
使用模板
在前面的例子中,我最终创建了一系列的div
元素来表示用户输入单词的历史。我使用了 DOM API,它可以工作,但是使用起来很笨拙,而且容易出错。幸运的是,WinJS 提供了一些与数据绑定特性相关的基本模板支持,这使得基于视图模型中的数据创建元素变得更加容易。清单 8-20 显示了我在前面的例子中使用 JavaScript 创建的元素——这些将作为我的模板的(简单)目标。
清单 8-20 。单词历史列表中的代码生成元素
`...
定义和使用模板
通过将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 中看到。
图 8-3。布局的初始状态
我将视图模型中的word
和wordLength
属性的值设置为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 。应用数值转换器
`...
您必须指定转换器的完整路径,该路径必须通过全局命名空间可用。对于我的示例应用,这意味着我必须将转换器称为ViewModel.Converters.defaultStringIfNull
。你可以在图 8-4 中看到结果,当word
或wordLength
属性为null
时,一个更有意义的值呈现给用户。
图 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
属性。
第三个参数是绑定的目标。对于我的例子,这将是跨度元素,它的id
是lengthspan
。最后一个参数是转换后的值将应用到的属性——这是另一个名称数组,因此我的函数将接收数组["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; }); ...
这种技术是用于支持声明性绑定的编程绑定的奇怪组合,它比任何一种技术本身都要多做很多工作。但是灵活性有时是值得努力的。
提示请注意,我没有根据所使用的目标元素做出任何决定——这样做是实现紧密耦合的途径,因为标记结构的知识将嵌入到转换器中。如果您必须根据应用的位置来定制转换后的结果,那么就限制自己根据元素类型或它所属的类来做出决定。
默认情况下,除非将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 对本章进行了总结。
提示
WinJS.Promise
对象是CommonJS Promises/A
规范的一个实现,你可以在[
commonjs.org](http://commonjs.org)
读到。这正在成为 JavaScript 异步编程的标准,并在 jQuery 库采用它作为其延迟对象特性的基础时得到了极大的普及。
创建示例项目
开始异步编程的最佳方式是直接投入进去。为了构建一些熟悉的东西,我将使用用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>
这个应用执行邮政编码的网络搜索。布局分成三个面板,你可以在图 9-1 中看到。最左边的面板包含一对input
元素,允许你输入邮政编码,旁边是Go
和Cancel
按钮。还有一个区域,我将在其中显示关于我发出的 Ajax 请求的消息。
图一。诺言 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
文件的内容。这个文件包含为布局中的button
和input
元素定位和设置事件处理函数的代码,但是它不包含实际向 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 能够发起出站网络连接。
图 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
定义的属性相对应的属性,包括url
、type
、user
、password
和data
,所有这些属性都不加修改地传递给XMLHttpRequest
对象。
提示您可能使用了 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
方法为成功和错误回调注册函数。
提示你没有有要用
then
的方法。如果您不关心异步任务的结果,只需丢弃或忽略该方法返回的Promise
来创建一个“一劳永逸”的任务。
WinJS.xhr
方法立即返回,Ajax 请求将在未来某个未指定的时间执行。我无法控制请求何时开始,只有当我的一个回调函数被执行时,我才知道请求何时结束。
如果我的成功回调函数被执行,我知道我有一个来自服务器的响应,我处理并显示在布局中。如果我的错误回调函数被执行,那么我就知道出错了,并显示错误的详细信息。你可以在图 9-3 中看到结果,该图展示了点击Go
按钮并完成创建的Promise
对象后应用的布局。
图 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
按钮的结果。
图 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
的一种简单方式,但是它们可能很难阅读。
提示
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 中的代码会产生以下效果:
- Schedule the first request
- Wait until
Promise
from the first request- Schedule the second request
- Wait until
Promise
from the second request- Add a message to the
ViewModel.State.messages
object
这通常是所需要的效果——在前一个活动完成之前,不要安排下一个活动。然而,如果您省略了return
关键字,您会得到一个非常不同的效果:
- Schedule the first request
- Wait until the first request
Promise
is satisfied- Schedule the second request
- To the
ViewModel.State.messages
object添加消息
如果你没有从回调函数中返回一个Promise
,那么后续的活动将会在回调函数执行完成后立即被调度——当你调用一个异步方法时,是在任务被调度后,而不是在任务完成后。
在我的示例应用中,这种差异很容易发现,因为我在请求的整个生命周期中都在编写消息。图 9-5 显示了两种情况下的消息顺序——左图显示了使用 return 关键字的效果,右图显示了没有 return 关键字的效果。指示器是All Requests Complete
消息在事件序列中出现的地方。
省略关键字return
并不总是错误的。如果您想将某个任务推迟到其前任完成之后,但不关心该任务的结果,那么省略return
关键字是完全合适的。只要确保你知道你的目标是什么效果,并根据需要包括或省略return
。
图 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
时,会调用错误回调。传递给函数的对象有三个属性(name
、message
和description
),它们都被设置为字符串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 中看到效果。
图 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,表示出现了错误或请求被取消。
来自被执行的处理函数的true
或false
值被作为参数传递给链中的下一个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
按钮时,您可以看到向用户显示的一组精简的消息。
图 9-7。通过一个链传递来自承诺对象的结果的效果
协调承诺
then
函数并不是协调异步任务的唯一方法。Promise
对象支持其他几种方法,这些方法可以用来创建特定的效果,或者使处理多个Promise
对象变得更容易。我在表 9-2 中总结了这些方法,并在下面演示了它们的用法。
使用任意方法
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 中看到该示例生成的输出。
图 9-8。用任意方式报告哪个承诺先兑现
提示
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
对象上使用any
和join
方法。一个Promise
能够支持对then
方法的多次调用,并将正确执行多组回调。你可以在图 9-9 中看到使用any
和then
方法的效果。
图 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
的内容。
注意这是一个高级主题,大多数应用都不需要。也就是说,即使您不需要立即使用这种技术,快速浏览这一部分也会帮助您理解由 WinJS 和 Windows 名称空间中的方法返回的
Promise
对象是如何工作的。
清单 9-18 。default.html 档案
`
这个简单的应用非常适合演示如何创建定制的Promise
。你可以在图 9-10 中看到布局。
图 9-10。custom promise app 的布局
当点击Go
按钮时,我调用calculateSum
函数,该函数生成前 10,000,000 个整数的和。这个任务需要几秒钟才能完成,在此期间,应用没有响应。当应用没有响应时,用户界面不会响应用户交互。对于这个简单的例子,您可以看出有问题,因为在点击了button
元素之后,直到求和计算完成,它才返回到未按下的状态。这是因为click
事件是在 CSS 更改被应用之前被触发和处理的,这意味着计算会阻止任何 UI 更新,直到它完成。这就是异步方法要解决的问题。
提示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 。在自定义承诺中增加取消支持
`
我在 HTML 的default.html
中添加了一个Cancel
按钮,点击这个按钮会调用在点击Go
按钮时创建的Promise
的取消方法。注意,我很小心地取消了由calculateSum
函数返回的Promise
,而不是由then
方法返回的Promise
——重要的是取消正在工作的Promise
,而不是随后将被启动的Promise
。
提示注意,你不必经常检查取消。我发现在安排下一个子任务之前执行检查是一种合理的方法,在响应性和复杂性之间取得了良好的平衡。
创造合成承诺
您会发现,WinJS.Promise
对象在 Windows APIs 中被广泛使用,有时您需要创建一个Promise
,作为您已经拥有的数据值的包装器。在这种情况下,WinJS.Promise
对象定义了一些有用的方法。我已经在表 9-3 中描述了这些方法,并在下面的章节中演示了两个最有用的方法。
我已经更新了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
对象定义了wrap
和wrapError
方法,它们创建轻量级的Promise
对象。这意味着它们被明确地用作适配器,并且创建起来不那么复杂和昂贵。当您想要创建一个满足预定结果的Promise
时,您可以使用wrap
方法;当您想要打包一个错误消息时,您可以使用wrapError
方法。清单 9-24 展示了这些方法在示例应用中的应用。
清单 9-24 。使用 wrap 和 wrapError 方法
`...
...`
你应该优先使用wrap
和wrapError
方法来创建你自己的合成Promise
物体。它们不仅更便宜,而且代码更简单,因此更容易阅读和维护。
总结
在这一章中,我已经向你展示了WinJS.Promise
对象背后的细节。WinJS 和 Windows APIs 中有许多异步方法,对它们的机制有很好的理解对于编写复杂的应用是必不可少的。重要的是要记住,Windows 应用中的 JavaScript 代码是在单线程上执行的,同时还有事件处理程序和 UI 更新。当您创建自定义异步方法时,您管理的是单线程上的工作调度,而不是创建多个并行线程。如果您在应用中遇到异步操作的问题,通常是因为您将代码视为多线程。在本书的第三部分中,我向你展示了 WinJS UI 控件,你可以用它来增强你的应用,并创建一个与其他 Windows 应用一致的外观。
十、创建 UI 控件示例框架
在本书这一部分的章节中,我向您展示了 WinJS UI 控件。这些是 WinJS UI 的重要组成部分,也是我在示例应用中一直使用的标准 HTML 元素的有益补充。这些控件不仅为用户提供了更丰富的体验,还提供了 Metro 应用的部分独特外观。
有各种各样的控制,我把它们都展示给你。每个控件都有许多改变其外观和行为的配置选项,为了尽可能容易地理解这些功能,我希望能够通过一个实例向您展示它们对控件的影响,而不仅仅是描述它们。
演示每个控件所需的标记和代码量很大,单独处理每个控件将需要为每一章列出无尽的页面,这对于我来说没有吸引力,对于您来说也没有吸引力。
相反,我已经构建了一个框架,可以用来简单明了地生成每个 UI 控件所需的示例,我将在接下来的章节中使用这个框架。在这一章中,我将介绍这个框架,并向您展示它是如何运作的,这样您就会理解我提供的较小的列表意味着什么。这种方法的一个好处是,我已经使用了我在前面章节中描述的相同的 WinJS 特性和技术来创建这个示例,因此您可以看到如何将它们应用到更重要的应用中。
注意你不需要详细阅读本章来理解后面的章节和它们包含的 WinJS UI 控件的描述。我已经包括了这一章,所以你可以看到我是如何创建这些例子的。这一章有很多代码和标记,可能会很难,所以你可能想浏览一下这些材料,并在你构建了最初的几个 Metro 应用后返回来更深入地阅读。
了解最终应用
如果你能看到我想要的结果,这将有助于理解应用。我的目标是向您展示一个简单的初始布局,带有一个包含每个 UI 控件命令的导航栏。你可以在图 10-1 中看到这个初始布局,它显示了导航条命令。
图 10-1。应用和导航条的初始布局只需一个命令
每个 NavBar 命令都有一个按钮,点击它会产生一个包含两个面板的页面。左侧面板将包含我正在演示的 UI 控件。右面板将包含其他 UI 控件,您可以使用这些控件来更改左面板中控件的设置。你可以在图 10-2 的中看到一个例子,它展示了我如何演示FlipView
控件(我在第十四章的中描述了它)。
图 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 theboolean
value.
ToggleSwitch
控件是 WinJS UI 控件之一,我在第十一章中描述了它。你可能想读完第十一章然后回到这里,但是我在这一章的重点是生成我需要的标记,我不会深入控件的细节。
注意虽然总体结果比冗长重复的清单更简洁,但框架本身相当复杂,这一章包含了大量代码,其中大部分与处理模板有关,我在第八章的中描述过。
创建基本项目结构
首先,我将创建应用的基本结构,以便向用户显示初始消息,并且导航条已经就位。我还将添加导航机制,并将用于视图模型的代码文件放在适当的位置,并保存我需要的每组配置控件的详细信息列表。我首先使用Blank App
模板创建一个新的 Visual Studio 项目调用UIControls
,并对 default.html 文件做一些基本的添加,如清单 10-1 所示。
清单 10-1。来自 UIControls 项目的初始 default.html 文件
`
<html> <head> <meta charset="utf-8"> <title>UIControls</title>
** **
** **
** **
**
**
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
文件中定义它们。事实上,您已经可以看到清单中的第一个模板——其id
为navBarCommandTemplate
的元素将用于为 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
数组包含我想要创建的每个命令的细节。数组中的每一项都有name
和icon
属性,我在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.js
和viewmodel.js
文件中的代码来完成。对于这个应用,default.html 文件只负责设置导航和导航条。
提示您会注意到清单中有一些语句被注释掉了。这些是在应用首次加载时自动显示给定内容页面的有用快捷方式,这在您测试和调试示例应用中的页面时非常有用(否则您必须使用 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 控件,比如FlipView
和ListView
时,我会用到它(我在第十四章和第十五章中描述过)。
添加 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
文件,我只使用它来开发和演示示例框架应用。
图 10-3。示例 app 的初始状态
创建测试内容页面
应用的基本结构已经就绪,这意味着我可以将注意力转向创建测试内容页面和填充它所需的代码。同样,向您展示完成的内容,然后再向您展示我是如何创建的,会更容易。在图 10-4 中可以看到点击导航栏上AppTest
按钮的结果。
图 10-4。点击导航栏上的 AppTest 按钮时显示的完成内容
在接下来的几节中,我将带您了解我用来创建这些内容的过程以及生成大量内容的代码。
创建内容页面
单击其中一个导航栏按钮会从项目pages
文件夹中加载一页内容,所以我的第一步是使用解决方案浏览器实际创建pages
文件夹。然后我可以添加AppTest.html
文件,这个文件将在点击导航栏上的AppTest
按钮时被加载。你可以在清单 10-7 中看到这个文件的初始内容。
清单 10-7 。AppTest.html
页的初始内容
`
这个文件显示了标准模式,我将遵循这个模式来演示每个 WinJS UI 控件。文件中的 HTML 标记包含两个div
元素。第一个包含我正在演示的 UI 控件。为了保持本章的简单,我将使用常规的 HTML input
元素,而不是 WinJS UI 控件之一——这将允许我专注于我正在构建的框架,而不必深入 WinJS 控件的细节。对于这个例子,我给input
元素分配了一个inputElem
的id
值,如下所示:
`...
另一个div
元素将包含允许用户配置正在演示的控件的元素。这是本章的主要焦点——描述我需要的元素并从模板中生成它们的过程,这样我就不必在的后续章节中重复大量的标记和代码。目标div
元素的id
是rightPanel
,因为它是布局中最右边的面板而得名:
`...
...`内容页面最重要的部分是包含在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
文件,目标元素是id
为rightPanel
的元素。input
元素是正在演示的元素,我使用的键是apptest
。在下一节中,您将看到如何使用这些值。
创建模板系统
现在我已经有了一个要处理的测试内容页面,我可以开始构建代码来生成我需要的元素,以演示input
元素的一些特性,正如您所记得的,它是一个真正的 WinJS UI 控件的简单替代。在接下来的小节中,我将向您展示我如何描述所需的配置元素集,以及我创建它们的方法。
我将从创建一个配置元素开始。它将是一个select
元素,可用于禁用或启用input
元素。你可以在图 10-5 中看到这个选择元素创建后的样子。它非常简单,是我开始描述代码不同部分的好地方。
图 10-5。生成选择元素来配置输入元素
描述一个选择元素
我必须从描述我想要生成的select
元素开始。我在/js/controls.js
文件中这样做,你可以看到我在清单 10-8 中添加的内容。select 元素有两个选项——No
和Yes
,,当选择No
值时,它们将把input
元素的disabled
属性的值设置为空字符串(""
),当选择Yes
值时,它们将设置为disabled
。
注意我花了这么多时间来生成一个简单的可以用四行 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
元素。
提示在后面的章节中,我将更简洁地列出定义对象。在这个例子中,我使用了在自己的行上显示每个属性,以便于理解这些对象是如何工作的。
我将很快解释对象中的每一个属性,但是首先我将把我用来生成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 中列出了这些属性。
提示此表解释了选择元素的定义对象的属性的含义。定义对象的每个类型都有一些额外的或不同的属性,我将在本章后面生成这类元素时解释这些属性。
如果你回头看清单 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
属性设置为由定义对象指定的值。然后,我使用定义对象中的values
和labels
数组来创建一系列的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
元素,带有Small
和Big
选项。重要的添加是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 中看到选择Big
和Small
值的结果。
图 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 中看到该定义对象中属性的含义。
您可以从我用来生成清单 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: "
]
});
})();`
你可以在表 10-3 中看到该定义对象中属性的含义。
您可以从 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: "
** { type: "buttons", title: "Buttons", labels: ["Add Item", "Delete Item"] },**
]
});
})();`
你可以在表 10-4 中看到该定义对象中属性的含义。
我不使用模板来生成按钮元素,我将事件处理程序留给内容页面,这意味着在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: "
{ type: "buttons", title: "Buttons", labels: ["Add Item", "Delete Item"] },
** { type: "toggle", id: "disabled", title: "Disabled", value: false},**
]
});
})();`
你可以在表 10-5 中看到该定义对象中属性的含义。
你可以在清单 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 应用一致的外观。
十一、使用ToggleSwitch
、Rating
和Tooltip
控件
在这一章,我开始详细描述 WinJS UI 控件,使用我在第十章中创建的框架应用。这些 UI 控件是创建与更广泛的 Windows 视觉主题一致的应用的重要构件,值得详细研究。
WinJS UI 控件的机制类似于您可能使用过的其他 JavaScript UI 工具包,如 jQuery UI。将控件应用于元素,并应用其他元素、样式和事件处理程序来创建丰富的视觉效果。
您可以轻松地在 Windows 应用中使用 jQuery UI 或类似的库,但最终会得到奇怪的结果,并且会错过一些控件与其他 WinJS 功能(如数据绑定)的紧密集成。在本章和接下来的章节中,我将依次描述每个控件。我告诉你如何应用和配置控件,何时应该使用控件,以及如何观察用户与控件的交互。
在这一章中,我从三个相对简单的控件开始:ToggleSwitch
、Rating
和Tooltip
控件。这些是最基本的控件,虽然它们很有用,但是它们并不有趣,或者与您可能遇到的其他 JavaScript UI 工具包没有什么不同。表 11-1 对本章进行了总结。
使用切换开关控制
我将从WinJS.UI.ToggleSwitch
控件开始。这是一个很好的开始控件,因为它很简单,而且因为我在第十章中创建的框架应用中使用了这个控件来演示本章和后续章节中其他控件的功能。
顾名思义,ToggleSwitch
控件让用户在on
和off
状态之间切换。鼠标用户可以点击ToggleSwitch
的空白部分来改变状态,触摸用户可以向左或向右滑动开关。ToggleSwitch
控件在从一种状态转换到另一种状态时会执行一个简短的动画。你可以在图 11-1 中看到ToggleSwitch
是如何出现的。稍后,我将向您展示我为创建这个布局而添加到示例框架中的内容。
图 11-1。示例应用中的 ToggleSwitch 控件
何时使用拨动开关控制
当你需要向用户展示一个二元决策时,你应该使用ToggleSwitch
控件。改变值的滑动动作非常友好,比普通的 HTML 复选框或带有Yes
和No
选项的select
元素更容易使用。我经常使用这个控件让用户配置应用设置(我在第二十章中描述过)。
提示如果你在一列中显示几个
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 代码中设置。
在演示ToggleSwitch
控件的代码中,我创建了改变checked
和disabled
属性的配置控件。通过查看templates.js
文件中的createtoggle
方法,你可以看到我是如何使用 JavaScript 来设置其他属性的,我在第十章的最后给你展示了这个文件。我使用来自定义对象的值来设置labelOn
、labelOff
和title
属性。
样式化 ToggleSwitch 控件
WinJS UI 控件可以使用一组 CSS 类来设置样式。这是使用 WinJS 控件的好处之一,也是我还没有感觉到需要在我的任何 Windows 应用项目中使用 jQuery UI 的原因之一(这说明了一些问题,因为我喜欢 jQuery 和 jQuery UI)。我已经在表 11-3 中列出并描述了ToggleSwitch
控件支持的一组类。
因为我使用了ToggleSwitch
控件来帮助演示许多其他 WinJS UI 控件,所以我在/css/default.css
文件中添加了一些 CSS 样式,这些样式使用了表 11-3 中的类。你可以在清单 11-5 中看到这些风格。
小心不要忘乎所以的设计 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.css
和ui-dark.css
文件中,我在第二章中描述过)将具有更大的特异性,而你的自定义值将不会有任何影响。
对于default.css
文件中的样式,我将一个border
和一些margin
和padding
应用于我用作配置控件的ToggleSwitch
控件,并更改了所有ToggleSwitch
控件标题的文本大小和颜色。
提示很难弄清楚定制风格到底有什么效果。通过在调试器中运行您的应用,切换到 Visual Studio
DOM Explorer
窗口,单击Select Element
按钮并单击布局中的控件,您可以看到 WinJS UI 控件的结构。您将能够看到围绕应用了data-win-control
属性的元素创建的 HTML 元素,并看到底层组件的样式。
处理 ToggleSwitch 控件事件
当用户更改开关的位置时,ToggleSwitch
控件发出change
事件(当以编程方式更改属性值时,不会发出该事件)。我将依次列出每个控件的事件,以便在您需要和翻阅本章时更容易找到这些信息。表 11-4 描述了change
事件,尽管只有一个而且是非常基本的事件。
我在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
事件设置了一个监听器,我在第十章中描述过。
提示当
ToggleSwitch
被禁用时,用户不能移动开关位置,但如果通过编程修改winControl.checked
属性,开关将正确反映变化。
使用评级控制
WinJS.UI.Rating
控件允许用户通过提供星级来表达意见。在图 11-2 中,你可以看到Rating
控件是如何出现在左侧面板中的。用户可以通过点击或触摸星星来指定评级,并通过在星星阵列上上下拖动鼠标或手指来更改评级。稍后,我将向您展示我在示例项目中添加的内容,以创建这个布局。
图 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
属性访问。
我为Rating
控件定义的配置控件允许您尝试更改除了tooltipStrings
之外的所有属性,我在Rating.html
页面的script
元素中设置了tooltipStrings
(我将在下面解释)。
管理评级
在averageRating
、maxRating
和userRating
属性之间有一个特定的交互,你需要在其中工作以从Rating
控件获得正确的效果。
maxRating
属性指定了Rating
显示的星的数量,并有效地设置了可以使用该控件选择的评级的上限。显示的星星数是一个整数。
属性可以用来显示其他地方的评级。平均值的常见用途是显示其他用户的评级或当前用户在以前提供的评级。您可以将averageRating
值指定为实数(如4.2
),部分星星将会显示出来。userRating
属性表示用户选择的值。这是另一个整数值。
只有当userRating
属性为零时,才会显示averageRating
值。一旦用户提供了一个等级(或者在外部设置了userRating
属性),就会隐藏averageRating
值并显示userRating
值。我在Rating
控件的例子中添加了控件,让你指定这些属性的值,你可以在图 11-3 中看到从averageRating
到userRating
值的转换。
图 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 中看到一个工具提示值是如何呈现给用户的。
图 11-4。显示工具提示的评级控件
提示如果你设置了一个比
maxRating
属性的值多一项的数组,那么当用户向左拖动或滑动以清除评级时(假设enableClear
属性为true
),将显示最后一个字符串值。
设置评级控件的样式
Rating
控件支持多种样式,您可以覆盖这些样式来定制控件的外观,每种样式都在表 11-6 中进行了描述。
这些类必须组合成非常特殊的排列来设计一个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 中描述过。
清单 11-11 展示了我如何在/pages/Ratings.html
文件中添加脚本元素来处理change
事件。我使用这个事件来更新布局右侧面板中的配置控件,这会影响到userRating
属性。
清单 11-11 。处理评级变更事件
`...
...`
我对由Template.createControls
方法返回的Promise
对象使用了then
方法,以确保在创建配置元素并将其添加到应用布局之前,我不会创建事件处理程序。
使用工具提示控件
当鼠标或手指停留在一个元素上或当键盘获得焦点时,Tooltip
控件弹出一个包含有用信息的窗口。这是一个简单的控件,但是在如何配置和应用一个Tooltip
方面仍然有很大的灵活性。你可以在图 11-5 的中看到一个工具提示的例子。HTML 中的其他控件用于配置Tooltip
的不同方面,我将在下面的章节中解释。
图 11-5。一个工具提示控件
何时使用工具提示控件
当你想为用户提供指导或一些补充信息时,可以使用Tooltip
控件,也许是为了帮助他们在选择应用中的特定选项时做出明智的选择,或者只是关于他们正在查看的内容的一些附加信息。
提示
Tooltip
是一个瞬态控件,会自动消失(我在下面解释发生这种情况的情况)。您不能对任何类型的交互式内容使用瞬变控件,如button
或Rating
控件。为此,您需要使用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 文件的内容
`
该文件遵循与前面示例相同的基本模式。该文件比前面的例子要长,因为它包含了一些简单的内容,我使用本节中演示的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" },** ]; ...
注意
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
元素包装在一个已经应用了Tooltip
的div
元素中。当用户将鼠标指针悬停或者将手指放在button
元素上时,将显示由contentElement
属性指定的元素的内容。在示例中,我已经将contentElement
属性设置为tooltipContent
,它引用了清单 11-16 中的元素。
清单 11-16 。用于工具提示控件的内容
`...
...`这个元素的内容是由img
和span
元素组成的标准 HTML 标记。请注意,我已经将内容元素放在了一个div
元素中,该元素的 CSS display
属性被设置为none
——这防止用户看到内容,直到它被Tooltip
控件显示出来。
提示这是我使用了在上一节中添加到项目中 img/apple.png`文件的地方。你可以在图 11-6 中看到这个图像。
当鼠标指针悬停在图 11-6 中的Press Me
按钮上时,可以看到结果。
图 11-6。设置工具提示控件的内容
contentElement
是可以在Tooltip
控件上设置的属性之一,可以通过data-win-options
属性设置,也可以通过winControl
属性以编程方式设置。你可以在表 11-8 中看到完整的属性集,我将在接下来的章节中演示它们。
创建内联工具提示&使用 innerHTML 属性
您还可以将Tooltip
控件应用到span
元素,如果您使用的是内嵌显示的内容,并且div
元素会扰乱布局,这将非常有用。我在Tooltip.html
文件中定义的另一个工具提示已经以这种方式应用,如清单 11-17 所示。
清单 11-17 。将工具提示控件应用于 span 元素
`...
Tooltip
将应用的元素仍然围绕着它所涉及的内容,在这个例子中是单词 apple。当鼠标指针悬停在文本块中的单词 Apples 上时,Tooltip
将显示给用户。
作为使用contentElement
属性的替代方法,我已经使用innerHTML
属性设置了Tooltip
控件的内容。这个属性让你将Tooltip
的内容设置为一个字符串值,这是我在图 11-5 中展示的工具提示。
设置显示持续时间
用户可以通过多种方式触发Tooltip
。如果用户触摸或按住元素上的鼠标按钮,那么将显示Tooltip
弹出窗口,直到手指从显示屏上移开或释放鼠标按钮。
如果用户通过键盘操作(比如在元素间跳转)或者将鼠标悬停在目标控件上来触发Tooltip
,那么弹出窗口会显示 5 秒钟,之后会自动关闭。
通过将infotip
属性设置为true
,可以将这个时间延长到 15 秒。该值旨在用于包含大量信息且用户不太可能在较短时间内解析的Tooltip
。
该示例右侧面板中的ToggleSwitch
为左侧面板中的内嵌Tooltip
设置了infotip
属性。通过使用这个开关,您可以看到不同时间跨度的效果。
提示如果你需要让你所有的
Tooltip
显示 15 秒,你可能要重新考虑你向用户呈现内容的方式。对于引导用户浏览你的应用的小信息片段来说,是完美的。考虑使用一个Flyout
作为通用弹出窗口,正如我在第十二章中所描述的。
设置工具提示位置
placement
属性控制Tooltip
弹出窗口相对于目标元素出现的位置。默认值是top
,意味着弹出窗口显示在元素的稍上方,但是您也可以指定left
、right
和bottom
。示例中右侧面板中的select
元素设置左侧面板中内联Tooltip
的placement
属性。你可以在图 11-7 中看到交替放置的效果。
图 11-7。左侧、底部和右侧工具提示位置
你不必担心调整placement
值来确保Tooltip
弹出窗口适合屏幕。如果由placement
属性指定的值意味着弹出窗口不合适,WinJS 将自动调整弹出窗口的位置。表 11-9 显示了当弹出窗口不能显示指定值时,对于placement
属性的每个可能值将尝试的一系列回退布局。
提示对于为左撇子用户配置的设备,回退序列中左右位置的顺序将会颠倒。
弹出窗口的确切位置是相对于鼠标或触摸事件的位置,并受到一个偏移量的影响,该偏移量取决于触发Tooltip
的事件类型。键盘事件的偏移量是 15 像素(例如,当用户切换到目标元素时),鼠标事件的偏移量是 20 像素,触摸事件的偏移量是 45 像素。较大的偏移量旨在允许用户阅读Tooltip
内容,而不会被光标、手写笔或手指遮挡。
以编程方式管理工具提示控件
通过使用open
和close
方法,你可以直接控制Tooltip
何时显示和隐藏——尽管如果你使用这些方法,你应该暂停一会儿,考虑你使用Tooltip
的方式是否与其他 Windows 应用一致。(如果你想要一个通用的弹出窗口,你应该使用一个Flyout
,正如我在第十二章中描述的那样)。
为了快速参考,我在表 11-10 中描述了这些方法。这看起来可能是多余的,但是 WinJS UI 控件不使用一致的方法命名方案,当你希望能够快速发现Tooltip
控件是否定义了show
或open
和close
或hide
时,就会出现这种情况。
open
方法采用可选参数,用于模拟不同种类的触发事件,支持的值有touch
、mouseover
、mousedown
和keyboard
。如果不提供参数,则使用default
值。每个值都有略微不同的效果,包括用于定位弹出窗口的偏移量(如前一节所述)、弹出窗口显示前的延迟以及弹出窗口关闭前的显示时间。
您需要小心,因为touch
、mousedown
和default
参数不会自动关闭弹出窗口。在touch
和mousedown
模式下,这是因为弹出窗口会一直显示,直到手指从屏幕上移开或鼠标按钮松开。如果您对open
菜单使用这些参数,那么您必须使用close
方法显式关闭Tooltip
弹出菜单。示例右侧面板中的Open
和Close
按钮调用了open
和close
方法(调用open
方法时没有参数,因此直到使用Close
按钮才会关闭弹出窗口)。
提示我发现不带参数调用
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
旨在呈现简单、自包含的内容。
就像Tooltip
控件的其他一些特性一样,如果你需要这些事件,我建议你停下来考虑一下你的应用设计。这可能是因为你有非常特殊的需求,但更有可能的是你要强迫Tooltip
控件做一些打破 Windows 应用交互规则的事情。考虑一下我在第十二章的中描述的Flyout
控制是否是一个更合适的选择。
总结
在这一章中,我已经向你介绍了三个最简单的 WinJS UI 控件——尽管,正如你所看到的,要在你的应用中获得正确的效果,还有很多细节需要考虑。在下一章,我将把注意力转向时间和日期选择器以及Flyout
和Menu
控件。
十二、使用TimePicker
和DatePicker
和Flyout
在这一章中,我继续探索 WinJS UI 控件,重点是TimePicker
、DatePicker
、Flyout
和Menu
控件。TimePicker
和DatePicker
控件允许用户指定时间和日期,顾名思义。这些是带有一些设计问题的基本控件,这些设计问题使它们比它们本应具有的功能更难使用(也更没用)。你已经在第七章的中看到了Flyout
控件,在那里我将它与 AppBar 一起使用。作为一个通用控件,?? 有着更广泛的存在,我解释了在这种情况下使用它所需要知道的一切。表 12-1 提供了本章的总结。
使用时间选择器控件
WinJS.UI.TimePicker
控件允许用户选择时间。HTML5 在input
元素中增加了支持时间和日期输入的功能,但是在 Internet Explorer 10 中不支持,你必须使用TimePicker
和DatePicker
控件来代替(我将在本章后面描述DatePicker
)。你可以在图 12-1 的中看到TimePicker
控件是如何显示给用户的。
图 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 中总结了这些属性,并且我已经为这些属性创建了配置控件。我将在接下来的小节中详细解释这些属性。
设置时钟类型
您可以使用clock
属性选择将要显示的时间类型。如果该属性设置为12HourClock
,TimePicker
显示三个选择元素,允许用户选择小时、分钟和周期(AM
或PM
)。如果clock
属性设置为24HourClock
,那么只显示两个select
元素,但是小时菜单中有 24 个项目,允许指定 24 小时时间。您可以使用标记为24 Hour Clock
的控件更改示例中的clock
属性,12 小时和 24 小时时钟显示如图图 12-2 所示。
图 12-2。设置时间选择器控制的时钟类型
设置分钟增量
minuteIncrement
属性指定可以为分钟值选择的最小间隔。例如,将minuteIncrement
属性设置为10
将允许用户将分钟设置为0
、10
、20
、30
、40
和50
分钟。值15
将允许0
、15
、30
和45
分钟。问题是在控件初始化后更改属性没有任何效果——您必须决定您想要的时间间隔,并使用data-win-options
属性指定它有一个配置选项。
在这个例子中,我使用data-win-options
属性将minuteIncrement
属性设置为10
,如下所示:
`...
你可以在图 12-3 的中看到这种效果,我点击了TimePicker
控件的分钟部分来显示可用的增量。
图 12-3。在时间选择器控件上限制分钟增量
指定显示模式
通过hourPattern
、minutePattern
和periodPattern
属性,您可以指定时间的各个组成部分的显示方式。实际上,他们只是让你指定使用多少字符,即使这样,你也必须努力工作。
名称空间包含对象,?? 控件用它来格式化和解析时间值。DateTimeFormatter
支持一个全面的基于模板的系统来处理时间和日期。这里有一个例子:
{hour.integer}:{minute.integer(2)}:{second.integer(2)} {period.abbreviated}
该模板包含小时、分钟和秒的组件。分和秒用两个字符显示,周期用缩写形式显示(例如AM
或PM
)。
TimePicker
控件中的模式属性作用于该模板的片段。您不能更改元素出现的顺序,但可以更改每个元素使用的字符数。因此,举例来说,如果您想确保小时总是使用 12 小时制显示,那么您可以将minutePattern
属性设置为{hour.integer(2)}
。括号字符({
和}
)是值的必需部分,这意味着在使用data-win-options
属性设置这些属性的值时必须小心——很容易混淆括号和引号字符的顺序。
考虑到您可以对这些属性进行的唯一更改是设置字符数,我认为让TimePicker
控件负责将整数值转换成模板片段会更明智。实际上,您需要对日期/时间格式的底层工作有足够的了解,但这样做不会有任何好处。但是,正如我之前所说的,TimePicker
控件并没有经过特别的考虑或实现。
我在示例的右面板中包含了两个input
元素,让您可以更改小时和分钟元素使用的字符数。我通过代理对象更新了hourPattern
和minutePattern
属性,如清单 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 Length
和Min Length
input
元素中看到输入3
的效果。
图 12-4。指定用于显示小时和分钟时间成分的字符数
以编程方式管理时间选择器
TimePicker
控件没有定义任何方法。我包含这一部分只是为了保持与其他控件的一致性,这样您就不会认为它被错误地忽略了。
设置时间选择器控件的样式
TimePicker
控件支持许多 CSS 类,这些类可以用来设计整个控件或其中一个元素的样式。我已经在表 12-3 中描述了类的集合。
在这个例子中,我使用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 Period
的ToggeSwitch
的状态将改变周期select
元素的可见性。
响应 TimePicker 事件
当用户改变控件显示的时间时,TimePicker
发出change
事件。你可以看到我对TimePicker.html
文件中的script
元素所做的添加,以处理清单 12-6 中的change
事件,在那里我显示了用户选择的时间,用TimePicker
作为应用布局右侧面板中span
元素的内容。
清单 12-6 。从 TimePicker 控件处理变更事件
`...
...`
TimePicker.current
属性返回一个标准的 JavaScript Date
对象,它允许我调用toLocaleTimeString
方法来获得一个值,我可以安全地在布局中显示该值,如图图 12-5 所示,在这里我使用了选取器来选择晚上 9:50。
图 12-5。响应时间选择器控件的变更事件
使用日期选择器控件
DatePicker
控件是对TimePicker
的补充,允许用户选择日期。DatePicker
与TimePicker
控件有很多相似之处——可悲的是,包括一些不太有用的特征,比如模板片段。
DatePicker
控件在结构和外观上与TimePicker
控件非常相似,并为用户提供了三个select
元素,用户可以用它们来选择日期。在图 12-6 中,你可以看到DatePicker
是如何显示的,以及我生成的用来演示不同日期相关特性的配置控件。
图 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 中列出和描述的配置属性。
使用不同的日历
属性允许你指定一个日历给 ?? 使用。默认值来自设备区域设置。在本例中,我在右侧面板中添加了一个select
元素,允许您选择GregorianCalendar
、HebrewCalendar
和ThaiCalendar
值,其效果可以在图 12-7 的中看到。
图 12-7。使用不同的日历显示日期
我添加了对这些日历类型的支持,以展示可用的横截面。该属性支持的全套值为:GregorianCalendar
、HijriCalendar
、HebrewCalendar
、JapaneseCalendar
、KoreanCalendar
、ThaiCalendar
、TaiwanCalendar
、UmAlQuraCalendar
和JulianCalendar
。
指定显示模式
DatePicker
控件使用模板片段的方式类似于TimePicker
控件,通过datePattern
、monthPattern
和yearPattern
属性公开。
datePattern
的格式是{day.integer(
n )}
,其中n
是用于显示日期的字符数。注意,虽然属性的名称是***date***Pattern
,但是片段是 **day**
.integer
(即date
对day
)。对于月份和年份组件,您可以选择完整值和缩写值。表 12-5 显示了这两个属性的一组支持值。
我添加了三个配置控件来管理日期的显示方式。通过Date Length
input
元素,您可以更改用于显示日部分的字符数,通过Month Style
和Year Style
select
元素,您可以看到完整和简化显示的样子。在图 12-8 中,您可以看到缩写的月份和年份设置是如何显示的。
图 12-8。显示月份和年份的缩写值
以编程方式管理日期选择器
DatePicker
控件没有定义任何方法。我包含这一部分只是为了保持与其他控件的一致性,这样您就不会认为它被错误地忽略了。
设置 DatePicker 控件的样式
DatePicker
控件支持许多 CSS 类,这些类可以用来设计整个控件或其中一个元素的样式。我已经在表 12-6 中描述了类的集合。
我没有在例子中使用任何样式,但是我发现它们对于隐藏控件中的单个组件很有用,这样用户可以指定一个不太精确的日期。因此,举例来说,如果我想让用户只选择月份和年份,我会在DatePicker.html
文件中添加一个style
元素,如清单 12-10 中的所示。
清单 12-10 。使用 CSS 类隐藏 DatePicker 控件的组件
`...
...`
隐藏其中一个组件会导致控件被调整大小,如图 12-9 所示。
图 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
控件。
图 12-10。使用弹出控件
何时使用弹出控件
Flyout
是一个通用控件,可以在任何想要在主布局之外呈现内容的情况下使用。Flyout
有一些特殊的用途,比如使用AppBar
(参见第七章)或者使用Menu
控件(在本章稍后描述),但是除此之外,你可以在你的应用环境中做任何有意义的事情。对于简单的纯信息内容,考虑使用WinJS.UI.Tooltip
控件,我在第十一章中描述了它,它需要更少的编程控制。
提示请务必阅读本章后面的使用弹出按钮进行用户交互一节,了解如何使用弹出按钮显示需要用户交互的元素,如
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
定义了show
和hide
方法,可以用来控制控件的可见性。表 12-7 总结了这些方法以供快速参考。
show
方法有一个强制参数,用它指定一个锚元素,它是Flyout
所在的附近。show
方法支持两个可选参数,允许您覆盖placement
和alignment
属性的值——我将在下一节解释这些属性的用途和值。
Flyout
控件支持我在第十二章中提到的 light-dissolve 技术,当用户点击或触摸Flyout
弹出窗口之外的显示屏时,它会自动消失。您可以通过调用hide
方法显式地解除Flyout
,但是这只在您响应Flyout
中的交互控件时有用,我将很快演示这一点。
示例中有两个Flyout
控件,您可以通过单击右侧面板中的Show
和Hide
按钮来查看如何应用show
和hide
方法。点击Show
按钮会出现一个显示一些静态文本的Flyout
(这是图 12-10 中的Flyout
)。当点击Show
按钮时,我使用标记中的img
元素作为锚元素,这就是为什么Flyout
位于图中图像的正上方。您可以通过点击Hide
按钮或点击Flyout
之外的应用布局中的任意位置来关闭Flyout
。
提示点击
Rate
按钮,显示示例中的另一个Flyout
。在本节稍后讨论使用Flyout
控件向用户呈现交互式内容时,我会回到这个控件。
配置弹出控件
正如您现在所期望的,有许多由Flyout
控件定义的属性,您可以使用它们来改变它的行为。表 12-8 总结了这些特性。
设置anchor
属性没有用,因为该值将被show
方法所需的强制参数替换。但是,您可以读取该属性的值来查看Flyout
锚定到了哪个元素。
您可以使用placement
属性覆盖Flyout
的默认位置。这与Tooltip
控件的工作方式相同,支持的值为top
、bottom
、left
和right
。这些值相对于传递给show
方法的锚元素。
如果placement
属性是top
或bottom
,您可以使用alignment
属性进一步细化位置,该属性接受值left
、right
和center
。
我在示例中添加到右侧面板的select
元素允许您更改由Show
和Hide
按钮控制的Flyout
的placement
和alignment
属性。选择您需要的数值组合,点击Show
按钮,查看Flyout
是如何定位的。Flyout
将自动重新定位,使其完全适合屏幕——然而,这可能意味着锚定元素可能会被Flyout
遮挡。
在图 12-11 的中可以看到alignment
的left
和right
值、placement
的top
值。
图 12-11。放置属性左右值的效果
设计弹出控件的样式
Flyout
控件支持一个 CSS 类:win-flyout
。您可以使用该类作为设计显示内容样式的起点。没有其他类,因为在Flyout
控件中没有固定的结构。我倾向于不使用这个类,更喜欢将样式直接应用于我在Flyout
控件中显示的内容。在清单 12-15 中,你可以看到我添加到使用win-flyout
类的Flyout.html
文件中的style
元素。
清单 12-15 。设计弹出控件的样式
`...
...`
这种样式的效果是将所有Flyout
控件中的文本居中。你可以在图 12-12 中看到它的效果。
图 12-12。对弹出控件应用样式的效果
处理弹出事件
Flyout
控件支持我在表 12-9 中描述的四个事件。我在示例中没有使用这些事件,但是在处理包含交互内容的Flyout
控件时,beforehide
和afterhide
事件会很有用——更多信息请见下一节。
使用弹出按钮进行用户交互
示例应用中的第二个Flyout
显示需要用户交互的控件——你可以在图 12-13 中看到这些控件是如何显示的。您可以使用一个Flyout
来显示任何内容,但是当您从用户那里收集数据时,有一些特殊的考虑。
图 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 对本章进行了总结。
使用菜单控制
Menu
控件提供弹出上下文菜单,该菜单被构造为向用户提供一个或多个由MenuCommand
控件表示的命令。MenuCommand
和Menu
控件之间的关系类似于AppBarCommand
和AppBar
之间的关系,我在第七章的中描述过。
默认情况下不显示Menu
控件,所以本例中左侧面板中的主要元素是一个图像。如果用鼠标右键单击或触摸并按住图像,将会触发contextmenu
事件。我通过使Menu
控件出现来处理这个事件,如图图 13-1 所示。
图 13-1。显示菜单控件
何时使用菜单控制
为用户提供命令的主要机制是使用 AppBar,我在第七章中描述过。Menu
控件提供了一种回退机制,当用户想要操作的对象不适合 AppBar 模型时,可以使用这种机制。坦率地说,这是一个非常主观的决定,我在项目中遵循的规则是尽可能地支持应用栏,因为并非所有用户都意识到 Windows 应用中可以使用上下文菜单。
命令应该在你的应用中只出现一次,这意味着你不应该在一个Menu
上重复应用栏支持的命令。微软建议在一个Menu
上最多使用 5 个命令,尽管 Windows 目前没有强制限制。
演示菜单控制
我在示例应用中添加了一个pages/Menu.html
文件来演示Menu
和MenuCommand
控件,你可以在清单 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
元素,并与AppBar
和Flyout
控件共享一些共同的特性和功能。使用MenuCommand
控件在Menu
中定义命令,该控件应用于button
和hr
元素,就像我在第七章的中描述的AppBarCommand
控件一样。Menu
控件支持与Flyout
控件相同的配置属性集。表 13-2 总结了这些特性。
一个Menu
的默认位置就在锚元素的正上方(如果有足够的屏幕空间)。您可以使用示例右侧面板中的select
元素来更改菜单的位置。在图 13-2 中,您可以看到placement
属性的left
和right
值的效果。
图 13-2。菜单放置属性的左右值的效果
注意如果你习惯于 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
控件中创建项目。
提示你可以在
Menu
中创建一个分隔符,将相关命令组合在一起。为此,将MenuCommand
控件应用于hr
元素,并使用data-win-options
属性将type
属性设置为separator
。你可以在清单 13-1 中看到一个分隔符的例子。
创建菜单序列
您可以配置一个MenuCommand
控件,当它被选中时显示另一个菜单,创建一个菜单链,允许用户在一组复杂的选项中导航。使用type
和flyout
属性将菜单关联在一起。你可以在清单 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 Color
的MenuCommand
,我将type
属性设置为flyout
,将flyout
属性设置为另一个应用了Menu
控件的元素的id
。你可以在清单 13-6 中看到第二个Menu
的定义。
清单 13-6 。应用第二个菜单控件
`...
结果是当您从第一个Menu
中选择Border Color
项时,显示第二个Menu
。你可以在图 13-3 中看到效果。第二个Menu
代替了第一个,而不是出现在它旁边。
图 13-3。将菜单链接在一起
提示如果你愿意,你可以用一个
Flyout
控件代替第二个Menu
。这对于向用户提供太复杂而不能用一组菜单项来处理的选项是有用的。也就是说,如果你需要一个Flyout
,那么你可能要重新考虑你的策略,看看是否有一种更简单的方式向用户展示你的命令。
创建互斥的菜单项集合
我最常用MenuCommand
来创建互斥的菜单项。我想在示例应用中创建两个这样的集合:第一个是主菜单中的Red
、White
和Green
项目,第二个集合由较小的二级菜单中的Red
、Black Border
和White
Border
项目组成。
第一组对应于用于img
元素的背景颜色,第二组对应于边框颜色。在这两种情况下,我都希望选择代表当前设置的MenuCommand
,并在单击其他项目时保持该选择的最新状态。互斥菜单项易于设置,但这是一个手动过程,并且在Menu
或MenuCommand
控件中没有特定的支持。
技术很简单——我需要将MenuCommand
控件上的selected
属性设置为用户选择的项目的true
,并将同一互斥集合中所有其他MenuCommand
的属性设置为false
——最有效的方法是使用WinJS.Utilities.query
方法定位给定集合中的所有元素。为了帮助我做到这一点,我已经确保每组中的MenuCommand
控件都有一个我可以轻松识别的共同特征。对于主菜单中的Red
、White
和Green
项,我将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 中看到在主菜单上选择一个项目然后选择另一个项目的效果。
图 13-4。创建互斥的菜单命令控件组
以编程方式管理菜单
Menu
控件支持的方法类似于AppBar
控件定义的方法,如表 13-4 所示,我在这里已经描述了这些方法。
我倾向于不使用显示和隐藏菜单命令的方法,因为它们让我觉得我的Menu
结构太复杂了。我也更喜欢在Menu
上保留相同的命令集,并简单地禁用那些目前不可用的命令。这就是我在示例中所做的。在选中边框命令之前,边框颜色命令是未选中的,如图图 13-5 所示。我宁愿清楚地表明命令确实存在,但并不适用,也不愿给出一组不断变化的命令。
图 13-5。禁用菜单命令而不是隐藏它
设计菜单控件的样式
Menu
控件支持两个 CSS 类进行样式化,如表 13-5 中所述。
要将样式应用于Menu
和MenuCommand
控件,您需要看看微软用来创建控件的 HTML 和 CSS,并设计一个选择器来覆盖默认情况下添加到 Visual Studio 应用项目中的ui-light.css
和ui-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 中看到我定义的样式的效果。
图 13-6。改变被禁用的菜单命令控件的颜色
处理菜单事件
在处理菜单控件时,感兴趣的主要事件是click
事件,当用户点击它时,该事件由MenuCommand
控件触发,表示菜单项已被选择——您可以在清单 13-1 中看到我是如何处理该事件的,在清单 13-7 中也是如此。
Menu
控件本身支持我在表 13-6 中描述的四个事件,这也是Flyout
控件支持的四个事件。我还没有找到这些事件的令人信服的用途,这就是为什么我没有把它们包括在Menu
控件的例子中。
使用 MessageDialog 控件
我在本书中描述的所有其他 UI 控件都在WinJS.UI
名称空间中,并且是用 JavaScript 编写的。然而,有一个控件在这个名称空间之外,但它在创建遵循 Windows 外观的应用时非常有用。这个控件是Windows.UI.Popups.MessageDialog
控件,你用它向用户显示一个对话框。你可以在图 13-7 中看到MessageDialog
的例子。
图 13-7。消息对话框 UI 控件
注意
Windows.UI.Popups
命名空间也包含了PopupMenu
控件。我没有在本书中描述这个控件,因为它与我在上一节中描述的WinJS.UI.Menu
控件具有相同的功能。
何时使用 MessageDialog 控件
当你需要让用户注意到一些重要的事情或者当你需要做出一个重要的决定时,MessageDialog
是很有用的。关键词是重要,因为当使用MessageDialog
控件时,应用布局变暗,对话框显示在整个屏幕上。所有用户交互都被阻止,直到用户关闭对话框(通过按下Escape
或Enter
键)或点击/触摸其中一个对话框按钮。
我认为MessageDialog
是最后的 UI 控件,因为它打断了用户的工作流程——这与 Windows 广泛的设计精神背道而驰。
您可以在由MessageDialog
控件显示的对话框窗口中添加多达三个按钮,这意味着您无法为用户提供细微的选择。这意味着你不仅在打断用户,而且在强迫他们做出一个清晰明确的决定,通常是一个yes
/ no
的选择。
你应该谨慎而不情愿地使用MessageDialog
。在我自己的应用项目中,我只在用户将要启动一个会导致永久、不可恢复的数据丢失的操作时使用MessageDialog
,比如删除文件或其他数据。我建议你使用同样的约束——不仅因为MessageDialog
是侵入性的,而且因为如果你太自由地使用它,用户将学会不读它就关闭对话框——增加了他们忽略真正重要的信息的机会。
提示你可以使用一个
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
控件,以探索我在接下来的章节中描述的特性。
图 13-8。MessageDialog.html 文件的布局
使用 MessageDialog 控件
MessageDialog
控件的工作方式与 WinJS UI 控件不同。该控件没有应用于 HTML 元素,也没有winControl
属性。相反,您创建一个Windows.UI.Popups.MessageDialog
对象并使用它定义的属性来配置对话框并显示给用户。MessageDialog
控件支持表 13-7 中显示的属性和方法,我将在下面的章节中描述和演示。
创建基本对话框
显示对话框最简单的方法是创建一个新的MessageDialog
对象并调用showAsync
方法。构造函数MessageDialog
有一个强制参数,它是一个字符串,包含应该在对话框中显示的内容。有一个可选参数,用作title
属性的值。
您可以使用示例应用看到最基本的对话框。将右侧面板中的所有ToggleSwitch
控件设置为No
并点击Show MessageDialog
按钮。你可以在图 13-9 中看到结果。(我编辑了这一部分的图片,以便更容易看到细节)。
图 13-9。用消息对话框控件创建的基本对话框
当您创建一个基本对话框时,MessageDialog
控件会为您添加一个Close
按钮,当它被点击时会自动关闭对话框。你可以在图 13-10 中看到给对话框添加一个标题的效果——这个效果是我在例子中通过将Title ToggleSwitch
设置为Yes
实现的。
图 13-10。一个带有标题的基本对话框
添加自定义命令
您可以通过放置最多带有三个自定义按钮的Close
按钮来自定义对话框。这些按钮使用Windows.UI.Popups.UICommand
对象指定,该对象定义了表 13-8 中所示的属性。
在示例应用中,您可以通过将标记为Add Commands
的ToggleSwitch
设置为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
对象,但是它返回的对象定义了then
、cancel
和done
方法,通常可以像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
的对象直到用户关闭对话框后才会实现。
设置默认命令
您可以使用defaultCommandIndex
和cancelCommandIndex
来指定当用户点击Enter
或Escape
键时将被传递给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
按钮。
注意确保你始终如一地使用默认命令功能,这样
Escape
键总是取消,Enter
键总是确认你呈现给用户的动作或决定。
延迟响应用户交互
MessageDialog.options
属性允许你使用来自Windows.UI.Popup.MessageDialogOptions
对象的值配置控件的行为,在表 13-9 中有描述。出于某种原因,MessageDialogOptions
对象只定义了一个特定行为的值,即当对话框被MessageDialog
对象显示时,在一小段时间内禁止用户交互。
您可以通过将标签为Ignore Input
的ToggleSwitch
设置为true
来应用示例应用中的acceptUserInputAfterDelay
行为。当你点击Show MessageDialog
时,对话框上显示的命令按钮将被暂时禁用,防止命令被点击。几秒钟后,按钮被激活,允许用户像往常一样与对话框交互。
总结
在下一章,我将返回到WinJS.UI
名称空间并描述FlipView
控件。这是第一个也是最简单的 WinJS 数据驱动控件,它提供了由数据对象集合支持的功能。正如你将了解到的,数据驱动控件建立在我在第八章中介绍的数据绑定和模板特性的基础上,以便灵活地向用户显示那些数据项。
十四、使用FlipView
控件
在这一章中,我将描述FlipView
控件,它是 WinJS 数据驱动的 UI 控件之一。数据驱动控件从数据源获取内容,并监控数据源以确保它们显示最新的可用数据。FlipView
控件一次显示数据源中的一个项目,并允许用户通过单击按钮或做出翻转触摸手势在它们之间移动。
在本章中,我将向您展示如何为数据驱动控件准备数据源,并将其与用于向用户呈现每一项的模板相结合。您将看到 WinJS 的许多特性是如何在一个数据驱动控件中结合在一起的,包括可观察数组、数据绑定和模板。我还将向您展示这些特性是如何相互作用导致棘手的问题的。最后,我将介绍一下FlipView
控件在从一个数据项移动到下一个数据项时使用的动画,作为在第十八章中全面讨论这个主题的前奏。表 14-1 对本章进行了总结
使用 FlipView 控件
数据驱动的 WinJS UI 控件使用模板来呈现数据源中的项。在这一章中,我将使用一个FlipView
来执行一个非常普通的任务——一次显示一组照片中的一张图片。你可以在图 14-1 中看到FlipView
是如何出现的。如同本书这一部分的其他章节一样,我使用我在第十章中创建的框架创建了如图所示的布局。
图 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 文件添加的link
和script
元素。这个/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`获得)。
图 14-2。将数据图像添加到 Visual Studio 项目中
如果此时运行应用,您将会看到如图图 14-3 所示的布局。FlipView
控件在布局中,但它还不可见——这是因为我还没有设置我想要显示的图像和FlipView
控件之间的关系——我将很快解决这个问题。
图 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
属性。
提示
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
控件时经常遇到的问题,我倾向于把它看作是的第一个图像问题。
图 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,如下所示:
`...
您可以从代码片段中看到,数据绑定已经过处理,因此来自第一个数据源项的值反映在 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
属性被再次处理。
图 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
对象时,您可以使用表中的属性查询它,以生成向用户显示它所需的元素。
提示使用函数生成模板时,必须返回单个
HTMLElement
对象,尽管这个顶级元素可以包含任意多个子元素。这与声明性 HTML 模板的约束相同。
为了帮助演示不同的模板方法,我改变了 JavaScript 文件中设置itemTemplate
属性的方式,将它链接到应用布局右侧面板中的select
元素。当您从select
元素中选取HTML
值时,将使用前一节中的声明性模板,当您选取Function
值时,将应用清单中的renderItem
函数。renderItem
功能为数据项产生一种不同的布局样式,以便更容易看出正在使用哪一个,如图 14-6 所示。
图 14-6。为 FlipView 控件生成元素的不同技术
这两种方法都解决了第一个图像问题,您可以选择适合您的编程风格的方法。在大多数情况下,我倾向于使用声明性模板,但是使用函数生成元素会更加灵活,尤其是当您希望根据单个数据项来定制元素的内容时。
配置 FlipView 控件
既然我已经解决了第一个图像问题和解决方案,我可以转向FlipView
控件支持的其他配置属性,我已经在表 14-3 中总结了这些属性。
itemTemplate
和itemDataSource
是最重要的属性,在解释如何解决第一个图像问题时,我已经解释了如何使用它们。在接下来的部分中,我将使用我在应用布局的右侧面板中创建的一些配置控件来演示orientation
和itemSpacing
属性。在本章的后面,当我谈到以编程方式操作FlipView
控件时,我将演示currentPage
属性。
设置方向
属性允许你改变用户在数据源中前后移动的方向。默认值为horizontal
,表示用户在触摸屏上左右滑动或者用鼠标点击控件左右边缘的按钮。另一个支持值是vertical
,它要求用户上下滑动,并为鼠标用户改变按钮的位置,使它们出现在控件的顶部和底部。我在右侧面板中添加了一个标记为Orientation
的select
元素,它允许您更改左侧面板中FlipView
控件的orientation
属性的值。在图 14-7 中,您可以看到为鼠标用户显示的horizontal
和vertical
值的不同按钮位置。我在图中突出显示了按钮,因为它们的默认样式很难看到(我将在本章的后面向您展示如何更改按钮样式)。
图 14-7。水平和垂直方向值按钮的不同位置
设置项目间距
itemSpacing
属性设置当用户使用触摸从一个项目翻转到另一个项目时项目之间显示的间隙。在图 14-8 的中,你可以看到这种差距的两个例子,默认值为 10 像素,较大值为 100 像素。
图 14-8。设置触摸交互项目之间显示的间距
我在这个属性的例子中包含了一个配置控件——一个标记为Item Spacing
的input
元素。itemSpacing
属性的影响可能看起来很小,但是设置项目间距可以对FlipView
控件的外观和感觉产生显著的影响。特别是,我发现当数据源内容只是松散相关时,使用更大的空间会产生更自然的感觉。当然,这纯粹是主观感觉,你应该做对你自己的应用和偏好有意义的事情。
以编程方式管理动画视图
在很大程度上,FlipView
控件为用户提供了与正在显示的内容进行交互所需的一切。当鼠标在FlipView
上移动时,鼠标用户会看到弹出按钮,触摸用户可以通过滑动动作从一个项目移动到下一个项目。即便如此,有些时候你需要更直接地控制控件如何操作,对于这些情况,FlipView
定义了我在表 14-4 中描述的方法。在接下来的小节中,我将向您展示如何使用这些方法。
移动浏览项目
您可以通过调用next
和previous
方法以编程方式在数据源中的项目间移动。我在示例的右边面板添加了按钮来演示这个特性,你可以从清单 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
元素的值。
提示如果您想直接导航到特定的元素,可以设置
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"}]
}
});
})();`
当应用启动时,这些数据项不是数据源的一部分,但是当您单击示例右侧面板上的Add
和Remove
按钮时,我会将数据项从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 中描述的三个事件。
只有当数据源中的项目数量改变时,而不是当一个项目被另一个项目替换时,才会触发datasourcecountchanged
事件。这意味着当你使用一个WinJS.Binding.List
对象作为你的数据源时,使用setAt
方法将导致FlipView
控件显示你指定的项目,但它不会触发datasourcecountchanged
事件。
当显示新页面时,触发pageselected
事件。当用户单击导航按钮时,或者当调用next
和previous
方法时,或者当使用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 中所总结的。
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
,这将不会产生任何效果。
图 14-9。让导航按钮在灯光图像上更容易看到
使用自定义动画
如果您查看布局右侧面板中的控件,您会注意到我添加了一个ToggleSwitch
来启用自定义动画。许多 WinJS UI 控件以某种形式使用动画,或者显示从一种状态到另一种状态的转换,或者指示内容以某种方式发生了变化。通常,动画是如此的简短,以至于他们几乎看不见,但是他们仍然足够吸引眼球,向用户传递重要的信号。
我在第十八章中深入解释了动画系统,但是FlipView
控件为改变它所使用的动画提供了支持,我想在本章中解释如何做到这一点。这意味着我将解释如何使用一个我还没有正确介绍的功能,所以你可能想去阅读第十八章,然后再回到这一节。
在本节中,你需要知道的关于动画系统的所有事情就是动画是由WinJS.UI.Animation
名称空间中的函数来表示的,并且两个这样的函数是fadeIn
和fadeOut
。这两个函数都以一个元素作为参数,它们的名字告诉你它们在这个元素上执行什么样的动画。
应用自定义动画
当用户导航到不同的项目时,FlipView
控件使用动画。要改变在一种或多种情况下使用的动画,您可以向setCustomAnimations
方法传递一个对象,该方法具有名为next
、previous
和jump
的属性。当用户移动到下一个或前一个数据项时,使用下一个和前一个属性的动画,当用户导航到更远的地方时,使用 jump 属性。
这些属性的值必须是返回一个WinJS.Promise
对象的函数,该对象在动画结束时实现。向您定义的函数传递参数,这些参数表示将要删除的元素,以及将替换它作为由FlipView
显示的选定项的元素。
在清单 14-16 中,你可以看到我添加到flipview.js
文件中的代码,用于为next
属性设置自定义动画,当右侧面板中标有Custom Animations
的ToggleSwitch
控件设置为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
控件使用默认的动画用于previous
和jump
场景。
我想执行两个动画——我想在输出元素上执行fadeOut
动画,在输入元素上执行fadeIn
动画。我需要返回一个只有当这两个动画都完成时才完成的Promise
对象,这就是为什么我使用了Promise.join
方法,我在第九章中描述了这个方法。
当ToggleSwitch
被设置为No
时,我通过向setCustomAnimations
方法传递另一个对象来返回默认动画,该方法对于我想要重置的属性具有空值。
您可以通过启动应用并使用控制导航按钮前进到下一个数据项来测试此功能。一个非常简短的动画将创建从右边出现的新项目的效果。当您将ToggleSwitch
设置为Yes
并再次使用导航按钮时,现有项目将淡出,并被下一个数据项所取代。
注意仔细考虑你选择的动画。虽然动画很快,但与其他 Windows 应用不一致的糟糕选择可能意味着与用户正在执行的交互不同的交互类型。尽可能坚持使用默认的动画,如果不可能的话,试着将用户执行的动作和你选择的动画联系起来。这是我在第十八章中更详细解释 WinJS 动画特性时回到的话题。
总结
在这一章中,我描述了 WinJS 数据控件中的第一个,FlipView
。我向您展示了如何使用WinJS.Binding.List
对象来创建数据源,以及如何将模板与数据相关联来配置向用户显示项目的方式。我还向您展示了第一图像问题,解释了它是如何产生的,并提供了解决该问题的不同方法。最后,我向你展示了如何用FlipView
控件来使用自定义动画——一旦你阅读了深入研究 WinJS 动画系统的第十八章,这些信息将会产生更多的共鸣。在下一章,我将向您展示ListView
控件,它是对FlipView
控件更大更复杂的补充。
十五、使用ListView
控件
在这一章中,我描述了ListView
控件,这是另一个 WinJS 数据驱动的 UI 控件。与上一章的 FlipView 控件有一个共同的基础,但是ListView
显示多个项目,并在如何实现这一点上提供了一些灵活性。在这一章中,我解释了可以使用的不同种类的模板和布局,描述了被调用项和被选择项之间的区别,以及如何处理ListView
控件发出的各种事件。我还将向您展示如何使用描述数据源的抽象,这样您就可以编写适用于任何数据源的代码,而不仅仅是那些使用WinJS.Binding.List
对象创建的数据源。表 15-1 对本章进行了总结。
何时使用 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
控件时,使用itemDataSource
和itemTemplate
属性设置数据源和用于显示数据项的模板,就像使用FlipView
控件一样。
ListView
控件没有我在第十四章的中为FlipView
描述的第一个图像问题,所以你可以使用data-win-options
属性安全地声明设置itemDataSource
和itemTemplate
属性(尽管你也可以编程地设置这些值,并且如果你喜欢的话,使用一个函数来生成你的模板元素——细节和例子参见第十四章)。在我的清单中,我使用添加到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
特性。
图 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
控件的大小适合显示多列。
图 15-2。使用列表布局显示元素
这是一个更加传统的垂直列表。我倾向于不像这样单独使用列表布局,但是它在创建语义缩放布局时非常有用,我在第十六章中对此进行了描述。
一个常见的错误是将layout
属性设置为字符串形式的对象名称。这是行不通的——您需要使用new
关键字来创建一个新对象,并将其赋值,就像我在清单中所做的那样。如果希望以声明方式设置布局,则必须在数据绑定中使用特殊的符号,如下所示:
... data-win-options="{layout: **{type: WinJS.UI.ListLayout}**}" ...
这个符号告诉ListView
控件创建一个ListLayout
对象的新实例。这是一种笨拙的语法,我倾向于通过在代码中设置布局来避免它。
注意原则上,你可以创建自己的布局对象来实现定制的布局策略,但是很难将
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
的效果。
图 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 1
、Group 2
等等。请注意,系统会向您传递一个完整的数据项,因此您可以使用您选择的数据项的任何特征来生成组描述。
第三个函数用于对组进行排序。您被传递了两个组的密钥,要求您返回一个数值。返回值0
表示这些组具有相同的等级,返回小于零的值表示第一组应该在第二组之前显示,返回大于零的值表示第二组应该在第一组之前显示。因为我的组合键是数字,所以我可以返回一个减去另一个的结果来得到我想要的效果。
应用组数据源
我将在常规数据源和组数据源之间切换,以响应中间面板中标有Groups
的ToggleSwitch
控件。您可以在清单 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;**
** }**
** });**
}
});
})();`
为了显示分组数据,我必须更新两个属性:itemDataSource
和groupDataSource
。itemDataSource
属性用于获取将要显示的项目,而groupDataSource
属性用于获取关于项目分组方式的信息。
第一步是设置itemDataSource
属性,使其指向分组 List
对象的dataSource
属性。第二步是将groupDataSource
设置为分组 List
对象的groups.dataSource
属性。
注意这导致了很多混乱,需要强调的是:对于
itemDataSource
和groupDataSource
属性,必须使用分组的数据源。
*按照与常规数据源相同的语法,itemDataSource
属性被设置为List.dataSource
属性。groupDataSource
属性被设置为List.groups.dataSource
属性——注意属性路径中添加了groups
。设置了这两个属性后,ListView
控件将分组显示数据源项,并显示每组的标题,如图 15-4 中的所示。
图 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
设置是有用的,即使网格本身会更宽并且需要更多的水平滚动。
图 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 文件添加并应用组标题模板
`
**
** **
**
模板没有什么特别的——它遵循了我在《??》第八章中展示的所有约定,并用于ListView
和FlipView
控件的项目模板。你可以在图 15-6 中看到结果,我已经在两个位置显示了从模板生成的组标题。
图六。使用模板显示群组标题
处理 ListView 事件
ListView
控件定义了我在表 15-2 中描述的四个事件。在很大程度上,这些事件与用户交互有关,我将在下面的章节中解释。
处理被调用的项目
当用户通过点击或用鼠标点击选择一个项目时,触发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 中看到结果,该图显示了同一项目的正常状态和调用状态。但是,您不必指出被调用的项,尤其是当它与项的选择相冲突时,我将在下一节中对此进行描述。
图 15-7。为用户标记被调用的项目
设定点击行为
通过改变tapBehavior
属性的值,可以改变ListView
响应用户点击项目的方式。这个属性可以设置为WinJS.UI.TapBehavior
枚举中的一个值,我已经在表 15-3 中描述过了。
这些值定义了调用和选择项目之间的关系。如果您只想让用户调用项目,那么您应该使用的值是invokeOnly
。如果您希望用户能够选择,但不能调用项目,那么您应该使用none
值。其他值允许同时调用和选择一个项目。我将在下一节解释项目选择。
您可以通过从应用布局的右侧面板中的select
元素中选择一个值来更改tapBehavior
属性的值,该元素的标签很明显是tapBehavior
。
处理项目选择
用户可以选择项目并调用它们。这可能会导致一些复杂的交互,因此配置ListView
非常重要,这样您就可以获得您想要的效果。您可以通过使用我在上一节中提到的tapBehaviour
属性和selectionMode
属性来实现这一点,后者是使用来自WinJS.UI.SelectionMode
枚举的值来设置的。我已经在表 15-4 中描述了这个枚举中的值。
提示您可以通过从标签为
selectionMode
的应用布局右侧面板的select
元素中选取一个值来更改selectionMode
属性的值。
通过监听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 中描述过了。
在清单中,我使用了getItems
方法来获取所选项的集合,以便构建一个可以在视图模型中设置的值,从而在示例的右侧面板中显示选择。getItems
方法返回一个由IItem
对象组成的数组,每个对象对应一个选中的项目。正如您在第十四章中回忆的那样,IItem
通过一个Promise
使其内容可用,这就是为什么我必须使用 then 方法来获得每一项的细节(参见第九章以获得Promise
对象的更多细节)。ListView
对选择的项目进行强调,以便用户可以看到整个选择,如图图 15-8 所示。注意,字母D
的项目被选中并且被调用。
图 15-8。通过 ListView 控件添加到所选项目的强调
设计 ListView 控件的样式
ListView
支持许多类,你可以用它们来设计控件外观的不同方面,如表 15-6 中所总结的。
并非所有这些 CSS 类都是ListView
控件独有的——例如,你会注意到win-item
类是与我在第十四章中描述的FlipView
控件共享的。当应用这些样式时,您需要确保将焦点缩小到已经应用了ListView
控件的元素上。在清单 15-17 中,你可以看到我添加到ListView.html
文件中的style
元素来演示这些风格。
清单 15-17 。使用 CSS 样式化 ListView 选择
`...
...`
我已经使用了win-selectioncheckmark
类来改变选中项目上显示的格子的大小和颜色,你可以在图 15-9 中看到它的效果。
图 15-9。使用 CSS 样式化 ListView 控件
以编程方式管理 ListView 控件
您可以使用许多不同的方法和属性来控制ListView
的行为。我已经描述了其中的一些,比如通过selection
属性可用的add
、remove
和set
方法,以及控制数据项如何显示的itemTemplate
和groupHeaderTemplate
属性。表 15-7 显示了从代码中驱动ListView
控件行为时有用的一些附加方法和属性。
我倾向于不使用indexOfElement
和elementFromIndex
方法,因为我更喜欢让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 For
的input
元素。在清单 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
会发生什么。
图 15-10。寻找字母 J
使用数据源
在我的例子中,ListView
控件的数据源是一个WinJS.Binding.List
对象。这是一个方便的安排,因为这意味着我可以通过操作List
的内容来改变ListView
显示的数据。
虽然这很方便,但不太现实。您可能无法通过这样一个有用的辅助渠道修改数据,甚至不知道您正在处理哪种数据源。在这些情况下,您需要依赖由IListDataSource
接口定义的功能。该接口定义了所有数据源都必须实现的方法,并且您可以依赖这些由ListView.itemDataSource
属性返回的对象定义的方法。我已经在表 15-8 中描述了IListDataSource
定义的方法。
我在这个例子的右边面板中定义了两个button
元素,标记为Add Item
和Delete 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 中描述过了。
你倾听事件的方式有点奇怪,最好用一个例子来解释。清单 15-23 显示了我在例子中定义的处理程序。
清单 15-23 。使用带有数据源的通知处理程序
... var handler = { ** countChanged: function (newCount, oldCount) {** ** itemCount.innerText = newCount;** ** }** }; list.winControl.itemDataSource.**createListBinding(handler)**; ...
第一步是创建一个对象,该对象定义与表中的方法相匹配的方法。您可以在清单中看到,我定义了一个名为countChanged
的方法,它有两个参数——这与由IListNotificationHandler
接口定义的countChanged
方法相匹配。
一旦实现了您感兴趣的方法,您就可以将处理程序对象传递给createListBinding
方法。从这一点来看,当数据源发生变化时,将调用处理程序方法。在我的例子中,当数据源中的项数改变时,我的countChanged
方法将被执行。您可以通过点击Add Item
或Delete Item
按钮来实现这一点,并查看示例右侧面板中Count
标签旁边显示的结果。
总结
在这一章中,我已经向你展示了ListView
控件,这是一个丰富而复杂的 UI 控件。布局和模板系统允许您控制项目的布局,我解释了调用和选择项目以及与它们相关的事件之间的区别。在本章的最后,我向您展示了如何使用数据源,而不是直接操作实现对象。这允许您创建适用于任何数据源的代码,而不仅仅是基于WinJS.Binding.List
对象的代码。在下一章,我将向你展示如何使用语义缩放,它结合了不同的 WinJS UI 控件来创建一个关键的窗口交互。*
十六、使用SemanticZoom
在这一章中,我描述了 WinJS UI 控件的最后一个,叫做SemanticZoom
。该控件代表 Windows 中的一个关键用户交互,并允许用户在数据集中的两个细节级别之间缩放。控件本身相对简单,依靠一对ListView
控件来完成所有的艰苦工作(我在第十五章的ListView
控件中描述过)。在这一章中,我将向你展示如何使用SemanticZoom
,并演示一种可以同时显示两个细节层次的替代方法。表 16-1 提供了本章的总结。
何时使用语义缩放控件
只有当数据可以被有意义地分组时,才应该使用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 文件的最初内容
`
**
这个文件包含了标准的双面板布局,我在本书这一部分的大多数例子中使用了这个布局,还有三个模板,我将用它们来演示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
控件。
图 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
控件时使用的技术相同。)
您用常规模板设置了itemTemplate
和groupHeaderTemplate
来显示项目和组标题。这些是我在SemanticZoom.html
文件中定义的模板,我在清单 16-7 中再次列出了它们。
清单 16-7 。放大的 ListView 控件的项目和显示模板
`...
如果您运行该示例并在此时导航到SemanticZoom.html
文件,您将能够看到添加和配置ListView
控件的效果,如图图 16-2 所示。我仍然没有SemanticZoom
控件,但是数据的两个视图中的一个已经就位。
图 16-2。将放大的视图添加到 SemanticZoom.html 文件
创建缩小的 ListView 控件
下一步是添加将显示缩小视图的ListView
控件,向用户显示组列表,而不显示这些组中的任何数据项。你可以在清单 16-8 的中看到我对SemanticZoom.html
文件添加的ListView
控件。
清单 16-8 。为缩小视图添加 ListView 控件
`...
这个ListView
将只显示组,这意味着我必须使用组数据源作为itemDataSource
属性的值。这是一个巧妙的技巧,因为它允许ListView
控件显示缩小的数据,而不需要理解它正在处理的数据的结构——这是SemanticZoom
控件,我将很快添加到示例中,它提供上下文并关联数据的两个视图。
图 16-3。两个 ListView 控件都显示在应用布局中
如果你现在运行这个例子,你将会在应用布局中看到两个ListView
控件,如图 16-3 中的所示。
根据您的设备分辨率,布局中可能没有足够的空间来显示两个ListView
控件,因此其中一个控件可能会溢出屏幕底部。即便如此,这也是一个机会,可以确保在应用SemanticZoom
控件并开始管理控件的可见性之前,它们已经被正确配置。
应用语义缩放控件
通过将data-win-control
属性设置为WinJS.UI.SemanticZoom
,将SemanticZoom
控件应用于div
元素,其中div
元素包含显示数据的两个视图的ListView
控件。
当您设置了ListView
控件并以您想要的方式显示数据时,您可以添加SemanticZoom
控件。你可以在清单 16-9 的中的SemanticZoom.html
文件中看到我是如何做的。
注意
ListView
控件在SemanticZoom
div
元素中声明的顺序很重要——放大视图必须出现在缩小视图之前。
清单 16-9 。应用语义缩放控件
`...
应用SemanticZoom
控件的结果是只有一个ListView
元素,最初,这是放大的视图。如果你将鼠标移到SemanticZoom
控件上,你会看到滚动条上方出现一个小按钮。在图 16-4 中,你可以看到放大的视图,我高亮显示了这个按钮以便更容易看到(直到你知道它在那里,它才那么明显)。
图 16-4。放大视图和缩小按钮。
点击该按钮将使SemanticZoom
控件显示向缩小视图转换的动画,允许用户浏览组级别的数据,如图 16-5 中的所示。
图 16-5。语义缩放控件显示的缩小视图
如果你点击一个组,那么SemanticZoom
控件将动画显示转换回放大的视图,显示你选择的组中的项目。
在语义缩放视图之间导航
我向您展示了缩小按钮,因为在没有提示的情况下很难注意到它,但是有几种不同的方式可以在由SemanticZoom
控件显示的两个视图之间导航。如果你有带滚轮的鼠标,你可以按住Control
键,向上移动鼠标滚轮放大,向下移动缩小。如果你喜欢使用键盘,那么你可以使用Control
和加号键(+
)放大,使用Control
和减号键(-
)缩小。
如果您是触控用户,那么您可以使用捏合/缩放触控手势来放大和缩小。我将在第十七章中详细讨论触摸手势,但是如果你选择了我在图 16-6 中突出显示的按钮,Visual Studio 模拟器将允许你执行捏/缩放手势。
图 16-6。捏手势模拟按钮并显示
当您选择收缩/缩放手势按钮时,光标将变为图中所示的两个空心圆圈,每个圆圈代表一根手指。按住鼠标按钮,模拟用手指触摸屏幕–光标会发生变化,圆圈被填满,如图的最后一帧所示。使用鼠标滚轮将模拟的手指移近或移远,创建收缩/缩放手势。将手指并拢使SemanticZoom
控件缩小,将手指分开使其放大。
配置语义缩放控件
SemanticZoom
控件支持我在表 16-2 中描述的配置属性。与其他 WinJS UI 控件相比,这是一个很小的属性集,因为SemanticZoom
中的复杂性被委托给它所依赖的ListView
控件。
我已经在示例的右面板中定义了配置控件来演示所有四个SemanticZoom
属性。locked
和zoomedOut
属性是不言而喻的,但是我将在接下来的章节中解释另外两个属性。
启用和禁用缩小按钮
属性控制我在图 16-4 中展示的缩小按钮的可见性。我在示例的右面板中加入了一个ToggleSwitch
,它改变了SemanticZoom
控件上的enableButton
属性。
该属性的默认值是true
,这意味着SemanticZoom
控件将显示按钮。将属性设置为false
会阻止按钮显示,但是用户仍然可以使用我在上一节中描述的其他技术在视图之间导航。如果您想防止用户在视图之间切换,那么将locked
属性设置为true
。
设置缩放系数
正如您已经注意到的,SemanticZoom
控件使用动画在视图之间切换,而zoomFactor
属性决定动画的戏剧性。该值可以在0.2
和0.85
之间,默认为0.6
。我无法在打印页面上演示动画,但是较小的值会产生更生动的缩放效果。我更喜欢一个更微妙的动画,这意味着如果我改变默认值,它通常是一个值0.8
,它创建了一个效果,清楚地表明一个过渡,而不是太引人注目。
处理 SemanticZoom 控件事件
SemanticZoom
控件定义了一个事件,当控件在缩放级别之间切换时会发出该事件。我在表 16-3 中描述了这一事件。
您可以在清单 16-10 中看到我是如何处理这个事件的。当事件被触发时,我更新了视图模型中的一个属性,这使得标记为Zoomed Out
的ToggleSwitch
配置控件与SemanticZoom
的状态保持一致。
清单 16-10 。处理 SemanticZoom 事件
`...
...`传递给 handler 函数的Event
对象的detail
属性在SemanticZoom
为zoomedOut
时设置为true
,在放大时设置为false
。
提示你通过直接处理控件的事件来响应用户与放大的
ListView
的交互。关于项目被调用或选择时ListView
用来发送通知的事件的详细信息,请参见第十五章。
设置 SemanticZoom 控件的样式
一个SemanticZoom
的大部分样式是通过底层ListView
控件使用的模板完成的(我在第十五章中描述过)。然而,有两个类可以用来直接设计这个控件的样式,如表 16-4 所述。
我不会在我的项目中使用这些风格。如果我想应用常规样式,那么我的目标是包含SemanticZoom
控件的父元素(在我的例子中,这是具有semanticZoomContainer
的id
的div
元素,我通常在我的所有内容中有一个等价的容器元素)。如果我想设计一个更具体的布局部分,那么我会瞄准ListView
控件,或者通常依靠项目和组标题模板来获得我想要的效果。
语义缩放控件的替代
在我看来,SemanticZoom
控件呈现的交互有一个缺陷,那就是当显示放大视图时,缩小视图呈现的整体上下文丢失了。当可以从数据中推断出上下文时,这不是问题,我的姓名数据就是这种情况。你可以在图 16-7 中看到我的意思。
图 16-7。数据中群体的性质非常明显
我只需要向您展示一两个组,就可以清楚地看到数据是按字母顺序分组的,并且根据所显示的组,SemanticZoom
显示的数据接近数据源的 50%。在这些情况下,SemanticZoom
控件是完美的,因为用户需要知道的一切都显示在布局中,或者可以很容易地从布局中推断出来。
注以下部分展示了如何将两个
ListView
控件连接在一起。然而,很难仔细阅读所有对放大和缩小视图的引用。我的建议是在 Visual Studio 中遵循这些部分,以便您可以看到每个更改的效果——它将为描述性文本提供上下文。
有时这种方法不起作用,而您希望同时显示两个视图,以便整体上下文和细节同时可见。我发现自己经常遇到这个问题,我通过用两个ListView
控件和一些事件处理程序代码替换SemanticZoom
控件来解决这个问题。
创建列表上下文示例
为了演示两个ListView
控件的排列,我在名为ListContext.html
的示例项目的pages
目录中添加了一个新文件,其内容如清单 16-11 所示。
清单 16-11 。ListContext.html 文件的内容
`
这个文件包含两个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
控件已经就位并填充了数据,但是它们还没有链接在一起,所以当您调用单个项目时,它们之间没有交互。
图 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
组的效果。
图 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
来显示最终的元素组时的效果。
图 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 的中看到这种变化的结果,其中您可以看到将放大视图滚动到数据集的末尾会导致缩小控件显示最后几组。
图 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 事件,如click
和moveover
来响应用户输入。对于简单的交互来说,这是一种可行的方法,但是需要不同的技术来充分利用 Windows 应用,特别是支持触摸手势,这使得用户可以轻松地表达复杂的命令。在本章中,我将向您展示如何确定 Windows 8 设备支持哪些输入技术,解释 Windows 如何使用一个通用的指针系统来表示这些输入,以及如何识别触摸手势。表 17-1 对本章进行了总结。
创建示例项目
我创建了一个名为AppInput
的项目来演示 Windows 应用可以支持的不同事件和手势。我将在自己的内容页面中展示每个主要功能,因此我创建了一个熟悉的带有NavBar
的主内容页面的应用结构,这将允许用户在应用中导航。在清单 17-1 中,您可以看到default.html
文件的内容,它将作为示例应用的母版页。
清单 17-1 。default.html 文件的内容
`
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 显示了应用的基本布局,如果您此时运行应用并调出导航条,您将会看到这个布局。在接下来的部分中,我将向应用添加内容页面,以演示应用支持指针和手势的不同方式。
图 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 中总结了这个属性,以便您在以后返回本章时可以轻松找到它,并且不想阅读文本来查找属性名称。
在清单 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 元素(如input
或textarea
元素)获得焦点时,软件键盘会自动显示文本。
如果此时运行应用,并使用Capabilities
NavBar 命令导航,您将会看到如图图 17-2 所示的内容。图 17 显示了在我的开发 PC 上运行的应用,正如你所料,它有一个键盘。
图 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
导航条命令的结果。
提示Visual Studio 模拟器将总是报告一个不带滚轮的 2 键鼠标,而不管连接到运行模拟器的 PC 的硬件如何。
图 17-3。确定当前设备的鼠标功能
确定触摸能力
最后一类输入是触摸,包括触摸屏和数字化平板电脑在内的各种设备类型。您可以使用Windows.Devices.Input.TouchCapabilities
对象查看设备是否支持触摸,该对象定义了表 17-4 中所示的属性。
如果设备有多个触摸面,那么contacts
属性返回能力最差的一个支持的接触点的数量。你可以看到我是如何使用清单 17-7 中的TouchCapabilities
对象的,它显示了我对DeviceCapabilities.html
文件的script
元素的添加。
注意我发现当
TouchPresent
属性描述的设备有更多接触点时,它经常返回1
。
清单 17-7 。使用 TouchCapabilities 对象
`...
...`
你可以在图 17-4 中看到这段代码生成的结果。我已经在我的开发 PC 上运行了示例应用,我在 PC 上连接了一个便宜的数字化平板。
图 17-4。显示设备触摸功能的详细信息
处理指针事件
您可以在 Windows 应用中愉快地使用标准的 DOM 事件,如click
和mouseover
。负责运行 JavaScript Windows 应用的 Internet Explorer 10 将生成这些事件,而不管用于生成它们的输入设备是什么。这意味着,例如,当点击一个button
元素时,它将触发一个click
事件,而不管用户是用鼠标、触摸屏上的手指还是数字化仪上的笔进行交互。这些事件由 IE10 生成,以提供与 web 应用的兼容性,您可以在您的应用代码中非常安全地使用它们,正如我在本书的示例应用中所做的那样。
然而,如果你想使用 Windows 触摸手势,那么你需要使用MSPointer
事件。这些事件与 web 应用开发中的标准 DOM 事件相对应,但是它们具有额外的属性,这些属性提供了所使用的输入类型的详细信息。在表 17-5 中,我列出了MSPointer
事件,并描述了它们被触发的情况。没有叫MSPointer
的事件——这个名字指的是事件名称以MSPointer
—MSPointerDown
、MSPointerUp
等开始。
提示
MSPointer
事件与 DOM 兼容性事件一起生成。您可以毫无问题地监听像MSPointerMove
和mousemove
这样的混合事件,尽管有时在MSPointer
事件和被触发的标准 DOM 事件之间会有一点延迟。
对这些事件的描述必然是模糊的,因为它们涉及广泛的交互类型。当用户点击鼠标时,当手指或手写笔触摸屏幕时,或者使用不太常见的设备进行其他交互时,指针可以触摸一个元素。为了演示这些事件是如何工作的,我在示例 Visual Studio 项目的pages
文件夹中添加了一个名为PointerEvents.html
的文件,您可以在清单 17-8 中看到。
清单 17-8 。PointerEvents.html 文件的内容
`
这个页面的布局由一个简单的彩色块组成,您可以与它交互以生成事件。我通过将事件添加到一个WinJS.Binding.List
对象来处理它们,该对象是一个ListView
控件的数据源(你可以在第八章的中了解到WinJS.Binding.List
对象,在第十五章的中了解到ListView
UI 控件)。你可以在图 17-5 中看到结果,显示了简单交互后的内容。
图 17-5。响应 MSPointer 事件
您可以像注册常规 DOM 事件一样注册您对指针事件的兴趣——但是您必须注意使用正确的大写。例如,事件名称是MSPointerDown
,而不是mspointerdown
、MsPointerDown
或任何其他排列。你可以在清单 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 中的事件,但在很大程度上,它们非常相似,并且以与仅使用鼠标的交互一致的方式触发。例如,当用户触摸屏幕上的元素时,就会触发MSPointerDown
和MSPointerUp
事件,无论用户使用的是鼠标还是触摸屏,都是如此。
有两件事很麻烦。MSPointerHover
事件是指当一个点在一个元素上移动而没有接触到屏幕时被触发。这听起来很合理,但我无法在真实的硬件上触发这个事件——尽管在应用模拟器中触发它很容易(只需选择模拟器窗口右边缘的Basic Touch Mode
按钮,并将鼠标指针移动到元素上)。
我也不能触发MSPointerCancel
事件。当设备中止交互时会触发此事件,微软给出的例子是当同时触摸点的数量超过触摸屏或数字化仪处理它们的能力时。我已经在我能找到的所有硬件上测试了清单 17-8 中的代码,但是还不能触发这个事件。
获取指针信息
一个MSPointer
事件的处理函数被传递一个MSPointerEvent
对象,它实现了一个常规 DOM Event
的所有方法和属性,但是增加了一些内容。许多附加功能被系统用来计算多个事件是如何形成一个手势的,但是有一些属性可能更有用,我已经在表 17-6 中描述了这些属性。
你可以看到我是如何通过我的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_MOUSE
、MSPOINTER_TYPE_PEN
和MSPOINTER_TYPE_TOUCH
对应于pointerType
属性返回的值。
提示来自数字化平板的事件通常被报告为
MSPOINTER_TYPE_MOUSE
事件,而不是MSPOINTER_TYPE_PEN
事件。这取决于数字化仪硬件是如何被识别的——我测试过的许多输入设备对系统来说就像鼠标一样,大概是为了更广泛的兼容性。
我在模板中显示的第三个字段指示事件是否由输入设备上的主要点生成。这与多点触摸设备有关,其中第一个接触点(通常是触摸屏幕的第一个手指)被认为是主要点。响应其他手指的移动或接触而触发的事件将为isPrimary
属性返回false
。
处理手势
手势是以特定顺序接收的一系列事件。因此,例如,一个点击手势由一个MSPointerDown
事件组成,在某个时刻,后面跟着一个MSPointerUp
事件。我说在某些时候是因为手势交互是复杂的——用户的手指可能会在指针被按下然后释放的时刻之间移动,指针可能会移动到你正在收听的事件所在的元素之外,等等。微软已经包含了一些有用的工具,可以更容易地处理手势,而不必处理它们派生的单个事件。在接下来的部分中,我将向您展示如何在用户执行手势时接收通知,以及您可以在应用中响应手势的一些不同方式。
处理手势可能相当复杂,所以我将从最简单的手势开始,逐步增加到更复杂的。为了演示基础知识,我在 Visual Studio 项目的pages
目录中添加了一个名为Gestures.html
的文件,你可以在清单 17-12 中看到。
清单 17-12 。Gestures.html 文件的内容
`
该内容遵循我用于指针事件的相同模式。有一个彩色的矩形,我监听它的事件——但是在这个例子中有一些关键的不同。
最基本的手势是tap
和hold
,分别用MSGestureTap
和MSGestureHold
事件表示。为了从一个元素接收这些事件,我必须创建一个MSGesture
对象,并告诉它我希望它对哪个元素进行操作,如清单 17-13 所示。
清单 17-13 。创建 MSGesture 对象
... var ges = new MSGesture(); ges.target = targetElem; ...
MSGesture
对象内置于 Internet Explorer 10 中,因此您不需要使用名称空间来引用它。使用target
属性设置想要接收手势事件的元素——在本例中,我指定了作为我的彩色矩形的div
元素。
提示
MSGesture
对象只处理单一元素。如果您想要接收多个元素的手势事件,那么您需要为每个元素创建MSGesture
对象。
MSGesture
对象解除了您跟踪单个MSPointer
事件的责任,但是您需要告诉它,通过addPointer
方法传递来自MSPointerDown
事件的细节,一个新的手势可能正在开始,该方法接受由MSPointerEvent
对象的pointerId
属性返回的值,如清单 17-14 所示。
清单 17-14 。开始一个手势
... ges.addPointer(e.pointerId); ...
此时,您不需要做任何其他事情——MSGesture
对象将跟踪来自元素的事件,并在手势出现时生成事件。手势事件的处理函数被传递一个MSGestureEvent
对象,该对象包含关于手势的信息。点击手势没有额外的细节,但是保持手势可以导致多个MSGestureHold
事件被触发。您可以通过读取detail
属性并将其与MSGestureEvent
对象枚举的值进行比较来确定这些事件的重要性,如表 17-7 中所述。
在例子中,我使用一个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 中看到这些值是如何显示的,以及手势事件的类型。
图 17-6。响应顶部和保持手势
表演基本手势
为了测试这个例子,您需要知道如何执行手势。在介绍每个手势时,我将向您展示如何使用鼠标在触摸屏上创建它,以及如何使用鼠标在 Visual Studio 模拟器中模拟触摸。
要使用鼠标执行点击手势,只需在元素上单击鼠标按钮并立即释放——对于鼠标使用,执行点击手势与生成click
事件并触发MSGestureTap
事件是一样的。要执行保持手势,单击鼠标按钮并按住它——几秒钟后会触发MSGestureHold
事件。
在触摸屏上,用一个手指按下并立即释放元素以执行点击手势,然后按住(即,不要将手指从屏幕上移开)以执行保持手势。
在模拟器中,使用模拟器窗口右边缘的按钮选择Basic Touch Mode
,如图 17-7 中的所示。光标变为代表手指(显示为带有十字光标的大圆)。您的光标现在是一个手指——按下鼠标按钮模拟用手指触摸屏幕,松开按钮模拟移开手指。如果你想回到模拟器中常规的鼠标交互,那么选择Mouse Mode
,它就在Basic Touch Mode
按钮的正上方。
图 17-7。在 Visual Studio 模拟器中选择基本触摸模式
搬运操作
操作是更复杂的手势,允许用户缩放、旋转、平移和滑动元素。为了演示操作手势,我在 Visual Studio 项目中添加了一个名为Manipulations.html
的文件。你可以在清单 17-16 中看到这个文件的内容。
清单 17-16 。Manipulations.html 文件的内容
`
对于这个例子,我已经创建了三个目标元素,每个元素都用我将在接下来的小节中应用的操作进行了标记。你可以在图 17-8 中看到初始布局。
图 17-8。可以应用操作手势的元素
表演操纵手势
这个示例演示的操作手势是旋转、缩放和平移,在深入研究示例代码之前,我将向您展示如何执行每个手势。您需要在示例中的相应元素上执行每个手势。
注意您可以使用触摸或鼠标进行平移动作,但旋转和缩放手势仅适用于触摸。
旋转元素
要旋转一个元素,用两个手指触摸屏幕,并围绕一个中心点做圆周运动。要在模拟器中执行该手势,从模拟器窗口的右边选择Rotation Touch Mode
按钮。光标将变为两个圆圈,代表手势的两个手指。将光标放在元素上,并按住鼠标按钮。向上滚动垂直鼠标滚轮执行逆时针旋转,向下滚动垂直鼠标滚轮执行顺时针旋转。释放鼠标按钮以完成手势。在图 17-9 中可以看到Rotation Touch Mode
按钮、模拟光标和效果。
提示不按鼠标键滚动鼠标滚轮,改变模拟手指的初始位置。
图 17-9。选择旋转触摸模式,旋转一个元素
缩放元素
要缩放一个元素(也称为捏/缩放手势)将两个手指放在显示屏上,将它们分开以放大元素。一起移动手指会缩小元素。要在模拟器中模拟该手势,选择Pinch/Zoom Touch Mode
按钮。光标将变为代表两个手指。在元素上按住鼠标按钮以开始手势,并使用鼠标滚轮调整接触点–向上滚动滚轮会将接触点分开,向下滚动滚轮会将它们一起移动。释放鼠标按钮以完成手势。在图 17-10 中可以看到Pinch/Zoom Touch Mode
按钮、模拟光标和缩放手势的效果。
提示两个接触点都需要在元素内才能发起手势。在不按下按钮的情况下使用鼠标滚轮来调整接触点之间的距离,以使它们合适。
图 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
事件发送关于手势的更新。
系统不会区分不同的手势。这些事件的处理函数被传递给一个包含附加属性的MSGestureEvent
对象,该属性包含用户所做的旋转、缩放和平移的详细信息。由你来决定你想对这些运动的哪些方面做出反应。我在表 17-9 中总结了这些特性。
这种方法的好处是用户可以同时执行多个手势。您可以选择要读取的属性值,并忽略那些表示您不感兴趣的手势的属性值。这是我在示例中采用的方法,我只想为布局中的每个元素支持一种手势。
使用 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
对象使得通过rotate
、scale
和translate
方法应用这些变化变得容易。
提示
CSSMatrix
对象和元素style.transform
属性是 CSS3 过渡和转换特性的一部分,我将在第十八章的中详细介绍。
唯一不方便的是,MSGestureEvent.rotation
值是用弧度表示的,而 CSS3 转换是用度数表示的——您可以在清单中看到我是如何从一种转换到另一种的。你必须记住将rotate
、scale
或translate
方法的结果设置为用户正在操作的元素的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 中看到操纵所有三个元素的结果。
图 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 中可以看到布局的原始状态和放大后的内容。该手势与标准缩放手势的主要区别在于,图像已在其容器的边界内缩放,但容器保持不变。
图 17-12。使用内容缩放手势
控制内容缩放手势的 CSS 属性在表 17-10 中进行了描述,在接下来的章节中,我将向您展示如何使用它们,并解释支持值的范围。
启用内容缩放功能
启用内容缩放手势需要两个属性。首先,您必须将 CSS overflow
属性设置为scroll
。该属性并不特定于内容缩放,但它是启用该功能所必需的。第二个属性是–ms-content-zooming
,必须设置为zoom
。结合这些属性可以启用元素的内容缩放功能。
你必须将这些属性应用到一个有内容的元素上,如清单 17-21 所示。您可以看到内容是一个img
元素,它包含在一个 div 中。这是内容缩放手势应用到的div
元素。
清单 17-21 。对包含内容的元素应用内容缩放功能
`
应用缩放限制
您可以使用-ms-content-zoom-limit-min
和ms-content-zoom-limit-max
属性来限制内容的缩放量。这些以原始大小的百分比表示。用户可以在执行捏合/缩放手势时将内容缩放到这些限制之外,但当手势结束时,它会迅速恢复到该限制。在这个例子中,我设置了 50%的最小比例和 200%的最大比例,如清单 17-22 中的所示。设置这些属性时,不要忘记%
符号。
清单 17-22 。应用缩放限制
... -ms-content-zoom-limit-min: 50%; -ms-content-zoom-limit-max: 200%; ...
提示
-ms-content-zoom-limit
便利属性允许您在一条语句中指定最小值和最大值。
设置缩放内容样式
-ms-overflow-style
属性允许您配置内容缩放后的显示方式。该属性支持的值如表 17-11 所示。
我倾向于使用-ms-autohiding-scrollbar
值,它可以确保用户意识到他们可以在元素的内容周围平移,但是使用滚动条,滚动条覆盖在内容的顶部,并且只在用户与内容交互时显示。相比之下,由scrollbar
值应用的滚动条增加了元素的大小,并且总是被显示。你可以在图 17-13 中看到不同之处。左图显示了-ms-autohiding-scrollbar
值的效果,右图显示了scrollbar
值。
图 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 中可以看到修改后的布局。
注意
msContentZoomFactor
是直接在HTMLElement
对象上定义的,而不是由 WinJS 定义的。这意味着您不需要使用winControl
来访问属性——您可以直接从document.getElementById
方法返回的对象中获取或设置值。
图 17-14。在 JavaScript 中使用内容缩放功能
总结
在这一章中,我向您展示了如何确定设备上可用的输入形式,以及如何使用MSPointer
事件来响应用户交互。这些事件提供了对常规 DOM 事件的重要增强,因为它们包含了输入机制的细节,可以用来识别手势。我向您展示了手势系统的工作原理,并演示了简单的手势和操作。在本章的最后,我演示了如何使用内容缩放功能来创建另一种效果。这个特性依赖于 CSS——我将在下一章回到这个主题,在那里我将描述 WinJS 动画系统。
十八、使用动画和工具
在这一节中,我将向您展示如何使用 WinJS 动画特性。Windows 应用通常具有的颜色和排版的一致性可能会使用户难以意识到内容已被更新或替换,因此用一个简短的动画来突出显示这种变化可能会很有用。我在前面的章节中提到了一些基本的动画特性,但是现在我要回到这个主题上来。我首先向您展示如何直接使用 WinJS 所依赖的 CSS 特性,然后解释这些特性是如何被 WinJS 动画便利方法打包的。WinJS 包含一些用于常见布局更新场景的预打包动画,我将向您展示如何使用这些动画——包括如何为动画准备元素以及如何在之后进行整理。
在本章的第二部分,我描述了一些在WinJS.Utilities
名称空间中有用的方法。我在整本书中一直在使用这些方法,并且我已经描述了当我第一次使用它们时它们做了什么。在这一章中,我给出了一个更完整的使用指南,并演示了一些额外的功能,包括计算布局中元素的大小和位置,以及创建一个灵活的日志记录系统,即使应用没有连接到调试器,您也可以使用该系统。表 18-1 对本章进行了总结。
使用动画
出于我不完全理解的原因,当涉及到应用中的动画时,一些程序员变得有点疯狂。不仅仅是 Windows 应用——你可以在网络应用、桌面应用以及任何地方看到这种疯狂的结果。你可以很快疏远你的用户,特别是如果动画阻止用户继续当前的任务。过多的动画是业务线应用中的一种残酷形式,用户将日复一日地重复执行相同的任务。在开发过程中看起来很酷很刺激的动画,当你的用户每天看一百遍的时候,会让他们筋疲力尽。当涉及到 Windows 应用中的动画时,我遵循一套简单的规则,我建议你也这样做:
- Only use animation to attract users' attention to changes or results that they may miss.
- 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 文件的内容
`
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 属性的值,转换允许您平移、缩放和旋转元素。
提示在这一节中,我主要关注与 WinJS 动画直接相关的 CSS3 特性。关于 CSS3 更完整的报道,请参阅我的另一本书html 5的权威指南,这本书也是由 Apress 出版的。
过渡和转换是 WinJS 动画使用的底层机制。通过理解 CSS3 的功能是如何工作的,你将会对 WinJS 的功能有更好的理解,并且如果需要的话能够创建你自己的效果。为了演示如何直接使用这些 CSS 特性,我在示例 Visual Studio 项目的pages
文件夹中添加了一个文件。这个文件叫做CSSTransitions.html
,如清单 18-4 所示。
清单 18-4 。使用 CSS 过渡和转换
`
` `这个例子在布局中有两个明确标记的元素,我将用它们来演示转换和变换。你可以在图 18-1 中看到初始布局,我将在接下来的章节中解释其功能。
图 18-1。过渡和变换的初始布局示例
应用过渡
当您应用转换时,您告诉浏览器您希望一个或多个属性的值在一段时间内逐渐改变。您可以将过渡应用于颜色和任何具有数值的属性。
提示有两种方法可以应用过渡和变换:通过 CSS 类或者使用 JavaScript 直接应用于元素。我使用了基于类的方法进行转换,并在本章的后面向您展示了基于代码的方法进行转换。
有几个 CSS 属性可以用来定义一个过渡的特征,我已经在表 18-2 中描述了这些属性。
在这个例子中,我定义了一个 CSS 类,它包含了我的转换。我省略了transition-property
,这意味着任何值被改变的属性都服从于transition-direction
和transition-delay
属性,如清单 18-5 所示。
清单 18-5 。使用 CSS 过渡属性
`...
...`
当这个类被应用到一个元素时,color
、background-color
和font-size
属性的新值将在 500 毫秒的延迟后 1 秒内被应用。我将这个类应用于目标元素以响应click
事件,如清单 18-6 所示。
清单 18-6 。响应于点击事件应用转换类
... WinJS.Utilities.toggleClass(e.target, "colorTransition"); ...
如果指定的元素上没有colorTransition
类,则WinJS.Utilities.toggleClass
方法会添加它,如果有,则移除它。这意味着元素将随着每个click
在正常状态和转换状态之间移动。你可以在图 18-2 的中看到这个效果,它显示了应用过渡时从初始状态的渐进过程。这些只是快照,如果你用示例应用进行实验,你会看到完整的效果是多么平滑。
图 18-2。一个 HTML 元素的过渡
该图显示了应用过渡时产生的逐渐过渡。请注意,colorTransition
类中所有属性的值被同时修改。
如果再次单击该元素,它会立即恢复到原始状态。这是因为transition-delay
和transition-duration
属性是colorTransition
类的一部分,当该类被删除时,应用渐变的指令也被删除。通过在colorTransition
类之外设置transition-duration
属性,可以确保所有的属性更改都是逐步进行的。
应用变换
在第十七章中,当我修改元素来响应手势时,我使用了一个变换。在这一节中,我将向您展示实现相同结果的不同方法,并演示变换和过渡如何协同工作。变换是通过transform
属性来控制的,我在表 18-3 中总结了这个属性,这样你将来可以很容易地找到参考。
transform 属性的值可能相当复杂,这取决于您想要进行的更改的性质。我在表 18-4 中列出了不同的变换值。
您可以通过将一个或多个单独的转换连接起来作为 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
对象,我在第十七章的中提到过。
在本例中,每次单击元素时,我都会依次遍历这些转换。变换和过渡可以组合在一起,为了演示这一点,我还为transitionDuration
和backgroundColor
属性应用了一系列值。我使用 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 中的,尽管你应该用示例应用来体验一下真实的效果有多平滑和优雅。
图 18-3。通过 DOM 应用变换和过渡
使用 WinJS 核心动画方法
WinJS.UI
名称空间包含一组方法,使得在 Windows 应用中应用过渡和变换更加容易。这些是控制 WinJS 动画整体状态的包装器函数,并为我在上一节描述的 CSS 功能提供了一个方便的包装器。表 18-5 总结了核心动画制作方法。
前三种方法允许您控制应用中的所有动画。将此功能作为设置向用户公开是一个好主意,这样他们就可以禁用动画——我在第二十章中向您展示如何显示用户设置。
提示这里有一个术语不匹配。您可以使用
executeAnimation
和executeTransition
方法来执行 CSS 过渡和转换。不同之处在于操作完成时元素所处的状态。使用executeAnimation
方法,元素被返回到它们的原始状态,而executeTransition
使元素保持修改后的状态,这与前面的例子非常相似。微软并不想在这些名字上为难——executeAnimation
方法支持一个叫做关键帧的特性,它执行更复杂的效果,被称为动画——这就是方法名称的来源。我没有在这一章描述关键帧,因为它们不太适合更广泛的 Windows UX 主题,但你可以在[
www.w3.org/TR/css3-transforms](http://www.w3.org/TR/css3-transforms)
获得更多细节。
executeAnimation
和executeTransition
方法将 CSS 变换和过渡应用于元素。这些方便的方法比直接使用 CSS 类或style
属性更容易使用。为了演示这些方法,我在示例项目中添加了一个名为CoreFunctions.html
的新页面,它使用executeAnimation
和executeTransition
方法再现了早期的内容。你可以在清单 18-9 中看到这个新文件的内容。
清单 18-9 。使用 executeAnimation 和 executeTransition 方法
`
这两种方法的第一个参数是您想要操作的元素集。这可以是单个元素(这是我在示例中使用的),也可以是元素的数组,这将导致相同的效果应用于数组中的所有元素。第二个参数是一个对象,其属性包含要应用的过渡或变换的详细信息。我已经在表 18-6 中描述了支持的属性名称。
使用 executeAnimation 方法
如果您想一次操作多个属性,那么您可以将包含表中属性的对象数组作为第二个参数传递给executeAnimation
或executeTransition
方法。这两个方法都返回WinJS.Promise
对象,你可以用它们来链接效果。如清单 18-10 所示,我在示例中使用了两种技术,你可以在第九章的中了解更多关于Promise
对象的信息。
提示当指定属性值时,使用 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-color
和color
属性——我是通过将两个对象的数组作为第二个参数传递给该方法来完成的。我在返回的Promise
上使用了then
方法来链接对executeAnimation
方法的第二次调用——这次是为了转换font-size
属性。结果是background-color
和color
属性被一起转换,当两个转换都完成时,font-size
属性被转换。
executeAnimation
和executeTransition
方法的区别在于,在动画结束时,元素会返回到其原始状态。最好的方法是使用示例应用并点击Transition
元素,但我试图捕捉图 18-4 中的效果。不会逐渐返回到初始状态——元素只是快速返回到调用executeAnimation
方法之前的位置。
图 18-4。使用 executeAnimation 方法
元素的状态在每次调用executeAnimation
结束时被重置,您可以在图中看到它的效果。在font-size
属性转换之前,background-color
和color
属性的值被重置。
使用 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
方法一样,您可以从清单中看到,我向该方法传递了一个对象数组,这样就可以转换transform
和background
属性。当直接使用 CSS 属性时,使用transform
属性具有相同的效果,并允许将变换应用于元素。
事实上,executeTransform
方法只是我之前向您展示的 CSS 功能的包装,我在清单中突出显示的语句说明了这一点。我可以从元素中读取transform
属性的值,以确定我在循环中的位置。
使用 WinJS 动画
WinJS.UI.Animation
名称空间包含一组在应用内容上执行预定义动画的方法。这些是标准的 Windows 动画,您应该在应用中执行与它们相关的活动时使用它们,例如,显示新的内容页面。使用这些方法有两个好处。首先,它们使用起来非常简单,比使用executeTransition
方法或直接使用 CSS 要简洁得多。第二个原因是,你的应用将与其他 Windows 应用保持一致,并受益于用户先前对这些动画的意义的体验。表 18-7 描述了 WinJS 内容动画。
提示
WinJS.UI.Animation
名称空间中还有其他方法,但是它们被系统用来在 UI 控件中应用动画,并不是通用的。
这些是标准化的动画,通常成对使用。作为一个例子,你可以看到我是如何在清单 18-12 中的default.js
中使用enterPage
和exitPage
动画方法的。我调用这些方法来响应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 页面。我将为每个动画构建这些内容,从enterContent
和exitContent
方法开始。你可以在清单 18-13 中看到ContentAnimations.html
文件的初始版本。
清单 18-13 。使用内容输入和内容退出动画
`
尽管动画方法是成对出现的,但您仍然要负责协调对这些方法的调用,并准备将被动画化的元素。使用exitContent
方法制作动画的元素只需要在页面上可见即可。将由enterContent
方法引入的元素需要被添加到 DOM 中,但不可见,这是通过将display
属性设置为none
来实现的,如下所示:
... content2.style.display = "none"; ...
要协调动画序列,以便从一个元素到另一个元素的过渡是平滑的,请遵循以下顺序:
- Call the
exitContent
method, passing in and out elements as parameters.- Use
then
method forWinJS.Promise
returned byexitContent
:
- Outgoing elements
- Set the
display
property on to none to clear the incoming element.- The
display
property on calls theenterContent
method, passing in the method as a parameter.
这些方法在改变opacity
属性的值时翻译元素。这个过程非常快——exitContent
动画耗时 80 毫秒,而enterContent
动画耗时 370 毫秒。
提示通过在 Visual Studio 项目的
References
部分的ui.js
文件中搜索方法名,可以看到每个动画是如何设置的细节。
点击示例中的button
元素触发动画。对于每个click
事件,我计算出哪个元素是可见的(因此是传出的),哪个元素是隐藏的(因此是传入的)——这允许示例在元素之间交替。
使用淡入和淡出动画
fadeIn
和fadeOut
方法操作动画元素的opacity
属性。这意味着在动画开始之前,输出元素需要有一个值为1
的opacity
和一个值为0
的输入元素。如果您希望一个元素替换另一个元素,那么您需要确保它们在布局中占据相同的空间-一种方法是使用网格布局并将两个元素分配给同一个网格单元。你可以看到我是如何应用这种技术,并调用动画方法的,在建立在前面例子基础上的清单 18-14 中。
清单 18-14 。使用渐强和渐弱方法
`
**
**
**
**
**
** **
**
通过将display
属性设置为–ms-grid
,可以很容易地将元素放置在同一位置。如果没有为行或列设置任何值,也没有为单元格分配任何元素,那么结果将是一个 1 x 1 的网格,所有内容元素都在同一个单元格中。在这个例子中,我通过将style
属性应用到容器元素来设置这种排列。我通常不直接对元素应用 CSS,但是我破例了,因为这是一个如此简单的例子。
fadeIn
和fadeOut
不会变换元素的位置(因此不需要显式地改变动画之间的display
属性)。你可以在图 18-5 的中看到示例的布局。
使用交叉渐变动画
crossFade
方法转换一对元素的opacity
属性,这样一个变得透明,而另一个变得不透明。crossFade
方法所需的准备与fadeIn
和fadeOut
方法相同,因为您需要为输出元素设置opacity
到1
,为输入元素设置0
。不同之处在于两个动画是同时开始的。你可以看到我是如何在清单 18-15 中添加对crossFade
方法的支持的。
清单 18-15 。使用交叉渐变方法
`
**
**
** <div id="crossfade1"**
** class="coloredRect coloredRectSmall column">One
** <div id="crossfade2"**
** class="coloredRect coloredRectSmall column">Two
**
** **
** **
`
交叉淡入淡出动画非常快。淡入和淡出动画都持续 167 毫秒,因此过渡是即时的。我发现效果有点太快,倾向于将fadeOut
和fadeIn
方法链接在一起。你可以在图 18-5 中看到触发动画的元素和按钮的布局。我建议您花一些时间对这三个元素进行实验,以感受用户将如何看到从一个元素到另一个元素的转换。
图 18-5。使用 enterContent 和 exitContent 动画
使用 WinJS 工具
在本书这一部分的例子中,我一直在使用WinJS.Utilities
名称空间的特性。在这一节中,我将描述其中最有用的特性,如果您使用过 jQuery 之类的 DOM 操作库,就会对其中的许多特性很熟悉。
查询 DOM
在本书的许多例子中,我给符号$
起了别名,这样它就可以引用WinJS.Utilities.query
方法。这是一个类似 jQuery 的方法,它在 DOM 中搜索匹配 CSS 选择器字符串的元素。结果作为一个QueryCollection
对象从query
方法返回——该对象定义了表 18-8 中描述的方法。
到目前为止,我已经在本书的例子中使用了几乎所有这些方法,并且在接下来的章节中也使用了它们。在这一章中,我不打算给出任何具体的例子,因为这些方法是不言而喻的,而且大多数 web 程序员至少对 jQuery 或类似的库有一点熟悉。
提示如果你正在做 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 文件
`
在这个例子中,我把所有的东西都放在了一起,所以你可以看到 CSS 如何影响元素的布局,以及这个布局如何影响WinJS.Utilities
方法。我定义了一个包含嵌套的div
元素的简单布局。外部元素设置为网格布局,其中可用空间以不同的数量分配给行和列。网格中空间的部分分配以及填充和边距的使用使得很难从标记中计算出内部元素的位置。在script
元素中,我使用了表 18-9 中的方法来获取职位的详细信息,并将它们写入 JavaScript 控制台窗口。你可以在图 18-6 中看到布局是如何出现的。
图 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 总结了这些方法,我将在下面的章节中解释。
提示注意,
log
方法在WinJS
名称空间中,但是其他方法在WinJS.Utilities
中。
编写日志消息
在这种情况下,您最常用的方法是WinJS.log
,它有三个参数:一个写入日志的消息,一个包含一个或多个标签(用空格分隔)的字符串,这些标签对消息进行分类,以及一个消息类型,如info
、error
或warn
。您可以使用任何字符串作为标签和类型,只要对您的应用有意义。
WinJS.log
方法不会总是被定义(原因我将很快解释),所以您需要在编写日志消息之前确保它存在。你可以在清单 18-17 的中看到我是如何做到的,在那里我修改了来自SizeAndPosition
项目的 default.html 文件,使用了WinJS.log
方法。
清单 18-17 。将 WinJS.log 方法应用于 SizeAndPosition 示例
`
我发现使用WinJS.log
方法最简单的方法是通过应用代码中定义的函数,这就是我在示例中使用logSizeAndPos
函数所做的。传递给该函数的任何消息都使用标签winjs
、app
、info
写入日志,并指定类型info
。如果在这种状态下运行应用,您将看不到任何输出,因为默认情况下,WinJS.log 方法尚未定义。我将在下一节解释如何设置。
启动日志
在调用WinJS.Utilities.startLog
方法之前,没有定义WinJS.log
方法。这将告诉系统您对哪种类型的日志消息感兴趣,从而允许您过滤被记录的内容。startLog
方法的参数是一个对象,它的属性有特殊的含义——我已经在表 18-11 中列出了公认的属性名。
从本质上讲,startLog
方法设置了一个过滤器来捕获某些日志消息,并在默认情况下将它们写入 JavaScript 控制台。清单 18-18 显示了对添加到SizeAndPosition
项目的脚本块中的startLog
的调用。
清单 18-18 。启动日志
`...
...`
在清单 18 中——我调用了startLog
方法,指定我对类型为info
且标签为app
、bugs
或info
的消息感兴趣。一个日志消息只需要有一个您指定给startLog
方法的标签就可以被捕获和处理。对startLog
的调用设置了过滤器,在中,创建了WinJS.log
方法。如果您现在运行应用,您将在 JavaScript 控制台窗口中看到输出。以下是输出的示例行:
winjs:app:info: Position Top: 59
Windows 应用日志记录系统已格式化输出,以便包含标记。如果您没有看到该消息,请检查JavaScript Console
窗口顶部的按钮。这些按钮可用于过滤控制台中显示的消息类型,您可能会发现有一个或多个按钮未按下。您可以在图 18-7 中看到按钮。
图 18-7。确保显示日志信息
创建自定义日志记录操作
JavaScript 控制台的问题在于,当应用在调试器之外部署和运行时,它是不可用的。这就是传递给startLog
方法的对象的action
属性发挥作用的地方——它允许你创建一个定制的日志记录方法,该方法与你的应用的其余部分集成,并且可以在调试器之外工作。action
属性被设置为一个函数,该函数被传递了日志消息、标签和类型,如清单 18-19 中的所示,这里我向SizeAndPosition
示例的script
块添加了一个自定义日志动作。
清单 18-19 。添加自定义日志动作
`
在这个例子中,我使用了startLog
来创建一个动作,该动作捕获那些类型为info
、标签为app
并且消息以工作Position
开始的消息。对于每个匹配的消息,我创建一个新的div
元素,并将其作为子元素添加到我添加到布局中的新容器元素中。这并不是一种特别有用的显示日志消息的方式,但是它确实证明了您可以对您需要的日志信息做几乎任何事情——这可能包括向用户显示它、将它保存到一个文件或者将它上传到一个服务器。
您可以通过调用formatLog
方法来创建与写入 JavaScript 控制台的字符串格式相同的字符串——这与默认操作使用的方法相同,并生成包含消息和标签细节的字符串。当然,您可以完全忽略这个方法,生成对您的应用有意义的任何消息格式。你可以在图 18-8 的中看到这些增加的结果,它显示了作为布局一部分的日志信息。(布局元素没有正确对齐,因为我在最初的示例中使用了奇怪的网格布局。)
提示注意,我在示例中保留了对
startLog
的原始调用。WinJS 日志记录系统支持多种过滤器和操作,这意味着消息仍然被写入 JavaScript 控制台,这在开发和测试过程中非常有用。
图 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 对本章进行了总结。
了解应用生命周期
Windows 8 积极管理 Metro 应用,以保持设备内存空闲。这样做是为了确保最大限度地利用可用内存,并减少电池消耗。作为这一战略的一部分,Metro 应用拥有明确的生命周期。除了最简单的应用,所有应用都需要知道 Windows 8 何时将应用从生命周期的一个阶段转移到另一个阶段,这是通过侦听一些特定的系统事件来完成的。我将描述这些事件,并向您展示如何处理它们,但在这一部分,我将描述生命周期,以便您了解您的 Metro 应用运行的环境。
激活
一个应用在启动时被激活,通常是当用户点击开始屏幕上的图标时。这是 Metro 应用的基本初始化,就像任何类型的应用启动时一样,您需要创建布局、加载数据、设置事件监听器等等。简而言之,激活的应用从不运行变为运行,并负责引导其状态以向用户提供服务。
注意并非所有的激活都是为了启动一个应用——正如我将在本章后面解释的,Windows 也会激活你的应用,这样它就可以执行其他任务,包括那些由契约定义的任务。契约是本书这一部分的主题,你将在第二十四章开始看到它们是如何工作的。
暂停
当一个应用不被使用时,它会被 Windows 8 暂停,最常见的原因是用户已经切换到使用另一个应用。这就是应用管理中积极的部分:在用户切换到另一个应用后几秒钟,一个应用就被挂起了。简而言之,一旦你的应用对用户不再可见,你就可以期待它被暂停。
暂停的应用被冻结。应用的状态被保留,但应用代码不会被执行。用户不会意识到某个应用已被暂停,该应用的缩略图仍会出现在正在运行的应用列表中,以便可以再次选择它。
恢复
当用户选择一个暂停的应用并再次显示它时,暂停的应用被恢复。Windows 会保留应用的布局和状态,因此在恢复应用时,您不需要加载数据和配置 HTML 元素。
终止
如果 Windows 需要释放内存,暂停的 Metro 应用将被终止。暂停的应用在终止时不会收到任何通知,应用的状态和任何未保存的数据都会被丢弃。应用布局的快照将从向用户显示的运行应用列表中移除,并替换为作为占位符的闪屏。如果用户再次启动应用,应用将返回激活状态。
注意 Windows 没有为 Metro 应用开发者提供关于应用何时终止的明确政策。这是最后的手段,但 Windows 可以在任何需要的时候自由终止应用,并且您不能根据可用的系统资源来假设应用被终止的可能性。
与 WinJS 合作。应用
既然您已经理解了生命周期的不同阶段,我将向您展示如何在应用 JavaScript 代码中处理它们。这个部分的关键对象是WinJS.Application
,它提供了对 JavaScript Metro app 的生命周期事件和一些相关特性的访问。
提示
WinJS.Application
只是包装了Windows.UI.WebUI.WebUIApplication
对象中的功能。使用WinJS.Application
的价值在于它以一种更符合 JavaScript 和 web 应用开发的方式呈现了生命周期特性。你可以直接使用WebUIApplication
类,但是我发现有一些有用的WinJS.Application
特性值得使用。
我将从WinJS.Application
支持的事件开始,我已经在表 19-2 中描述过了。对于处理应用生命周期,重要的事件是activated
和checkpoint
。事实上,我很少使用其他事件。
当您创建一个新的 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*
属性,我将在本章中遵循这个约定。
提示注意,清单中的最后一条语句是对
WinJS.Application.start
方法的调用。WinJS.Application
对象将事件排队,直到start
方法被调用,此时事件被传递给你的代码。调用stop
方法会导致WinJS.Application
再次开始对事件进行排队。一个常见的问题是忘记调用start
,创建一个激活时不做任何事情的应用。
这个清单中有几个问题。首先,Visual Studio 添加到新项目中的代码不是很有帮助。第二,WinJS.Application
对象不会转发WebUIApplication
对象发送的所有事件。如果你使用/js/default.js
文件中的代码构建一个复杂的应用而不解决这两个问题,你迟早会碰壁。在我构建出EventCalc
示例应用后,我将解释这两个问题并演示它们的解决方案。
构建示例应用
示例应用是一个非常简单的计算器,允许您将两个数字相加。该应用还显示其接收的生命周期事件的详细信息。在图 19-1 中可以看到没有任何内容的 app 布局。
图 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
属性更改时生成计算历史。
提示注意,在
onactivated
处理程序中,我对传递给函数的参数对象调用了setPromise
方法。我将在本章后面的处理闪屏部分解释这个方法。
触发生命周期变更
将应用从生命周期的一部分转移到另一部分的最简单方法是使用 Visual Studio。如果你查看工具栏,你会看到一个标有Suspend
的菜单项。如果你点击标签右边的小箭头,你会看到菜单也包含了Resume
和Suspend and
?? 的选择,如图图 19-2 所示。(您可能需要从 Visual StudioView
–Toolbars
菜单中选择Debug
位置才能看到工具栏。)
图 19-2。使用 Visual Studio 控制应用生命周期
菜单项迫使应用从一个生命周期阶段转移到另一个阶段。它们很有用,因为它们允许您在附加调试器的情况下测试代码。在应用开发的早期阶段,我经常使用这个功能。
然而,Visual Studio 只能模拟生命周期事件,这意味着调试器运行时和不运行时会有细微的差别。这意味着您应该花时间通过直接从操作系统生成事件来测试您的应用的行为方式,而不依赖于 Visual Studio 调试器。
问题是,当调试器没有被使用时,一个影响是没有JavaScript Console
窗口可用于显示调试消息,这使得更难弄清楚发生了什么。正是因为这个原因,我在示例应用中添加了一个ListView
控件,这样我就可以在应用本身的布局中记录生命周期事件的到来——这是我经常用于应用最终测试的一种技术。
在 Windows 8 中生成生命周期事件
生成生命周期事件实际上非常简单,只要您耐心等待,看看 Windows 8 显示的指示生命周期变化的指示。在接下来的小节中,我将向您展示如何在不使用 Visual Studio 的情况下生成每个activated
、resuming
和suspending
事件。正如我所说的,这是测试你的用户将会看到什么的唯一现实的方法。
启动应用
触发activated
事件最简单的方法是启动应用,尽管重要的是不要用 Visual Studio 调试器来做这件事。你可以从开始屏幕中选择应用的磁贴,或者从 Visual Studio Debug
菜单中选择Start Without Debugging
。
提示在 Visual Studio 中至少启动一次之前,示例应用的磁贴不会添加到开始屏幕。之后,您应该会看到应用的磁贴列在屏幕的最右侧。有时磁贴会不可见,特别是如果你一直在模拟器和本地机器之间切换以运行应用——在这种情况下,我发现通过键入应用名称的前几个字母并从结果列表中选择它来搜索应用足以使文件正确显示。
当你启动EventCalc
应用时,你会看到默认的闪屏(因为我没有改变所用颜色或图标的清单设置),然后看到如图图 19-3 所示的应用布局。请注意,左侧的ListView
控件中已经记录了activated
事件。我用消息Launched
记录了这个事件,以表明接收到的事件是一个启动应用的请求——这一点我将在本章后面详细解释。
图 19-3。在没有调试器的情况下启动示例应用
暂停应用
让 Windows 暂停应用最简单的方法就是按Win+D
切换到桌面。您可以通过启动Task Manager
,切换到Details
选项卡并找到WWAHost.exe
进程来跟踪应用生命周期的进程(这是用于运行 JavaScript Metro 应用的可执行文件的名称——因此,您启动的每个 Metro 应用都会有一个进程)。您可能需要点击任务管理器中的More Details
按钮才能看到Details
选项卡。
注意您需要在本地机器上启动任务管理器,即使示例应用正在模拟器中运行。任务管理器不能在模拟器中运行,但是
WWAHost.exe
进程在本地机器的任务管理器中仍然可见。
几秒钟后,Windows 将暂停应用,进程状态将在Task Manager
中变为Suspended
。你可以在图 19-4 中看到一个暂停的应用是如何显示在任务管理器中的。
图 19-4。使用任务管理器观察一个 Metro 应用被挂起
注意连接到 Visual Studio 调试器的应用永远不会在
Task Manager
中显示为挂起,因为它们保持活动状态,以便调试器可以控制应用。如果你没有看到挂起的消息,通常是因为应用是用调试器启动的。
我需要使用任务管理器来检查应用的状态,因为我再也看不到应用的布局,而且由于我没有使用调试器,我也看不到任何调试消息。在下一节中,当我恢复应用时,您将看到收到了suspending
事件的证据。
恢复应用
你可以通过将应用带回到前台来恢复应用,最简单的方法是将鼠标移动到左上角(或在触摸屏上从左边缘滑动)并单击缩略图。应用将返回并填满屏幕,您将看到在应用布局的左侧ListView
控件中记录了一个新事件,如图图 19-5 所示。
图 19-5。app 收到暂停和恢复事件
图中显示suspending
事件被 app 接收。这发生在应用对用户隐藏之后,但在进程在Task Manager
中显示为暂停之前。这个小间隔是我将在本章后面返回的东西,因为它为应用提供了一个准备被挂起的机会,这对某些类型的应用来说是无价的。
图没有显示的是resuming
事件的接收。这是 Visual Studio 添加到项目中的代码的问题之一。我将向您展示如何确保您的应用很快获得该事件。
终止应用
你可以通过输入Alt
+ F4
来终止应用的执行。应用会突然终止,并且不会发送警告事件。以这种方式退出应用允许您确保您的应用在下次启动时正确恢复,并检查它使用的任何远程服务(web 服务、数据库等。)能够正确地恢复资源。
这是终止你的应用的两种方式之一。另一种情况发生在应用暂停,Windows 需要释放一些资源的时候。在这两种情况下,您的应用将被终止,而不会收到任何通知事件。在这一章的后面,我将向你展示当你的应用被暂停时如何准备终止,我也将向你展示当你的应用下次启动时如何判断它是如何被终止的。
您可以通过使用 Visual Studio 工具栏菜单中的Suspend and Shutdown
项(包含Suspend
和Resume
项的菜单)来模拟 Windows 终止应用的情况。
获取激活类型和以前的应用状态
在我可以修复default.js
文件中的代码以便获得所有的生命周期事件之前,我需要做一些准备工作,以便我可以弄清楚当事件到来时我被要求做什么。为此,我需要深入研究事件的细节,以发现激活类型和我的应用在事件被调度之前所处的状态。
当一个应用被发送activated
事件时,传递给onactivated
函数的事件对象有一个detail
属性,该属性返回一个Windows.ApplicationModel.Activation.IActivatedEventArgs
对象。
IActivatedEventArgs
对象定义了我在表 19-3 中描述的三个属性,这些属性为你提供了所有你需要的信息,让你知道 Windows 要求你的应用做什么。我将在接下来的章节中描述其中的两个属性,并在本章的后面返回到第三个属性。
确定激活的种类
一个应用可以被激活用于一系列不同的目的,例如在应用之间共享数据,从设备接收数据,以及处理搜索查询。这些激活目的是由 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 中列出了这些值。
如果你的应用之前的状态是notRunning
或closedByUser
,那么你正在处理一个的重新开始的发布。您需要初始化您的应用(设置 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
值。我将notRunning
、closedByUser
和running
的值视为相同,并调用performInitialization
函数来设置我的事件监听器和数据绑定。我已经更改了传递给writeEventMessage
函数的消息,以使我收到的事件的意义更加清晰。
我也是在前一个状态是terminated
的时候调用performInitialization
函数。我将在本章后面添加一些额外的代码,以区分我处理这种状态的方式与notRunning
、running
和closedByUser
。对于所有这些先前的状态,我需要初始化我的应用,以确保我的事件处理程序和数据绑定已经设置好。
我还没有对suspended
值做任何事情,除了调用writeEventMessage
函数来记录事件的接收。我不需要初始化我的应用,因为系统正在恢复执行,我的事件处理程序和数据绑定已经存在。当我谈到后台活动时,我会在这一部分添加一些代码,但是目前,什么也不做是将对suspended
状态的响应区分开来的原因。
提示你会注意到我调用了
WinJS.UI.processAll
和WinJS.Binding.processAll
方法,而不管我正在处理哪种激活。我将在本章的后面回到这段代码。
你可以在图 19-6 中看到我显示的三个激活事件消息中的两个。您可以使用 Visual Studio 非常容易地自己重新创建这些事件。首先,使用Debug
菜单中的Start Debugging
项启动应用——这将产生如图左侧所示的Fresh Launch
消息。
图 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
属性具有返回预期值的kind
和previousExecutionState
属性。
这并不理想,但是如果您希望能够响应整个生命周期的变化,这是很重要的。图 19-7 展示了修复的效果,您可以通过启动示例应用,然后从 Visual Studio 工具栏菜单中选择Suspend
和Resume
项来复制这个结果。
图 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.processAll
和WinJS.Binding.processAll
方法。只有当这两种方法都完成时,应用布局才会替换闪屏。
提示我在第十章中描述了
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);
}));
}
};
...`
只有在计算完所有值后,才会将缓存的结果分配给视图模型属性。风险在于,在所有数据可用之前,用户将看到应用的布局。
我发现一项任务花费大约 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 的变量,但它还不存在。
图 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.processAll
和WinJS.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 所示。如果您仍然有应用初始化要执行,那么这将提示您向用户显示一些临时消息或内容。
图 19-9。闪屏关闭时显示消息
处理 App 终止
处理一个终止后又启动的 app,需要做一些工作。一个应用不会被警告即将被终止——系统会暂停应用,然后,如果资源紧张,终止应用进程以释放内存。Windows 无法告诉应用它将被终止,除非让它退出挂起状态,这将需要一些 Windows 试图回收的资源。
处理应用终止需要代表应用做一些仔细的工作,因为微软想对用户隐藏终止。这是一个明智的想法,当一个应用遵循这种模式时,它会为用户创造更好的整体地铁体验,用户不会在意有限的设备资源导致的应用终止。用户没有关闭应用,当他们从“正在运行”的应用列表中选择闪屏占位符时,他们会期望应用会让他们从停止的地方继续。
要做到这一点,您需要存储应用挂起时的状态。这就像购买应用终止保险一样——你希望你的应用只是被恢复,你不必恢复状态,但你需要采取预防措施,以防你的应用被终止。
WinJS.Application
对象通过它的sessionState
属性帮助你存储你的应用状态。通过将一个对象分配给该属性来存储状态,该对象将作为 JSON 数据永久存储。使用的 JSON 解析器非常简单,不会处理复杂的对象,包括可观察的对象,这意味着您通常需要创建一个新的对象来表示您的应用的状态——当然,如果您的应用被终止然后被激活,请准备好使用该对象来恢复该状态。
为了演示这项技术,我在viewmodel.js
文件中添加了两个新函数来保存和恢复应用状态数据,如清单 19-18 所示。
注意
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
中定义的数据绑定,并在历史记录中创建一个新的(意外的)条目。不设置这个值的缺点是,最后提供给用户的结果不会显示在中间的列中。
这是处理应用状态数据的典型方式——你通常可以非常接近地恢复到应用暂停前的状态,但总有一些事情很难恢复。至于你在处理这些问题时能走多远,这是一个判断的问题。
提示我在这个例子中也省略了 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 的中看到状态是如何恢复的,以及生命周期事件通知,显示应用被启动之前已经被终止。
图 19-10。保存和恢复 app 状态的效果
处理 App 暂停
除了存储应用的状态,你还可以使用oncheckpoint
处理函数来释放应用正在使用的资源,并控制应用正在执行的任何后台任务。
例如,在资源方面,您可能需要关闭与服务器的连接或安全地关闭文件。你不知道你的应用将被挂起多长时间,也不知道它是否会在挂起时被终止,所以使用oncheckpoint
处理程序让你的应用进入安全状态是有意义的。如果您使用远程服务器,最好尝试关闭连接,以便其他用户可以使用它们–大多数服务器最终会发现您的应用已经消失,但这可能需要一段时间,而且仍然有一些企业服务器对并发连接的数量有严格的限制,即使这些连接没有主动处理请求。
就后台任务而言,如果在应用暂停时,您有使用setImmediate
、setInterval
或setTimeout
方法延迟的工作,那么这项工作将在应用再次恢复时执行——这并不总是有用的,尤其是当应用恢复时,状态可能会发生变化,这使得处理被延迟执行的函数的结果更加困难。
在这一节中,我将向您展示如何在应用挂起之前进行准备。我将使用一个简单的后台任务来完成这项工作,因为这意味着您不必按照示例设置服务器。后台任务将计算出自应用上次激活以来已经过了多少秒——这在现实世界中并不是一件有用的事情,但它是一个有用的示例,因为它允许我向您展示当后台活动的上下文变得陈旧时会发生什么。清单 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 中看到这些变化的结果,图中显示了一个已经运行了一段时间的应用实例。
图 19-11。显示应用激活后经过的时间
要查看过时的上下文问题,请启动应用并运行一段时间。然后暂停和恢复应用。应用暂停时不会进行任何后台工作,但应用恢复后会立即重新开始。从字面上看,该应用会从停止的地方继续运行,这意味着后台任务生成的进度更新是基于任务首次启动的时间,而不是该应用最近一次激活的时间。
示例中的这个问题是微不足道的,但是现实世界中经常会出现类似的问题。执行oncheckpoint
函数是停止后台任务的机会,这些任务依赖于应用再次启动时将失效的数据。有两种停止后台工作的方法—取消并忘记和停止并等待。我将在接下来的章节中解释这两个问题。
使用取消和忘记技术
这是最简单的技术——您只需调用执行后台工作的Promise
的cancel
方法。你可以在清单 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 提供了本章的总结。
准备示例 App
我将建立在我在第十九章中用来解释生命周期事件的EventCalc
例子之上。在那一章中,我向你展示的一个特性是当应用即将被挂起时如何存储会话状态。我当时提到,您不应该将用户设置存储为会话数据,所以我构建同一个示例应用来告诉您故事的其余部分是合适的。提醒一下,你可以看到EventCalc
是如何出现在图 20-1 中的。
图 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 所示。
图 20-2。改变设置属性的值
在接下来的小节中,我将向您展示向用户呈现这些设置属性并持久存储用户选择的值的机制。
向用户呈现设置
Windows 提供了一个处理设置的标准机制,它是通过设置魅力(你可以通过魅力栏或使用Win+I
快捷方式打开它)来触发的。
设置通过设置窗格呈现,如图图 20-3 所示,默认设置窗格显示了应用的一些基本细节,并包含一个Permissions
链接,向用户显示应用在其清单中声明了哪些功能。“设置”面板的底部有一组标准按钮,用于配置系统范围的设置。
图 20-3。默认设置窗格
如果你点击Permissions
链接,你会看到一个设置弹出按钮的例子,如图 20-4 中的所示。这就是设置的处理方式——它们被分组到类别中,在设置窗格中显示为链接,单击链接会显示一个设置弹出按钮,其中包含更多详细信息,通常还包含允许用户更改应用设置的控件。
图 20-4。权限弹出按钮
提示您可以通过更改应用清单的
Packaging
部分中的值来更改Permissions
弹出按钮中显示的详细信息。
如图所示,设置弹出按钮有一个标题区,其中包含一个后退按钮——单击此按钮将返回设置窗格。设置窗格和设置弹出按钮都是轻触式的,这意味着用户可以通过单击或触摸屏幕上的其他地方来关闭它们,就像我在本书的前一部分描述的弹出 UI 控件一样。
在这一部分,我将在设置窗格中添加额外的链接,以允许用户查看和更改我在/js/settings.js file
中定义的设置。我将向窗格添加两个新链接:Colors
链接将打开一个窗格,让用户设置backgroundColor
和textColor
设置属性,History
链接将打开一个窗格,让用户设置showHistory
设置属性。在这个过程中,我将演示你的应用如何将自己集成到标准设置系统中,以及你如何创建设置弹出按钮,其外观和行为与图 20-4 中的默认Permissions
弹出按钮相同。
注通过设置魅惑辅助设置是契约的一个例子。契约是一组定义明确的交互、事件和数据对象,允许应用在标准的 Windows 功能中具体化。设置只是我在本书中解释的契约之一——当我向你展示如何将你的应用集成到 Windows 搜索功能中时,你会在第二十一章中看到一个更复杂的例子。
定义弹出型 HTML
我需要做的第一件事是为每个设置面板创建一个 HTML 文件。首先,我创建了一个名为colorsSettings.html
的文件,其内容可以在清单 20-4 中看到。这是一个相对简单的文件,所以我将 CSS 和 JavaScript 放在 HTML 标记所在的文件中。
清单 20-4 。colorsSettings.html 文件的内容
`
Background Color
Text Color
这个 HTML 的核心是一个名为SettingsFlyout
的 WinJS UI 控件。该控件应用于div
元素,仅用于向用户呈现设置。width
选项是由SettingsFlyout
定义的唯一配置选项,它允许你请求一个标准的弹出按钮(使用narrow
值)或一个有额外空间的按钮(使用wide
值)。
设置弹出文件的内容通常需要一些 JavaScript 来处理用户输入,您可以看到我已经使用了WinJS.UI.Pages.define
方法(我在第七章中描述过)来确保在我设置我的事件处理程序和应用数据绑定之前加载文档中的元素。
提示我建议你在创建设置弹出按钮时使用列表中的 HTML 作为模板。它包含了你需要的一切——包括标题和后退按钮的标题,以及一个用于设置内容的区域。
对于这个设置弹出按钮,我定义了两个input
元素,它们是绑定到ViewModel.Settings
属性的数据,并且在change
事件被触发时更新这些属性。对于另一个设置弹出按钮,我已经创建了一个非常相似的文件,叫做historySettings.html
,它的内容你可以在清单 20-5 中看到。
清单 20-5 。historySettings.html 文件的内容
`
对于这个弹出按钮,我为width
配置选项选择了wide
值。ToggleSwitch
控件(我在第十一章中描述过)允许用户切换计算历史的可见性。当切换值改变时,ViewModel.Settings
名称空间中相应的属性也会更新。
注意我的设置弹出按钮有点稀疏,在一个真实的项目中,我可以很容易地在一个弹出按钮上显示所有三个选项。我这样做是为了演示如何添加多个类别,但在实际项目中,如果需要将设置组合在一起,我建议您合并设置并使用弹出按钮中的标准 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
名称属性中的每一个分配一个具有href
和title
属性的对象。必须将href
属性设置为您想要显示的弹出文件的名称,并将title
属性设置为您想要包含在设置窗格链接中的文本。每个div
名称必须是唯一的,如果div
元素名称或href
值不正确,您的链接将从设置窗格中被忽略。
第二步是调用WinJS.UI.SettingsFlyout.populateSettings
方法,传入传递给处理函数的对象(其detail.applicationcommands
属性已经被分配了我想要的链接和弹出按钮的详细信息)。第二步加载弹出 HTML 文件的内容并处理它们。这些增加的结果是当用户激活设置符时,设置窗格包含一些新的链接,如图图 20-5 所示。
图 20-5。向设置面板添加自定义链接
如果您点击或触摸这些链接,则会显示相应的设置窗格。图 20-6 显示了我创建的两个窗格,展示了width
设置的narrow
和wide
值之间的差异。
图 20-6。点击链接时显示的自定义设置弹出按钮
我没有做太多的工作来使设置弹出按钮吸引人,因为我在这一章的重点是窗口设置机制。但是在一个真实的项目中,如果我只有一个ToggleSwitch
要显示,我不会使用 wide 设置,我也不会期望用户通过输入十六进制代码或 CSS 颜色名称来选择颜色。这些都是显而易见的,你应该考虑如何分组和安排弹出按钮的内容,以使配置应用的过程尽可能简单和轻松。
使设置持久
我已经关联了“设置”弹出按钮中的控件和元素,以便视图模型在它们发生变化时立即更新。您可以尝试更改设置的效果,并立即看到效果——但下次启动应用时,将使用我在settings.js
文件中定义的默认值,您的更改将会丢失。
这就把我带到了应用数据或应用数据的话题上,这些数据是你的应用运行所需要的,但你不想让用户直接访问——比如设置。当用户与应用数据交互时,是通过某种形式的中介,比如设置弹出按钮。在接下来的部分中,我将向您展示如何存储和检索应用数据设置。
提示应用数据的替代品是用户数据,用户可以直接使用这些数据——我将在第二十二章–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;**
** }**
** });**
** });**
})();`
注意除非另有说明,否则我在本节中引用的所有新对象都是
Windows.Storage
名称空间的一部分。
这项技术的核心是ApplicationDataContainer
对象,其中数据存储为键/值对。有两个内置容器——第一个允许您存储数据,以便数据位于运行应用的设备上,另一个存储漫游数据,这些数据会自动复制到用户登录的任何设备上。
稍后我将回到漫游数据容器,但是对于这个例子,我从最简单的选项开始,使用本地容器。为了获得本地容器对象,我读取了ApplicationData.current.localSettings
属性,如下所示:
... storage.ApplicationData.current.localSettings.values[setting] = newVal; ...
属性返回一个可以用来存储数据的对象。容器中的数据是永久存储的,这意味着您不必担心显式指示 Windows 保存您的应用数据-一旦您在存储容器中设置了值,您的数据就会被存储。
ApplicationData
定义了许多有用的属性,我在表 20-2 中总结了这些属性,并在接下来的章节中进行了描述。
一旦获得了想要使用的容器,就可以通过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;
}
});
});
...`
提示我忽略了任何可观察属性的旧值为
null
的更新。这是因为编程数据绑定在首次创建时会被发送一个更新,提供初始值和旧值的null
。我只想在值改变时存储它们,因此检查了null
。
恢复设置
如果你不能在需要的时候读取设置,那么将设置存储为应用数据就没有多大用处。在清单 20-9 中,你可以看到我在/js/settings.js
文件中添加的内容,以便在应用首次加载时恢复任何保存的设置。
注意我定义了恢复设置的函数,并在
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
。当我收到数据绑定通知时,我会检查这个变量的值,如果加载正在进行,我会放弃更新。
提示注意,在我传递给
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 将在用户登录的任何位置复制你的应用数据,这意味着你可以在多个设备上创建一致的体验。
注意如果用户没有与其 Windows 登录相关联的 Microsoft 帐户,则无需处理任何错误——在这种情况下,数据不会被复制。
如果在应用运行时漫游设置被修改,ApplicationData
对象将触发datachanged
事件。这使您有机会更新您的应用状态以反映新数据,确保用户在其他地方所做的更改尽快得到应用。
提示在示例中,我简单地调用了
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
键将新值存储在容器中。它是触发加速行为的密钥名称,并且不需要明确的动作来触发复制。
注意虽然使用复制时没有性能保证,但我的经验是常规设置大约每 5 分钟复制一次,而
HighPriority
设置在几秒钟内复制一次。在高峰时间,常规设置的复制通常会减慢到大约每 10 分钟一次。
确保数据一致性
某些应用设置无法自行安全复制。考虑示例应用的以下场景:
- On the host computer, the user sets
backgroundColor
towhite
andtextColor
togreen
. 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.- The user gets on the train and starts the same application on another device. They changed
backgroundColor
togreen
, but did not set thetextColor
attribute to the default value (white
). Windows copies the newbackgroundColor
value.- 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.
- The application program applies the new value set by
backgroundColor
, presentsgreen
text to the user on thegreen
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 应用于漫游数据的配额和容量限制的限制)。
提示你可以使用一个
ApplicationDataCompositeValue
对象作为HighPriority
设置的值。这允许您优先选择一组相关属性,以在设备之间保留您的应用状态。
有了这一改变,ViewModel.Settings
名称空间中的属性和应用数据容器中的设置名称之间没有直接的联系。我使用特殊的HighPriority
设置复制了showHistory
属性的值,并将backgroundColor
和textColor
属性复制为一个名为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 应用中处理文件是非常简单的,但是它不同于我见过的任何其他处理文件系统的方法。在接下来的部分中,我将向您展示每个步骤,并详细解释选项。
注意除非我明确声明,否则我在本节中引用的所有新对象都是
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 章中向您展示更多可用的功能,届时我将描述如何使用用户数据,而不是应用数据。
注意与设置一样,应用对文件的数据支持可以是本地的,也可以是漫游的,我选择了本地选项。两个实例中的 API 是相同的,不同之处在于漫游文件是复制的。如果要使用漫游 app 数据文件,那么应该使用
ApplicationData.current.roamingFolder
属性返回的StorageFolder
。注意不要超过漫游数据报价,否则您的文件将不会被复制。
表 20-4 总结了使用StorageFolder
对象打开、创建或删除文件的基本方法。(我在第二十二章中描述了其他的StorageFolder
方法,但是这些是你最常用于 app 数据文件的方法。)
Windows 应用中几乎所有与文件相关的操作都是异步执行的,这就是为什么表中的所有方法都返回一个Promise
对象。完成后,这些Promise
对象将把一个或多个StorageFile
对象传递给then
方法,代表指定的文件。你可以看到我是如何使用清单 20-15 中的createFileAsync
方法得到一个StorageFile
对象的。
提示本章的剩余部分严重依赖于
WinJS.Promise
对象。如果你还没有阅读第九章,那么你应该现在就阅读,并在完成后返回这里。无法回避Windows.Storage
名称空间对象的异步特性。微软为 Windows 应用引入异步支持的主要目标之一是防止应用在执行文件操作时挂起。在处理文件时,你必须使用承诺,即使一开始可能会感觉有点反直觉。
清单 20-15 。获取存储文件夹和存储文件对象
... folder.**createFileAsync**(historyFileName, storage.CreationCollisionOption.openIfExists) .**then**(function (**file**) { // statements to operate on the StorageFile go here }); ...
注意异步文件操作中出现的任何错误都会传递给
then
方法的error
函数。我在这一节中没有定义错误处理程序,因为我想向您展示如何处理文件,而不是处理错误,但是您应该小心处理真实项目中的错误。关于使用Promise
时如何报告错误的详细信息,参见第九章。
createFileAsync
方法的可选参数是来自CreationCollisionOption
对象的一个值,该对象枚举了一些值,这些值决定了当您试图创建一个已经存在的文件时,Windows 将采取什么操作。表 20-5 描述了可用的值。
我使用了openIfExists
值,这意味着如果没有文件,示例应用将创建一个新文件,如果有,将重用现有文件。
写入文件
StorageFile
对象支持对文件的所有操作——你可以重命名或删除它所代表的文件,打开允许你读写数据的流等等。
我不会直接做这些事情,因为FileIO
对象定义了一组非常方便的方法,使得执行基本的读写选项变得简单(比直接使用StorageFile
对象方法容易得多)。表 20-6 描述了FileIO
定义的便利方法。
提示读写缓冲区的方法本身并不是特别有用。它们旨在与来自
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
对象中,从而将每个项目显示给用户。
注意如果你在添加
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
文件的内容:
`
这个清单定义了一个简单的应用,其中 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 搜索机制,允许用户在您的应用中查找数据,就像他在操作系统中定位文件和应用一样。
这一章不是关于给你的应用添加搜索功能。这是一款已经有能力处理搜索的应用,并使用契约向用户展示这种能力。您可以提供对您的应用和应用数据有意义的任何类型的搜索功能,并且,正如您将看到的,使用户能够轻松访问和使用它。本章开始时,我将构建一个能够搜索其应用数据的简单应用,然后我将使用该应用来演示如何实现搜索契约。
创建示例应用
在《??》第十六章中,我使用了一系列流行的名字来帮助演示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 中看到应用的布局以及首次启动时显示的数据和消息。
图 21-1。示例 app 的初始布局和数据
添加应用图片
我在项目的images
文件夹中添加了一些文件,如图图 21-2 所示。我已经使用了文件名以tile
开头的文件来设置应用清单的应用 UI 部分的字段,就像我在第四章的中所做的一样。这些图像包含如图所示的放大镜图标。另一个文件,user.png
,包含一个人物图标,我将在本章后面使用 Windows 显示我的搜索结果时使用这个文件。您可以在图中看到这两个图标(我添加了黑色背景以使图标可见—实际文件是透明的)。我已经将这些文件包含在本书的源代码下载中,您可以从apress.com
获得。
图 21-2。向示例应用添加磁贴和用户图标
测试示例应用
正如我在介绍中提到的,这一章不是关于你如何在你的应用中实现搜索,而是关于你如何将搜索功能集成到 Windows 中,以便你的用户有一致和丰富的搜索体验。我在示例应用中执行搜索的方式非常简单,您可以通过使用 Visual Studio JavaScript Console
窗口看到它是如何工作的。启动应用(确保使用Start Debugging
菜单项)并进入 JavaScript 控制台窗口。在提示符下输入以下内容:
ViewModel.search("jac")
(JavaScript 控制台也会说Undefined
——你可以忽略这个。)当你按下 Enter 键时,你会看到左边的ListView
显示的名字集合将被限制为包含搜索词jac
的名字,如图图 21-3 所示。右边的ListView
将显示一条新消息,报告所请求的搜索词。
图 21-3。测试 app 搜索能力
你必须使用JavaScript Console
来执行搜索,因为我没有在布局中添加任何搜索元素或控件——当我将应用搜索功能集成到更广泛的 Windows 搜索体验中时,这将由操作系统提供。在接下来的小节中,我将向您展示如何执行这种集成,并解释您可以采用的不同方法。
实施搜索契约
第一步是更新应用清单,以声明您打算支持搜索契约。为此,双击 Visual Studio Solution Explorer
窗口中的package.appxmanifest
文件,并选择Declarations
选项卡。从Available Declarations
菜单中选择Search
并按下Add
按钮。你会看到Search
被添加到Supported Declarations
列表中,如图图 21-4 所示。你可以忽略页面的Properties
部分——只有当你想使用一个单独的应用来处理搜索时,这些才是有用的,在这一章,我将向你展示如何直接向应用添加搜索支持。
图 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 所示的消息。
图 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 所示。
图 21-6。选择搜索符
当你激活搜索功能时,一个名为搜索窗格的新显示会覆盖在应用上,允许你进行搜索。有一个搜索词的文本输入框,在它下面你会看到一系列图标。这些图标代表搜索目标的范围,包括SearchContract
示例 app,如图图 21-7 所示。(您可能需要向下滚动列表才能看到该应用。)
提示如果搜索窗格报告无法搜索到应用,您需要停止 Visual Studio 调试器,卸载
SearchContract
应用,然后再次启动 Visual Studio 调试器。在开发过程中,Windows 并不总是能够正确响应明显的更改,但对于通过 Windows 应用商店安装的应用来说,这不是问题。(我在本书的第五部分中向你展示了将你的应用发布到商店的过程。)
图 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
事件,其kind
为search
。出于这个原因,我一直小心地构建我的代码,这样我就不会对我将要处理的事件的数量或种类做任何假设。
应用不运行时进行搜索
我想探索的最后一个场景要求应用已经关闭。停止调试器或在应用中按Alt+F4
关闭应用。无需重启应用,从开始屏幕选择搜索符。
当您从开始屏幕中选择 Search Charm 时,搜索范围将设置为安装在设备上的应用的名称(视觉提示是在文本输入框下的列表中选择了Apps
)。输入jac
作为搜索词,并按回车键执行搜索。你会在图 21-8 中看到结果(除非你刚好有一个 app 的名字包含jac
)。
图 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 中总结了这些排列。
使用搜索窗格
我在前面几节中向您展示的技术是搜索契约的基本实现,一旦您的应用中有了搜索功能,大部分工作就是确保您正确处理激活事件。
您还可以通过直接使用搜索窗格来创建定制的搜索体验,搜索窗格可通过Windows.ApplicationModel.Search
名称空间中的对象获得。这个名称空间中的关键对象是SearchPane
,它允许您访问 Windows 搜索窗格并与之集成。SearchPane
对象定义了表 21-3 中所示的方法和属性。
此外,SearchPane
对象支持几个事件,我已经在表 21-4 中描述过了。这些事件在搜索过程的关键时刻触发,允许您以比使用基本契约实现更复杂的方式做出响应。
在接下来的部分中,我将向您展示一些使用这些方法、属性和事件的高级搜索技术,包括如何从应用布局中触发搜索过程,并提供 Windows 可以用来在搜索过程中帮助用户的不同类型的建议。
注意除非我另有说明,否则我在本节中引用的对象都在
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 中看到结果。
图 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 将显示您的应用提供的最多五个建议。
图 21-10。搜索窗格中显示的建议
每当您键入一个额外的字母时,该列表都会被优化。当你输入所有三个字母时,会显示四个建议,所有建议都包含术语jac
。示例应用中的ListView
在你输入时不会受到影响——但是如果你点击这些建议中的任何一个,系统将触发搜索激活,这将具有搜索该术语的效果。
理解建议示例
要想给系统提供建议,需要一长串的对象,但是请相信我——它并不像看起来那么复杂。一个SearchPaneSuggestionsRequestedEventArgs
对象被传递给suggestionrequired
事件的处理函数。除了有一个长得离谱的名字,这个对象定义了两个有用的只读属性,我已经在表 21-5 中描述过了。
每次用户修改搜索文本时,queryText
属性都会更新。当用户缩小搜索范围时,通常会收到一系列事件——可能从j
的queryText
值开始,然后是ja
,最后是jac
。您读取queryText
属性的值,并使用 request 属性返回的SearchPaneSuggestionsRequest
对象向系统提供可以呈现给用户的建议。SearchPaneSuggestionsRequest
定义了我在表 21-6 中描述的方法和属性。
我们已经到达了这个链中的重要对象,即SearchSuggestionCollection
对象,它可以通过传递给处理函数的事件对象的request.searchSuggestionCollection
属性获得。SearchSuggestionCollection
对象定义了四个方法和一个属性,我已经在表 21-7 中进行了总结。
最简单的方法是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); }); }); ...
提示我为从
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 中看到结果。
图 21-11。搜索建议添加分隔符的效果
要查看示例应用中的效果,您需要找到一个匹配以不同字母开头的名称的搜索字符串。在示例中,我使用了字符串aa
,它匹配Aaliyah
、Aaron
和Isaac
。您可以在图中看到,这些名称已经使用我传递给appendSearchSeparator
方法的值进行了分隔。
提示我建议你在提供搜索结果时使用分隔符之前仔细考虑一下,因为它们会占用宝贵的空间,而这些空间可以用来给用户提供额外的建议。只有当没有分隔符的建议对用户没有意义时,才使用分隔符。
提供结果建议
当用户正在搜索的术语与你的应用中的数据项完全匹配时,你可以给出一个结果建议。这为用户提供了该项目的概述,并帮助他决定他是否已经找到了他正在寻找的东西。你可以在图 21-12 中看到一个例子,我在这里搜索过alex
。Alex
是我正在处理的列表中的一个名字,也是其他名字的一部分,比如Alexa
和Alexander
,这就是为什么你会看到混合的查询建议(到目前为止我一直在做的那种建议)和结果建议。
图 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);** ** }** }); }); ...
我的数据并不完全适合这个模型——这并不罕见。我发现数据项要么太复杂而不能用作结果建议,要么太简单,导致我不得不添加一些填充数据。在这个例子中,数据过于简单。
对于text
和detailText
参数,我使用了匹配的名称和一个报告名称中有多少字符的字符串——这不是有用的数据,但它是您最终使用的填充类型。如果您真的没有什么有用的东西要说,您可以将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**")); ...
注意不要忘记在指定图像文件的 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
在建议列表中没有被优先考虑。
提示如果你的应用支持不同的数据搜索方式或者支持搜索不同的数据集,你可以使用
SearchPane.searchHistoryContext
属性为每个数据集保存单独的搜索历史。给这个属性分配一个代表用户将要执行的搜索类型的值,下次使用相同的值给searchHistoryContext
属性时,Windows 将只考虑用户所做的选择。
异步提出建议
到目前为止,我已经同步生成了我的所有建议,这很好,因为我的所有数据都存储在内存中,可以立即使用。
如果生成建议所依赖的代码返回一个Promise
而不是直接给你数据,你就需要采取不同的方法。当您的数据包含在文件或一个WinJS.UI.IListDataSource
对象中时,您会经常发现这种情况,该对象从它的许多方法中返回Promise
对象,并在满足Promise
时产生数据。
为了演示这个问题,我在我的viewmodel.js
类中添加了一个ViewModel.asyncSuggest
方法。这个方法使用了我在这一章中一直使用的数据,但是它通过一个Promise
来呈现结果,这个结果只有在搜索完成后才能实现。为了更真实地展示这一点,搜索是作为一系列小操作来执行的,这些小操作与对setImmedate
方法的调用交织在一起,以允许 JavaScript 运行时执行其他操作。你可以在清单 21-15 中看到增加的内容。
提示关于
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 对本章进行了总结。
创建示例应用
对于这一章,我已经创建了一个名为UserFiles
的新示例项目。我需要一些示例文件,所以我创建了一个img/flowers
文件夹,并添加了一些 JPG 图片。这些是我在第二十一章中使用的相同图像,我已经将它们包含在本书附带的源代码下载中(可从apress.com
获得)。您可以在图 22-1 中看到文件名列表,它显示了UserFiles
项目的解决方案资源管理器窗口。
图 22-1。向示例应用添加图像文件
该项目的其余部分是一个简单的应用。您可以在清单 22-1 中看到default.html
文件的内容。布局中有两个面板——一个包含一系列触发本章示例代码的按钮,另一个包含一个我用来显示消息的WinJS.UI.ListView
控件。WinJS.Binding.Template
控件用于显示ListView
中的信息。
清单 22-1 。用户文件项目的 default.html 文件
`
<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 所示的基本布局。
图 22-2。示例 app 的布局
执行基本的文件操作
在接下来的小节中,我将向您展示如何执行一些基本的文件操作。这将允许我演示使用Windows.Storage
对象处理用户文件而不是应用文件的区别,并提供一些关于应用在处理文件系统时所受限制的细节。
提示我在本章中没有提到的一个基本文件操作是读取文件的内容。你可以在第二十一章(我演示了如何读取应用数据文件的内容)和第二十三章(我展示了如何在应用布局中使用文件内容)中看到这一点。
复制文件
在这一节中,我将向您展示如何将应用包中包含的花卉图像文件复制到用户的My Pictures
文件夹中。您可以在清单 22-5 的中看到我对default.js
文件中的switch
语句所做的补充。在 Windows 应用中处理文件的代码相当冗长,所以我将只展示我对每个示例所做的修改。
注意除非我另外明确说明,否则我在本章中描述的新对象都在
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 文件位置相关。
提示甚至访问由
KnownFolder
对象定义的位置集也受到限制。想要读写这些位置的应用必须在它们的清单中做一个声明,我将在本章后面演示。
在这个例子中,我将应用包中的文件复制到用户的Pictures
文件夹中,如清单 22-6 所示。
清单 22-6 。使用用户的图片文件夹
... storage.**KnownFolders.picturesLibrary**.createFolderAsync("flowers", storage.CreationCollisionOption.replaceExisting) ...
每个属性返回一个代表文件系统位置的StorageFolder
对象。一旦我得到代表Pictures
文件夹的StorageFolder
,我就调用createFolderAsync
方法来创建一个flowers
文件夹。这将是我复制图像文件的位置。在第二十一章中,我向你展示了StorageFolder
对象定义的创建或定位StorageFile
对象的基本方法,但是StorageFolder
也为其他文件夹定义了有用的方法。表 22-3 描述了这些方法中最有用的。
当使用createFoldersAsync
和renameAsync
方法时,你可以使用一个由CreationCollisionOption
对象定义的值来提供一个可选参数,我在表 22-4 中总结了这个值。
在示例中,我使用了replaceExisting
选项,这将删除任何名为flowers
的现有文件夹,并用一个新的空文件夹替换它。由createFolderAsync
方法返回的Promise
向then
方法传递一个StorageFolder
对象,该对象代表我感兴趣的flowers
文件夹。
警告删除文件夹当然是破坏性的。在运行此示例应用之前,您应该确保您的
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
方法。
启用清单中的功能
如果您在此时运行示例应用并单击Copy Files
按钮,您将会看到 Visual Studio 中显示的一条消息,如图 22-3 中的所示。
图 22-3。试图访问文件系统时显示错误
正如我前面提到的,应用对文件系统的访问非常有限。如果你想访问一个不是由用户通过文件选择器提供的位置(我会在第二十三章中解释),那么你必须使用由KnownFolders
对象定义的一个位置。您还必须在应用清单中声明您的应用能够访问该位置。如果您不这样做,一旦您的应用尝试访问文件系统,您就会看到如图所示的错误。
要声明该功能,在 Visual Studio Solution Explorer
窗口中双击package.appxmanifest
文件打开清单,并导航到Capabilities
选项卡。你会看到能力列表包括KnownFolders
位置的项目,我在图 22-4 中突出显示了这些项目。如图所示,我已经检查了与KnownFolders.picturesLibrary
位置相对应的Pictures Library
能力。(Internet (Client)
功能在您创建新的 Windows 应用项目时默认选中。)
图 22-4。允许访问应用清单中的位置
您必须为每个想要使用的位置启用该功能。这将为您的应用提供该位置的访问权限,并向您的应用的权限窗格(可通过 Settings Charm 获得)添加一个条目,如果您通过 Windows 应用商店分发您的应用,该条目将显示给用户。
提示您应该只检查您需要的功能。这个想法是为了给用户提供他们需要的信息,让他们在把你的应用安装到他们的设备上之前,对他们的信任程度做出明智的选择。实际上,用户在任何平台上都不会太关注这些信息,他们会下载并安装任何看起来有趣的东西。即使如此,要求您需要的最低能力也是一个好的做法。
通过添加响应按钮点击的代码并在清单中声明该功能,我已经可以将项目中的文件复制到设备文件系统中了。
为了测试这个例子,启动应用并点击Copy Files
按钮。你会在布局中间面板的ListView
控件中看到一系列消息,报告每个文件都被复制,如图图 22-5 所示。如果您打开文件浏览器并导航到您的Pictures
库,您将看到一个包含样本图像的 flowers 文件夹。
图 22-5。将文件从 app 包复制到文件系统
在我继续之前,有几点需要注意。首先,我已经从应用包中复制了文件,因为这是我设置和分发示例的最简单的方法。虽然这在实际项目中可能是一项有用的技术,但是您当然可以在您的应用清单中声明的任何已知位置上执行文件操作(我将在第二十三章中向您展示如何获得用户许可来处理其他位置的文件)。
要注意的第二点是,由于我在使用createFolderAsync
方法时指定的碰撞选项,单击Copy Files
按钮将删除flowers
文件夹,创建一个新文件夹,并再次复制文件。
确保复制顺序
如果您重复点击Copy Files
按钮,您将会看到ListView
控件中显示的消息以不同的顺序显示。你可以在图 22-6 中看到效果,这里我已经显示了三次点击按钮的消息顺序。
图 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 中描述了这些值。
例如,我指定了default
值,这意味着如果你启动应用并单击Delete Files
按钮,你将能够在 Windows 回收站中找到已删除的文件。
提示您会注意到,在这个例子中,当我向
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
方法,您定义的任何自定义排序都将仅在默认排序之后应用。
提示
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)
获得完整名单的详细信息。
提示这些是字符串值,尽管它们看起来像是对
System
名称空间中对象的引用。在将这些值分配给propertyName
属性时,必须用引号将它们括起来。
一旦我定义并配置了我的QueryOptions
对象,我就调用我对其内容感兴趣的StorageFolder
对象的getFilesAsync
方法,并返回一个由按照我指定的顺序排序的StorageFile
对象组成的数组。
提示应用排序时,使用有效的文件系统属性名很重要。如果您提供的名称无效,应用将被终止,不会发出警告。
在示例中,我枚举了排序数组的内容,并显示了每个文件的名称。为了得到这个名字,我使用了一个由StorageFile
对象定义的描述性属性,我在表 22-8 中总结了这个属性。我再次展示了每个属性为snowdrop.jpg
文件返回的值。
其中一些属性提供了用户友好的值,您可以直接将其包含在应用布局中,而其他属性则更具技术性和细节性,旨在用于您的代码中。
文件系统属性和通过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 所示。
图 22-7。列出文件时应用自定义排序顺序
过滤文件
除了排序,您还可以使用QueryOptions
对象来过滤从getFilesAsync
方法获得的StorageFile
对象。QueryOptions
对象定义了许多支持过滤的属性,我已经在表 22-9 中描述过了。
这些过滤属性分为两类——基本和AQS——我将在接下来的章节中解释这两种属性。三个基本的过滤属性是fileTypeFilter
、folderDepth
和indexOption
。您可以在清单 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
文件,但是我已经指定了多个值来向您展示这是如何做到的。该属性的默认值是一个空数组,表示不应根据文件扩展名排除任何文件。
提示文件扩展名以前导句点指定,即。
.pdf
而不是pdf
。
设置搜索深度
使用由FolderDepth
对象定义的值之一来设置QueryOptions.folderDepth
属性,我已经在表 22-10 中描述过了。
我在清单中选择了shallow
值,它将我通过getFilesAsync
方法接收的文件限制在flowers
目录中(当然,虽然没有子目录可供我操作我的示例文件)。使用deep
值时要小心——您最终可能会查询大量文件,这可能是一个耗时且消耗资源的操作。
使用以前的索引数据
Windows 为用户的内容编制索引以加快搜索速度,从而避免了通过文件系统来获取文件属性和内容的详细信息。问题是,索引是作为后台任务完成的,对文件的最新更改可能不会反映在索引中—本质上,索引数据是在更快的搜索和准确性之间的权衡。您可以使用QueryOptions.indexerOption
属性指定查询是否使用索引数据,该属性设置为来自IndexerOption
对象的值。我已经在表 22-11 中描述了可用值。
我在示例中使用了useIndexerWhenAvailable
值。当您对文件的内容感兴趣时,索引数据的影响最大,访问文件系统意味着依次搜索每个文件。使用以前的索引数据可以大大加快这种搜索的速度。
查看过滤结果
我为各种筛选器属性选择的值的效果是,我的查询选择了正在搜索的文件夹中的 JPG、DOC 和 PNG 文件,并且 Windows 应该使用缓存的文件数据(如果可用)。你可以在图 22-8 中看到结果。
图 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
组合在一起,该关键字必须总是大写(您也可以在查询中使用OR
和NOT
)。
请注意,每个搜索属性后跟一个冒号,然后是比较符号,如下所示:
System.ItemType:=".jpg" AND System.Size:>300kb AND folder:flowers
约束使用folder
关键字表示,并将查询限制在路径包含名为flowers
的文件夹的文件上。(这匹配路径中任何名为flowers
的文件夹,而不仅仅是直接的父文件夹)。
如果运行应用并点击Filter (AQS)
按钮,AQS 查询将用于过滤Pictures
库中的文件,产生如图图 22-9 所示的结果。
图 22-9。使用 AQS 过滤文件
AQS 也可以在 Windows 搜索窗格中使用。我想我从来没有见过用户使用 AQS,但它可以在应用开发过程中测试你的查询。图 22-10 显示了先前清单 22 中的 AQS 查询——在搜索窗格中使用。如果您的文件系统包含 Visual Studio 项目中图像的额外副本,您可能会看到更多结果(我清理了我的文件以获得此结果)。
图 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
中的所有文件,并按照名称的字母顺序对它们进行排序。我已经过滤了文件,只接受了jpg
和png
文件扩展名(在示例文件夹中只有这种类型的文件,但是您已经明白了)。我已经在表 22-12 中描述了所有六个CommonFileQuery
值。其中一些查询只有在应用于音乐文件时才有意义,因为它们依赖于System.Music
文件属性。
使用虚拟文件夹
对象过着双重生活。在本章的前面,我向您展示了可以用来在文件系统上定位或创建新文件夹的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 中所示的结果。
注意如果您已经删除了示例文件,您需要先点击
Copy Files
按钮。
图 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 所总结的。
在这个例子中,我使用了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 中的一样。
图 22-12。监控文件夹中的新文件
这种技术有一些严重的局限性。首先,contentschanged
事件只有在新文件被添加到文件夹中时才会被触发。当文件被删除或修改时,您不会收到该事件。其次,在添加新文件和触发事件之间可能会有几秒钟的延迟。第三,这不是一个健壮的特性,并且contentschanged
事件并不总是在它应该触发的时候被触发,并且对于一个单独的改变经常被触发多次。但如果你能忍受这些问题,那么监控文件夹可能是一种有用的方法,可以确保你的应用让用户了解最新的内容。
总结
在这一章中,我向您展示了如何使用文件和文件夹,从复制和删除文件等基本功能开始。然后,我向您展示了如何对文件进行排序、过滤和查询,如何创建虚拟文件夹来将相关文件分组在一起,以及如何监视文件夹中的新文件。
所有这些特性和技术的共同点是它们都是在你的应用内部实现的,用户看不到。但由于我们处理的是用户的文件和内容,所以我们以一种清晰、明显、与其他应用和 Windows 本身一致的方式来表达文件和文件夹处理功能是很重要的。在下一章中,我将向您展示如何做到这一点,使用 Windows 为处理设备文件系统提供的大量集成功能。
二十三、集成文件服务
在这一章中,我将基于第二十二章中的技术向你展示如何向用户公开文件操作。我将向您展示如何使用文件选择器来请求用户选择文件和文件夹,如何缓存位置以便您的应用保留对它们的访问,以及如何使用文件系统作为 WinJS 数据驱动 UI 控件(如FlipView
和ListView
)的数据源。表 23-1 对本章进行了总结。
创建示例应用
对于这一章,我已经创建了一个名为FileServices
的示例应用,它遵循单页内容模型并使用WinJS.Navigation
名称空间,由应用导航栏上的按钮驱动。本章中的例子不容易放入一个布局中,所以这种方法将让我在同一个应用中向你展示多个内容页面。您可以在清单 23-1 的中看到我对default.html
文件所做的修改。
清单 23-1 。来自文件服务项目的 default.html 文件
`
` `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'}">**
** **
** **
其id
为contentTarget
的div
元素将成为其他页面内容的目标,这些内容将被导入到文档中以响应单击导航条按钮。你可以在default.html
文件中看到 NavBar 命令,我将在本章中添加它们相关的文件。初始内容是一条提示用户去导航条的消息,如图图 23-1 所示。
图 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 所示。
图 23-2。启用对图片库的访问
我将在本章的后面返回到清单来声明额外的功能,但是对Pictures
库的访问已经足够开始了。
显示图像文件
对于本章的第一个例子,我将从一些非常简单的事情开始:在应用布局中显示一个图像文件。这是一个很好的切入点,因为它让我在第二十二章中向您展示的文件系统功能的基础上进行构建,并演示它们如何与 JavaScript Metro 应用中的 HTML 布局相关联。
注意除非我另有说明,否则我在本章中使用的对象都在
Windows.Storage
名称空间中。
对于这一节,我已经在项目中添加了一个名为pages/displayFile.html
的文件。这是一个包含标记、CSS 和 JavaScript 的一体化文件,你可以在清单 23-4 中看到内容。
清单 23-4 。displayFile.html 文件的内容
`
这个内容的简单布局由一个用于显示文件的img
元素和一个用于显示文件名的div
元素组成。如果你启动应用,通过点击Display File
命令使用导航条导航到这个文件,你会看到类似于图 23-3 的东西。
提示如果您的图片库中没有文件,您会看到一条
No Files Found
消息。只需重启应用,点击Copy Sample Files
按钮,然后选择Display File
导航条命令来补救这种情况。
图 23-3。显示在图片库中找到的第一个文件
我说您将看到类似于下图的内容,因为本例中的代码对Pictures
库中的文件进行了深度查询,并显示了第一个文件的内容。(我在第二十二章中描述了文件查询。)对于我的机器,找到的文件是我在前一章中使用的一组示例图像中的astor.jpg
文件,但是它可能是您机器上的一个完全不同的图像。
提示如果
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>
提示
createObjectURL
方法返回的 URL 特定于创建它的系统——您不能与其他设备共享该 URL。
使用文件和文件夹选择器
应用只能自由访问由KnownFolders
对象定义的文件系统位置,这些位置在应用清单的功能部分中声明。为了访问其他位置,您需要用户的明确许可,这是使用文件和文件夹选择器获得的。使用选择器,您可以要求用户指定要打开或保存的文件,或者选择一个文件夹。
为了演示拣选器的使用,我在 Visual Studio 项目中添加了一个名为pages/pickers.html
的新文件,其内容如清单 23-5 所示。当点击导航栏中的Pickers
命令时,会显示该内容。首先,这个文件只包含演示其中一个选择器的代码,稍后我会为其他选择器添加代码。
清单 23-5 。pages/pickers.html 文件的初始内容
`
这个例子包含三个按钮,我将用它们来激活拣选器。还有一个img
和div
元素,我将使用它们来显示选定的文件。此文件的代码演示了文件打开选择器的使用,它允许用户选择一个或多个文件。因为这是我第一次向您展示一个选择器,所以我将介绍它是如何呈现给用户的,然后解释代码是如何工作的。
举个例子
当您第一次选择导航栏上的命令时,您会看到一个非常基本的布局。img
元素被隐藏,仅显示按钮和一条消息,如图 23-4 中的所示。
图 23-4。pickers.html 文件的初始布局
点击Open File Picker
按钮显示拾取器,如图图 23-5 所示。选取器填充屏幕,允许用户在文件系统中导航,并显示当前文件夹中文件的缩略图。
图 23-5。文件打开选择器
导航到图中所示的flowers
文件夹,选择其中一幅图像(该图像将被检查)。激活Open
按钮—点击它选择文件。文件拾取器消失,选中的图像显示在应用布局中,如图图 23-6 所示。
图 23-6。显示用文件拾取器选择的图像
理解代码
现在您已经看到了文件选择器是如何呈现给用户的,我将解释示例中的代码是如何创建您所看到的行为的。当您需要用户选择一个或多个文件时,您可以使用位于Windows.Storage.Pickers
名称空间中的FileOpenPicker
对象。要使用选取器,您需要创建对象的新实例,然后为它定义的属性设置值。您可以在表 23-1 中看到FileOpenPicker
属性列表。
我已经重复了创建和配置 23-清单 23-6 中的FileOpenPicker
的语句。您可以看到我已经将png
和jpg
扩展添加到了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
位置。
使用来自Windows.Storage.Pickers.PickerViewMode
对象的值设置viewMode
属性,它允许您指定选取器如何显示文件和文件夹。有两个定义的值,我已经在表 23-3 中给出。
我在示例中使用了thumbnail
值,这在您希望处理图像文件时非常理想,我的示例应用就是这样。用户不能在选取器中更改视图,因此您必须确保为您希望处理的内容选取一个合理的值。
挑选文件
FileOpenPicker
对象定义了两个方法,当您准备好显示选择器并让用户做出选择时,您可以调用这两个方法。这些方法在表 23-4 中描述。
在这个例子中,我使用了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
对象的数组。
提示在这个例子中,我限制了用户可以选择的文件类型,这意味着我可以安全地在一个
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 所示。
图 23-7。用拾取器保存文件
除了用户能够为将要保存的文件指定名称和类型之外,保存选取器的外观类似于打开选取器。我已经将选取器的初始位置设置为Documents
库,这就是为什么会列出这么多不同的文件夹。您可以通过创建一个新的FileSavePicker
对象来创建一个新的选取器,并通过该对象的属性来配置它。我已经在表 23-5 中列出了房产,其中部分房产与FileOpenPicker
共有。
在示例中,我使用了属性来约束用户的选择,以便他们可以选择文件的位置和名称,但只能选择用打开的选择器加载的文件类型。这是因为我不想进入文件类型转换的世界,而是想演示一下选择器是如何工作的。
挑选文件
当您准备好向用户显示选取器时,调用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
文件的附加内容,用于在点击文件夹选择器按钮时使用文件夹选择器(并处理缩略图)。
注意当然,显示缩略图并不等同于阅读文件内容。正如我在前面的例子中演示的那样,处理图像文件的内容很容易,因为您可以依赖 IE10 对使用 HTML
img
元素显示图像的内置支持。在第二十二章中,我向你展示了如何读取文件内容,并提到了对处理二进制内容的支持,这可能是你所需要的,取决于你的应用所操作的文件类型。
清单 23-10 。对 pickers.html 文件的补充,增加了对文件夹选择器的支持
`...
...`
使用文件夹选择器的技术与前面的示例类似。你创建一个Windows.Storage.Pickers.FolderPicker
对象并通过它的属性配置它,我已经在表 23-6 中列出了。
在这个例子中,我将初始位置设置为Pictures
库,并使用一个星号(*
字符)来指定应该向用户显示所有类型的文件。
提示如果
fileTypeFilter
数组没有包含至少一项,那么FolderPicker
对象将抛出一个异常,因此你必须列出你想要显示的文件类型或者使用一个星号。
pickSingleFolderAsync
方法显示选取器并允许用户选择一个文件夹。你可以在图 23-8 中看到拾取器是如何出现在用户面前的,它显示了包含本书前一章手稿的文件夹的内容。
图 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
值。
在示例中,我选择了用户选择的文件夹中的第一个文件,并在创建缩略图时使用了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 所示。
图 23-9。临时挑选文件夹
你可以在图 23-10 中看到拾取一个非图像文件的效果。我选择了包含手稿章节文本的 Microsoft Word 文件,该文件使用与 Word 文件相关的缩略图显示。
图 23-10。Word 文件的缩略图
缓存位置访问
正如您多次看到的那样,Metro 应用在文件系统方面受到严格的访问限制。您的应用可以自由访问由KnownFolders
对象定义的位置,但前提是您想要访问的每个位置都在应用清单中声明为一项功能。如果您需要处理不同位置的文件,那么您需要使用选择器来获得用户授予的显式访问权限。
如果您的应用允许用户创建一些内容,然后将其保存到单个文件中,这种模式就很好——在这种情况下,使用选择器是非常合理的,因为每个文件的打开或保存位置可能会有所不同。
但是许多 Metro 应用将需要持久访问用户选择的位置,为了管理这一点,您必须使用Windows.Storage.AccessCache
名称空间中的对象。首先,我向示例 Visual Studio 项目添加了一个名为access.html
的新文件,您可以在示例应用中使用Access Cache
NavBar 命令来访问该文件。你可以在清单 23-12 中看到内容。
清单 23-12 。access.html 文件的初始内容
`
` `这个例子背后的想法是展示两个相关的文件操作。布局中有两个button
元素,为了测试这个例子,启动应用,在导航栏上选择适当的 common,然后单击Pick Folder
按钮。该应用将显示文件夹选择器,以便您可以选择一个位置。选择任何不在Pictures
库中的文件夹(因为应用已经在清单中声明了对Pictures
的访问,所以已经可以访问那个位置)。
选择文件夹后,点击Load File
按钮。该应用显示所选文件夹中第一个文件的名称——但它还没有显示缩略图,如图图 23-11 所示。
图 23-11。使用示例应用选择文件夹
现在,为了看看这个例子有什么不同,从 Visual Studio Debug
菜单中选择Restart
来重新启动应用。(重要的是重启,而不是刷新app。)
点击Load File
按钮(不是Pick Folder
按钮),你会看到缩略图和先前选择的文件夹中第一个文件的名称被显示出来,如图图 23-12 所示。我在文档库中选择了一个文件夹,应用通常无法访问该文件夹。
图 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 所述。
在这个例子中,我使用了futureAccessList
属性,它返回一个Windows.Storage.AccessCache.StorageItemAccessList
对象。我通过调用add
方法存储我的位置,传入我希望在将来再次使用其位置的StorageFile
或StorageFolder
对象。
add
方法返回了一个我需要注意的字符串令牌——我使用了我在第二十章的中描述的应用设置特性来持久地存储选择的文件夹作为folder
设置。StorageItemAccessList
对象定义了我在表 23-9 中列出的方法和属性。
当需要我显示文件的缩略图和名称时,我检索存储在应用设置中的令牌,并将其传递给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
属性通过getFileAsync
和getFolderAsync
方法返回访问缓存位置所需的令牌。
作为替代,您可以使用StorageApplicationPermissions.mostRecentlyUsedList
。这就像futureList
一样,但是它只包含 25 个最近使用的位置。当您添加第 26 个项目时,最少使用的项目会被自动删除,使您无需直接管理内容。
使用文件数据源
在第十四章、第十五章和第十六章中,我解释了如何通过数据驱动的 WinJS UI 控件FlipView
、ListView
和SemanticZoom
使用数据源。在那些章节中,我使用了WinJS.Binding.List
对象作为数据源,即使是在我演示如何显示图像的时候。
更好的方法是创建由文件系统查询直接驱动的数据源,允许您对用户存储在设备上的文件进行操作。为了演示这一点,我将pages/dataSources.html
文件添加到示例 Visual Studio 项目中,其内容可以在清单 23-15 中看到。
清单 23-15 。dataSources.html 文件的内容
`
这个例子在Pictures
库中查询 PNG 图像文件,生成一个数据源,然后与FlipView
控件一起使用,这样用户就可以浏览他们的图像。你可以在图 23-13 中看到应用的布局,你可以使用Data Sources
导航条命令将其加载到示例应用中。
图 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 中描述。
对于这个例子,我使用了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.Storage
和WinJS.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 提供了本章的总结。
创建示例应用
本章的示例应用将提供一些基本功能,我将通过实现文件契约来增强这些功能。我要建立一个简单的相册应用,它的初始化身将让用户从文件系统中选择图像文件添加到相册中。每个图像的缩略图将显示在一个ListView
UI 控件中(我在第十五章中描述过)。我将使用Windows.Storage.AppCache
名称空间中的对象缓存用户选择的文件位置(我在第二十三章中描述过),这将使用户的文件选择持久化。
我使用Blank App
模板创建了一个名为PhotoAlbum
的新 Visual Studio 项目。您可以在清单 24-1 中看到default.html
文件的内容。
清单 24-1 。来自相册 app 的 default.html 文件
`
` `**
** `该应用将使用标准内容导航模型,通过使用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
文件中创建的ViewModel
和App
名称空间中的对象来实现相册的基本功能,我将在下一节中演示。switch
语句可能看起来有点奇怪,因为它只有一个default
块,但是我将在本章中为不同类型的激活事件添加处理程序。
测试示例应用
为了测试这个例子,启动应用并点击Open
按钮。导航到一个有一些图像文件的文件夹(我使用了我在前面章节中创建的Pictures/flowers
文件夹),用选择器选择一个或多个图像。点击选择器中的Open
按钮打开文件,你会看到缩略图显示在应用布局中,如图图 24-1 所示。
图 24-1。将图像载入示例应用
从 Visual Studio Debug
菜单中选择Stop Debugging
停止应用,然后再次启动应用。由于文件位置缓存在futureAccessList
对象中,你会看到你之前加载的图像再次显示。
对于示例应用来说,这是一个很长的设置过程,但是它为我实现文件契约提供了一个很好的基础,现在我可以用最少的额外代码来实现它。
添加图像
我在这个示例应用中使用了许多图像,所有这些图像都可以在 Visual Studio 项目的 images 文件夹中找到。前两个图像被称为jpgLogo.png
和pngLogo.png
,我将在实现文件激活契约时使用它们。你可以在图 24-2 中看到这些图像。这两个文件显示相同的图像,但背景不同。
图 24-2。将在文件激活契约中使用的图像
我还将 Visual Studio 在images
文件夹中创建的所有默认图像替换为具有相同尺寸的图像,但显示的图标与图中的图像相同。
对于logo.png
、slashscreen.png
和storelogo.png
文件,我添加到项目中的图像在透明背景上显示一个白色图标,这意味着它们不可能在白色页面上显示——但是如果你想象一下图 24-2 中的一个图像没有彩色背景,你就会明白了。
我已经删除了smalllogo.png
文件,用一个名为small.png
的 30×30 像素文件代替,它以白色显示相同的图标,但背景为黑色,看起来和图 24-2 中左边的图像一样。然后我更新了应用清单,为Small logo
字段指定了 small.png 文件,如图 24-3 中的所示。
图 24-3。更改小 logo 文件
这个名称的改变很重要,因为它解决了一个奇怪的错误——我将在本章后面实现文件激活契约时解释这个问题。
提示你可以在本书附带的源代码下载中找到所有这些图像文件,从
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 中看到助手应用,它显示了我在前面章节中使用的一个样本图像。
图 24-4。文件助手应用
实现文件激活契约
文件激活契约允许您声明您的应用愿意并且能够处理某种类型的文件。为了理解我的意思,打开桌面,使用文件浏览器找到一个 PNG 或 JPG 文件——前几章中的花卉图片是理想的。右键单击该文件并选择Open with
菜单,您将看到一个应用列表,包括桌面和 Windows 应用商店应用,可以打开该文件。你可以在图 24-5 中看到在我的台式电脑上打开 PNG 文件的应用。
图 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 所示。
提示通过点击
Add New
按钮,您可以在同一个声明中支持多个文件扩展名,这将在表单中添加一个新的Supported File Type
部分。我使用了两个声明,因为我希望每种文件类型有不同的图像和描述性文本。
图 24-6。向应用清单添加文件类型关联声明
当您使用 Visual Studio 启动应用时,应用包会安装到设备上,这包括创建 Windows 文件关联。尽管我只实现了契约的一部分,但您已经可以看到文件关联的效果:启动应用,然后导航到桌面(最简单的方法是键入Control+D
)。打开Explorer
并找到任何 PNG 或 JPG 文件。如果右击文件并选择Open with
菜单,你会看到PhotoAlbum
app 已经添加到列表中,如图图 24-7 所示。(显示的名称取自清单的Application UI
部分的Display name
字段——我更改了这个值,在单词Photo
和Album
之间添加了一个空格。)
图 24-7。浏览器中与文件类型相关联的应用
注意当我创建示例应用时,我特意更改了在
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
控件并缓存文件位置,以便应用持久工作。
注意注意,处理文件激活事件的
case
块一直到default
块。这意味着albumView.html
文件将用于为file
事件以及常规发布事件提供内容。你可以在第十九章中了解更多关于发布活动的信息。
要测试事件的处理,启动应用,导航到桌面,右键单击 PNG 或 JPG 文件。选择Open with
菜单项,点击列表中的Photo Album
。您打开的文件的缩略图将被添加到布局中的ListView
,与文件的displayName
值一起显示。
提示如果没有得到正确的结果,那么右击开始屏幕上的
Photo Album
应用图标,选择Uninstall
,然后从 Visual Studio 再次启动应用。当应用重新启动时,Windows 并不总是会选择更改,卸载应用包可以解决这个问题。
将应用设为默认处理程序
我还想描述文件激活处理程序的另一个方面。为此,您需要将应用设置为文件类型的默认处理程序。从 Windows 桌面上,使用文件资源管理器找到并右键单击一个 PNG 文件,选择Open with
Choose default program
菜单项。你会看到一个弹出窗口,如图图 24-8 所示。(该应用显示有我在清单的应用 UI 部分中定义的徽标。我使用了与文件关联相同的图标,但是背景是透明的。)选择Photo Album
项,使示例应用成为默认处理程序,然后对 JPG 文件重复这个过程。
图 24-8。让应用成为 PNG 文件的默认处理程序
这不会改变应用的行为方式,但它确实意味着 Windows 将使用我在本章前面添加到应用的图像,这些图像被指定为文件关联声明的一部分,作为向用户显示 PNG 和 JPG 文件时的文件图标。你可以在图 24-9 的中看到这是如何出现的。
图 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 中的清单。
图 24-10。将文件保存提货人契约声明添加到应用清单
测试保存选择器声明
仅仅进行清单声明就将应用定义为一个保存位置,尽管我还没有实现作为契约一部分的激活事件。现在看看这是如何工作的,将更容易理解应用为实现这个契约而必须做的其余工作。
测试 save picker 契约声明需要使用我在本章开始时创建的FileHelper
应用以及PhotoAlbum
本身,并且需要几个步骤。
首先启动PhotoAlbum
app。该应用不需要运行到保存位置,但 Visual Studio 会在启动时安装该应用,这将向 Windows 注册清单声明中定义的文件类型。点击Open
按钮,选择一些图像,这样应用中就会有一些内容。
其次,启动我在本章开始时创建的FileHelper
应用,点击Open
按钮,选择一个图像文件——最好是你刚才没有选择的文件。点击Save
按钮。该应用将显示文件保存选择器。如果你点击Files
链接旁边的箭头,你会看到一个保存目的地列表,包括Photo Album
,如图图 24-11 所示。
图 24-11。在选取器中显示为保存位置的应用
如果从列表中选择Photo Album
,你会看到类似于图 24-12 所示的布局。这是文件选择器中显示的PhotoAlbum
应用布局的奇怪组合。
完成实现契约所需的工作是双重的:我需要更新应用,以便在选择器中显示更有用的布局,并支持从FileHelper
应用保存文件。在下一节中,我将向您展示如何做到这两点。
图 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 所示。
注意要明确的是,我能够像这样传递对象是因为我在
js/setup.js
文件中定义的函数将WinJS.Navigation.state
属性的值传递给了WinJS.UI.Pages.render
方法。你可以在第七章的中了解更多关于WinJS.Navigation.state
属性和WinJS.UI.Pages.render
方法的知识。
清单 24-12 。pages/savePickerView.html 文件的内容
`
这个文件有两个部分。该标记是自包含的,并向用户提供有用的消息和单行图像缩略图,以显示相册中已有的内容。当收到 activated
事件时,我使用这个标记作为应用布局,数据绑定和ListView
控件向用户提供内容。你可以在图 24-13 中看到结果。(我通过重启PhotoAlbum
应用,切换到FileHelper
应用,点击Save
按钮,这样我就可以从文件位置列表中选择PhotoAlbum
了。)
图 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 中描述的属性。
在示例中,我使用了title
属性来指定字符串Save to Photo Album
,这可以在图 24-13 中看到。当您提供的存储存在某种层次结构时,此属性非常有用,因为您可以使用它来指示文件的保存位置。FileSavePickerUI
对象还定义了两个事件,其中一个我在例子中使用了。您可以在表 24-4 中查看这些事件的详细信息。
我对这一章感兴趣的是targetfilerequested
事件,因为它向应用发出用户想要保存文件的信号。事件的处理程序被传递了一个Windows.Storage.Pickers.Provider.targetFileRequestedEventArgs
对象。这个对象只定义了一个名为request
的属性,这是完成契约所需要的。请求属性返回一个Windows.Storage.Pickers.Provider.TargetFileRequest
对象,该对象定义了表 24-5 中显示的属性。
一个简单的操作涉及到很多对象。当您接收到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 中看到这个效果,它显示了一个图像添加到应用布局中。
图 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 中所示的清单。
图 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 文件的内容
`
该文件定义的布局非常类似于我用于保存选择器契约的布局——一个WinJS.UI.ListView
UI 控件显示相册中当前的图像。用户将选择文件来挑选它们。
要看到这种布局,您需要经历一系列特定的事件。首先,重启PhotoAlbum
应用——open picker 应用不需要运行才能工作,但您需要重启应用,以便 Visual Studio 通知 Windows 您的应用已声明支持 open picker 合约。
现在切换到FileHelper
应用,点击Open
按钮显示一个打开的拾取器。点击位置标题旁边的箭头,你会看到PhotoAlbum
被列为文件来源,如图图 24-16 所示。
图 24-16。显示为要打开的文件源的示例应用
如果选择列表中的PhotoAlbum
项,将触发激活事件,显示新的布局,如图图 24-17 所示。
图 24-17。使用开放选取器契约打开文件
选择其中一个文件并点击Open
按钮,您将看到您选择的图像显示在FileHelper
应用中。
从PhotoAlbum
应用的角度来看,激活事件的detail.fileOpenPickerUI
属性返回的对象是一个Windows.Storage.Pickers.Provider.FileOpenPickerUI
对象,它的工作方式与保存选取器契约的相应对象略有不同。为了演示它是如何工作的,我将分解对象并依次讨论它的属性、方法和事件。
由FileOpenPickerUI
对象定义的属性显示在清单 24-6 中,它们用于获取显示给用户的拣选器信息,并设置一些基本的配置选项。
selectionMode
属性对你呈现给用户的布局影响最大,因为它表明试图打开文件的应用是接受一个文件还是多个文件。该属性返回由Windows.Storage.Pickers.Provider.FileSelectionMode
对象定义的值之一,我已经在表 24-7 中列出了这些值。
确保您在打开的文件选择器中呈现给用户的布局遵循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 中描述。
当调用addFile
方法时,传递一个代表用户选择的StorageFile
和一个代表文件的惟一 ID。然后,您可以使用此 ID 来检查该文件是否已经是选择的一部分,或者将其从选择中删除。没有方法来枚举选择器中的文件,这是一个问题,因为selectionchanged
事件并没有指出选择中发生了什么变化。这意味着我必须在每次用户改变ListView
选择时清除选择的文件,并为每个选择的项目添加新的条目,以确保我没有在选择器中留下任何已被取消选择的文件。
除了调用addFile
方法之外,不需要任何显式操作。你传递给addFile
的StorageFile
对象被交给为用户打开文件的应用,所以你唯一的义务就是确保StorageFile
对象与用户的选择相对应。通过监听FileOpenPickerUI
定义的事件,你可以更深入地了解挑选过程,我已经在表 24-9 中描述了这些事件。
总结
在这一章中,我向你展示了如何实现三个关键契约:文件激活、保存选择器和打开选择器。这些契约允许您将应用集成到 Windows 中,以便您可以代表用户处理文件,并为其他应用提供存储服务。在下一章中,我将向您展示共享契约,这是 Windows 8 的一项关键功能。
二十五、共享契约
共享契约允许用户在应用之间共享数据项。这是 Windows 应用的关键功能之一,它允许用户使用互不了解且仅共享相同数据格式的应用创建临时工作流。
需要两个应用参与共享契约。作为共享源的应用拥有用户想要共享的一项或多项数据。Windows 向用户提供了一个应用列表,称为共享目标,能够处理这些数据,用户选择他们想要从共享源接收项目的应用。在这一章中,我将向您展示如何在契约中创建两个参与者,并演示如何简化重复的操作,使用户的共享更简单。表 25-1 对本章进行了总结。
创建示例应用
我将从创建一个共享源开始,这是一个提供数据供另一个应用使用的应用。我创建了一个简单的助手应用,名为ShareHelper
。我将介绍不支持共享的基本应用,然后展示如何实现共享源代码功能。清单 25-1 显示了来自ShareHelper
应用的default.html
文件。
清单 25-1 。来自 ShareHelper 应用的 default.html 文件
`
这个应用的布局由一个大的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 所示。
图 25-1。声明访问图片库
如果你启动应用,你会看到你的Pictures
文件夹中的图片显示在ListView
中,如图图 25-2 所示。
图 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 中描述的事件。
注意在本章中,我引入的所有新对象都在
Windows.ApplicationModel.DataTransfer
名称空间中,除非我另有说明。
要为这些事件注册一个处理函数,必须对由DataTransferManager.getForCurrentView
方法返回的对象调用addEventListener
方法。(如果你是一名. NET 程序员,这是另一个其结构更有意义的对象,但是只要你记得调用getForCurrentView
,它在 JavaScript 中工作得非常好。)
对事件做出反应
我对本例中的datarequested
事件感兴趣,该事件在用户激活 Windows Share Charm 时触发,提示我准备要共享的数据。传递给datarequested
事件处理程序的对象有点奇怪,因为request
属性,而不是detail
属性,包含服务于共享操作所需的信息。
request
属性返回一个DataRequest
对象,用于创建一个数据包,该数据包将与另一个应用共享。我总结了表 25-3 中DataRequest
对象定义的方法和属性。
当您接收到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
添加数据的最有用的方法。
您可以使用表中所示的方法将数据添加到将与其他应用共享的包中。您的包可以包含不同类型的数据,但是如果您两次调用相同的方法,则第二次传递的数据将替换包中已有的数据。
提示你不必事先指定你的数据包的内容——你可以在
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();
});
...`
注我在这个数据包上使用
setUri
方法的方式上故意引入了一个问题。在这一章的后面,我会回来解释这个问题以及为什么会经常遇到这个问题——所以,在你阅读完这一章的其余部分之前,不要使用setUri
方法。
描述数据
DataPackage
对象定义了一个名为properties,
的属性,该属性返回一个DataPackagePropertySet
对象(由于术语属性出现得如此频繁,本节中的对象和成员名称可能有点令人困惑)。
您使用DataPackagePropertySet
来提供关于共享操作及其数据的附加信息,我已经在表 25-5 中描述了该对象定义的属性。
这些属性不是为您设置的,尽管其中一些属性——如applicationListUri
和fileTypes
——看起来应该设置。另一方面,您不需要为这些属性中的任何一个提供值,所以您只需要处理那些有助于用户理解共享操作的属性。
在我的例子中,我已经为其中三个属性设置了值,如清单 25-7 所示。当我在本章稍后将PhotoAlbum
应用设为共享目标时,我将使用这些值来显示共享操作的细节,在下一节中您可以看到 Windows 是如何使用它们的。
清单 25-7 。用属性描述数据包
... datapackage.properties.title = "Share Images"; datapackage.properties.description = "Images from the Pictures Library"; datapackage.properties.applicationName = "ShareHelper"; ...
提示您也可以使用
insert
方法向数据包添加自定义属性。在本章后面的添加自定义属性部分,我将向您展示一个这样的例子。
测试共享源代码应用
为了测试数据共享,使用 Visual Studio Debug
菜单中的Start Debugging
项启动ShareHelper
应用,并在ListView
中选择一个或多个图像。
打开魅力条,选择分享魅力(可以直接用Win
+ H
激活分享魅力)。您将看到共享弹出按钮,其中包含可以处理由ShareHelper
应用准备的数据包的应用列表,如图图 25-3 所示。
图 25-3。挑选应用的哪些数据将与哪些数据共享
我已经展开了共享弹出按钮的一部分,以便您可以看到我为数据包指定的属性的效果,以及它们是如何显示给用户来描述数据的。
图中有三个 app 可以从ShareHelper
app 接收数据:Mail
、People
和SkyDrive
。如果你在系统上安装了其他应用,你可能会看到其他条目,但请注意,列出的所有应用都是 Windows 应用商店应用,共享契约仅适用于应用,不包括传统的 Windows 桌面程序。
了解常见的共享问题
Windows 根据创建共享数据包时调用的set
<XXX>
方法选择处理共享数据的应用。我调用了setStorageItems
和setUri
,所以 Windows 正在寻找可以处理文件或 URL 的应用。
关键词是或——应用只需要声明支持包中任何一种类型的数据,就可以成为合适的共享目标。应用可以自由地从共享包中获取它们支持的数据,而忽略其他任何东西。
在内置 Windows 应用的情况下,Mail
应用(一个电子邮件客户端)将使用文件作为新电子邮件消息的附件,People
应用(一个社交媒体工具)将与我的社交媒体联系人共享 URL,SkyDrive
应用将图像文件保存到云存储中。
这是我在清单 25-6 中制造的问题。我使用setUri
向包中添加了一个 URL,它向用户提供了一些额外的信息,但与用户试图共享的内容没有直接关系。然而,如果用户选择People
或Mail
应用作为分享目标,那么图像文件——真正的内容——将被丢弃,链接将被给予比它应有的更重要的对待。用户的联系人将被发送一个 URL,而没有用户试图共享的图像所提供的任何上下文——这对用户和消息的接收者来说是一个非常混乱的结果,可悲的是,这是共享数据时常见的错误。
如果您从“共享”弹出菜单中选择Mail
应用,您就可以发现问题。Mail
应用将显示在一个小的弹出菜单中,允许你通过选择收件人和添加一些文本来完成电子邮件,如图图 25-4 所示。
提示您需要在本地机器上执行这个测试,因为
Mail
应用无法在 Visual Studio 模拟器中正常启动。
图 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
中选择一些图像,并再次激活共享魔咒,你将在目标列表中只看到Mail
和SkyDrive
应用——这是因为People
应用不能处理共享数据包中的文件,因此不会被 Windows 选为数据的目标。如果您选择Mail
应用,您会看到用户选择的图像文件已经作为附件添加到消息中,如图图 25-5 所示。
图 25-5。用邮件应用分享文件
创建共享目标
在这一部分,我将通过把我在第二十四章的中创建的PhotoAlbum
应用变成一个共享目标并允许它接收包含文件的数据包来展示共享契约的另一面。
提醒一下,PhotoAlbum
应用的基本功能允许用户选择要在简单相册中显示的图像文件,该相册显示为在WinJS.UI.ListView
控件中显示的一系列缩略图。
在前一章中,我在此基础上实现了文件激活、保存选取器和打开选取器合约,展示了应用集成到操作系统的不同方式——这是我将在本章分享合约中继续讨论的主题。提醒一下,图 25-6 显示了PhotoAlbum
显示图像时的样子。
图 25-6。相册示例 app
我不打算重新列出示例应用的代码和标记,因为你可以在第二十四章中找到它们,或者从Apress.com
下载该项目作为源代码包的一部分。我知道不得不前后翻页到另一章会令人沮丧,但另一种选择是花 10 页列出你已经看过的代码,我宁愿用这些空间向你展示新的契约和特性。
更新清单
与共享源不同,共享目标必须在清单中声明它们的功能。这是有意义的,因为共享源需要自由地共享用户正在处理的任何数据,而共享目标将有预先确定的方式来处理一组数据类型。
对于这一章,我将增强PhotoAlbum
应用,使其能够处理包含.jpg
和.png
文件的数据包。为此,从 Visual Studio 的Solution Explorer
窗口打开package.appxmanifest
文件,并导航到Declarations
部分。从Available Declarations
列表中选择Share Target
并点击Add
按钮。与其他契约一样,显示附加细节的表格,如图图 25-7 所示。显示红色警告是因为我还没有填充表单。
图 25-7。将份额目标申报添加到清单中
声明数据格式
你可以用两种方式之一来表达你对共享目标的支持。第一种是使用数据格式,这可以通过单击清单表单的Data formats
部分中的Add New
按钮来完成。当你点击这个按钮时,你会看到一个Data format
字段,你可以在其中输入你的应用支持的数据格式。
Data format
字段接受与DataPackage
对象中的方法相对应的值,我在本章前面已经描述过了。表 25-6 提供了共享源可以使用的DataPackage
对象中的方法和相应的Data format
值之间的快速参考,共享目标必须在清单中声明这些值才能接收那种包。
提示请记住,如果正在共享的数据包包含至少一种您声明的类型,Windows 会将您的应用包括在呈现给用户的共享目标列表中。您不需要声明
Data format
值的精确组合来接收包含多种类型的包。同样,你不应该声明支持任何你的应用不能使用的数据类型。
如果您想支持多种类型的数据,比如文件和 URL,再次点击Add New
创建额外的Data format
字段,并从表格中输入适当的值。完成后,键入Control+S
保存清单更改。
声明文件类型支持
您还可以通过使用清单的Supported file types section
指定单个文件类型来声明对共享目标联系人的支持。这允许您支持包含您的应用可以处理的文件类型的数据包,而不是任何文件(这是StorageItems
数据格式的效果)。
区别很重要。例如,Mail
应用想要处理文件,但它并不关心它们是什么,因为每个文件都适合作为电子邮件的附件。我需要对PhotoAlbum
应用更有选择性,因为我只支持图像文件——例如,我无法在相册应用中使用 Excel 电子表格,因此我需要确保我的应用只是 PNG 和 JPG 文件的共享目标。
提示我将向您展示清单声明的两个部分是如何分别工作的,但是您可以在同一个声明中使用这两个部分来支持数据类型和文件类型的混合。您必须为此联系人指定至少一种文件类型或数据格式,否则您可以随意组合。
要为PhotoAlbum
应用设置清单声明,请单击Add New
按钮创建一个新的File type
字段,并输入.png
文件扩展名(包括句点)。再次点击Add New
按钮,在File type
字段输入.jpg
。清单应该类似于图 25-8 中所示的清单。当您添加了第一种文件类型后,红色警告标记将会消失,告诉您清单声明是有效的。
图 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
文件夹中。在接下来的小节中,我将向您展示这个文件的内容。
注意需要注意的重要一点是,你的应用的一个新实例被启动来处理
shareTarget
事件。这意味着如果您的应用已经在运行,那么将会创建第二个实例。您将需要提供某种协调,以便用户启动的应用实例反映 Windows 启动的实例所做的更新,以处理共享数据包。对于这个例子,我将把用户共享的文件复制到本地应用数据文件夹中,PhotoAlbum
应用将监视这个文件夹的变化。
处理共享操作
在清单 25-10 中,您可以看到shareTargetView.html
文件的内容,我创建这个文件是为了给PhotoAlbum
应用添加对共享目标契约的支持。script
元素中的两个关键函数是占位符,我将在本节稍后完成。
清单 25-10 。shareTargetView.html 文件的最初内容
`
即使有不完整的函数,这个文件中仍然有一些重要的事情在进行。对我来说,解释它们的最好方式是向你展示完成后的页面是什么样子,然后再返回。图 25-9 显示了 Windows 用来处理共享数据包的shareTargetView.html
文件。
注意此时您将无法重新创建这个图,因为 default.js 文件中的关键函数尚未实现。
图 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 文件夹后调用reportDataRetrieved
和reportCompleted
方法。
如果我从processPackage
函数得到的WinJS.Binding.List
对象不包含任何我可以操作的文件,我就调用reportError
方法。在理想情况下,我不需要这样做,因为 Windows 已经将包的内容与清单声明中的文件类型进行了匹配,但我将此检查添加到我的共享目标应用中,以处理编写得不好的应用。一些共享源应用错误地设置了数据包中fileTypes
属性的值(如本章前面所述)——该值会覆盖真正正在使用的文件类型,这会导致 Windows 发送不包含任何有用文件的我的应用包。
注意错误呈现给用户的方式有点奇怪。调用
reportError
方法时,共享目标 app 立即关闭。然后向用户显示一条通知消息,告诉他们出现了一个问题。只有当他们点击通知时,他们才能看到您传递给reportError
方法的消息。
处理数据包
ShareOperation.data
属性返回一个DataPackageView
对象,它是数据包的只读版本。您使用这个对象来了解发送给您的包,并获取其中包含的数据。表 25-8 显示了DataPackageView
对象定义的方法。
从包中检索数据的方法都是异步的,并返回一个WinJS.Promise
对象,该对象在完成时产生适当类型的数据。
contains
方法让您检查包是否包含给定的数据类型。你可以将表 25-6 中的一个字符串值传递给这个方法,或者使用StandardDataFormats
对象中的一个值,我已经在表 25-9 中列出了。
你可以在清单 25-12 中看到我是如何使用contains
和getStorageItemsAsync
方法的,它展示了我是如何在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
属性返回一个对象,您可以使用该对象获取由共享源应用添加到数据包中的属性。我读取了applicationName
和 title
属性的值,并检查了ShareHelper
应用添加到包中的自定义属性是否存在。我使用这些值来设置布局中一些 HTML 元素的内容。
提示在告诉 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
应用——你会看到你分享的图片显示出来。
图 25-10。分享来自 Windows Photos 应用的图片文件
在我继续之前,还有最后一点需要注意。我让您在Pictures
库之外定位一个图像文件的原因是,我想演示打包StorageFile
对象的方式将访问这些文件的隐含权限转移到目标应用。一切都按照它应该的方式运行——例如,您不必担心确保目标应用已被授予读取包含该文件的文件夹的权限。
当然,有了这种隐含的许可,就隐含了对您的信任,即您不会对数据做一些意想不到的事情。您应该确保在没有获得用户明确许可的情况下,不要删除或修改原始文件。
创建快速链接
快速链接是一个预先配置的动作,允许用户使用他们之前做出的细节或决定来执行简化的共享动作。在我的PhotoAlbum
应用中,我让用户选择他们想要复制和导入的数据包中的图像。每次用户与PhotoAlbum
共享文件时,强迫用户做出相同的决定是重复和令人讨厌的,尤其是因为他们很有可能已经花时间在 share source 应用中选择了他们想要的图像。
为了简化我的应用,我将创建一个快速链接,允许用户导入所有文件,而无需与我的应用布局进行任何交互。使用快速链接有两个阶段——创建它们和接收它们——我将在接下来的小节中向您展示这两个阶段。
创建快速链接
您可以通过向ShareOperation.reportCompleted
方法传递一个QuickLink
对象来创建一个快速链接,这个对象可以在Windows.ApplicationModel.DataTransfer.ShareTarget
名称空间中找到。配置QuickLink
对象,使其包含应用重复用户刚刚执行的共享操作所需的所有信息。QuickLink
对象定义了表 25-10 中所示的属性。
你可以在清单 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
应用中添加代码来处理快速链接——我将在下一节中做这件事——但是图中显示了快速链接是如何呈现给用户的。
图 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 提供了本章的总结。
重温示例应用
对于这一章,我将继续使用我在第二十四章中创建并在第二十五章中扩展的PhotoAlbum
应用。作为一个提醒,这个应用的基本功能让用户选择要在一个简单的相册中显示的图像文件(这只是一系列显示在WinJS.UI.ListView
控件中的缩略图)。
我在此基础上实现了文件激活、保存选取器和打开选取器,以及共享合约,展示了应用集成到操作系统的不同方式——这是我将在本章中通过添加对更多合约的支持来继续的主题。提醒一下,图 26-1 展示了PhotoAlbum
显示图像时的样子。
图 26-1。相册示例 app
注意再次声明,我不打算重新列出示例的代码和标记,因为你可以在第二十四章中找到它们,或者从
Apress.com
下载该项目作为源代码包的一部分。
实施自动播放契约
自动播放契约允许您的应用在新存储连接到 Windows 8 设备时自动做出响应。AutoPlay 近年来已经失宠,因为它一直被用作让个人电脑感染恶意软件的手段,但针对 Windows 8 进行了改进,并有可能被更广泛地使用,特别是在用户喜欢简单而不是安全的平板电脑设备上(一般来说,我发现如果有机会,用户会更喜欢任何东西而不是安全)。
像所有的契约一样,实现自动播放是可选的,但却是值得的,尤其是当你的应用以任何方式处理媒体文件的时候。正如你将看到的,自动播放契约建立在我在第二十四章中展示的文件激活契约的基础上,一旦你实现了文件激活,只需要一点额外的工作。
更新清单
自动播放契约需要进行两项清单更改。首先,我需要声明我的应用想要访问可移动存储。为此,从 Visual Studio 的Solution Explorer
窗口中打开package.appxmanifest
文件,点击Capabilities
选项卡,在功能列表中勾选Removable Storage
项,如图图 26-2 所示。
图 26-1。在应用清单中启用移动存储功能
此外,我还必须告诉 Windows 我的应用希望如何集成到自动播放功能中,这需要移动到清单的Declarations
选项卡。
从Available Declarations
列表中选择AutoPlay Content
,点击Add
按钮。填写属性部分以匹配图 26-3 ,点击Add New
按钮创建新的Launch action
部分。
图 26-3。宣布支持自动播放契约
很难阅读图像中的文本,所以我在表 26-2 中列出了清单表单字段的必需值。
Content event
值是您希望应用得到通知的 Windows 事件的名称。对于我的例子,我对ShowPicturesOnArrival
、MixedContentOnArrival
和StorageOnArrival
事件感兴趣。这些是而不是 JavaScript 事件,声明中的条目作为 Windows 内部和你的应用之间的映射——我将很快向你展示这些事件是如何呈现给你的应用的。确保如我所展示的那样将事件名称大写——如果您采用 JavaScript 全小写约定,您的契约实现将会失败。表 26-3 列出了最常见的 Windows 自动播放事件。
要弄清楚每种事件被触发的环境,需要对 Windows 进行一些研究。我发现在广泛的事件中注册兴趣更容易,这就是为什么如果用户连接包含图像的存储设备,我会为我可以预期的每个事件创建声明。
verb
值是一个特定于应用的字符串,您将使用它来标识 Windows 事件。我的示例应用只能将文件添加到相册中,这就是为什么我的动词是addpictures
、addmixed
和addstorage
,以及为什么Action Display Name
,它是给用户的应用将通过自动播放执行的动作的描述,在每种情况下都被设置为Add to Photo Album
。一个更复杂的应用可能会为每个事件提供多个操作,例如,播放、复制、打印或压缩存储设备上的文件。
响应激活事件
自动播放契约的激活事件的detail.kind
属性被设置为ActivationKind.file
。这与用于文件关联契约的值相同。通过读取detail.verb
属性的值来区分事件相关的契约,对于文件关联,该属性将被设置为open
,对于自动播放,该属性将被设置为您分配给清单声明的verb
字段的值。对于我的例子,这意味着我可以预期文件关联的verb
属性是open
,自动播放契约的属性是addpictures
、addmixed
或addstorage
,这取决于哪个 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 文件的内容
`
该文件呈现的布局基于一个ListView
控件,在该控件中,我显示我在存储设备上找到的图像,允许用户选择应该导入到相册中的图像。还有一个button
,用户点击它表示他们已经选择了想要的文件。
注意我可以使用一个标准的打开文件选择器,让用户选择他们想要从存储设备导入的文件,但是没有办法禁用选择器导航控件。这意味着用户可以离开存储设备,从任何地方挑选文件。这并不总是所有应用的问题,但我想在这个例子中限制用户的选择,这就是我使用自定义布局的原因。
这个文件中的代码是我在前面章节中使用的技术的复述,并做了一些调整以适应自动播放契约。来自激活事件的detail.files
属性的值作为folders
参数传递给ready
函数——我更改了名称,因为当自动播放契约的事件被触发时,detail.files
属性返回的数组实际上包含了StorageFolder
对象。通常只有一个文件夹,它是可移动存储设备的根目录。我处理数组中的所有项目,只是为了预防可移动设备的不同行为(微软对于是否会有多个文件夹被发送到应用非常含糊。)
为了安全起见,我查询了数组中的每个文件夹,并构建了我在一个WinJS.Binding.List
对象中找到的文件的详细信息,我将它用作ListView
控件的数据源。此时,我不想将存储设备的内容与应用中的其余图像混合在一起,这就是为什么我在本地将List
定义到这个文件中,而不是使用视图模型中的那个。
提示 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 中所示。
图 26-4。配置自动播放测试契约执行
接下来,插入一些包含 JPG 或 PNG 文件的可移动存储设备,如 u 盘或相机存储卡。我在测试中使用了 u 盘,但几乎任何可移动存储设备都可以。你会看到一个弹出的提示信息,就像图 26-5 中的所示。
图 26-5。自动播放祝酒词
单击 toast,告诉 Windows 您要在存储设备上执行什么操作。窗口将显示一组选项,如图 26-6 所示。根据您安装的应用,您可能会看到不同的选项。我突出显示了您应该选择的操作,这显示了示例应用和我在前面的清单中指定的消息。
图 26-6。选择可移动硬盘的自动播放动作
点击Add to Photo Album
项,Windows 将启动示例应用。激活事件将加载autoPlayView.html
文件作为布局,查询存储设备的文件内容,找到的图像文件在ListView
控件中呈现给用户,如图图 26-7 所示。
图 26-7。通过呈现可移动存储设备上的图像来响应自动播放事件
选择图像并点击Add Selected Images
按钮会将图像复制到本地应用数据文件夹,并将布局切换到pages/albumView.html
文件。
测试提示
当您测试 AutoPlay 契约的实现时,插拔存储设备可能会变成一个乏味的过程。你可以做一些事情来简化这个过程。第一个是在自动播放控制面板中为您正在使用的设备类型更改设置,如图图 26-8 所示。您可以看到示例应用包含在应用列表中,当连接新的存储设备时,可以选择该列表作为默认应用。
图 26-8。将示例应用设置为可移动驱动器的默认自动播放动作
您可以通过插入硬件一次,然后在每次想要测试时使用文件资源管理器触发自动播放操作,来避免完全处理硬件。在文件浏览器窗口中右键单击设备,在弹出菜单中选择Open AutoPlay
,如图 26-9 所示。
图 26-3。无需移除和连接存储设备即可触发自动播放操作
这些技术使测试自动播放契约实现成为一种更加愉快的体验,尤其是当您处理大量不同的事件和动词时。
实现协议激活契约
协议激活契约让你的应用处理标准的 URL 协议,比如mailto
,它用于启动创建和发送电子邮件的过程。该合约还可用于处理自定义协议,这些协议可用于执行应用之间的基本通信,或在您的网站和 Windows 应用之间移交任务(您在网页中嵌入了具有特定协议的链接,当用户单击该链接时,该链接会激活应用)。
在本节中,我将演示如何处理自定义协议,并使用它将数据从一个应用传递到另一个应用。
创建助手应用
首先,我需要创建助手应用,它将向用户呈现使用我的自定义协议的链接。我创建了一个名为ProtocolHelper
的新应用,这个应用非常简单,所有的东西 HTML、CSS 和 JavaScript——都包含在default.html
文件中,如清单 26-3 所示。
清单 26-3 。来自 ProtocolHelper 应用的 default.html 文件
`
这个应用执行对Pictures
库的深度查询,并显示它找到的第一张图像的缩略图。该布局还包含一个a
元素,其href
属性包含一个带有自定义协议的 URL,如下所示:
<a id="linkElem" **href=”photoalbum:C:\Users\adam\Pictures\flowers\astor.jpg”**>Protocol Link</a>
协议设置为photoalbum
,网址的其余部分包含助手应用找到并显示的文件路径。在图 26-10 中可以看到助手应用运行时的样子。
注意为了确保助手应用可以找到文件,您需要检查清单中的
Pictures Library
功能。
图 26-3。protocol helper app 的布局
点击Protocol Link
会让 Windows 寻找一个可以处理自定义协议的应用。当然,目前还没有这样的应用,所以你会看到一个类似于图 26-11 所示的消息。在下一节中,我将向您展示如何在PhotoAlbum
应用中添加协议激活支持。
图 26-11。没有应用处理 URL 协议时显示的消息
增加协议激活支持
现在我有了一个包含与photoalbum
协议的链接的应用,我可以返回到PhotoAlbum
应用并添加对处理该协议的支持,这意味着当你单击助手应用中的链接时,PhotoAlbum
将被激活并被赋予计算出该做什么的任务。
添加清单声明
第一步是更新清单。从 Visual Studio Solution Explorer
窗口打开package.appxmanifest
文件,并导航到Declarations
选项卡。从Available Declarations
列表中选择Protocol
,点击Add
。将显示文本字段,供您输入想要支持的协议的详细信息。在Name
字段中输入photoalbum
并键入Control+S
保存对清单的更改。(在本例中,您可以忽略Logo
和Display name
字段,它们用于区分支持相同协议的应用,这种情况下不会发生。)你可以在图 26-12 中看到完整的清单部分。
图 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
库中找到的文件的路径。除了schemeName
和path
之外,Windows.Fountation.Uri
对象还有许多有用的属性,我已经在表 26-4 中列出了它们,以及如果使用的 URL 是[
www.apress.com/books/index.html](http://www.apress.com/books/index.html)
时它们将返回的值(一个虚构但有用的 URL 来演示)。
综上所述,您可以看到我通过调用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 应用打印是基于将应用的当前布局发送到打印机,这意味着很容易获得基本的打印工作,但需要更多的努力才能产生有用的东西。我将首先向您展示基本的打印机制,然后向您展示如何创建仅用于打印的内容。
注正如你所料,你需要一台打印机来跟踪本章的这一节。您不需要实际打印任何东西,但是您需要有一个 Windows 识别为打印机的设备,以便它出现在 Devices Charm 中。
实现基本打印
印刷契约不需要清单声明。相反,您注册一个函数来处理由可以在Windows.Graphics.Printing
名称空间中找到的PrintManager
对象发出的printtaskrequested
事件。此事件表示用户已发起打印请求,您可以相应地准备您的应用。
为了演示契约是如何操作的,我在albumView.html
文件中添加了打印支持,这是PhotoAlbum
应用用来显示它所编目的图像的内容。您可以在清单 26-5 中看到我所做的更改,我将在接下来的章节中介绍我所使用的对象。
注意我在本章这一部分介绍的新对象都在
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
时呈现给用户的消息,表示此时没有要打印的内容。
图 26-13。接收到 printtaskrequested 事件时调用 createPrintTask 方法的效果
如图所示,我有几台旧的惠普打印机可供选择,当然,您在列表中看到的会有所不同。
配置打印任务
一旦创建了PrintTask
对象,就可以对其进行配置,为用户提供打印任务的合理初始值。您可以通过返回一个PrintTaskOptions
对象的PrintTask.options
属性来实现。您可以为打印任务配置许多选项,但我不打算在此列出。首先,它们中的许多是大多数应用不会关心的小众设置,其次,用户可以在打印过程的下一阶段设置它们(我将很快谈到)。
相反,我在表 26-5 中列出了四个配置选项的示例。其中两个在很多情况下都很有用,另外两个说明了您对打印任务的控制级别。
许多配置选项采用由Windows.Graphics.Printing
名称空间中的对象定义的值。因此,例如,orientation
属性被设置为由PrintOrientation
对象定义的值之一,这可以在表 26-6 中看到。
我不想让你觉得我没有必要跳过这一部分。我想谈谈打印契约的核心内容,即准备您的应用内容,以便您获得良好的打印结果。你可以在打印任务上设置太多的细节,如果我把所有的细节都列出来,我会没有空间,而且这些设置中的大部分从来没有被使用过。相反,你可以看到我是如何应用清单 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;** }; }); ...
注意我在这里走了一条捷径——这是我在真正的应用中不推荐的。如果你查看表 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 中所示。
图 26-14。完成打印任务
Windows 向用户显示了内容的预览,以及更改打印任务设置的机会(More settings
链接允许用户查看和更改我前面提到的更神秘的配置选项)。如果你继续打印这个文档,你会得到如图图 26-15 所示的结果。(我使用保存图像的打印机驱动程序捕捉到了这一点,这使我可以向您显示结果,而不必打印到纸上,然后再次扫描页面。)
图 26-15。打印输出
我添加了此图所示的边框,因为 Windows 所做的更改之一是更改打印任务中的背景颜色,以便打印图像时不会消耗掉用户所有的墨水/碳粉。
这是我所做的唯一更改,否则,打印输出将与创建打印任务时应用的布局相匹配。这是好的,因为这意味着在应用中支持基本的打印很简单,这也是坏的,因为这意味着布局中的所有东西——包括按钮和滚动条——都被发送到打印机。在接下来的部分中,我将向您展示控制发送到打印机的内容的不同技术。
操作应用布局进行打印
改善打印效果的第一种方法是临时操作专门用于打印作业的应用布局。你可以使用 CSS,当然也可以使用 JavaScript,我会在本章的这一节向你展示这两种方法。
使用 CSS 操作应用布局
CSS 的一个鲜为人知且不常使用的功能是能够创建仅适用于特定类型媒体的样式。这意味着我可以轻松地向我的albumView.html
文档添加一个style
元素,在打印时改变内容的布局。你可以在清单 26-9 中看到一个简单的例子,在这里我创建了一个样式,隐藏了Open
和Clear
按钮,并为打印执行了一些其他小的布局调整。
清单 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 中看到的变化。(我又一次给这个图加了边框。)
图 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 中描述过,这些事件可以用来跟踪打印进度。
为了恢复布局,我对completed
事件感兴趣,我通过调用App.adaptLayout
函数来响应该事件。我已经提供了这个函数作为addEventListener
方法的一个参数,这意味着它将被传递给 event 对象,该对象具有恢复布局的效果(因为除了 Boolean true 之外的任何值都被作为将图像恢复到List
的请求):
... printTask.addEventListener("**completed**", App.adaptLayout); ...
您可以在图 26-17 中的打印结果上看到这些变化的结果。
图 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 中看到。
注意我为
PrintTask.completed
事件注册了一个处理程序,当打印任务完成或取消时,它将应用导航回 albumView.html 页面。这样做的一个副作用是,我不得不改变对由PrintManager
对象发出的printtaskrequested
事件的兴趣注册方式。如果您试图使用addEventListener
方法为printtaskrequested
事件添加第二个侦听器,则会抛出异常,并且由于当应用导航回albumView.html
文件时会执行ready
函数中的代码,因此我需要通过向onprinttaskrequested
属性分配一个新函数来替换现有的侦听器——这确保了最多只有一个侦听器,并且不会遇到异常。
清单 26-13 。printView.html 文件的内容
`
这个文件包含一个非常简单的布局,显示选中的图像及其名称。script
元素中的代码使用从ListView
数据源中选择的项目来配置布局中的元素,然后调用setSource
方法来为 Windows 提供要打印的内容。最后,调用延迟对象的complete
方法,向 Windows 指示异步任务已经完成,可以向用户显示内容预览。你可以在图 26-18 中看到该文件产生的打印结果。
图 26-18。使用专用内容进行打印
从图中可以看出,只需稍加努力,您就可以创建专门用于打印的内容,并且不会受到试图调整主要用于在屏幕上显示的布局的限制。
总结
在这一章中,我已经向您展示了如何实现另外三个契约:自动播放、协议激活和打印。这三者在提供一流的应用体验方面都有自己的位置,你应该考虑实现它们,以便将你的应用更紧密地集成到更广泛的 Windows 体验中。在下一章中,我将向您展示如何控制在 Windows 开始屏幕上为您的应用创建的磁贴。
二十七、使用应用切片
在第四章中,我向你展示了如何设置一个应用在 Windows 开始屏幕上使用的图片。这是磁贴的基本操作,但应用可以更进一步,创建实时磁贴,即使在应用不运行时,也可以通过开始屏幕显示有用的信息。在本节中,我将向您展示如何创建动态切片,以及通过应用更新动态切片的不同方式。
使用动态磁贴有两个基本原因:因为您希望用户更频繁地运行您的应用,或者因为您希望用户不那么频繁地运行您的应用。如果你有一个旨在吸引用户注意力的应用,那么你希望将用户的目光吸引到你的应用磁贴上,并提醒他们你在那里。这对于游戏来说是真实的,例如,你想吸引用户并提醒他们你的游戏比他们通过开始菜单想要做的更有趣或更令人兴奋。在这些情况下,你有责任创建吸引人但不分散注意力的磁贴——这符合你的利益,因为 Windows 8 允许用户禁用令人讨厌的应用的动态磁贴。Windows Store 中的一些早期应用有动态磁贴设计,它们的风格非常激进,以至于我发现自己在移动磁贴,这样我就看不到它们了。
如果你有一个旨在提高用户生产力或工作生活的应用,那么你希望使用磁贴为用户提供关键信息的及时摘要,以便他们可以轻松获得关键事实,而无需启动你的应用,等待初始化,导航到正确的部分,等等。简而言之,你在不启动你的应用的情况下,通过给用户提供他们需要的信息来帮助他们。这需要仔细考虑用户关心什么,并为用户提供改变显示在磁贴上的信息种类的方法。
对于这类 app 来说,你的责任就是创建一个内容及时、明显、准确的磁贴,也就是说当 app 状态发生变化的时候更新磁贴。
我对实时应用磁贴的看法和我对 UI 动画的看法是一样的:如果少用的话,它们是个好东西,但是很快就会变得烦人和分散注意力(你应该总是提供一种方法来禁用它们)。不要误判你的应用对用户的重要性,不要把他们的开始屏幕变成维加斯老丨虎丨机。表 1 提供了本章的总结。
为本章创建示例
我为这一章创建了一个名为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 的初始布局。正如承诺的那样,它非常简单,我将在本章的后面添加额外的按钮来演示其他特性。
图 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.png
、tile150.png
和tile310.png
,你可以在图 2 的清单的Application UI
部分看到我是如何应用这些图像的。
图二。为应用磁贴设置图像
提示当您使用磁贴并更改应用使用的设置时,您可能会发现磁贴图标不会显示在开始屏幕上。我发现右键点击应用,点击应用栏上的
Uninstall
,重新启动应用,往往就能解决问题。有时应用磁贴根本不显示——在这种情况下,键入应用名称的前几个字母来执行搜索,然后按 escape 键返回主开始屏幕;瓷砖通常会出现。如果所有这些都失败了,从模拟器和开发机器上卸载应用,重新启动,并在不启动模拟器的情况下从 Visual Studio 启动应用。
测试示例应用
目前没有太多的功能,但是如果您从 Visual Studio 启动应用,然后切换到开始屏幕,您将能够看到示例应用的静态磁贴。您可以使用Larger
和Smaller
AppBar 命令在正常和宽按钮配置之间切换,您可以在图 3 中看到。
图三。示例 app 的方形宽静态瓷砖
如图所示,我添加到项目中并在清单中应用的图像显示了一个警铃;我为示例应用选择了这张图片,因为它将像一个提醒程序一样创建磁贴更新。我不打算创建提醒逻辑,但我需要一个更新和提醒是理想的主题。
创建动态磁贴
创建动态切片的基本原则包括三个步骤:
- 选择一个 XML 模板。
- 用您的数据填充模板。
- 将填充的 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 内容非常相似,尽管您将在下一节中看到,在处理动态切片时,您不必进行太多的操作。
提示除了
Windows.Data.Xml.Dom
名称空间,你会发现应用也可以使用Windows.Data.Html
和Windows.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 所示。
警告Visual Studio 模拟器不支持实时图块。要测试 live tiles,你必须使用真正的 Windows 8 设备。
图 4。一个基本活瓦
您可以看到,小图标用于实时磁贴更新,以帮助用户识别与磁贴相关的应用。你还可以看到,我所做的更新并不是特别有用——在一个正方形的磁贴中没有太多的空间,当依赖文本时,很难向用户提供有意义的信息。
提示你不能指望用户看到你的磁贴更新。首先,您的应用磁贴可能不在最初显示的开始屏幕上,用户可能不会滚动它以使它变得可见。其次,用户可以使用开始屏幕应用栏禁用实时磁贴。这意味着您应该使用实时磁贴来显示应用本身也提供的信息,而不是将磁贴视为核心应用功能的一部分。
创建更有用的实时互动程序
我的基础磁贴更新还有一个问题,就是不影响宽磁贴。为了解决缺乏实用性的问题并支持所有的图块格式,我将采用不同的方法,如清单 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 显示了微软为该模板提供的描述和图片。
图 5。tileSquareBlock 模板的描述
我不会显示单个提醒的详细信息,因为正如上一个演示所示,在一个狭窄的磁贴上没有空间,所以我调用带有摘要信息的populateTemplateText
方法,如下所示:
... var squareXML = populateTemplateText(getTemplateContent (squareTemplate), **[textMessages.length, "Reminders"]**); ....
模板中text
元素的顺序与它们在模板中出现的顺序相匹配,这使得为模板设置内容非常简单。对于宽磁贴,我选择了tileWideBlockAndText01
模板,你可以在图 6 中看到微软是如何描述的。
图六。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
屏幕。您可以通过选择图块并从开始屏幕应用栏中选择Larger
或Smaller
按钮来切换图块尺寸。
图 7。示例应用的方形和宽瓷砖更新
为你的应用选择正确的模板至关重要。你需要找到一种向用户传达信息的方式,这种方式是有帮助的,并且(可选地)会鼓励他们打开和使用你的应用。
使用图像模板
并非所有的应用都受益于在磁贴中只显示文本。为此,有显示图像或图像和文本混合的模板。还有 peek 模板,在两种显示之间交替,通常是所有图像,然后是文本或文本和图像的混合。清单 11 显示了tileSquarePeekImageAndText02
模板的 XML,它包含了text
和image
元素的混合。
清单 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
文件中的文件名)。
图 8。向 Visual Studio 项目添加图像文件
提示注意,我通过
attributes
属性设置了image
元素中src
属性的值,而不是使用由XmlElement
对象定义的setAttribute
方法。这是因为 Windows 8 的 DOM 支持不一致,调用getElementsByNameTag
有时会返回一个XmlElement
对象的集合,有时反而会返回一个IXmlNode
对象的集合。IXmlNode
对象没有定义setAttribute
方法,所以我必须找到src
属性并使用innerText
属性设置其内容。
更新后,平铺最初显示图像,然后切换到带有动画的文本显示。几秒钟后,这个过程重复进行,如此继续下去。我觉得这种 live tile 有点烦人,但它可能正是你的应用所需要的。您可以在图 9 中看到图块的不同状态。请注意,我用于宽尺寸的模板有五个图像。我不需要调整图像的大小来显示它们;这是作为磁贴更新的一部分自动完成的。
图九。使用包含图像的图块模板
清除瓷砖
有时您需要清除图块的内容,通常是因为显示的信息现在已经过时,并且没有新的或值得注意的内容来代替它。为了演示如何清除文件,我向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 中展示了两种不同的模板类型。
两个模板的内容是相同的,尽管使用正确的模板很重要,以防它们在未来的版本中发生变化。您可以在清单 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 的中,您可以看到应用于示例应用磁贴的数字和字形徽章。我已经展示了应用于静态和动态瓷砖的徽章。
图 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 的初始布局。我将在本章的后面添加额外的按钮。
图 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 所示。
图 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,就像我在本章前面所做的那样。(我将只为方块大小创建通知,但是组合模板的过程与本章的第一部分相同)。
提示通知队列最多可容纳五个通知;如果您添加的项目超过五个,最新添加的项目将会推出旧的项目。
当我来更新磁贴时,差异就出现了。我使用填充的 XML 创建了TileNotification
对象,然后给tag
属性赋值。所有这些都发生在updateTileQueue
函数中,它允许系统区分它必须显示的不同通知。您将很快了解到,您可以通过重用标记值来替换单个通知。结果是,我将三个通知放入队列,磁贴将在它们之间旋转,大约每五秒钟从一个通知切换到另一个通知。您可以在图 13 中看到显示的三个通知。
图十三。在一个磁贴中显示多个通知
注意同样,您需要在本地机器上运行这个示例应用,因为 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 所示。
您可以从清单中看到,我已经将到期时间设置为首次显示通知后的 30 秒。ScheduledTileNotification
对象的时间值是绝对的,这意味着您的Date
对象应该用日、月和年来定义(而不是仅仅用一段时间来表示)。如果您启动示例应用,单击Schedule Notification
按钮,并切换到开始屏幕,您可以看到这些更改的效果。
您看到的确切效果将取决于通知开始显示时磁贴的状态。如果队列中已经有通知,则计划的通知将作为定期轮换的一部分显示。到达到期时间后,计划通知将从队列中删除,仅显示原始通知。
如果图块是静态的(即队列中没有通知),情况会略有不同。通知被显示为磁贴的唯一内容,并且当到期时间到达时,磁贴重置为其静态。您可以在图 14 中看到该示例对通知的影响。
图十四。向磁贴添加预定通知
确定通知是否启用
在本章中,我将向您展示的最后一项技术是确定应用磁贴是否会显示实时更新。应用磁贴的设置由Windows.UI.Notifications.NotificationSetting
对象中的值表示,我已经在表 4 中列出了这些值。
我已经在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 所示。他们将在enabled
和disabledForApplication
设置之间切换。
图 15。启用和禁用单个应用的磁贴通知
如果没有为切片启用通知,则生成通知不会导致任何问题-通知会被直接丢弃。这意味着,如果您想提醒用户图块通知可用,只需检查设置值。对设置值的重要性保持敏感——如果用户只禁用了你的应用的通知(disabledForApplication
值),那么你的通知可能很烦人或者没有为用户提供价值。如果用户已经禁用了所有应用的动态磁贴(值为disabledForUser
),那么他们不太可能为你的应用破例。如果你遇到了disabledByGroupPolicy
值,那就继续——提醒用户通知可用是没有意义的,因为他们不太可能覆盖设置。这在大型企业部署中很常见,在这些部署中,以安全和易于管理的名义禁用了许多系统功能。
总结
如果使用得当,动态磁贴可以为应用的核心功能增添强大的功能,让用户不必启动应用,或者吸引用户来启动应用。在本章中,我向您展示了如何使用 live app 磁贴,从选择和填充单个模板的基本技巧开始,然后继续演示如何组合多个模板以及如何使用徽章。
在很大程度上,这些基本技术将是您所需要的,但是我也向您展示了一些用于要求更高的应用的高级特性。其中包括使用通知队列显示消息的循环序列,更新队列中各个通知的内容,以及控制通知的计划时间。我已经向你展示了如何判断是否显示实时更新,从而结束了这一章。将这项技术放在最后可能有点奇怪,但在大多数情况下并不需要,因为如果动态磁贴被禁用,通知就会被丢弃。磁贴并不是 Windows 应用唯一可用的通知机制——在下一章,当我向您展示如何使用 toast 以及介绍系统启动器功能时,您将会了解到这一点。
二十八、使用Toast
和系统启动器
在这一章中,我将向你展示如何使用 toast 通知,这是一种吸引用户注意力的更直接、更具侵入性的机制。Toast 通知是对我在上一章向您展示的更微妙的磁贴通知和徽章的补充,用于更重要或紧急的信息。像任何一种通知一样,祝酒词应该谨慎使用——太少的通知会让你的用户得不到他们想要的提示和提醒,太多的会让用户疲劳和烦恼。当有疑问时,允许用户使用我在第二十七章中描述的设置特性和技术来配置发出 toast 通知的环境。
我在本章中还描述了系统启动器功能,它允许你启动用户选择的 app 来处理你的 app 无法处理的文件和 URL。这是一个简单但有用的特性,值得了解如何使用它。表 1 提供了本章的总结。
使用 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 通知。
图 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
语句。
注意提供 toast 通知功能的对象是
Windows.UI.Notifications
名称空间的一部分,为了简洁起见,我在示例中将其别名为wnote
。除非我另有说明,本章中我提到的所有对象都是这个名称空间的一部分。
我已经为这个例子定义了不同的提醒数据,并向名为img/reminder.png
的项目添加了一个图像,如图 2 中的所示。
图二。添加到项目中的 reminder.png 图像,用于 toast 通知
当我在本章后面显示 toast 通知时,您将再次看到该图像。除了reminder.png
文件,我还在images
文件夹中添加了我在上一节中使用的相同的闹钟图标集,并将这些文件应用到应用清单的应用 UI 部分,如图图 3 所示。
提示你可以从
apress.com
开始获得这本书的免费源代码下载中的所有图片文件。
图三。在清单中应用图标图片
使用 toast 通知的一个重要区别是,您必须明确声明您将在应用清单中使用它们。为此,在 manifest Application UI
选项卡上查找Notifications
部分。将Toast
设置为Yes
,如图图 4 所示。
图 4。启用吐司通知
警告如果您忘记在清单中声明您将使用 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 中看到示例制作的吐司是什么样子。
注意Visual Studio 模拟器不支持 toast 通知,就像它不支持 live tiles 一样。为了测试这个例子和本章中的所有其他例子,你必须使用一个真实的 Windows 8 设备,比如你的开发机器。
图五。敬酒通知
我选择了一个包含图像和一些文本行的 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 通知的时间段:short
和long
。默认是short
,这意味着祝酒词显示 7 秒钟。long
持续时间显示 25 秒的通知。
您可以通过向模板 XML 中的 toast 元素添加一个duration
属性并将其设置为long
或short
(这是仅有的两个受支持的值),来指定将使用哪个设置。为了支持设置持续时间,我向示例中的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 所示。正如你将看到的,微软一直非常禁止何时使用它们。
第一次显示通知时,Default
、IM
、Mail
、Reminder
、SMS
音很短,播放一次。Alarm
、Alarm2
、Call
和Call2
声音更长,并且只要通知可见就重复。
使用表中的值来指定 toast 通知的声音效果时需要小心。如果您想要使用Alarm
、Alarm2
、Call
或Call2
声音,那么您需要确保 audio 元素上的loop
属性被设置为true
,并且toast
元素上的duration
属性被设置为long
。如果没有正确设置这两个属性,那么用户将会听到默认的声音(相当于指定了Notification.Default
值)。
但是,如果您想在duration
属性设置为long
时使用Default
、IM
、Mail
、Reminder
或SMS
声音,那么您必须确保loop
设置为false
。如果loop
是true
,那么用户将会听到Alarm
的声音,与您指定的值无关。
提示如果你不想让任何声音伴随你的祝酒通知,使用一个
silent
属性设置为true
的audio
元素。
对于我的示例应用,我想使用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 通知的声音,否则将使用提供的值设置src
和loop
属性。
清单 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 中显示了这一点(此图中的红色突出显示是我添加的,没有向用户显示)。
图六。出现在祝酒通知上的解雇图标
第三个结果是用户忽略了通知。在由duration
属性指定的时间段之后,系统将通过使弹出窗口慢慢消失来代表用户消除通知。您可以通过监听ToastNotification
对象发出的一组事件来响应这些结果。我在表 3 中总结了三个事件。
在接下来的小节中,我将向您展示如何处理activated
和dismissed
事件。当 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 中总结了这些值。
清单 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;
} ...
提示如果你的应用正在运行,你只能知道通知何时被取消。系统不会启动您的应用来告诉您用户没有回复或取消了通知。
安排祝酒通知
您可以计划在未来某个时间向用户显示 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 秒,并允许通知暂停两次(这意味着用户将总共看到通知三次——一次在预定时间到达时,两次在通知暂停后)。
警告 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
对象定义的值。这些值与我在第二十七章中描述的值相同,当时我向你展示了如何决定是否向用户显示实时磁贴通知。
使用应用启动器
有些时候,你想通知用户一些你的应用不能直接处理的重要数据。例如,我可能有一个监控新文件的Pictures
库的应用,但是它只能显示有限范围的格式。我想在有新文件时通知用户,即使我不能直接在应用中显示它们。
这是一个人为的例子,因为 Internet Explorer 将很乐意显示大多数类型的图像文件-但它让我构建一个示例应用来演示应用启动器,您可以使用它来调用其他应用来为您处理您的数据。虽然图像格式可能不是问题,但是我在本节中描述的技术可以在您处理无法直接处理的数据时应用。
虽然这个示例应用很简单,但是考虑到我将要演示的特性的简单性,它是相当长的。我不会为此道歉:它允许我演示不同的功能如何一起使用——在这种情况下,在Windows.Storage
和Windows.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 中看到该图像,它显示了应用的布局。
图 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
库所必需的。
图 8。启用对图片库的访问
第二个变化是声明应用将生成 toast 通知。导航到清单的Application UI
部分,将Toast capable
选项设置为Yes
,如图图 9 所示。(您也可以为应用设置徽标,但这不是必需的。我在第二十四章的中重复使用了我为PhotoAlbum
应用创建的标志。)
图九。为示例应用启用 Toast 通知
测试示例应用
启动示例应用并单击Start
按钮,开始监控Pictures
库中的新文件。如果你拷贝一个新文件到Pictures
库,你会看到一个 toast 通知,如图图 10 所示。(新文件可能需要几秒钟才能报告给应用。)
注意你需要在本地开发机器等真实的 Windows 8 设备上运行这个应用,因为通知不会显示在 Visual Studio 模拟器中。
图 10。敬酒通知报告一份新发现的文件
如果您通过点击或触摸通知来激活它,那么应用将使用标记中的img
元素来显示图像,但前提是它是 JPG 文件。如果不是,那么什么都不会发生。我将在下一节中解决这个问题。
使用应用启动器
启动文件的默认应用是通过Windows.System.Launcher
对象完成的,它定义了表 6 所示的方法。
提示从这些方法中可以看出,这个对象也可以用于启动 URL 的默认应用。在这一章中,我将着重于文件激活,但是 URL 激活的过程遵循相同的模式。
使用Windows.System.LauncherOptions
对象指定启动器的选项。这让你可以对启动过程进行一些细粒度的控制,包括指示 Windows 如果没有安装可以处理该文件类型的应用应该发生什么。您可以在表 7 中看到由LauncherOptions
对象定义的属性。
您可以在清单 27 的中看到我如何在我的示例应用中使用Launcher
和LauncherOptions
对象,它显示了我对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 所示。
图 11。帮助用户定位处理文件类型的 app
这是一个简单的功能,但它允许你构建应用,这些应用可以操作它们不直接支持的格式的文件。该特性还利用了文件和协议激活契约,允许您构建补充应用,使能够处理特定的文件格式。
总结
在这一章中,我向你展示了如何使用 toast 通知来吸引用户的注意。谨慎而周到地使用,这是一个重要的特性,可以提高你的应用对用户的价值。过度使用,你会创建一个令人讨厌的、侵扰性的应用,它会打断用户对他们不重视的通知的注意力。我还向您展示了如何使用 app launcher,它允许您启动文件或 URL 的默认应用,而不知道哪个应用被配置为默认应用,甚至不知道是否安装了合适的应用。在下一章中,我将向您展示如何使用 Windows 对传感器的支持,将真实世界的数据引入您的应用。
二十九、使用传感器
Windows 8 支持一个传感器框架,您可以使用它来获取有关设备运行条件的信息。在本章中,我描述了最常遇到的传感器,从位置传感器开始,通过它您可以确定设备正在世界上的哪个地方使用。
即使设备中没有特殊的位置感应硬件,如 GPS 接收器,Windows 也会尝试确定设备位置。我在本章中描述的其他传感器确实需要特殊的硬件,如光和加速度传感器,尽管这种设备越来越常见,特别是在笔记本电脑和平板电脑中。
在本章中,我将向您展示如何读取来自不同传感器的数据,但您如何使用这些数据为用户带来好处则取决于具体的应用。因此,示例通常只是将传感器数据显示为文本。
当你阅读这一章的时候,你会遇到一些地方,我已经注意到,在我的测试硬件中,某些特性对传感器不起作用。这并不罕见,因为传感器及其设备驱动程序的质量有很大的可变性,因此需要仔细测试。
使用传感器时,注意不要根据你收到的数据对用户在做什么做出大胆的推断。例如,当使用光线水平传感器时,不要仅仅因为光线水平低就认为现在是夜间——这可能是因为设备被放在包中,或者只是因为传感器被遮挡。始终允许用户覆盖基于传感器数据对应用行为所做的更改,并在可能的情况下,在对当前条件做出假设之前,尝试从不同来源收集数据——例如,通过将光线水平与当前时间和位置相结合,您可以避免假设工作日是晚上。表 1 提供了本章的总结。
创建示例应用
我将创建一个示例应用,它将允许我演示我在本章中介绍的每个传感器。我将按照我在本书其他地方使用的模式来实现这一点,即创建一个使用导航条导航到内容页面的应用,每个页面包含一个主要功能。我创建了一个名为Sensors
的新 Visual Studio 项目,您可以在清单 1 中看到default.html
文件的内容。这个文件包含目标元素,我将在其中加载内容页面和导航栏,以及每个页面的命令。
清单 1。default.html 文件的内容
`
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 命令栏会产生错误,因为我还没有添加激活时加载的文件。
图 1。示例 app 的基本布局
注意为了让这个例子尽可能简单,我编写了每个内容页面,就好像它是唯一将被显示的页面一样。这意味着在显示来自不同传感器的数据之前,您必须重新启动示例应用。
使用地理定位
地理定位对于应用开发来说是一个越来越重要的功能,因为它为根据用户所处的位置定制用户体验提供了基础。许多 Windows 8 设备将配备 GPS 硬件,但 Windows 也可以尝试通过结合从一系列替代来源获取的信息来确定没有 GPS 的设备的当前位置——例如,包括设备 IP 地址和附近无线网络的名称——这种技术可能会惊人地准确。在接下来的小节中,我将向您展示如何使用地理定位特性,以及如何使用 Visual Studio simulator 测试地理定位。
注意地理定位是一个功能的例子,您可以使用特定于 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 所示。
图二。启用清单中的位置访问
警告如果您试图在未启用清单中的
Location
功能的情况下访问设备位置,将会产生错误。
获取位置快照
获取设备当前位置最简单的方法就是拍快照,也就是说你向系统询问当前位置,系统提供,然后你就完事了。另一种方法是跟踪当前位置,这意味着当位置发生变化时,系统会为您的应用提供更新。我将在本章的后面向您展示位置跟踪是如何工作的,但是拍摄位置快照相对简单,让我介绍支持地理定位特性的对象,这些对象在Windows.Devices.Geolocation
名称空间中定义。
注意除非我另有说明,否则我在本节中引用的所有新对象都可以在
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 中描述过了。
default
和high
值并没有指定位置应该被确定的精确程度。相反,它们指定设备应该如何获取位置。default
值将倾向于不太准确的信息,这些信息可以快速、轻松、免费地获得。high
值将获得设备可以产生的最精确的位置,并且它将使用它所拥有的所有资源来获得该精确度,即使这意味着消耗大量的电池电量或使用可能花费用户金钱的服务(例如来自蜂窝服务提供商的定位服务)。
提示请注意,您无法指定用于获取位置的技术,甚至无法找出设备获取位置的不同选项。你所能做的就是设置你想要的精确度,剩下的就交给 Windows 了。
关于为属性desiredAccurcy
使用哪个值的决定通常最好由用户来做,尤其是出于财务方面的考虑。发现一个写得很糟糕的应用耗尽了你所有的电池电量已经够糟糕了,但发现它在这样做的同时一直在耗费你的钱,这是无法忍受的。您可以看到我是如何在清单 8 的示例中指定高精度选项的,在这里我重复了来自geolocation.js
文件的语句。
清单 8。配置地理定位器对象的精确度
... geoloc.desiredAccuracy = **geo.PositionAccuracy.high**; ...
我不担心获取位置的成本,因为我将使用 Visual Studio 模拟器向应用提供位置数据。
监控地理位置状态
Geolocator
对象定义了statuschanged
事件,它提供了关于地理定位特性状态的通知。一个Geoloctor
对象的状态是通过locationStatus
属性获得的,并且将是由PositionStatus
枚举定义的值之一,我已经在表 3 中列出了这些值。
我通过读取locationStatus
属性的值并在应用布局右侧面板的ListView
控件中显示一条消息来处理statuschanged
事件。如果启动 app,会提示允许 app 访问位置数据,如图图 3 所示。
图三。请求位置访问
如果点击Block
按钮,Windows 会拒绝你的 app 访问位置数据,并且会触发statuschanged
事件,表示locationStatus
属性已经更改为disabled
,如图图 4 所示。
图 4。禁止访问位置数据的影响
如果您在应用中获得了disabled
值,那么您将知道您将无法访问位置数据。在这一点上,根据你的应用,你可能能够继续并向用户提供某种减少的功能,或者你可能需要显示一条消息,鼓励用户授予你的应用所需的访问权限。你应该而不是做的是忽略disabled
值,只是尝试读取位置数据——你不会得到任何数据,用户也不会明显看出缺乏位置访问是你的应用产生奇怪结果的原因。
要访问位置数据,激活Settings
图标,点击Permissions
链接,改变Location
拨动开关的位置。当你重新加载 app 时,你会看到状态显示为ready
,表示有位置数据可用,如图图 5 。
图 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 中描述的两个属性。
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 中描述的属性。
并非Geocoordinate
对象中的所有属性都将被填充——例如,这可能是因为设备没有能够提供高度细节的硬件,或者因为某些属性仅在位置被跟踪时可用,而不是拍摄快照。在示例中,当由getGeopositionAsync
方法返回的Promise
被满足时,我使用latitude
、longitude
和timestamp
属性在应用布局的右侧面板中显示一条消息。
测试位置快照
现在您已经看到了各种对象是如何组合在一起的,是时候测试示例应用拍摄位置数据快照的能力了。最简单的方法是使用 Visual Studio 模拟器,它能够模拟位置数据。启动 app 前,点击模拟器窗口的Set location
按钮,勾选Use simulated location
选项,输入纬度值38.89
和经度值-77.03
,如图图 6 (这里是白宫所在地)。
图六。输入模拟位置数据
点击Set Location
按钮应用模拟位置,然后启动示例应用。点击Get Location
按钮生成位置快照,并在应用布局中显示消息。您可以在图 7 中看到本次测试使用模拟数据的结果。
图 7。生成位置快照
不一定要用模拟数据。如果您没有在模拟器弹出窗口中选择Use simulated location
选项,那么应用将从 Windows 中读取位置数据。我使用了模拟数据,因为我想创建一个可以持续重复的结果,但我建议也使用真实数据——尤其是如果你的设备不支持 GPS。虽然可变,但非 GPS 位置数据的准确性可能相当惊人——仅使用无线网络名称,我的电脑就可以确定其位置在我家 200 英尺以内。
追踪位置
下一个向您展示的功能是在位置变化时跟踪位置的能力。如果您想监控设备的位置,您可以定期调用getGeopositionAsync
方法,但是这是一个很难实现的过程。您不希望过于频繁地拍摄位置快照,因为设备可能不会移动,而且您会不必要地消耗设备资源(可能还会消耗用户的钱)。如果您拍摄快照的频率太低,您将错过设备移动的时刻,并以部分数据磁道结束。
为了更容易跟踪位置,Geolocator
对象定义了positionchanged
事件,当设备的位置超出您定义的阈值时,就会触发该事件。您可以看到我是如何在清单 10 中的示例应用中添加位置跟踪支持的,它展示了我对startTracking
和stopTracking
函数的实现,以及一个编写显示位置信息的新函数。
清单 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
事件。
提示一米大约是 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 中可以看到追踪位置的效果。
图 8。使用 positionchanged 事件跟踪位置
使用光线传感器
如今,光传感器越来越常见,用于改变照亮设备屏幕的电量,以最大限度地减少电池消耗(屏幕通常是设备功耗的最大消耗者,在光线较暗的情况下调暗屏幕可以节省大量电力,并使屏幕使用起来不那么累)。对于一个应用来说,光传感器最常见的用途是试图找出设备何时在户外或用户何时入睡,这两者都可以与其他信息(如时间或位置)相关联,以改变应用的行为。几年前,我有一台 PDA,它利用光传感器提供不同寻常的选项,例如黎明警报和提醒我是否在室内呆了太长时间——虽然不总是成功,但玩起来很有趣。
注意与即使设备没有专用的位置硬件也能产生位置的位置传感器不同,光传感器(以及我在本章中描述的其他传感器)需要实际的硬件。硬件相当常见,为了测试示例项目,我使用了我在第六章的中提到的戴尔 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 中描述的两个属性。
illuminanceInLux
属性返回以勒克斯为单位的亮度。维基百科对勒克斯单位有很好的描述,并有一个表格描述了一些有用的勒克斯范围。可以在[
en.wikipedia.org/wiki/Lux](http://en.wikipedia.org/wiki/Lux)
看到文章。我使用这些勒克斯范围的简化集来猜测设备的工作条件,包括室外、室内和办公室内——你可以在light.html
文件的displaySensorReading
函数中看到我的映射。
注意您会注意到对
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
页面的布局和我测试示例应用时的光线水平。
图九。使用光传感器
我建议在根据光线水平决定应用行为时要谨慎,因为光线水平和设备运行条件之间没有明确的相关性。在图中,你可以看到我的照明水平是 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 中显示的属性。
对象定义了一个事件,但是我不能让它在我的设备上触发。为了解决这个问题,我已经注释掉了事件处理代码,并用一个对setInterval
的调用来代替它,我用它来重复轮询传感器的倾斜值。作为显示读数过程的一部分,我对布局中的一个div
元素进行了变换,以便显示一个随着设备旋转而“自动调平”的正方形——这与我在第十八章中描述的变换类型相同。你可以在图 10 中看到该应用的布局(请记住,这张截图是在设备倾斜时拍摄的)。
图十。跟踪和显示设备倾斜
使用加速度计
加速度计测量加速度,通常与位置数据一起使用,以确定设备移动的方式和时间-例如,如果位置跟踪报告设备以每小时 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 中描述的属性。
我在displaySensorReading
函数中读取这些属性的值,这产生了如图图 11 所示的布局和数据。
图 11。测量加速度
您可以通过监听由Accelerometer
对象发出的readingchanged
事件来跟踪设备加速度。传递给处理函数的事件对象的reading
属性返回一个AccelerometerReading
对象,您可以在acceleration.html
文件中看到我是如何处理这个事件的。
注
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 中所示的属性。
并非所有的指南针硬件包都能够产生磁航向和真北航向,例如,我的测试设备中的传感器只产生磁方位。在本例中,我将CompassReading
对象传递给displaySensorReading
函数,该函数显示数字标题并对div
元素进行旋转,以显示一个始终指向北方的箭头。您可以在图 12 的中看到布局和传感器数据。
图 12。显示指南针的数据
您可以通过监听readingchanged
事件来跟踪航向的变化,当指南针传感器报告的方向发生变化时,Compass
对象会触发该事件。我在示例中使用这个事件来保持示例布局最新。
总结
在本章中,我向您展示了您的应用如何利用 Windows 8 传感器框架来获取真实世界的数据。像所有高级功能一样,传感器数据需要谨慎使用,但您可以通过使用您收到的数据来创建灵活和创新的应用,以适应他们的环境。在本书的下一部分,我将向您展示如何准备您的应用并将其发布到 Windows 应用商店。
三十、创建要发布的应用
在本书的这一部分,我将向您展示如何在 Windows 应用商店中发布应用。我将从头开始,创建一个应用,从创建到提交给微软审查。在此过程中,我将向您展示如何应用驱动您的业务模式的功能,并确保应用符合 Windows 应用商店的政策。在这一章中,我将带您完成开始之前需要的步骤,然后创建一个应用,我将在接下来的章节中介绍它的发布过程。
决定你的应用
显然,应该从决定你的应用要做什么开始。对于这一章,我将创建一个照片浏览器应用。这不是任何真正的用户想要付费的东西(尤其是因为 Windows 8 中已经包含了这样的应用),但它是这本书这一部分的理想例子,因为功能简单且独立,让我专注于发布过程的不同部分。
决定你的商业模式
当创建一个应用时,首先要做的是决定你想要使用的商业模式。Windows 应用商店支持一系列不同的应用销售方式,包括免费赠送、试用版、收取固定费用、收取订阅费、应用内广告和销售应用内升级。
对于我的示例应用,我将提供一个免费的限时试用来吸引用户,然后向他们收取 5 美元的基本应用费用。但我的 Windows 8 财富将以应用内升级的形式出现,其中一些将获得永久许可,一些将在订阅的基础上出售。
注意我不打算演示应用内广告,因为它与 Windows 8 商店没有直接关系。微软有一个广告 SDK,可以很容易地在一个应用中包含广告,你可以从
[
advertising.microsoft.com/windowsadvertising/developer](http://advertising.microsoft.com/windowsadvertising/developer)
获得。如果你想使用另一家广告提供商,你需要仔细检查广告内容和获取广告的机制是否违反了应用认证要求(我在第三十三章中向你展示了如何测试)。
在表 30-1 中,我已经列出了我的应用的不同可购买功能、它们的价格以及它们的用途。该应用的免费试用将支持 30 天的所有功能。
让我再次强调,我不会真的卖这个应用,我只是需要一个工具来告诉你如何卖你的。因此,虽然我的应用及其升级既乏味又昂贵,但它们将允许我演示如何在一个应用中创建和组合一系列商业模式。
准备就绪
在开始创建我的应用之前,我需要做几件事情。首先是创建一个 Windows Store 开发者账户,允许你向商店发布应用,并从中获得任何收入。您可以以个人身份或代表公司创建一个帐户,Microsoft 会更改该帐户的年费(目前个人为 49 美元,公司为 99 美元)。您将需要一个 Microsoft 帐户来打开 Windows 应用商店开发人员帐户,但您在下载 Visual Studio 时应该已经有了一个。
提示微软将 Windows Store 账户作为其开发者产品的一部分,如 TechNet 和 MSDN。如果您已经购买了这些服务中的一项,您可能不必直接为帐户付费。
在 Visual Studio 的Store
菜单中选择Open Developer Account
,开始创建帐户的过程。获得帐户的过程需要填写一些表格并设置支付细节,这样你就可以从你的应用销售中获得利润。一旦你创建了一个账户,你会看到如图图 30-1 所示的仪表板。此仪表板提供了你的商店帐户的详细信息,包括你的应用和付款。
图 30-1。Windows Store 仪表盘
保留应用名称
开始开发应用之前,您可以在 Windows 应用商店中保留应用的名称。预约有效期为一年,在此期间,只有您可以发布该名称的应用。保留一个名字是一个明智的想法,这样你就可以继续创作艺术品、网站和营销宣传材料,而不用担心在你开发应用时别人会用这个名字。
你可以通过从 Visual Studio Store
菜单中选择Reserve App Name
项或者点击 Windows 应用商店仪表盘中的Submit an app
链接来保留名称(反正Reserve App Name
菜单会带你去那里)。仪表盘呈现了发布一个 app 所需的不同步骤,但我目前唯一关心的是第一步,也就是App name
步骤,如图图 30-2 所示。
图 30-2。显示应用发布流程第一步的 Windows 仪表盘
点击App name
项,输入您想要预订的姓名。对于我的 app,我保留了名称Simple Photo Album
,如图图 30-3 所示。
图 30-3。选择应用的名称
您保留的名称是应用将在商店中列出的名称,与您的 Visual Studio 项目的名称不同。我将在第三十三章中向您展示如何将 Visual Studio 项目与仪表板关联起来,但我甚至还没有创建我的 Visual Studio 项目)。
这意味着当你阅读这一章时,我对
Simple Photo Album
名称的保留可能已经失效,并且可能已经被其他人使用。
一旦您保留了您的应用名称,仪表板将更新发布过程的第一步以反映您的选择,如图 30-4 所示。
图 30-4。完成应用发布流程的第一步
这就是目前所需要的全部准备工作。保留名称后,我现在可以创建我的 Visual Studio 项目并开始构建我的应用。
创建 Visual Studio 项目
我已经使用Blank App
模板创建了一个新的 Visual Studio 项目。我将项目命名为PhotoApp
,只是为了演示您可以将项目的名称与应用在商店中的名称和用户名称分开。你可以在图 30-5 中看到成品 app 的外观。
图 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 元素对象上可以找到的标准的addEventListener
和removeEventListener
方法,以及dispatchEvent
方法,该方法允许您向注册的侦听器发送任意类型的事件。使用eventMixin
对象比编写自己的事件处理代码更简单,也更不容易出错,通过ViewModel.Store.event
属性使对象可用,我提供了一个点,应用的其他部分可以在这里注册事件。
提示你不必创建一个
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 FlipView
和ListView
UI 控件来显示图片。我还定义了一组可观察的属性,我将使用它们来跟踪用户当前在应用中启用了哪些功能——这不同于用户是否许可了它们。我需要跟踪用户使用某个特性的权限以及它当前是否开启,而ViewModel.State
名称空间中的属性跟踪后者。
提示这种方法要求我在启用
ViewModel.State
名称空间中的相应属性之前,检查用户是否有权使用某个特性。这就是ViewModel.Store.checkCapability
函数的用途,当我向您展示/js/default.js
文件的内容时,您将很快看到我是如何处理它的。
定义布局
为了创建我在图 30-5 中展示的布局,我将清单 30-3 中显示的元素添加到default.html
文件中。这个应用不需要任何导航或内容页面,因为它需要这样一个简单的布局。
清单 30-3 。在 default.html 文件中定义示例应用的标记
`
**
**** <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}">**
** **
** ** `
标记可以分为三类。第一类是进入左侧面板的控制元素(由属性为buttonsContainer
的div
元素表示)。除了标准的 HTML button
元素,我还应用了WinJS.UI.ToggleSwitch
控件,我在第十一章的中描述过。您可以在图 30-6 中看到详细的控制按钮,并且您会注意到这些按钮代表了我将作为升级出售给用户的功能。
图 30-6。控制元素
我需要某种方式让用户在试用期间购买基本功能或升级到全套功能,这就是我添加Buy/Upgrade
按钮的原因。您可以看到我是如何将ToggleSwitch
控件的 checked 属性链接到ViewModel.State
名称空间中的可观察值的。
下一部分标记包含在div
元素中,该元素的id
属性是imageContainer
。我使用一个FlipView
控件作为主图像显示,一个ListView
控件显示缩略图。我在第十四章和第十五章中介绍了这些 WinJS UI 控件,我在这个应用中对它们的使用非常简单和标准。
标记的最后一部分是用于ListView
和FlipView
控件的模板。这些使用了我在第八章的中描述的WinJS.Binding.Template
功能。两个模板都有一个img
元素,对于用于FlipView
控件的模板,我使用一个div
元素来显示当前显示文件的名称。在图 30-7 中,您可以看到控件和模板是如何组合成应用布局的右侧面板的。
图 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.fileTypes
和ViewModel.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 中列出了代码的关键特性,你可以在本书的什么地方找到它们。
更新清单
创建基本应用的最后一步是更新清单。为了访问Pictures
库中的文件,我需要打开package.appxmanifest
文件,导航到Capabilities
部分,勾选Pictures Library
选项,如图图 30-8 所示。
图 30-8。允许访问清单中的图片库
我还在images
文件夹中添加了一些新文件,用于磁贴和闪屏。我添加的所有文件都显示相同的图标,在透明背景上绘制成白色。你可以在图 30-9 中看到该图标,它显示在黑色背景上,以便在页面上可见。
图 30-9。用于示例应用的图标
我添加的文件被命名为tile<size>.png
,其中<size>
是以像素为单位的图像宽度。你可以在图 30-10 中看到我是如何将图像应用到应用中的,图中显示了货单的Application UI
部分以及我所做的更改。
图 30-10。更改应用使用的磁贴和闪屏图像
测试示例应用
本章剩下的工作就是测试示例应用。使用 Visual Studio Debug
菜单中的Start Debugging
项启动应用。默认情况下,ViewModel.State
命名空间中的属性设置为false
,这意味着您将看到基本的应用布局,如图图 30-11 所示。
图 30-11。基础 app 布局
当然,当你运行应用时,你会看到什么取决于你的Pictures Library
的内容。当应用首次启动时,它将只显示根Pictures
目录中的 PNG 文件,这将限制显示的内容。在基本模式下,您可以通过滑动FlipView
或点击其导航按钮来浏览图像。
通过启用ToggleSwitch
控件,您可以扩大显示图像的范围,以便包含 JPG 文件和Pictures Library
中更深层次的文件。当然,还会显示ListView
控件,显示可用图像的缩略图。
Refresh
按钮将清除数据源的内容,并从磁盘重新加载文件。此时Buy/Upgrade
按钮没有任何作用,但是我会在第三十二章中连接它,这样用户就可以进行购买。
总结
在本章中,我开始了创建和发布应用的过程,首先为我的应用保留名称,然后构建将提供给用户的基本功能。从一开始,我就添加了检查用户是否购买了应用关键功能的支持,我将在下一章实现这些检查背后的策略,我还将向您展示如何将您的应用集成到 Windows 应用商店中。
三十一、Windows 应用商店集成
在本章中,我将向您展示如何将您的应用与 Windows 应用商店集成,并实现您的应用商业模式。我将向您展示提供商店功能访问的对象,以及如何在您的应用发布到商店之前使用它们来模拟不同的许可场景。我将向您展示如何确定您的用户有权使用哪些功能,以及如何强制实施该模型来阻止在没有合适许可证的情况下使用应用。
您会发现,与 Windows 商店集成的技术相当简单,但是实施业务模型和模拟不同的场景可能相当复杂,需要大量的测试。
关于应用许可的一般建议
在我开始为我的示例应用实现和实施业务模型之前,我想提供一些一般性的建议。期望你的用户付费使用你的应用是完全合理和公平的,但你需要对你提供的功能的价值和你所处的世界持现实态度。
你必须接受你的应用会被广泛盗版。它会在发布后的几个小时内出现在意想不到的地方,任何许可执行和权限管理都会立即被破坏。用户(如果你有一个特别吸引人的应用,会有很多用户)会从你的辛勤工作中获益,而不用付给你一分钱。
如果这是你的第一个应用,你会经历一系列常见的情绪:震惊、沮丧、愤怒和不公平感。你可能会非常沮丧,以至于更新你的应用来添加更复杂的许可方案,或者要求定期检查一些额外的验证服务。这是你能做的最糟糕的事情。
每次你添加额外的权限管理和许可层,你就让你的应用更难使用。但你实际上并没有让复制变得更难,因为没有一个现存的方案能够抵挡住想要免费拷贝的人的关注。你唯一惩罚的人是你的合法用户,他们现在不得不千方百计让你的应用工作。在竞争激烈的市场中,你希望消除尽可能多的障碍来吸引用户使用你的应用——而你增加的每一个使用障碍都会让竞争对手的应用更有吸引力。
一个更明智的方法是接受人们总是会抄袭你的软件,并花些时间考虑为什么会这样。
你可能想知道是什么让我觉得我可以告诉你让所有那些罪犯敲诈你——但这是我花了很多时间思考的事情,因为它每天都影响着我。
书籍和应用有很多共同点
我的书在出版后几小时内就出现在文件共享网站上。这种情况已经发生了很多年,我开始明白,这并不是第一次出现的问题。
首先,从纯实践的角度来看,我没有办法阻止我的书被分享,即使我想这样做。第二,我开始意识到人们下载非法拷贝有各种原因,其中一些可能有利于我的长期销售数字。以下是我为自己的书考虑的几大类别:
- 收集者
- 快速修补程序
- 预算员
- 疼痛回避者
收藏者分享我的书是因为他们可以——他们喜欢收集大型图书馆的书籍、音乐、应用、游戏以及任何他们感兴趣的东西。对我来说,这些人并不代表销售的损失,因为他们从一开始就不会买一本,以后也不会。
速战速决者是那些有特定问题想要解决的人,他们会复制我的一本书,看看里面是否有解决方案。这些人也不代表销售失败,因为对他们来说,一个特定问题的解决方案并不代表 20-50 美元的价值。但是这些人确实代表了潜在的未来客户——他们可能记得他们发现我的书很有用,并为他们想更深入了解的主题购买了一本(或我的另一本著作)。
预算者是那些可能更喜欢买一本,但又买不起的人。书可能很贵,如果手头紧的话,书有多重要也没关系。这些人是也是潜在客户,因为他们现在可能破产了,但情况不会总是这样。当他们有更多的钱时,他们可能会开始买书,我希望他们在买书时对我的书有正面的感觉。
痛苦回避者是那些想要内容,但无法以适合他们的形式获得的读者。他们希望他们的内容以一种特定的方式或以一种特定的方式传递,而我和 Apress 不会给他们。所以他们转向文件共享,因为它以他们需要的方式给了他们所需要的东西。这些人是潜在的客户,无论是当我的作品以他们想要的方式出现时,还是当他们的需求发生变化时。
所以,总的来说,有一类人会抄袭我的书,因为他们可以也永远不会买一本,还有三类人今天会抄袭我的书,但将来可能会成为大客户。他们可能还会向其他人推荐我的书,这些人可能会付费购买一本(这种情况比你想象的更常见)。
收藏家对我来说是一个失败的事业,所以我不会因为他们而失眠。其他类型的文件复印机是我想培养的人,让他们体验我的内容,希望我将来能从他们身上赚些钱。我可以诚实地说,我从未把我的任何一本书放在文件共享网站上,但我很想这么做,因为我认为这有助于确保未来的收入。
同样重要的是,让我的书更难用并不会让任何一个复印机去买书。速战速决者会在别处找到解决方案,预算者不会有更多的闲钱,所以他们只会抄袭别人的书,而逃避痛苦者仍然无法以他们想要的方式获得内容。他们永远不会知道他们是否喜欢我的写作风格,因为他们永远也不会看到——我现在和将来都不会从他们那里得到任何销售。
关注重要的事情
我试着考虑他们想要什么,而不是试图惩罚复印机。需要快速修复吗?我在每章的开头添加了汇总表,以便更容易找到具体的解决方案。没有现金吗?我写了 Windows Revealed 的书,花几美元就能让你快速入门。需要一种特定格式的电子书?我与 Apress 合作,它提供了一系列不同的无 DRM 格式。
我对文件共享者的回应是努力让我的书更有吸引力、更有用,而不是更难使用。而且,如果我成功了,我会取悦我的付费读者,因为他们希望快速找到解决方案,获得一系列电子书格式,并获得新主题的廉价有效的快速入门书籍。
我没有浪费时间去烦恼,而是为我的书被如此广泛地分享而暗自自豪,并一直希望今天抄袭我的书的人明天会为这些书付高价。
我给你的建议是,对你的 Windows 应用商店应用采取类似的方法。想想为什么人们复制它们,并试图为那些未来可能购买的人增加价值,同时为现在已经购买的人增加价值。你不能停止文件复制,所以你可以很好地接受它,并认为它是真正的自由市场暴露。
我不希望人们抄袭我的书,但如果他们不打算买一本,我宁愿他们抄袭我的书,而不是竞争对手的书,对书来说是真的,对应用来说也是真的。
处理基本的商店场景
实现应用业务模型的过程有点奇怪,因为您创建了一系列代表不同许可场景的 XML 文件,然后在您的应用中实现处理它们的代码。这些文件被称为场景文件,代表当你的应用在商店中发布并被用户下载或购买时,你的应用可以访问的数据。在本节中,我将创建一个非常基本的场景文件来介绍该格式,向您展示如何访问该场景所代表的数据,并在应用中对其进行响应。
创建场景文件
我已经在项目中添加了一个名为store
的新文件夹,我将在其中放置场景文件。我的第一个场景描述了用户下载了应用,但没有购买任何应用内升级或订阅的情况。对于我的业务模型,这意味着我对表示三种场景感兴趣:
- 用户购买了基本应用功能的永久许可证。
- 用户下载了尚未过期的免费试用版。
- 用户下载了一个已经过期的免费试用版。
为了表示和测试这些场景,我在 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
元素包含用户获得该软件的许可证的详细信息。我将在接下来的小节中解释这两个元素。
注意 Windows 对场景文件的内容非常挑剔。确保
CurrentApp
元素关闭后没有内容,包括空行。如果应用在启动时崩溃,请尝试从 Visual Studio 重新加载应用,如果这不起作用,请仔细查看您的场景文件,看看您是否添加了 Windows 不期望的内容。
了解清单部分
场景文件的ListingInformation
部分包含了应用的描述,并为您提供了应用如何在商店中显示给用户的详细信息。我不太注意场景文件的这一部分,尽管当我为我想要提供的应用内升级和订阅创建定义时,我会在第三十二章中添加它。在表 1 中,我描述了商店整合和测试过程中的各种元素及其效果。
虽然你需要在一个场景文件中为这些元素创建值,但是ListingInformation
部分中的值取自你发布应用时提供的信息,我将在第三十三章中演示。使用什么值进行测试并不重要,它们是必需的,但这些值只是真实数据的占位符,您不必使用您计划为真实 Windows 应用商店部署提供的相同值。
注意【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 中描述了场景文件这一部分的元素。
对于我的初始场景,我已经将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
。我这样做是为了让我定义的功能总是需要一个许可,但是如果我忘记定义一个功能,我不会通过禁用一个即使他们愿意付费也不能激活的功能来破坏用户的体验。
提示这可能会让你觉得奇怪的谨慎,但是我已经写了一些这样的 Windows Store 实现,它们变得非常复杂——如果你忘记连接一个应用功能,在慷慨方面犯错误会带来更好的体验。
获取当前应用的数据
用于管理 Windows 应用商店集成的对象位于Windows.ApplicationModel.Store
名称空间中。这个关键对象叫做CurrentApp
,它提供了对从 Windows 商店获得的许可证信息的访问,并定义了允许您启动应用和升级购买流程的方法。表 3 显示了CurrentApp
对象定义的方法和属性,我将在本章中使用它们来管理示例应用。
我将在这一章中使用关键方法和属性,我将在使用它们时引入来自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 中描述的属性。
从表中可以看出,场景文件中的元素直接对应于由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
对象的细节来处理这些场景的。
注意我对基本应用功能的第三个设想是,用户已经购买了许可证。在这种情况下,我什么都不做,因为我想远离我的付费客户。嗯,至少在他们尝试使用需要升级的功能之前是这样,但是我会在第三十二章回到这个话题。
在接下来的小节中,我将填充事件处理函数中当前包含注释的三个部分。
处理有效试用期
我想借此机会提醒用户,他们正在试用我的应用,让他们有机会在启动应用时购买许可证。您可以在清单 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 所示。
图 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,没有连接的设备无法进行购买)。
提示在开发免费试用的应用时,我喜欢加上一个宽限期。几年前,我想在一次长途飞行中使用一个试用应用,却发现它已经过期,并且我的连接能力不足意味着我无法升级,尽管我愿意这样做。相反,我对开发人员决定不再购买应用的硬性规定感到恼火。因此,对于我自己的项目,我通常会添加一个按钮,将试用期延长几天,以便他们有机会进行购买。我只允许一次扩展,之后应用停止工作。
如果购买成功,我就关闭对话框,这样用户就可以使用这个应用了。如果购买没有完成,我调用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 所示的对话框。
图二。处理过期的试用期
此时,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 显示了对话框和你可以选择的选项来模拟结果。
图三。模拟 app 购买流程
S_OK
选项将模拟一次成功的购买,而其他值模拟不同种类的错误。在我的例子中,我没有区分不同种类的错误,并以同样的方式对待所有失败的购买。
测试失败的购买
如果您选择一个错误条件并点击Continue
按钮,您将看到如图图 4 所示的错误信息。点击Close
按钮关闭对话框,因为应用仍未获得许可,导致应用再次显示过期警告。
图 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
按钮时,您将能够使用该应用。
图 5。确认成功购买应用
现在,在 JavaScript 控制台窗口中重新输入这些命令,您将看到许可信息已经更改,表明用户已经许可了应用,如下所示:
Active: true Trial: false
当用户获得许可证时,licensechanged
事件被自动触发,这意味着我在/js/store.js
文件中的处理函数将重新评估可用的许可证,并更新用于响应对ViewModel.Store.checkCapability
函数的调用的属性,确保应用状态与用户获得的许可证保持一致。
注意重要的是要理解场景文件不会改变——只会改变正在运行的应用中许可信息的状态。如果重启 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 的说法中,应用内升级被称为产品,与定义基本功能的应用相对。要定义一个新产品,您必须在场景文件的ListingInformation
和LicenseInformation
部分添加一个元素,就像我在示例中所做的那样。
注意对于这个场景文件,我希望应用的基本功能可以在试用期尚未到期。您可以看到我是如何在
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。
从场景文件的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); }); }, ...
提示我发现如果我得到了意想不到的结果,那通常是因为我忘记了加载正确的文件。
使用许可信息
我现在的目标是使用我定义的许可证信息来设置用户对应用中不同功能的权限。我通过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
功能对用户不可用。
图 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
文件后重启应用,您应该可以访问所有功能。
销售升级产品
现在,我的应用强制执行产品许可证,我可以提示用户购买升级。如何做到这一点取决于你的应用的性质。我建议你仔细考虑这一点,因为以有益和礼貌的方式提示用户和不断向他们索要金钱是有区别的。
对于这一章,我将使用一个简单的方法,即当用户试图使用一个未经许可的特性时,提示用户进行升级。
提示我打算提示用户升级,因为我想把重点放在升级机制上,但我不会在真正的应用中这样做,因为这让用户很烦。当我第一次提示用户升级时,我通常会提供一个选项来禁用对同一特性的任何进一步提示。我建议你考虑类似的方法。你可以使用应用数据功能永久记录用户的偏好,我在第二十章的中描述过。
您将回忆起当用户试图使用一项功能时,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 所示。
图二。请求打印时提示用户订阅
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 中看到我呈现给用户的对话框。
图三。向用户出售升级和订阅
如果用户点击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 所示。
图 4。模拟购买升级
这是模拟应用购买的同一个对话框,它有相同的结果选择。该对话框显示所购买产品的名称,我在图中突出显示了该名称。
确保选择了S_OK
选项,并点击Continue
按钮模拟成功购买。当您关闭确认购买成功的对话框时,您会看到应用布局更新为显示缩略图,并且ToggleSwitch
已经移动到Yes
位置,如图图 5 所示。
图 5。购买缩略图功能许可证的影响
其他功能仍然未经许可,这意味着如果你滑动其他ToggleSwitch
控件或激活设备的魅力,你会再次得到提示。
测试订阅升级
重启应用,使许可信息重置为场景文件的内容,并再次滑动Show Thumbnails
切换开关。这一次,当出现提示时,点击Subscribe
按钮。您将再次看到购买模拟器对话框,但这次购买的产品被报告为theworks
,如图图 6 所示。
图六。模拟购买解锁多种能力的升级
选择S_OK
选项并点击Continue
按钮,模拟一次成功的购买。缩略图将像以前一样显示,但这一次应用的其他功能也已解锁——例如,如果您将All Folders
ToggleSwitch 滑动到Yes
位置,将使用深度查询来定位您的Pictures
库中的文件,如图图 7 所示。
图 7。购买适用于多种能力的升级的效果
创建应用内商店
我对示例应用的最后一个添加是连接Buy/Upgrade
按钮,以便为我想卖给用户的各种产品创建一个店面。到目前为止,我的所有销售提示都是由用户试图执行特定操作而触发的,但我也想让用户有机会在任何时候出于任何原因进行购买。在接下来的部分中,我将对应用进行一系列的添加,以便用户可以点击按钮,看到可用的选项,并进行购买。
注意这种添加需要相对大量的代码,但它对许多应用来说是一种重要的添加,需要我在本书中向你展示的许多技术——包括数据绑定、模板和
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 商店(或任何其他商业平台,就此而言)集成的一个方面是应用可能处于的不同状态的数量,以及满足所有这些状态的需要。你可以在getStoreLabel
、isBasicAppPurchased
和isFullyUpgraded
函数中看到这一点的缩影,我使用所有这些函数来管理Buy/Upgrade
按钮在应用布局中的呈现。
如果用户是通过免费试用期的一部分,那么我希望按钮提供他们购买应用的机会。Windows Store 不允许用户购买应用内升级,直到购买了基本应用,因此我需要确保我为用户提供正确的交易,以便提供合理而有用的用户体验。
isBasicAppPurchased
允许我告诉什么时候我需要向用户出售基本应用,什么时候我需要出售升级。另一方面,当用户购买了theworks
升级时,我想禁用button
元素,因为没有什么可卖的了。为此,我创建了 isFullyUpgraded
函数,当不再有任何产品留给用户时,该函数返回true
。
提示请注意,我没有透露任何定义基本应用功能的细节,也没有透露必须购买哪些产品才能创建完全升级的条件。我热衷于在应用的不同部分之间保持强烈的分离感,而不是在
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
文件紧密耦合,所以我使用两个新事件发出购买请求:apppurchaserequested
和productpurchaserequested
。我将在下一节处理这些事件。
定义商场互动
为了理清后端流程,我需要处理我在 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...
}
})();`
收到事件后,我调用现有的buyApp
或buyUpgrade
函数,并在成功购买后调用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'}">**
**
**
**
**
**
**
**
** **
**
** ** ...`
这个新标记非常简单。我将为每个产品生成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> ...
我想模拟一个还没有到期的试用期,所以我将isActive
和isTrial
元素设置为true
,并在ExpirationDate
元素中指定一个日期,对我来说是将来的某一天。你必须使用不同的日期来获得正确的效果。
测试基本应用购买
启动该应用,在您取消关于试用期还剩多少天的提醒后,您会看到布局中的按钮标记为Purchase
。如果您点击此按钮,您将看到购买模拟器对话框,其中将显示正在购买基本应用。
图 8。模拟购买基本应用功能
确保选择了S_OK
选项并点击Continue
按钮,模拟一次成功的购买。当您关闭确认您购买的消息时,您会看到按钮标签已更改为Upgrade
。
测试升级采购
不用重启 app,再次点击按钮就会看到应用内商店,如图图 9 所示。我使用了一个非常基本的布局,但是您可以看到我是如何显示虚拟产品数据来为用户提供升级的。
单击其中一个Buy
按钮,您将看到购买模拟器对话框,尽管是针对一个在场景文件中不存在的产品。
图九。显示虚拟产品数据的应用内商店
添加真实产品数据
现在我知道我的应用内商店工作了,我可以添加真正的产品数据,这是通过修改/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;**
** });**
},
...`
CurrentApp
和CurrentAppSimjulator
对象定义了loadListingInformationAsync
。该方法返回一个Promise
,完成后会产生一个Windows.ApplicationModel.Store.ListingInformation
对象,其中包含 Windows 应用商店中关于您的应用及其升级的列表信息——该信息对应于场景文件的ListingInformation
部分。ListingInformation
对象定义了我在表 3 中描述的属性。
我对应用内商店感兴趣的是productListings
属性,因为它返回了一个ProductListing
对象的列表,每个对象描述了我的应用的一个升级。ProductListing
对象定义了我在表 4 中描述的属性。
属性返回的对象将对象呈现为一个列表,这就是为什么我使用了一个 ?? 循环。对于每个产品列表,我创建一个具有我的应用内商店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 所示。
图 10。显示真实产品数据的应用内商店
您可以看到,JPG Files Upgrade
显示为 purchased,这与场景文件的LicenseInformation
部分中的数据相匹配。点击The Works Upgrade
的Buy
按钮,您将看到购买模拟器对话框。如果您模拟一次成功的购买并关闭确认对话框,您将会看到Upgrade
按钮现在已被禁用,表示没有进一步的升级可用。
总结
在本章中,我向您展示了如何利用 Windows 应用商店支持销售应用内升级。我演示了如何销售单个和多个应用功能的升级,以及如何确定用户购买了哪些功能。我还演示了如何创建一个应用商店,它允许用户随时购买升级。在下一章,也是本书的最后一章,我将向你展示如何准备并发布你的应用到 Windows 应用商店。
三十三、发布到 Windows 应用商店
在这一章中,我完成了我在第三十章中开始的流程,并准备好我的应用,以便它可以提交到 Windows Store 进行认证和上市。我将向您展示如何准备 Windows 应用商店仪表板上的应用列表,如何测试您的应用以发现问题,以及如何将您的应用上传到 Microsoft 以便进行测试。
准备应用列表
现在,我已经有了一个可以运行并经过测试的应用,我需要返回到 Windows 应用商店仪表板,并完成发布过程中的更多步骤。您可以通过从 Visual Studio Store
菜单中选择Open Developer Account
来打开仪表板。
在第三十章中,我通过为 app 保留一个名称,完成了发布过程的第一步;你会看到这一步在 Windows Store 中显示为完成,如图图 1 所示。
图 1。显示已完成第一步的 Windows Store 仪表盘
在接下来的几节中,我将完成更多的步骤,以便为应用的最终检查、测试和更改做好准备。
完成销售详情部分
点击Selling Details
链接,设置基础应用的定价信息。我将对我的示例应用收取 4.99 美元,并提供 30 天的试用期(你可以在图 2 中看到我是如何配置的)。
图二。设置 app 的销售详情
检查您想要销售应用的市场。我使用Select all
链接选择了 Windows Store 运营的所有市场,以便尽可能广泛地销售我的应用。微软会自动为我的应用定价,价格相当于我在当地的美元价格。点击Save
按钮,保存销售详情;您将返回到步骤列表。
完成高级功能部分
点击Advanced Features
链接,设置应用内升级的列表信息。(这一部分也可用于配置 notification services,我没有在本书中介绍,因为它们需要服务器功能才能工作)。
使用 web 表单创建您想要提供的一组升级。确保Product ID
字段对应于您已经用场景文件测试的产品之一是很重要的;否则,您将创建用户无法许可的应用功能。你可以在图 3 中看到我为我的示例应用创建的升级集。点击Save
按钮保存这些设置并返回主列表。
图三。创建应用内升级
完成年龄评定部分
点击Age rating and rating certificates
链接,选择你的应用适合的年龄段。Windows 应用商店仪表板提供了每个年龄组的目标受众和应用功能的相关信息。选择最合适的群体,请记住,面向年轻用户的应用将无法访问设备和传感器。我选择了12+
类别,如图图 4 所示。点击Save
按钮保存这些设置并返回主列表。
图 4。选择应用的年龄等级
完成加密部分
点击Cryptography
链接,指定您的应用是否使用加密技术。包括美国在内的相当多的国家限制使用或出口加密技术,因此对这一部分做出准确的声明尤为重要。我选中了 No 选项,如图 5 所示,因为我的示例应用没有使用任何形式的加密技术。
图 5。宣布使用加密技术
这是最后一个最需要的改变,所以当你点击Save
按钮时,你应该看到前五个步骤已经完成,如图图 6 所示。稍后我会回来完成剩下的步骤。
图六。正在进行发布流程
将应用与商店关联
下一步是更新 Visual Studio 项目,使其与 Windows 应用商店列表相关联。为此,从 Visual Studio Store
菜单中选择Associate App with the Store
菜单项。一旦你提供了你的开发者证书,你就可以从你创建的应用列表中选择,如图图 7 所示。
图 7。为应用选择列表
选择一个列表并点击Next
按钮。我已经选择了我的Simple Photo Album
列表,但是因为你不能用完全相同的名称创建列表,你将看到你创建的列表。
您将看到从 Windows 应用商店列表中获取并在应用清单中使用的值的详细信息,如图 8 所示。
图 8。将在清单中使用的商店值列表
单击Associate
按钮,将 Visual Studio 项目与 Windows 应用商店列表相关联。您的清单将被更新,以便项目中的关键字段与您在清单中提供的信息相匹配。
提供商店标志
此时,您需要添加一个徽标,该徽标将显示在 Windows 应用商店中您的应用旁边,它是以 50 × 50 像素文件的形式提供的。我使用了与磁贴和闪屏相同的标志,我将它添加到项目的images
文件夹中的一个名为store50.png
的文件中。
要应用商店徽标,打开package.appxmanifest
文件,导航到Packaging
选项卡,并更改Logo
字段的值,如图图 9 所示。
图九。更换店标
删除商店模拟代码
在发布之前,从应用中移除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
方法,我在第九章中描述过。
提示此时你也可以移除场景文件,因为它们不会被发布的应用使用。
构建应用包
下一步是构建将上传到 Windows 应用商店的包。首先,从 Visual Studio Store
菜单中选择Create App Packages
。将显示Create App Packages
向导,第一个问题询问您是否要为 Windows 商店创建一个包,如图图 10 所示。
图十。创建应用包向导
选择Yes
并点击Sign In
按钮。提供您的开发者账户凭证,然后从列表中选择您希望应用包关联的列表,如图图 11 所示。
图 11。选择与商店关联的列表
这可确保您的列表中的最新信息用于构建应用包。单击Next
按钮,您将能够设置创建包的位置,设置版本信息,并指定您的应用将在哪些处理器架构上运行。
我已经接受了默认值,如图图 12 所示。这意味着这个包将在我的项目文件夹中的AppPackages
文件夹中创建,版本将是1.0.0
,我的应用将能够在任何平台上运行。
图 12。设置版本和架构选项
点击Create
按钮,生成 app 包。创建包后,您将有机会运行 Windows 应用认证包,如图图 13 所示。微软在批准其应用在 Windows Store 中销售之前,会对其进行一系列测试,你可以通过运行认证工具包来防止潜在的问题,该工具包将检查你的应用是否存在一些与微软相同的问题。
提示如果您尚未安装 Windows 8 SDK,您可以下载其中的认证套件。SDK 在
[
msdn.microsoft.com/en-us/windows/hardware/hh852363.aspx](http://msdn.microsoft.com/en-us/windows/hardware/hh852363.aspx)
可用。
图十三。开始认证检查
点击Launch Windows App Certification Kit
按钮开始测试过程。该套件将运行您的应用,并对其进行一系列测试,这大约需要 5 分钟。在此期间,你会看到应用暂时运行,然后消失-不要在测试时尝试与应用交互。
测试完成后,您将看到结果,并有机会看到详细的报告。该报告包含所发现的任何问题的详细信息,并就如何解决这些问题提供了一些基本建议。
我的示例应用没有任何问题,所以几分钟后我会看到如图 14 所示的摘要屏幕。
图十四。显示认证测试成功的摘要
完成应用列表
现在,应用包已经完成并经过测试,是时候返回 Windows Store 仪表板并完成应用列表了(您可以通过从 Visual Studio Store 菜单中选择Open Developer Account
返回到Dashboard
)。
完成包装部分
点击Package
链接,将应用包上传到 Windows 应用商店。如果您在创建包时接受了默认位置,那么您将能够在 Visual Studio 项目的AppPackages
文件夹中找到您需要的文件。该包文件的扩展名为appxupload
,你可以将它拖拽到网页上,如图图 15 所示。上传软件包文件后,点击Save
按钮返回主列表页面。
图 15。上传包到商店
完成描述部分
点击Description
链接,输入您的应用的详细信息。当用户浏览或搜索 Windows 商店时,这些信息会呈现给用户,经过深思熟虑的信息可以使您的应用更容易找到——鉴于应用市场的竞争如此激烈,这一点非常重要。特别是,我建议您注意提供高质量的屏幕截图(可以用 Visual Studio 模拟器拍摄),并仔细考虑您指定的关键字。
在所需信息列表的最后是您在应用中提供的升级描述,如图 16 所示。确保你提供的描述对用户有一定的意义。
图十六。提供升级描述
填写完所有必需的详细信息并上传图片后,单击Save
按钮返回主列表页面。
完成测试人员注意事项部分
单击Notes for Testers
部分,提供微软测试人员验证您的应用所需的任何信息。这是需要包含的重要信息,因为测试人员对你的应用的关注和兴趣是有限的,你需要确保他们能让你的应用快速而轻松地工作。输入测试提示后,单击Save
按钮返回主列表页面。
提交您的应用
主列表页面应该显示所有步骤现在都已完成。当你准备好提交你的应用时,点击Submit for certification
按钮,如图图 17 所示。
图 17。提交 app 认证
您的应用将被提交进行认证,您将在测试过程中看到其进度的细节,如图 18 所示。一个应用通过认证需要几天时间,但微软会在你的应用通过该过程的每个阶段时向你发送电子邮件。
图十八。通过认证流程跟踪您的应用
如果您的应用顺利通过所有测试和审查,它将在 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 应用的功能。