Windows-商店应用开发入门指南-全-
Windows 商店应用开发入门指南(全)
原文:Beginning Windows Store Application Development – HTML and JavaScript Edition
零、简介
几个月前,当我被问到写一本关于用 JavaScript 为 Windows 8 构建应用的书时,我的第一个想法是,“写这本书我能带来什么?”其他作者已经讨论了这个主题,但是当我回顾了一些其他的书,我意识到我可以通过这个项目来填补这个空白。
我希望看到一本书,不仅涵盖基本的技术概念,而且还能从头到尾引导初学者完成构建商业质量的真实应用的过程。我想要一本书,不仅提供代码片段,还提供改善读者应用用户体验的技巧。我想要一本书,更多的是一个指南,而不是参考,介绍对读者来说可能是新的概念,而不是更深入地挖掘你可能在另一本书中找到的熟悉的主题。我希望有一天你可以拿起一本书,几天后你就可以构建真正的应用。
开始 Windows Store 应用开发—HTML 和 JavaScript 版本是我提供一本我想读的书的尝试。在写这本书的时候我学到了很多,我希望你通过阅读它也能学到很多。
这本书是给谁的
本书面向具有使用 HTML、CSS 和 JavaScript 构建 web 应用的经验,并对使用现有技能构建 Windows 8 应用感兴趣的开发人员。对于那些有使用其他技术(如. NET)为早期版本的 Windows 构建应用的经验的人来说,它也是一个很好的初学者指南。对于那些经验较少的人来说,它涵盖了 HTML、CSS 和 JavaScript 主题,但本书的重点不是这些技术本身,而是使用这些技术来构建 Windows 应用商店应用。
在本书中,我将提醒您,就像构建 web 应用一样,您可以自由地遵循您喜欢的 HTML、CSS 和 JavaScript 实践。例如,当创建页面控件时,我选择将 Visual Studio 创建的 CSS 和 JavaScript 文件保存在 HTML 文件所在的目录中,这将在第五章第一次演示,但在整本书中大量使用。因为我的大部分背景是. NET 开发人员,所以这很熟悉,因为默认情况下,Visual Studio 将 ASPX 和 ASCX 文件保存在与文件背后的 C#或 VB 代码相同的目录中。但是,您可以选择将所有 JavaScript 文件移动到 js 文件夹,将所有 css 文件移动到 CSS 文件夹。此外,您可能会注意到,我写的 JavaScript 代码不是地道的 JavaScript。请随意修改代码示例,以符合您的编码风格。
这本书的结构
虽然你当然可以直接跳到你感兴趣的主题,但这本书是为从头到尾阅读而写的,每一章都建立在前一章的基础上。在第一章到第三章中,你会发现 Windows 8 概念的概述,比如触摸界面和手势,以及微软设计语言。在第四章到第八章中,我将向您介绍如何使用 Visual Studio,包括各种可用的项目模板,以及您可能在应用的用户界面(UI)中使用的不同控件。
在第九章和第二十二章之间,你将构建一个功能齐全的真实应用。每章将涵盖 Windows 应用商店应用开发的核心概念,用 HTML 和 JavaScript 实现。每章的主题不一定是下一章的先决条件,但是示例应用已经过精心设计,因此每一章中的代码示例都建立在前面几章的基础上。
最后,在第二十三章中,我将介绍一些你应该采取的步骤来打造你的应用并将其发布到 Windows 商店中。
下载代码
本书中所示示例的代码可在 Apress 网站www.apress.com
上获得。在该书的信息页面上的源代码/下载选项卡下可以找到一个链接。该选项卡位于页面的相关标题部分下。
此外,在第九章到第二十三章中构建的示例应用的源代码可以在 GitHub 上的www.github.com/daughtkom/Clok
处获得。
联系作者
如果你有任何问题或意见——甚至是你认为我应该知道的错误——你可以通过我的个人网站联系我,地址是www.scottisaacs.com
,或者在 Twitter 上联系我,地址是www.twitter.com/daughtkom
。
一、欢迎来到触摸优先的世界
2010 年 4 月,我第一次听到定义微软新战略的短语:“三块屏幕和云。”这指的是一种有针对性的方法,以确保微软的产品在手机、台式电脑和电视屏幕上无处不在,并且这些平台通过在云端与数据结合在一起提供无缝体验。三个屏幕上展示的产品分别是 Windows Phone 7、Windows 7 和 Xbox 360。微软仍然主导着电视屏幕,其 Xbox 系列约占全球销售的所有游戏主机的一半,并继续专注于将该平台转移到游戏之外,但对我来说,Windows 8 给三个屏幕和云带来了不同的意义——其中三个屏幕包括手机、平板电脑和个人电脑,所有这些都运行在 Windows 8 核心上,并与云服务绑定,如图 1-1 所示。
图 1-1 。Windows 8 三屏和云的愿景
这本书是关于在这个新环境中开发应用的,但是在你开始任何开发之前,你必须了解这个环境以及它将如何被使用。在这一章中,我将提供一些关于 Windows 8 用户界面的背景知识,以及用户将如何与运行在该平台上的应用进行交互。我将主要关注触摸,但由于 Windows 8 是一个触摸优先的环境,而不是只有触摸的环境,我也将讨论触摸何时不合适,并涵盖替代的输入方法。
移动到更自然的交互
1985 年,用户主要通过使用键盘与 PC 进行交互,但第一台 Macintosh 正在增加鼠标的普及程度,微软推出了 Windows 1.0,它本质上是一个外壳,允许人们点击打开程序和文档,而不是要求他们记住适当的命令来打字。这些基于鼠标的环境在商业和消费市场上都取得了成功,并使大众能够进行计算:到 Windows 95 发布时,个人电脑在人们的家庭中并不罕见。
多年来,计算机和软件制造商一直在考虑可以放在口袋里或挂在腰带上随身携带的计算机。苹果早在 1992 年就试图实现这一愿景,但直到 2000 年代中期,技术才真正跟上,硬件制造商才能制造出小型、轻量级的计算设备,能够运行与台式机相当的软件。当硬件为消费者的黄金时间移动计算做好准备时,Windows 品牌在市场上已经根深蒂固,微软用 Windows CE、Pocket PC 和各种 Windows Mobile 进行了几次尝试,以创建一个简单的 Windows 缩小版的移动体验。这种方法产生的屏幕需要很高的交互精度,而运行移动版 Windows 的电脑在很大程度上被视为专用设备,不被普通消费者接受。
2010 年 Windows Phone 7 的推出,可能是受三年前苹果 iPhone 的成功和随后安卓的流行的推动,摒弃了小版本 Windows 的概念,并采用了一种被称为微软设计语言的全新用户界面概念。微软设计语言基于一套以用户为中心的核心设计原则,手指成为与计算机交互的主要工具。与之前版本的微软移动操作系统不同,Windows Phone 设备不再将手写笔作为标准组件。
注你可能对地铁这个名词比较熟悉。 Metro 是微软和其他公司以前在一些不同的上下文中使用的代号。微软设计语言被广泛称为 Metro 设计语言。此外,Windows 开始屏幕通常被称为 Metro 界面。以前称为 Metro 应用的应用正式称为 Windows 应用商店应用。
随着 Windows 8 的推出,微软抓住机会按下了用户界面期望的“重置”按钮,并通过将移动世界中必然会出现的交互带到桌面环境中,而不是将桌面概念带到移动世界,扭转了之前的策略。
Windows 8 触摸语言
随着触控作为一等公民在 Windows 8 中的全面融入,了解操作系统识别的触控手势语言非常重要。这不仅对于 Windows 8 用户来说很重要,对于希望确保用户能够尽快学习应用并获得一致体验的开发人员来说更是如此。Windows touch 语言主要由八种手势组成,我将在本节中讨论。
按住
按住手势 如图图 1-2 所示,类似于鼠标右键手势。该手势旨在允许用户了解关于目标的一些信息,或者向用户提供额外的选项,如上下文菜单。这种手势是通过用单个手指触摸屏幕并暂停直到系统确认保持来实现的,通常是通过描绘保持的用户界面元素来实现的。
图 1-2 。按住
龙头
虽然按住手势可以很容易地等同于单个鼠标手势,但对于轻击手势来说就不一样了。在图 1-3 中所示的点击手势旨在调用用户界面元素上的主要动作。通常,这是一个动作,比如激活一个按钮或跟随一个链接。与点击手势最相似的鼠标手势是左击,但是左击也用于在触摸语言中具有自己的手势的其他任务,例如选择。这个手势是通过将手指放在用户界面元素上,然后立即将手指垂直抬起来实现的。
图 1-3 。龙头
幻灯片
Windows touch 语言中的滑动 手势,如图图 1-4 所示,用于平移或滚动超出屏幕或屏幕部分边界的内容。在鼠标驱动的环境中,这是通过使用滚动条来实现的,但在触摸环境中,滑动手势更自然,滚动条要么必须增长到占用屏幕太多空间的程度,要么就是一个很难触摸的目标。为了完成滑动手势,手指放在屏幕上,然后上下或左右拉动,这取决于内容的方向。
图 1-4 。幻灯片
偷窃
滑动 手势用于传达选择,很像使用鼠标和键盘与计算机交互时使用的左键单击、Ctrl+左键单击和 Shift+左键单击。为了实现这个手势,如图图 1-5 所示,手指放在屏幕上所选项目的上面或旁边,然后在项目中划过。手势的方向取决于内容的方向,水平方向的内容被垂直滑动,垂直方向的内容被水平滑动。这种与滑动相反的手势有时会被称为交叉滑动。使用这种手势,而不是点击,可以消除在没有键盘修饰键(如 Ctrl 和 Shift)来帮助鼠标选择的情况下尝试完成多选时可能出现的混乱。
图 1-5 。偷窃
少量
在图 1-6 中所示的捏捏 手势在大多数鼠标中没有直接的等效物,被认为是一种“缩放”手势。夹点从具有高细节级别的窄视图缩小到具有较少细节的更宽视图。你将在后面的章节中看到,除了光学变焦,应用还可以在语义级别利用这种手势,并使用它来导航摘要和详细数据。为了完成捏手势,两个手指分开放置,并且与作为手势目标的元素的中心大致等距,然后手指一起滑动,直到达到期望的缩放或者手指相遇。
图 1-6 。少量
注意在许多带有滚轮的鼠标上,滚动时按下 Ctrl 键执行与收缩或拉伸手势相同的动作。
伸展
拉伸 手势,如图图 1-7 所示,是捏手势的反义词,在执行和结果上都是如此。拉伸手势用于从较宽、不太详细的视图放大到较窄、包含更多细节的视图。就像捏一样,你会发现应用可以被设计成允许手势是光学缩放还是语义缩放。为了完成手势,手指放在一起,以要缩放的元素为中心,然后沿着屏幕向相反的方向移动,直到达到所需的缩放级别或者其中一个手指到达屏幕的边缘。
图 1-7 。伸展
从边缘滑动
随着你对 Windows 8 和微软设计语言的了解越来越多,你会发现内容才是王道,任何分散内容注意力的东西都应该从屏幕上消失。您还会发现,用户必须能够尽可能轻松地执行操作。Windows Store 应用通过将不常访问的命令放在屏幕边缘的所谓应用栏和魅力栏 来平衡这些需求。图 1-8 中的从边缘滑动手势用于访问这些命令。为了实现该手势,手指被放置在屏幕边缘之外,然后被拉到屏幕上。
图 1-8 。从边缘推送
转动
转动 手势,如图图 1-9 所示,用于旋转视图或视图内的内容。这种手势的一个例子是经典视频游戏俄罗斯方块的触摸版,其中下落的方块可以旋转到一起。为了完成这个手势,将两个手指放在屏幕上,然后将两个手指绕着一个圆的圆周拉动,或者一个手指绕着另一个手指旋转,后者保持静止。
图 1-9 。转动
成功触摸界面的关键
构建一个成功的触摸界面需要设计者和开发者的仔细思考和考虑。这些考虑因素中的许多都包含在管理微软设计语言的设计原则中,我将在第二章中讨论,但在这一节中,我将讨论一些对触摸界面至关重要的概念,无论它们是否使用这些原则。
响应性
虽然响应速度对任何应用都很重要,但对于触摸应用的用户来说,永远不要看着没有响应的屏幕是特别重要的。用户意识到,即使只是在潜意识层面,鼠标指针是比手指末端更精确的工具,因此如果不容易看出用户的最后命令被接受并被执行,用户可能会觉得他或她没有击中目标并再次发出命令。响应性可以通过一些操作来实现,例如给出一个长时间运行的过程已经开始的视觉线索,或者确保内容随着用户的手指在屏幕上拖动而移动。
触摸目标
如前一节所述,鼠标指针是一种比人类指尖精确得多的工具。虽然在某些应用中没有什么可以消除用户错过目标的可能性,但是使用间隔很大的大触摸目标是最大限度减少错过目标的重要方法。在所有可能的情况下,目标应不小于 7 毫米见方,它们之间至少有 2 毫米。一般来说,当击中错误的目标造成严重后果或难以纠正时,该目标的比例应该更大,并且它与其他目标之间也应该有更大的空间。
直观的界面
对最终用户来说,最好的应用“就是工作”通常,这是因为应用使用户更容易做需要做的事情,而不是弄清楚如何做需要做的事情。如今,许多桌面应用通过在工具提示中提供详细的说明来弥补直观性的不足,当用户用鼠标指针浏览应用时,工具提示就会出现。触摸界面仍然可以使用工具提示,触摸语言为这种类型的学习定义了按住手势,但这比使用鼠标需要更多的努力,所以应该在清晰传达用户应该做什么的设计上投入更多的努力。
注关于实现高质量基于触摸的用户体验的额外指导,请参考这两篇 MSDN 文章:
http://msdn.microsoft.com/en-us/library/windows/apps/xaml/hh465415.aspx
和http://msdn.microsoft.com/en-us/library/windows/desktop/cc872774.aspx
。
触摸不到
和 Windows 8 一样,本章非常重视用户通过触摸手势与电脑的互动。然而,应该注意的是,Windows 8 用户界面被称为触摸优先 ,而不是纯触摸。Windows 8 拥有在 Windows XP 和 Windows 7 上运行的大部分硬件上运行的能力,并且在许多情况下,由于为适应移动设备而进行的优化,性能会更好。这意味着,尽管供应商们正争先恐后地向市场推出创新的触摸硬件,但在可预见的未来,应用开发人员必须承认,他们的许多用户将只使用键盘和鼠标来开发应用。
除了仍在使用的大量旧硬件之外,了解一些使用场景并不能很好地转化为触摸环境也很重要。用户坐着数小时进行数据输入,将会比用户使用键盘和鼠标进行同样的任务,伸出手臂去够像今天大多数显示器一样设置的触摸屏显示器更舒服,并且更少疲劳和损伤。硬件供应商将通过继续创新来满足这一新需求,您可能会看到一些变化,如多点触控板取代传统的鼠标和显示器,这些鼠标和显示器可以调整为平放或至少倾斜在桌面上。此外,预计会看到类似于微软 Kinect 设备的设备不断发展,并以比今天更具创新性的方式使用。
结论
在本章中,您将 Windows 8 视为您的应用将生活在其中的触摸优先世界。您了解了 Windows touch 语言中定义的基本手势,以及最终用户期望应用如何对这些手势做出反应。您还了解到,无论未来的计算机是什么样子,今天的计算机通常看起来非常像 Windows 8 上市前一天甚至五年前销售的计算机,您的应用必须考虑到今天计算机的用户。不管用户是用手还是鼠标进行交互,Windows 应用商店应用都应该流畅、直观、响应迅速。
二、微软设计语言
除了上一章讨论的基本触摸原则,微软的设计团队开发了微软设计语言,以前称为 Metro,用于指导 Windows Phone 7、Windows Phone 7.5 以及现在的 Windows 8 和 Windows Phone 8 的用户界面开发。微软设计语言的灵感来自于在大都市地区和公共交通中看到的简单易懂的语言,并努力将这种简单和直观的味道带到计算中。在这一章中,我将介绍微软设计语言的元素,展示一些例子,并解释 Windows 8 是如何整合它们的。在进入微软设计语言本身之前,我将介绍一下瑞士设计风格,它的影响在微软设计语言的元素中可以清楚地看到。
瑞士设计风格
微软的设计语言受一种设计风格的影响最大,这种风格被称为瑞士设计风格,或国际印刷风格,它于 20 世纪 50 年代在瑞士发展起来,并在 20 世纪 60 年代和 70 年代真正开始形成自己的风格。
包豪斯的影响
瑞士的设计风格深受包豪斯运动的影响,瓦尔特·格罗皮乌斯于 1919 年在德国魏玛成立了包豪斯艺术学院。包豪斯运动的指导原则是功能胜于形式,因此支持简洁的交流和鲜明的对比胜于抽象的想法和渐变。它促进了为工业化社会设计的艺术和建筑,并且可以大规模生产。包豪斯运动对现代设计和建筑的发展产生了重大影响。如今,网站http://Bauhaus-online.de
由柏林包豪斯设计档案馆/博物馆、魏玛经典基金会和包豪斯德绍基金会(见图 2-1 )维护,旨在保存和传播关于学校的信息,并教育人们了解该机构的影响。
图 2-1 。德国德绍的包豪斯建筑
瑞士设计风格的元素
瑞士设计风格的特点是有许多元素,我将在本章中讨论。这些元素包括排版,摄影,图像,大量使用空白,以及严格的组织。这些元素结合在一起,产生了瑞士风格设计作品的独特外观和感觉。
排印
受瑞士设计风格原则影响的艺术的前沿和中心是字体设计。瑞士风格的开发者,以及今天使用这种风格进行设计的人,坚定地认为文本应该清晰简单,不必要的装饰不仅遮蔽了文本中所传达的信息,还会分散人们的注意力。为了与文本应该清晰、简洁和简单的理念保持一致,瑞士设计通常采用无衬线字体,文本左对齐,右参差不齐。图 2-2 和 2-3 是新闻稿的示例,设计为两端对齐的列和衬线字体(Times New Roman),后面是使用无衬线字体(Helvetica)和左对齐设计的相同新闻稿,以符合瑞士设计风格原则。请注意两个例子之间的显著差异,特别是字体,以及无衬线字体如何产生更整洁的外观。标题是这方面特别好的例子。
图 2-2 。非瑞士风格的模拟时事通讯
图 2-3 。使用瑞士风格排版的模拟时事通讯
除了关注简单的无衬线字体之外,瑞士设计在排版方面的另一个关键元素是使用对比字体大小和粗细来吸引对文本中某些点的注意或强调。当使用不同的字体大小时,这需要字体大小的明显差异,因此尽管一些设计学校可能提倡 12 点的标题和 10 点的正文,但瑞士设计可能要求 18 点的标题和 10 点的正文,以确保这两种文本元素之间的差异没有问题。
摄影
瑞士设计风格的另一个特点是,设计应该传达一种真实感,当用照片代替图画时,视觉元素会被认为“更真实”。
图 2-4 展示了一片水域上的日落。照片捕捉到了水中的波纹和阳光对水面的影响,给观众一种非常真实的感觉。
图 2-4 。水面上日落的照片
图 2-5 也描绘了一片水域上的日落。照片中出现了许多相同的元素,如水中波纹的阳光反射和轮廓,但推动瑞士设计的理论认为,当使用插图而不是摄影时,观众不会感到他们所看到的是真实的。照片和画都很赏心悦目,但照片更符合瑞士风格。
图 2-5 。水面上日落的画
肖像学
虽然照片比图画或其他插图更受欢迎,但在许多情况下,使用瑞士设计创作的作品通常会大量使用图标,以增加或取代文本。尤其是当瑞士设计用于必须向国际观众传达信息的场合,或者您不能确定需要传达信息的观众是否能理解印刷的文字,不管它们是用什么语言写的。1972 年德国慕尼黑夏季奥运会期间,丰富的图标与瑞士设计的其他元素结合在一起,在国际舞台上大放异彩。奥托·艾舍为奥运会设计了瑞士风格的小册子和传单,并使用了现在人们熟悉的图形图标系统来代表参加奥运会各种活动的个人。这促进了与出席奥运会的国际观众的交流。你还可以在公共汽车站和火车站、公共洗手间(图 2-6 )以及许多消费品上的警告标签中看到瑞士设计和图标的突出例子。
图 2-6 。熟悉的瑞士风格设计有助于避免尴尬的错误
大量使用空格
在瑞士设计中,内容为王。在一个空间里随意放入太多东西会被认为过于杂乱或嘈杂,会分散对所传达信息的注意力。这导致了一个包含大量空白的设计目标,以确保出现在空白区域的任何东西都会立即成为关注的焦点。
图 2-7 显示了一只狗在一个多雪的国家里站岗。“哨兵”是一个描述性的说明,但没有特别注意狗或说明,因为内容都允许一起运行,没有任何分离,因为树木产生的“噪音”分散了说明的信息。虽然这个数字在视觉上很吸引人,但它缺乏瑞士设计原则所青睐的鲜明对比。我将利用大片雪地中的自然空白来突出照片中我希望引起注意的部分和标题,如图图 2-8 所示。
图 2-7 。没有空白的照片和说明
图 2-8 。照片和说明带有空白以形成对比
在图 2-8 中,我所做的唯一改变是将文本从树木产生的噪音中移除,让标题独立存在于不间断的空白中。这缩小了照片的焦点,排除了与主题没有直接关系的部分,真正使标题突出。更多的照片可以从顶部和底部裁剪,以使主体更加集中,但在这种情况下,足够的空间来确保冬季场景不会逃过观众的眼睛。无论是第一个版本还是第二个版本都不应被视为更好或更差,因为在有些情况下,目的是关注整个环境,而坚持瑞士设计的原则不是目标,在这种情况下,第一种处理方式可能是首选。
严密的组织
为了与简洁和避免任何干扰内容的主题保持一致,瑞士设计通常以严格的组织为标志。这体现在几何图形的一致性,以及在文本中使用字体大小来传达信息层次,以及坚持使用网格系统以结构化的方式布局文本和其他视觉元素。网格的使用绝对不局限于瑞士风格,它已经在排版设计中使用了几个世纪。在基于网格的设计中,设计图面被划分为一个或多个网格,这些网格用于定位带有单元格的文本和元素。这提供了一个有组织和一致的外观。有时,网格布局的使用可能不太明显,因为网格线不需要与设计图面的边缘垂直和平行,这使得设计可以遵循网格布局,而内容在查看者看来是倾斜的。
图 2-9 显示了通过使用网格布局实现的结构组织,但它也展示了在瑞士设计风格中,通过使用字体大小的明显差异来描绘信息层次中的不同级别,排版被用来实现组织的方式。在信息层次的最高层,页眉以 56 磅的字体大小显示。在下一个级别,组标题的字体大小大约是页面标题的一半。在此页面的最底层,项目标题大约是组标题大小的一半。
图 2-9 。演示网格布局和层次结构的 Windows 应用商店应用
微软设计语言
微软设计语言在很大程度上植根于我刚才提到的瑞士设计风格,它指导着 Windows Phone 7/7.5/8 和 Windows 8 操作系统以及 Zune 和 Xbox 360 用户界面的用户体验设计,努力提供一致的外观和感觉,而不管您正在与什么设备进行交互。
微软设计语言原则
微软最早关于微软设计语言的指南将其描述为五个指导原则的汇合,而不是一本规则或食谱。在这一节中,我将介绍您在做出设计选择时应该权衡的原则。
展现对工艺的自豪感
在你的用户界面中,即使是最小的细节也不应该碰运气。用户看到的和经历的一切都应该是计划的一部分,并按照计划工作。此外,信息应该按照精心设计的视觉层次来呈现,并且应该使用基于网格的设计来布局。
快速流畅
应用应该允许用户直接与内容交互,并且应该通过使用 motion 为交互提供反馈来保持持续的响应。应用通常应该以“触摸优先”为设计理念。
真正的数字
微软失败的用户体验实验的一个最明显的例子是 1995 年微软 Bob 的发布。该应用是操作系统的外壳,旨在通过为不同的操作提供真实世界的类比来抽象出计算机的整体“计算机性”。如果您想要检索文档,请单击文件柜。需要写信吗?点击桌上的笔!鲍勃的失败最终是由两个因素造成的。第一个是它被认为是幼稚和傲慢的(许多类似于 Bob 的贝壳确实在幼儿园教室里很受欢迎)。第二,它根本不是人们与计算机交互的有效方式,引入旨在隐藏计算机的抽象概念往往会使交互效率大大降低,尤其是对于一天中大部分时间都必须使用计算机的人来说。微软设计语言原则承认人们知道他们正在与计算机交互,并呼吁设计师拥抱这种媒介。这包括使用云来保持用户和应用的连接,并有效地使用运动和大胆,充满活力的颜色与用户交流。
少花钱多办事
Windows 8 提供了丰富的功能,允许在您的设备和云中运行的应用相互交互。这使得应用能够专注于做一组定义非常狭窄的事情,并以非凡的方式做一件事情,而不是做不好几件事情。为了与包豪斯和瑞士设计的影响保持一致,内容应该是注意力的主要焦点,并且应该很少出现其他东西来分散对内容的注意力。Windows 应用商店应用的全屏特性甚至消除了对窗口 chrome 的需求,允许完全沉浸式的体验,因此当用户在您的应用中时,您的应用会受到他或她的所有关注。
团结一致赢得胜利
在 Windows Store 应用中工作的一个关键是样式已经设置好了。Windows Store 应用的用户在打开您的应用时,会期望他们已经对它有了一定程度的熟悉,因为他们熟悉其他 Windows Store 应用的外观和感觉。一些对单个应用,最终对应用所在的生态系统真正有害的事情是设计决策,这些决策从根本上改变了应用的设计范式,为用户提供了比他们习惯拥有的更“新”和“更好”的东西。你应该努力让你的用户知道你的应用做了它应该做的事情,但是试图通过改变用户界面和导航范例来给用户惊喜只会让他们迷惑,让他们对你的应用失去信任。Microsoft 提供了指南、工具、模板和样式表,使开发人员可以轻松创建外观一致的 Windows 应用商店应用,您应该充分利用这些资源。
Windows 应用商店应用的用户体验指南
除了微软为 Windows Store 应用发布的更为通用的原则之外,还发布了一套全面的指导原则,以便为在这一新生态系统中运行的应用的外观、感觉和行为提供详细的规范性指导。虽然这些指南的全部内容可以在 MSDN 图书馆网站http://dev.windows.com
上免费获得,但本节并没有全面论述,而是涵盖了最适用于体验 Windows 8 的设计人员/开发人员的几个方面。
应用布局
应用应该使用网格布局来设计,按照内容的要求,使用层次导航方案或平面视图来组织。
当采用分级方法时,分级的顶部代表最低级别的细节,并且导航分级中的每个后续级别随着细节的增加而放大。通常,最高级别,有时被称为中心 ,是应用的入口点,显示用户可以进入的一个或多个组(参见图 2-10 )。
图 2-10 。最高层分层导航(Hub)
通过从主枢纽选择一个组,显示下一级导航(通常称为部分)。章节页面被安排来提供一些关于章节本身的上下文,并列出最低导航级别和最高细节级别的单个项目(见图 2-11 )。
图 2-11 。部分级别的分层导航
在节页面中,用户可以通过使用返回箭头(如图 2-11 所示)返回上一级导航到中心,这是一种通过滑动手势(如果启用触摸)或通过使用屏幕左右边缘垂直居中的箭头导航到同级节页面的方法,或者选择项目以继续到详细页面。在导航的详细页面级别,呈现项目数据的粒度视图(参见图 2-12 )。与部分页面一样,显示后退箭头是为了允许沿层次结构向上导航到组织项目的部分页面。与部分页面一样,用户可以通过在支持触摸的系统上使用滑动手势,或者通过与屏幕左右边缘的箭头进行交互,来选择在同一部分的详细页面之间导航。分层导航特别适合于浏览适合于主-详细分类的信息并与之交互。
图 2-12 。详细页的分层导航
许多应用不适合主从分类,这种分类适合分层导航结构,更侧重于基于文档的风格,熟悉 Microsoft Word、Excel 或 Internet Explorer。对于这种类型的应用,平面导航系统要好得多。平面导航的核心是内容被分成页面,页面上的信息要么不相关,要么在同一层级(见图 2-13 )。导航条在用户激活时出现,用于在活动文档之间切换,通常显示一个用户可以访问的命令,以将文档添加到会话中(见图 2-14 )。
图 2-13 。Internet Explorer 的设计使用整个视窗呈现单一文档的平面视图
图 2-14 。激活导航栏以切换活动文档的 Internet Explorer
字体设计
由于它非常强调排版和以文本为中心的内容,如果不提供文本格式和使用的建议,Windows 应用商店应用的用户体验指南就不完整。遵循瑞士设计的传统,在构建应用时应该使用一致的字体。应该使用哪种特定的字体取决于文本的目的。打算用于 UI 元素上的按钮或标签的文本应该倾向于 Segoe UI 字体,这在整个 Windows 8 用户界面元素中使用(参见图 2-15 )。
图 2-15 。Segoe UI 用于标签和其他 UI 元素
以只读方式呈现给读者的文本块,如新闻文章,应该倾向于使用衬线字体,因为读者习惯于用衬线字体呈现扩展的文本块(见图 2-16 )。这种字体应该以 9 磅、11 磅或 20 磅呈现,这取决于吸引焦点或显示重点的需要。这与瑞士风格在所有事情上对无衬线字体的偏好有所不同,因为微软设计团队发现在扩展阅读中衬线字体对眼睛更容易。
图 2-16 。只读文本块的 Cambria】
供用户阅读和编辑的连续块应该使用无衬线字体 Calibri(见图 2-17 )。这种字体的推荐大小为 13 磅,与 11 磅的 Segoe UI 高度相同,因此在同一行上一起使用时,两者将保持一致的外观。
图 2-17 。用于阅读编辑文本块的 Calibri】
不管字体如何,当需要强调某些文本时,强调的适当方式是通过使用与字体大小或字体粗细形成鲜明对比。在信息层次的同一层次上,权重用于强调,而大小用于区分不同的层次。使用下划线或斜体等文本装饰会降低清晰度,不应在 Windows 应用商店应用中用作强调。
其他 Windows 应用商店应用用户体验指南
在这一节中,我已经提到了一些用户体验指南,但有意地将重点放在处理应用视觉外观的指南上,将更多的行为方面留给本书其他部分讨论的主题,当我讨论开发人员可以用来构建优秀的 Windows 应用商店应用的工具时。如果你想在一个地方看到所有这些指南,或者不想等待,我建议你更深入地看看 MSDN 网站的 Windows 应用商店部分(http://msdn.microsoft.com/en-US/windows/default.aspx
)。
Windows 8 用户界面中的微软设计语言
除了桌面模式,Windows 8 用户界面在很大程度上基于微软设计语言指南和原则。让我们从查看开始屏幕开始(参见图 2-18 )。
图 2-18 。启动屏幕并激活魅力
开始屏幕具有全屏网格,显示对用户最重要的应用(由用户选择要包含在开始屏幕中的应用来指示),并且用户从中选择他或她想要运行的应用。这假设用户想做的第一件事是运行他或她通常使用的一个应用,网格的布局是尽可能高效地完成这个非常具体的任务。通过激活应用栏(未示出),用户可以请求呈现所有应用,而不是他们更窄的收藏夹列表,从而允许用户通过额外的步骤运行安装在机器上的任何应用。如果用户不想运行某个应用,而是想执行一些其他任务,比如更改系统设置或搜索文件,用户可以激活屏幕右侧的魅力条,显示一个附加命令列表。
在本章的前面,您看到了在 Windows UI 模式下运行的 Internet Explorer 是平面导航风格的一个很好的例子。关于分层导航风格的一个例子,你可以看看 Windows 商店,那里的应用可以购买或免费下载。当您进入应用时,会显示中枢,显示可用应用的不同类别(参见图 2-19 )。
图 2-19 。Windows 商店中心页面
从该中心,用户可以直接选择某些详细信息项目,也可以选择深入查看部分页面。
在整个 Windows 8 界面以及随其提供的应用中,您可以看到一个重复出现的主题,即基于印刷字体的界面、鲜艳的颜色和动画,以确保用户认为这些应用响应迅速,并提供与其他应用和云中信息的连接。
结论
在本章中,您了解了 Microsoft 设计语言,它是 Windows 8 用户界面和 Windows 应用商店应用的基础,您了解了一些影响 Microsoft 设计语言发展的早期风格和设计范例。无论您何时构建 Windows 应用商店应用,这些概念都会保留在后台或前台,并且会影响您做出的每个设计决策。虽然本质上足够简单,以至于没有多少设计技巧的开发者也可以有效地创建这些用户界面,但是指南也提供了由视觉设计艺术和工艺领域的技术人员创建的更复杂的设计。这些熟练的设计师被鼓励更深入地钻研包豪斯、瑞士风格和微软的用户体验设计准则。
三、设计 Windows 应用商店应用
在一个完美的世界里,应用开发人员会收到清晰、简洁的文件包,上面精确地列出了他们的应用应该是什么样子,以及应该做什么。他们从那张纸开始工作,从他们的角度来看,那张纸可能是自发产生的,并产生一个工作的和有用的应用。虽然许多开发人员已经设法找到了这样一个世界,但对于大多数以写代码为生的人来说,这种安排似乎就像到达香格里拉一样遥不可及。
没有拿到完整设计的开发人员必须变得比那些将需求转化为代码的人更优秀,相反,他们必须承担我认为更加困难和有趣的软件设计任务。本章面向那些自愿或必须参与 Windows 应用商店应用设计的开发人员,旨在提供此过程中重要步骤的概述。在这篇文章中,我将介绍与决定应用应该做什么以及如何呈现给用户相关的重要概念。在这一章中,我主要关注的是收集作为设计输入的需求,因为一个完全理解应用需要解决的问题的新手设计师比一个不理解的熟练设计师能创造出更有用的应用。
注收集需求、设计和构建软件有许多不同的方法。虽然我在本章中使用的一些术语可能倾向于一种或另一种方法,但我的意图是捕捉重要和相关的概念,而不管您使用什么方法(如果有的话)来构建您的软件。
沟通是关键
一位同事曾经告诉我,在应用的开发中,没有什么真理应该被认为是不证自明的。几年后,我和一个亲戚的一次谈话强化了这一点。这位亲戚以“我有个朋友在做法庭报道,需要软件帮忙”开始了一段对话。像这样的东西需要多长时间才能建成?”我开始回复:“你刚才问要多久才能建好。。。我的亲戚很快插话道,“但是我没有告诉你这个该死的东西需要做什么!”通常,像这样的对话揭示了客户心中的想法和构建软件的人所听到的之间的脱节,但在一个非典型的转折中,我的亲戚发现了许多商业伙伴不知道的东西——即,如果你想要构建一些东西,你必须清楚地传达你的要求。图 3-1 说明了这种断开,通常称为阻抗不匹配。
图 3-1 。阻抗不匹配
阻抗不匹配经常导致开发人员构建所要求的而不是所需要的,其主要原因是参与该过程的每个人都非常清楚地看到自己的观点,并且无法想象其他人会以不同的方式看待事情。如果不能完全避免,阻抗不匹配可以减少,方法是在设计过程开始时,承认人们对不同主题的理解会有所不同,并致力于创造一个一切都不是理所当然的环境。
注意没有作为团队的一部分参与软件开发的开发人员仍然应该在他们的头脑中区分开发人员和用户的角色,迫使他们自己站在用户的角度来看待事情。在这里,强迫自己在心里“解释”一切,就好像试图避免阻抗不匹配一样,这将有助于发现隐藏的需求。
应用应该擅长什么?
这似乎是一个显而易见的观点,但是当开始设计你的应用时,首先要确定的是它的用途。在这一点上,细节是不必要的;只需创建一个应用的一般陈述或描述,清楚地说明应用的用途或目的。一个设计良好的应用会有一件它真正擅长的事情,尤其是 Windows Store 应用,正如你将在第十九章中了解到的,它们可以协同工作来解决比单个应用开发人员预想的更大的问题。最好使用模板语句,如“该应用将 _____ 以便 _____”来帮助您不仅关注该应用将做什么,还关注为什么该应用将做或它提供的好处。如果我正在构建一个应用来跟踪一辆车的汽油里程数,语句可能是这样的:“这个应用将计算一辆车的燃油经济性,以便我可以更好地预测我的燃油成本。”
注意一定要以一种在整个设计和开发过程中非常明显的方式记录你的应用的高级目的。这构成了应用的主干,当您决定某项功能是否属于应用时,您会经常参考它。如果它不是某种法律或监管手段所必需的,并且对应用所声明的目的没有贡献,那么它就不属于应用。
确定功能需求
一旦应用的主要目的被确定为一种指导原则,确定支持主要目的的必要需求的工作就开始了,称为功能需求。根据您正在构建的应用的类型以及参与需求过程的其他人的可用性,有几种技术可以发现或引出需求。一些更常用的技巧包括:
- 采访 : 利益相关者,或者对正在生产的软件或者软件产生的输出或利益有某种兴趣的人,被咨询以了解他们对应用的期望和需要。在访谈过程中,利益相关者应该感到他们可以自由地表达他们的需求,而不会被告知他们不能拥有某些东西,以确保他们不会忽略提及关键需求。
- 头脑风暴 : 利益相关者和设计团队的成员一起为需求出主意。这个会议开始于一个“一切皆有可能”的氛围中,就像在面试技巧中一样,同样的原因是不要阻止利益相关者说出他们的需求。当所有参与者可以同时在同一个房间,使用白板和便笺等工具时,头脑风暴会议通常是最有效的,但是一个训练有素的团队可以通过使用电话会议工具远程实现类似的效果。关键是让每个人都集中注意力,同时积极参与。
- 流程映射 : 对现有流程进行遍历并完整记录,以捕捉为实现目标而执行的所有步骤。这种技术需要一个现有的过程,并且当每一个步骤都可以被仔细检查时效果最好。仅仅知道当前做了什么是不够的,但是理解每一步背后的动机以及它如何有助于实现最终目标也是至关重要的。
注意“我们总是”和“我们从来没有”是两个可以阻止组织改进的短语,除非组织愿意在需要开始一项有益的活动或结束一项没有价值的活动时加上“直到现在”。这让我想起了一个古老的故事,一个女人被她的母亲教导开始准备烤肉时,从每一端切下 1 英寸,就像她祖母所做的那样。当祖母来吃晚饭时,她注意到她的孙女正在切烤肉的末端,就问她为什么要这样做。“奶奶,你总是这样做的,”孙女回答道。老奶奶只是笑着回应道:“但是我的锅短了 2 英寸。”软件项目提供了一个很好的机会来问“为什么”,并确保类似的情况在你的组织中不存在。
评估已确定的要求
识别需求的技术都指定了不要阻止对任何涉众或团队成员来说重要或有效的需求的交流。这并不意味着每一个确定的需求都可以或者应该在最终产品中实现,只是它们都应该可以被评估。一旦确定了潜在需求的领域,下一步就是审查每个需求的适当性。适当性的决定因素简单明了。如果你能直接地(诚实地)交流满足需求对于允许应用满足它的目标是多么必要,那么这个需求就是合适的。这个规则的例外是,一些需求是由外部力量驱动的,比如契约义务和法规要求,这些需求必须被满足,而不管它们是否有助于满足应用的更高层次的目标。图 3-2 展示了用于决定是否将一个潜在需求提升为将要实现的需求的决策过程。
图 3-2 。潜在的需求到需求决策
另一个经常用来确定潜在需求是否应该提升为实际需求的度量是将项目分类为“必须拥有”、“最好拥有”或“不需要”这个想法是“必须拥有”的项目成为需求;不考虑“不需要”的项目;如果在考虑了“必须拥有”的项目之后还有额外的资源可用,那么“最好拥有”的项目也会得到考虑。这种排名模式的危险在于,太多的注意力很容易被放在“值得拥有”的项目上,导致更多的时间、精力和最终的金钱花费在成功申请实际上并不需要的项目上。对于一个没有经验丰富的项目经理的项目,建议采用图 3-2 中描述的更加严格的需求定义过程。
提示敏捷方法的实践者倾向于在所谓的用户故事中表达需求。用户故事通常采用某种形式的陈述“作为一个 _____,我需要系统 _____ 以便 _____”虽然术语用户故事是特定于某些方法的,但是识别关键涉众和每个需求的目的的想法对于任何方法都是有价值的实践。
根据应用的目的度量需求的行为不仅仅是保持应用忠实于目的的一种练习,它还旨在帮助保持驱动任何项目的三个关键因素之间的平衡,无论是构建软件还是摩天大楼。
- 时间:为了满足组织目标,项目必须在什么时候完成?
- 钱:能花多少?
- 范围:要完成的工作主体是什么?
这三个因素通常是所谓的项目管理三角、的一部分,如图图 3-3 所示。三角形是描述这些因素之间关系的一种很好的方式,因为,正如三角形的边一样,一个因素的改变不会影响其他两个因素。例如,如果有更多的资金,可能会雇用额外的开发人员,完成项目所需的时间将会缩短。通常,控制软件开发项目最简单的方法是保持对范围的严格控制。
图 3-3 。项目管理三角
在一些项目中,“必须具备”的项目不能全部符合范围,因为项目受到时间、资金或两者的限制。在这些情况下,必须对项目进行评估,以确定是否有必须实现但可以等到以后实现的项目。这种优先化过程提供了时间来真正批判性地思考需求,并且可以决定是能够产生有价值的东西,还是因为需求过程停滞而不得不放弃项目。
分解需求
一旦确定了应用满足其目标的必要需求,一个称为分解的迭代过程就开始了。软件开发中的分解是指将一个大问题分解成单独的步骤。通过迭代分解,步骤本身被分解成更小的部分,这种情况会一直持续下去,直到没有什么需要分解的了,或者直到你“完成”了。 Done 是一个有点主观的术语,但是我认为它已经达到了这样一个程度,熟悉项目的开发人员应该能够坐下来,将需求作为构建应用的蓝图。在开发人员非常熟悉他们正在解决的问题的组织中,当开发工作将由不熟悉这些问题的开发人员执行时,“完成”将不会被分解到几乎同样粒度的级别。
注分解是将令人生畏的问题转化为一系列容易解决的小问题的重要方法。记住关于如何吃大象的建议:一次吃一口。
构建交互流
到目前为止,焦点已经完全集中在应用作为一个整体需要完成什么上,您应该很清楚为了满足这些需求,应用需要输入和输出什么信息。一旦确定了这些需求,您就可以将注意力转移到确定用户如何最有效地将信息输入和输出应用上。在这里,你第一次开始考虑屏幕的想法,但它仍然是一个有点模糊的概念,因为你正在试图确定什么去哪里。在设计过程的这一点上,我通常倾向于避免暗示已经决定了屏幕将如何布局以及使用何种控件的语言。我更喜欢“然后用户选择保存操作”这样的短语,而不是“然后用户点击保存按钮”这是一个细微的差别,但是它将焦点放在确定完成应用目标所需的步骤顺序,以及如何将信息组织到用户交互的屏幕上。完成这一步后,您应该对应用会有哪些屏幕以及什么会触发这些屏幕之间的移动有一个很好的想法。图 3-4 显示了一个导航图,这是一个帮助定义和记录这些流程的有用方法。在其中,您可以清楚地看到应用中预期的视图,以及用户将如何在它们之间移动。
图 3-4 。导航图
线框
一旦团队确定了应用的流程,就该处理线框了。线框是应用屏幕的低保真度草图,专注于屏幕将容纳的信息和命令,而不是担心如何使它们漂亮,并陷入美学细节中。可以在餐巾纸、白板(一定要拍照)的背面捕捉线框,或者通过 Expression Blend 中的 Visio、PowerPoint、Balsamic 或 SketchFlow 等工具捕捉线框。在这一步中,您将决定用户使用什么类型的控件来与应用进行最有效的交互。在 Windows 8 应用中,线框应该反映全屏体验,用户可以专注于内容。图 3-5 展示了一个样本线框。请注意,没有花费任何努力使它看起来像一个 Windows 应用;相反,它关注的是信息以及不同交互的结果。
图 3-5 。三维线框模型
视觉设计
在应用的线框达成一致后,一些项目团队将把线框传递给视觉设计师,视觉设计师将使用诸如微软的 Blend for Visual Studio 2012 之类的工具将线框中的想法转化为视觉上有吸引力的界面。理想情况下,设计人员将遵循微软设计语言和瑞士设计风格中的指导原则,开发出与其他 Windows 8 应用外观一致的应用。Blend for Visual Studio 可以生成基于 HTML 的项目,这些项目与 Visual Studio 兼容,因此设计人员的工作可以成为开发人员添加代码以创建成品应用的基础。
注意微软有他们 Blend 软件的多个版本。Expression Blend 可用于使用 SketchFlow 构建原型,以及构建 WPF 和 Silverlight 应用。Blend for Visual Studio 2012 随 Visual Studio 一起安装,可用于设计 Windows 应用商店应用。
大多数情况下,团队没有专门的视觉设计师。他们可能有一个比团队中其他开发人员更有设计眼光的开发人员,或者视觉设计可能只是听天由命。与某些设计范例不同,使用新的 Windows 设计准则实际上给了不喜欢艺术的开发人员一个机会来创建一个吸引人的用户界面。此外,Microsoft 在项目模板中包含内置样式,可用于帮助确保应用具有新的 Windows 外观。
结论
在这一章中,我简要介绍了设计应用的许多概念和步骤。虽然重点是团队执行流程时的情况,但是当您作为一个团队创建应用时,所有步骤都值得考虑。重要的是要记住,除了极少数例外,伟大的应用都是有意的。它们首先被定义,然后被设计,只有当这两个过程完成时,它们才被构建。Microsoft 为新的 Windows 应用商店应用提供了指导,简化了视觉设计的任务,但是为了生成适合其预期目的的应用,需求定义的工作仍然必须以尽可能彻底的方式完成。
四、Visual Studio 2012 和 Windows 应用商店应用类型
在应用开发中,集成开发环境(IDE)可以让您感觉轻松工作并专注于应用应该解决的问题,也可以让您感觉非常分心,试图找出如何在 IDE 中操作,以至于无法专注于制作软件的实际任务。随着 Visual Studio 最近几个版本的发布,微软逐渐建立起了拥有可用的最佳开发 ide 之一的声誉。甚至许多不关心为微软平台开发的开发人员也会说(如果不情愿的话)很难找到更好的开发环境。在本章中,您将了解 Visual Studio 2012,这是该系列中的最新版本。因为完整地介绍这些工具和特性需要一本独立的书,所以我将在本章中介绍一些我认为对您很好地了解环境以完成本书中的练习最重要的主题。除了了解 Visual Studio 的一般知识,您还将了解用于 Windows 应用商店应用开发的项目模板。
Visual Studio 版本
Visual Studio 通常被用来描述在微软平台上开发应用的集成开发环境,但它不是一个单一的产品,而是指整个产品线。除了免费提供的速成版,Visual Studio 2012 阵容还包括:
- 适用于 Windows 8 的 Visual Studio 速成版 2012
- Visual Studio 网络版速成版
- 适用于 Windows 桌面的 Visual Studio 速成版 2012
- 适用于 Windows Phone 8 的 Visual Studio 速成版 2012
- Visual Studio 测试专业版 2012
- Visual Studio 专业版 2012
- Visual Studio 高级版 2012
- Visual Studio 旗舰版 2012
Visual Studio 速成版 2012 每个版本都提供了一个开发针对 Microsoft 堆栈不同部分的应用的环境,并且可以在不投资于完整的 Visual Studio 2012 产品的情况下使用。visual Studio Express 2012 for Windows 8 专注于提供必要的工具来构建和测试 Windows 应用商店应用,并为在 Windows 应用商店中共享和销售您的 Windows 应用商店应用提供支持。visual Studio Express 2012 for Windows 8 足以完成本书中的练习,此版本中可用的功能将是本章讨论的重点。以下是适用于 Windows 8 的 Visual Studio Express 2012 的主要功能:
- 对代码进行基本分析,找出可能会阻止 Windows 应用商店认证的错误或做法
- 集成调试器
- 运行 Windows 应用商店应用的模拟器
- 探查器帮助识别需要调整的代码
- 单元测试支持
除了 Visual Studio Test Professional 2012 是为应用开发组织中被分配了测试角色的人员设计的之外,Visual Studio 2012 的非速成版是为专业开发人员设计的。Visual Studio Professional 2012、Visual Studio Premium 2012 和 Visual Studio Ultimate 2012 都在应用开发的以下领域逐步增加了功能:
- 设计
- 建筑
- 测试
- 分析
- 解决纷争
您可以在www.microsoft.com/visualstudio
找到每个 Visual Studio 2012 版本的完整功能对比。您还可以在该网站上找到 Visual Studio Express 2012 for Windows 8。如果您尚未安装 Visual Studio 2012 版,我建议您在进一步阅读之前安装 Visual Studio Express 2012 for Windows 8。
Visual Studio 入门
首次打开 Visual Studio 2012 Express 时,会出现默认视图,如图图 4-1 所示。此时,用户界面中最重要的功能是菜单栏(图中标为 A)和起始页(标为 B)。菜单栏提供了对许多命令的访问,但是当第一次打开 Visual Studio 时,你很可能会去文件菜单(如图图 4-2 所示),在那里你会选择新建项目或打开项目。起始页提供了开发人员感兴趣的项目的链接,例如关于如何在 Visual Studio 中提高工作效率或执行某些开发任务的文章。
图 4-1 。Visual Studio 初始用户界面
图 4-2 。文件菜单
注意如果您将 Visual Studio 的设置更改为非默认设置,或者如果您使用的是 Visual Studio 的不同版本,如高级版或旗舰版,您的用户体验(如工具窗口、菜单和工具栏)可能会与本章中显示的不同。例如,当我开始使用 Visual Studio 2012 Express for Windows 8 编写这本书时,我从 Visual Studio 的不同版本导入的配置在文件菜单中有一个“新建项目…”项,而不是在文件菜单中有一个“新建”子菜单,并且在该子菜单中有一个“项目…”项。我已经将我的配置重置为 Express edition 的默认配置,因此本章和后续章节将说明 Visual Studio 2012 Express for Windows 8 的默认状态。
从文件菜单中选择新建项目,打开新建项目对话框,如图图 4-3 所示。此对话框在窗口的左侧显示按类别分组的可用项目类型。选择一个类别会在窗口的中间部分显示该类别中的项目类型列表。窗口底部是用于为项目指定名称的字段、项目在磁盘上的位置以及要创建和添加项目的解决方案的名称。本书没有涉及解决方案,所以在这一点上,我将把它们描述为同时打开和使用的相关项目的集合。默认情况下,在解决方案中创建新项目的选项是启用的,因为许多应用会将业务逻辑、数据访问代码和用于向用户提供界面的代码分离到他或她自己的项目中,以帮助创建这些职责的清晰划分。解决方案的另一个常见用途是在解决方案中有一个单独的项目来测试应用。
图 4-3 。新建项目对话框
一旦创建或打开了一个项目,就会显示附加的功能。在屏幕的右侧,解决方案资源管理器窗口(如图 4-4 中的所示)填充了您项目的文件/文件夹结构,允许您导航到项目中的任何文件,双击打开代码编辑器。图 4-5 显示default.js
在代码编辑器中打开。
图 4-4 。解决方案资源管理器窗口
图 4-5 。代码编辑器
图 4-6 中的所示的属性窗口包含不同的内容,这取决于 Visual Studio 中当前选择的内容。如果当前选定的项是 HTML 文件中的控件,则显示附加到该控件的属性。如果选定项是解决方案资源管理器中的文件,则显示选定文件的属性。
图 4-6 。属性窗口
我将在本章讨论的最后一个用户界面元素是工具栏上的调试按钮,如图图 4-7 所示。此按钮用于在本地计算机、内置 Windows 8 模拟器或网络上的远程计算机中启动应用的构建和调试会话。通过激活此按钮中的下拉菜单,您可以更改应用的默认运行时环境。
图 4-7 。调试按钮
在开发过程中,我经常在我的本地机器上运行我的应用,但模拟器很方便,因为它提供了在不同分辨率下测试的能力,改变方向,在不支持触摸的显示器上模拟触摸事件,并捕捉屏幕截图,当你准备在第二十三章中向 Windows 商店提交应用时,这些将会派上用场。如果您有一台平板电脑,或任何其他机器,您想用它来调试您的应用,那么一旦您配置了远程机器和 Visual Studio 项目,使用远程机器选项是非常无缝的。你可以在http://msdn.microsoft.com/en-us/library/windows/apps/hh441469.aspx
的 MSDN 上找到这个过程的一个很好的演示。我按照这些说明在几分钟内配置了我的 Windows RT 平板电脑进行调试。
我刚刚引导您浏览了 Visual Studio 界面,只是停下来向您展示成功使用本书所需的那些特性。我强烈建议您探索 Visual Studio 中可用的不同窗口、菜单和选项,并了解每种窗口、菜单和选项如何帮助您完成开发任务。
Windows 应用商店应用类型
在本节中,我将介绍可以使用 Visual Studio 2012 附带的项目模板创建的不同应用类型。你可以在新项目对话框中找到这些模板,分类在已安装的模板
JavaScript
Windows Store 下。我将在此仅介绍这些应用类型:
- 空白应用
- 固定布局应用
- 网格应用
- 拆分应用
- 导航应用
我将在第十八章中使用 C#语言介绍 Windows 运行时组件项目类型。
空白应用
空白应用是所有可用的 Windows 应用商店应用项目模板中最基本的。它创建的项目包括一组开始的图像,这些图像将被替换为应用徽标和闪屏的自定义图像,以及一个标准样式表和一个空白页。当您的单页应用不需要其他模板提供的布局时,这种项目类型非常适用。
固定布局应用
与空白应用一样,固定布局应用项目模板为您的 Windows 应用商店应用提供了一个非常基本的起点。事实上,空白应用和固定布局应用模板之间的唯一区别是,固定布局应用模板适用于需要固定纵横比的应用。您的应用的内容包含在一个ViewBox
控件中,该控件可以缩放其内容以适应应用。
这是推荐给游戏的,因为你可以用 1366 x 768 的分辨率来设计你的场景,这在今天的平板电脑上很常见。如果设备具有不同的分辨率,您的游戏将相应地缩放,以便在所有设备上对用户显示相同的效果。
网格应用
网格应用模板提供了空白应用模板所提供的一切,但它也为应用提供了屏幕和应用代码,该应用通过不同级别的细节来浏览分层数据。该应用由三个页面组成:一个显示所有组的高级视图,其中包含每个组内项目的汇总视图(如图图 4-8 所示),一个提供有关该组的附加信息及其包含的项目列表的组详细信息页面(如图图 4-9 所示),以及一个项目详细信息页面,其中提供了该组中单个项目的最详细信息(如图图 4-10 所示)。正如您在图中看到的,这个项目模板提供了一个实际上已经为您预构建的应用,只需要您修改它以适合您的数据。
图 4-8 。默认网格应用分组项目视图
图 4-9 。默认网格应用组详细信息视图
图 4-10 。默认网格应用项目详细信息视图
拆分应用
Split App 项目模板与 Grid App 模板一样,提供了一个现成的应用,用于浏览分层数据。Grid 应用和 Split 应用的主要区别在于 Split 应用仅使用两个视图来显示信息。第一个视图如图 4-11 中的所示,显示了物品被分类到的组列表。与 Grid App 不同,这个视图只包含关于组的信息,不显示任何项目信息。选择任意组导航到该组的项目屏幕(如图图 4-12 所示),在屏幕左侧提供该组中的项目列表,在屏幕右侧显示所选项目的详细信息。
图 4-11 。默认拆分应用群组视图
图 4-12 。默认拆分应用项目视图
导航应用
导航 App 项目模板是我个人的最爱。它包括必要的组件,以支持 Windows 应用商店应用常见的导航风格,而不会在您的解决方案中填充大量不必要或过于特殊的文件。这个项目类型将是你在第九章开始构建的应用的基础,随着你学习更多可以应用到应用中的概念,你将在几个章节中继续构建。通过创建页面控件,新的屏幕被添加到你的应用中,这个过程我将在第五章的中介绍,并将在本书的其余部分继续介绍。
如果空白应用模板是一张白色打印纸,导航应用模板将是一张绘图纸——两者本质上都是空的,但其中一个提供了一些有用的结构。相比之下,Grid 应用或 Split 应用模板可能是一本涂色书中的页面,其中提供了应用的轮廓,只需填写细节。
结论
在本章中,向您介绍了 Visual Studio 2012 以及可以使用内置项目模板构建的 Windows 应用商店应用类型。为了进一步学习,请考虑 Grid 应用和 Split 应用模板,检查一些现有的 Windows 应用商店应用。您可能会惊讶于您经常看到这两种方法的组件。例如,Windows 8 安装的新闻和商店应用都采用了网格应用的方法,而邮件应用是基于拆分应用模板设计的。
五、HTML 控件
正如许多新技术一样,许多人不知道 HTML5 到底是什么。对某些人来说,这就是视频。对一些人来说,这是关于语义标签的,比如新的header
和nav
标签。对某些人来说,这是一种让网站在移动设备上运行的新魔法。对其他人来说,它只是我们在过去几十年中所熟知和喜爱的 HTML 的下一个版本。不管你听说过什么,可以肯定地说 HTML5 不是一个单一的东西。事实上,很多被认为是 HTML5 的东西是三种东西的组合:HTML、JavaScript 和 CSS。互联网上不乏关于 HTML5 及其功能的信息。HTML5 Rocks 网站(www.html5rocks.com
)是一个很好的资源。
如果你还不熟悉 HTML5,这一章是为你准备的。在这篇文章中,我将概述一些更常见的元素。因为我属于“HTML5 只是 HTML 的下一个版本”的阵营,所以我一般简称它为 HTML。此外,我经常将 HTML 元素称为控件,特别是当提到用户与之交互的界面元素时。
幸运的是,如果你熟悉 HTML 及其提供的控件,你会很高兴地知道所有这些知识现在都适用于 Windows 8 应用开发。我们将用于开发 Windows 8 应用的 HTML 和 JavaScript 与我们用于开发网站的 HTML 和 JavaScript 相同。也就是说,你仍然应该浏览这一章,因为它确实涵盖了一些特定于 Windows 应用商店应用开发的概念。
在开始之前,您需要创建一个 Visual Studio 项目来使用这些示例。
Visual Studio 项目
正如我在第四章中提到的,Visual Studio 有许多不同的版本。在目前可用的八个版本中,有四个可用于创建 Windows 应用商店应用。这四个是
- 适用于 Windows 8 的 Visual Studio 速成版 2012
- Visual Studio 专业版 2012
- Visual Studio 高级版 2012
- Visual Studio 旗舰版 2012
在本书中,我将使用免费速成版来讨论所有的例子。虽然其他版本为专业开发人员提供了一些额外的好处,但是您会发现使用免费的开发人员工具来构建真实世界的应用是非常可行的。
注意如果你有专业版、高级版或终极版,一定要利用它们包含的额外功能。请注意,在这些版本中,一些默认设置(如菜单项、键盘快捷键和工具栏)可能与本书中的屏幕截图和说明不同。
在接下来的几页中,我们将为本章以及第六章、 7 和 8 中将要进行的工作准备环境。我会走过去
- 创建项目
- 查看默认项目内容
- 添加新页面
- 导航到新页面
我们开始吧!
创建项目
正如在第四章中所描述的,在 Visual Studio 中有许多项目模板可用于使用 HTML 和 JavaScript 构建 Windows 8 应用。我发现,对于我的口味来说,导航应用模板达到了适当的平衡,包括足够的框架来开始一个应用,而不包括太多的“新项目膨胀”因此,让我们从基于该模板创建一个项目开始。
注本书附带的源代码包括一个名为 WinJSControlsSample 的完整项目,其中包含了在第五章、第六章、第七章和第八章中使用的样本代码。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
-
打开 Visual Studio。
-
Select File
New Project. This will open the New Project dialog (see Figure 5-1).
图 5-1 。新建项目对话框
-
在“新建项目”对话框的左侧窗格中,选择模板
JavaScript
Windows Store。
-
选择导航应用项目模板。
-
为项目命名:WinJSControlsSample。
-
单击“确定”创建您的项目。
此时,您有了一个新项目。现在,通过按 Ctrl+F5 构建并运行它。你应该会看到类似于图 5-2 的东西。
图 5-2 。现成的导航应用
注意有几种方法可以让应用停止运行。你可以从触摸屏顶部向下滑动,或者用鼠标“抓取”应用的顶部,然后将其拖到屏幕底部。或者,您可以在键盘上按 Alt+F4。最后,如果您正在调试(即,您按了 F5 而不是 Ctrl+F5),您可以使用 Alt+Tab 返回 Visual Studio 并停止调试器。
太好了!这不是很令人兴奋,但这是一个开始。现在,让我们准备在接下来几章的示例中使用的项目。
查看默认项目内容
使用导航应用模板创建项目时,会包含许多文件。名为default.html
的文件是应用的起点。Visual Studio 还创建了相应的default.css
和default.js
文件,以及一个navigator.js
文件,其中包含我们导航应用的逻辑。添加了一些占位符标志图像,以及一个名为 home.html 的PageControl
。在阅读本书的其余部分时,您将会看到这些文件中的大部分,所以我在这里不会深入讨论它们。
让我们首先切换您的应用,以便使用灯光主题。打开default.html
并在清单 5-1 中找到代码。这一步不是必需的,但它确实使接下来几章中的截图更容易阅读。你将在第九章开始构建的应用中使用一个定制的黑暗主题。
清单 5-1。 变换主题
<title>WinJSControlsSample</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
将 CSS 参考从ui-dark.css
更改为ui-light.css
,然后在home.html
中进行相同的更改。现在,浏览default.html
并找到清单 5-2 中的代码。PageControlNavigator
将是您将添加到该应用中的所有页面控件的宿主。注意这里有一个对/pages/home/home.html
的引用。这是您的应用的主页,当应用启动时,它将被加载到default.html
中。当您浏览整个应用时,default.html
将始终可见,其他页面将动态加载到这个PageControlNavigator
中。
清单 5-2。 页面控制导航器
<div id="contenthost"
data-win-control="Application.PageControlNavigator"
data-win-options="{home: '/pages/home/home.html'}"></div>
添加新页面
现在,让我们为您将在本章中看到的示例向应用添加一个新页面。
-
Right-click the pages folder and select Add
New Folder. Name the new folder
htmlcontrols
(see Figure 5-3).图 5-3 。添加新文件夹
-
右键单击
htmlcontrols
文件夹并选择添加新项目..。。这将打开“添加新项目”对话框。
-
选择页面控件项目。
-
给页面控件起一个名字:
htmlcontrols
。
Visual Studio 现在创建了三个文件,它们共同组成了页面控件:htmlcontrols.css
、htmlcontrols.html
和htmlcontrols.js
。在这一章中,你的大部分代码将进入htmlcontrols.html
文件。打开htmlcontrols.html
并添加form
元素,如清单 5-3 所示。
清单 5-3。 添加表单
<body>
<form id="myForm">
<!-- SNIPPED -->
</form>
</body>
虽然您的应用不需要,form
将允许您在本章后面利用一些内置的验证和按钮功能。但是,默认情况下,当提交 HTML 表单时,表单中的数据会在请求新页面时发送到服务器。
因为您不希望在提交表单时导航到新页面,所以您修改了htmlcontrols.js,
的ready
函数,如清单 5-4 所示。这可以防止应用离开您的页面。
清单 5-4。 防止默认表单提交
ready: function (element, options) {
document.getElementById("myForm").addEventListener("submit", function (e) {
e.preventDefault();
});
},
现在,准备示例应用还有最后一件事要做:让用户导航到这个新页面。
导航到新页面
如上所述,现在应用将在启动时加载home.html
。然而,您将把您的代码添加到htmlcontrols.html
中。您需要提供某种方式来从一个页面导航到另一个页面。打开home.html
,找到主section
元素。用按钮的代码替换占位符内容,就像我在清单 5-5 中所做的那样。
清单 5-5。 新 home.html 主要内容章节
<section aria-label="Main content" role="main">
<p><button id="htmlButton">Chapter 5 - HTML Controls</button></p>
</section>
现在,打开home.js
并修改清单 5-6 中突出显示的代码。
清单 5-6。 修改为 home.js
"use strict";
var nav = WinJS.Navigation;
WinJS.UI.Pages.define("/pages/home/home.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
htmlButton.addEventListener("click", function (e) {
nav.navigate("/pages/htmlcontrols/htmlcontrols.html")
}, false);
}
});
就这样。保存所有更改并运行应用。当应用启动时,您应该会看到一个类似于图 5-4 的页面。
图 5-4 。应用的主页
点击按钮应该会将您带到类似于图 5-5 的页面。因为您使用导航应用模板和页面控件,所以后退按钮会自动连接。单击它将返回到应用主页。
图 5-5 。htmlcontrols.html 的当前内容
控制器
HTML 已经被用来构建 web 应用很多年了。使用 HTML 和 JavaScript 构建 Windows 8 应用在许多方面与构建 web 应用相似。HTML 为构建应用提供了许多控件。在这一章中,我将快速介绍许多最常见的控件。这一章并不是对这些 HTML 控件的详尽的参考,而是对每一个控件都提供了一个简短的描述和示例用法。
注意如果你正在执行本章中的所有示例,请确保将所有的示例代码放在
htmlcontrols.html
的<section aria-label="Main content" role="main">
和</section>
元素之间。
标签
标签可能是我将要介绍的所有 HTML 控件中最简单的一个。默认情况下,它不会以任何方式改变其内容的外观,尽管可以用 CSS 对其进行样式化。标签通常用于使用for
属性将一些文本与输入字段相关联。当标签控件的for
属性与输入控件的id
属性匹配时,单击或触摸该标签会选择或切换输入控件。这对可用性非常好,尤其是在使用触摸时,因为它为用户选择输入字段提供了一个更大的上下文目标。
向页面添加标签非常简单。在清单 5-7 中,您可以看到在页面上放置一个标签所需的单行代码。通过将id
属性设置为"myLabel"
,您可以在您的 JavaScript 代码中引用这个标签,并使用 CSS 样式化标签。
清单 5-7。 添加标签
<label id="myLabel" for="myTextbox">This is text in a label</label>
将for
属性设置为"myTextbox"
会将该标签与该页面上某处定义的另一个控件相关联。当用户点击myLabel
时,myTextbox
控件将被聚焦。这段代码的结果可以在图 5-6 中看到。
图 5-6 。标签
除了将文本与输入字段相关联之外,还可以考虑为其他文本使用标签,比如错误消息和其他动态用户反馈,或者甚至作为一种使文本具有样式的简单方法。虽然这在功能上是可能的,但许多人认为这在语义上是不正确的。对于这样的情况,您应该考虑使用 HTML span
或div
元素,或者其他在语义上更有意义的元素。一个这样的例子可以在MessageDialog
的讨论中的第六章的中看到,其中一个span
元素用于动态显示用户在MessageDialog
中的选择。
链接
只有一个屏幕的应用当然有用,但是通常情况下,你的应用需要多个屏幕。有几种方法可以从一个屏幕导航到另一个屏幕,但最简单的方法之一是使用 HTML 链接。
超文本链接
清单 5-8 展示了你可以添加到htmlcontrols.html
中的代码,以便在页面上放置一个链接。添加这段代码,然后添加另一个名为otherpage
的页面控件,遵循与创建htmlcontrols
页面控件相同的步骤。这是您的链接将导航到的页面。
清单 5-8。 添加链接
<a id="myLink" href="/pages/otherpage/otherpage.html">Link to another page</a>
现在运行应用。事情看起来很好,你应该在页面上看到一个类似于图 5-7 所示的链接。点击链接将导航至otherpage.html
;所以,试试吧。
图 5-7 。环
另一页看起来像你期望的那样吗?可能不会。我知道我第一次点击我正在构建的应用中的链接时,结果有点不和谐。我从otherpage.html
看到了我期望的内容,但是我期望应用的格式保留在新页面上。相反,页边距和其他样式已经消失,如图 5-8 所示。
图 5-8 。另一页,但不完全是预期的
发生了什么?在前面的清单 5-2 中,您看到了default.html
中实现了一个PageControlNavigator
的代码。你所有的页面,比如otherpage.html
,都将被加载到那个容器中——或者至少这是我所期望的。相反,点击链接会导致一个顶级导航,otherpage.html
被全屏加载,取代了default.html
和导航容器。事实证明,使用PageControlNavigator
需要以不同的方式处理导航。
导航方法
当您从导航应用项目模板创建此应用时,您隐含地做出了这样的决定:为了应用处理页面之间的导航并提供一致的用户体验的便利性,您牺牲了使用简单链接的能力。为了让导航如您所愿,您需要一种特殊的方法在页面之间导航。Windows 8 为此提供了WinJS.Navigation.navigate
方法。在清单 5-6 中已经介绍了这个方法。您向home.html
添加了一个按钮,并且在home.js
中,您向按钮的click
事件添加了一个事件处理程序。然后当你查看home.html
和htmlcontrols.html
时,导航“刚刚工作”要让这个链接按预期工作,您只需在这里使用相同的方法。让我们从htmlcontrols.html
中的链接中移除href
属性(参见清单 5-9 ,让我们为htmlcontrols.js
中的就绪函数添加一个链接的click
事件的处理程序(参见清单 5-10 )。
清单 5-9。 改变链接
<a id="myLink">Link to another page</a>
清单 5-10。 处理链接点击事件
ready: function (element, options) {
myLink.addEventListener("click", function (e) {
nav.navigate("/pages/otherpage/otherpage.html")
}, false);
},
现在如果你运行应用并点击链接,你会看到预期的结果(见图 5-9 )。页边距和后退按钮似乎对otherpage.html
是正确的。事实上,back 按钮已经被连接起来,当它被点击时可以导航回htmlcontrols.html
。
图 5-9 。这是预料中的事
任务完成!正确嗯,从技术上来说,是的,但是如果你在一个页面上有五个链接呢?如果你的应用中有 25 个链接呢?如果您有一个从数据源动态生成的链接列表会怎么样?更新每个链接并为每个链接添加一个click
事件处理程序,虽然可能,但效率不是很高。幸运的是,还有一种选择。
使用查询将 HTML 链接转换为使用 Navigate 方法
Windows 8 提供了WinJS.Utilities.query
方法,该方法允许您获取与查询选择器匹配的元素集合,然后对该集合中的每个元素做一些事情。在您的例子中,您将为每个匹配链接查询的元素添加click
事件处理程序(参见清单 5-11 )。
清单 5-11。 向链接集合添加事件处理程序
WinJS.Utilities.query("a").listen("click", function (e) {
e.preventDefault();
nav.navigate(e.target.href);
});
我将带您浏览一遍,解释每条语句的作用。函数调用WinJS.Utilities.query("a")
查找当前页面上的所有链接——所有a
元素。对于找到的每个链接,调用listen
方法来处理click
事件,并提供一个匿名函数作为事件处理程序。调用e.preventDefault
会阻止默认行为,即链接的href
属性中指定地址的顶级导航的发生。然后,对navigate
的调用按照预期执行导航。
注意如果您熟悉 jQuery,
WinJS.Utilities.query
的行为与 jQuery $
函数非常相似。两者都采用选择器(www.w3.org/TR/css3-selectors/
)并返回匹配 DOM 元素的集合。如果你更喜欢 jQuery,你会很高兴知道你可以在你的 Windows 8 应用中使用它,以及 WinJS 功能。
现在你有几个选择来放置清单 5-11 中的代码。一种选择是将它放在每个页面控件的ready
函数中,类似于您在home.js
( 清单 5-6 )中对按钮点击处理程序所做的。这是完全正确的,但是可能会导致几个文件有重复的代码。当您为您的项目选择导航应用模板时,Visual Studio 向js
文件夹添加了一个名为navigator.js
的文件,这里定义了PageControlNavigator
控件。这似乎是添加您的click
事件处理程序的好地方。在清单 5-12 中,我将定义一个名为NavigationUtilities
的类,并将一个名为HandleLinkClickWithNavigate
的静态方法添加到该类中。我将添加清单 5-11 中的代码作为HandleLinkClickWithNavigate
方法的主体。
清单 5-12。 定义导航工具类
WinJS.Namespace.define("Application", {
PageControlNavigator: WinJS.Class.define(
// SNIPPED
),
NavigationUtilities: WinJS.Class.define(
function NavigationUtilities(element, options) { /* empty constructor */ },
{ /* no instance methods */ },
{ /* static methods */
// change all links to use navigation methods instead
HandleLinkClickWithNavigate: function () {
WinJS.Utilities.query("a").listen("click", function (e) {
e.preventDefault();
nav.navigate(e.target.href);
});
}
}
)
});
现在,您必须确保在页面加载后调用此方法。如清单 5-13 中的所示,你可以通过调用htmlcontrols.js
的ready
函数中的HandleLinkClickWithNavigate
来实现。
清单 5-13。html controls . js 中的修改就绪函数
ready: function (element, options) {
Application.NavigationUtilities.HandleLinkClickWithNavigate();
document.getElementById("myForm").addEventListener("submit", function (e) {
e.preventDefault(); });
},
您必须做的最后一件事是再次添加链接的href
属性,这是您之前移除的,使它看起来再次类似于清单 5-8 中的。现在,当您运行应用并单击链接时,导航按预期进行,但您还有一个额外的好处,即您添加到应用中的任何其他链接也将按预期工作。
不过,要记住的一点是,这种方法会导致所有的链接都这样。如果您有任何想要以不同方式运行的链接,您将不得不修改HandleLinkClickWithNavigate
方法。一种可能是将查询从WinJS.Utilities.query("a")
更改为WinJS.Utilities.query("a:not(.defaultClick)")
,然后向您希望保留默认行为的任何链接添加一个 CSS 类defaultClick
。有许多方法可以解决这个问题,这只是一个选项。
文本输入控件
大多数应用接受用户以某种形式输入的文本。它可能是搜索字段、登录屏幕或数据输入表单,但应用不接受文本输入的情况很少见。HTML 为文本输入提供了几种选择:单行文本输入、密码输入和多行文本输入。
单行文本输入
向应用页面添加基本文本输入是通过 HTML input
元素完成的。正如你将在本章中看到的,input
元素用于许多不同类型的输入,这些输入由type
属性指定。也许最广泛使用的是文本输入,通常称为文本框。将清单 5-14 中的代码添加到htmlcontrols.html
中,运行您的应用,您会看到一个简单的文本输入,类似于图 5-10 中所示的内容。
清单 5-14。 添加文本输入控件
<input type="text" id="myTextbox" />
图 5-10 。文本输入
该控件还有一些其他属性,可以设置这些属性来更改文本输入控件的行为。一些更常见的属性是
placeholder
:这个属性允许你提供一些文本,用来给用户提供一些指示,通常是有效输入的一个样本。当字段为空时,文本可见,但当字段成为焦点时,文本消失。maxlength
:这个属性允许你指定一个文本输入控件中允许的最大字符数。这是一个简单的第一级数据验证,在用户输入最终将被保存到具有指定最大长度的数据库字段时特别有用。required
:这个属性表示用户必须在文本输入控件中输入一些内容才能生效。这是一个验证属性,是 HTML 提供的验证功能的一部分。pattern
:这个属性允许你指定一个正则表达式,用来验证用户输入到文本输入控件中的文本。这是一个验证属性,是 HTML 提供的验证功能的一部分。title
:这个属性允许你指定文本作为控件的工具提示。此外,如果指定了 pattern 属性,并且用户输入了无效文本,则该属性的内容也会作为错误消息的一部分显示给用户。
清单 5-15 显示了使用这些属性的简单语法。
清单 5-15。 给你输入的文本添加占位符文本
<input type="text" id="myTextbox"
placeholder="Enter your name"
maxlength="15"
required
pattern="^[A-Za-z]*$"
title="Only characters, A-Z or a-z" />
当您运行该应用时,您将看到一个带有占位符文本的控件,如图 5-11 中的所示,您将不能在该字段中输入超过 15 个字符。此外,当用户试图提交表单时,将进行验证以确保用户输入了某些内容(由于required
属性)并且只输入了字母字符(因为pattern
属性只允许字母)。提交带有无效文本的表格,或者根本没有输入任何文本,将导致向用户显示一条错误消息(参见图 5-12 )。
图 5-11 。文本输入,带占位符文本
图 5-12 。文本输入,无效文本包含空格
注意虽然总是强制执行
maxlength
属性,但是在表单提交之前不会触发 HTML 验证。我将在本章后面的“按钮”部分讨论提交表单。关于 HTML5 提供的新表单验证功能的很棒的教程可以在www.html5rocks.com/en/tutorials/forms/constraintvalidation/
找到。
只要用户输入有效文本,就不会显示错误消息。在图 5-13 中可以看到文本输入控件的一个新特性,这是 Windows 8 应用(以及微软的 Internet Explorer 10 网络浏览器)的一个新特性。当用户输入文本时,文本输入控件中会出现一个×按钮。单击此按钮将清除文本输入控件,以便用户可以输入新文本。
图 5-13 。有文本的文本输入,有焦点
如果您有理由隐藏这个 clear 按钮,您可以使用 CSS 来样式化-ms-clear
伪元素。可以将清单 5-16 中的代码添加到default.css
中,从应用的所有文本输入控件中移除清除按钮。
清单 5-16。 从文本输入控件中移除清除按钮
::-ms-clear {
display: none;
}
密码输入
密码是另一种通常输入到应用中的文本类型,HTML 提供了密码输入控件。这些控件的行为非常类似于我刚刚提到的单行文本输入控件,但是增加了屏蔽用户输入的安全性好处,使得窥探的眼睛看不到正在键入的内容。要添加一个密码输入控件,添加从清单 5-17 到htmlcontrols.html
的代码。
清单 5-17。 添加密码输入控件
<input type="password" id="myPassword" />
运行应用,你会看到一个密码字段,看起来很像一个文本输入控件(图 5-14 )。但是,默认情况下,在字段中键入不会显示您的文本。相反,它显示了在图 5-15 中看到的熟悉的黑点。
图 5-14 。密码输入
图 5-15 。带文本的密码输入(用点屏蔽)
大多数可以应用于文本输入控件的可选属性也可以应用于密码输入控件。具体来说,placeholder
、maxlength
、required
、pattern
和title
属性都创建了密码控件的行为,就像它们创建常规文本控件一样。
我提到密码输入控件默认不显示用户的文本。但是,与清除文本输入控件上的文本的×按钮类似,密码输入控件也有一个显示输入文本的按钮。当点击并按住按钮时,密码被显示,当松开按钮时,密码再次被圆点掩盖(参见图 5-16 )。类似于用 CSS 移除文本输入控件上的×按钮,这个按钮可以通过样式化-ms-reveal
伪元素来移除。
图 5-16 。密码输入,显示密码
多行文本输入
有时,您需要比单行文本输入控件所能提供的更多的文本。使用 HTML 的textarea
元素,您可以创建一个多行文本输入控件,它允许长文本和带回车的文本。将清单 5-18 中的代码添加到htmlcontrols.html
中。
清单 5-18。 添加多行文本输入控件
<textarea id="myTextarea"></textarea>
这将创建最基本的多行文本输入控件,如图 5-17 所示。如果内容太大,控件容纳不下,滚动条就会出现(参见图 5-18 )。
图 5-17 。多行文本输入
图 5-18 。多行文本输入,带文本和滚动条
该控件还有一些其他属性,可以设置这些属性来更改多行文本输入控件的行为。一些比较常见的是
rows
:这个属性允许你指定控件的高度,以文本行为单位。例如,设置rows="5"
将增加上面控件的高度,使五行文本可见。多行文本输入控件的高度也可以在 CSS 中用height
属性设置。cols
:这个属性允许你指定控件的宽度,这个宽度是根据一行中字符的大概数量来度量的。例如,设置cols="50"
将增加上面控件的宽度,这样每行大约可以容纳 50 个字符。因为在大多数字体中,字符宽度不是恒定的,所以有些字符比其他字符占用更多的空间,根据内容的不同,每行的字符可能更多或更少。多行文本输入控件的宽度也可以在 CSS 中用width
属性设置。maxlength
:这个属性允许你指定多行文本输入控件中允许的最大字符数。这是一个简单的第一级数据验证,在用户输入最终将被保存到具有指定最大长度的数据库字段时特别有用。required
:这个属性表示用户必须在文本输入控件中输入一些内容才能生效。这是一个验证属性,是 HTML 提供的验证功能的一部分。title
:这个属性允许你指定文本作为控件的工具提示。
选择控件
你想要多大的比萨饼?你的比萨饼想要哪种配料?你想在家吃饭,带走,还是叫外卖?与订购比萨饼一样,应用可以向用户提供多种选择。HTML 提供了一些用于选择的控件:下拉列表、复选框和单选按钮。
下拉列表
下拉列表可以在很小的空间内为用户提供大量的选项。让我们添加一个并尝试一下。将清单 5-19 中的代码添加到htmlcontrols.html
。
清单 5-19。 添加下拉列表控件
<select id="mySelect">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
<option selected="selected" value="option4">Option 4</option>
<option value="option5">Option 5</option>
<option value="option6">Option 6</option>
<option value="option7">Option 7</option>
<option value="option8">Option 8</option>
<option value="option9">Option 9</option>
<option value="option10">Option 10</option>
</select>
这段代码做了很多事情。它向页面添加一个下拉列表;它在下拉列表中添加了十个选项供用户选择;它使第四个选项成为页面加载时的默认选项(见图 5-19 )。下拉列表的默认行为是只允许一个选择,并且只显示选中的选项。点击或触摸下拉列表展开控件,向用户显示所有选项(参见图 5-20 )。如果列表太长,滚动条会自动添加。
图 5-19 。下拉列表控件
图 5-20 。展开的下拉列表控件
该控件还有一些属性,可以设置这些属性来更改下拉列表的行为。一些比较常见的是
selected
:这是option
元素的一个属性,见于清单 5-19 ,允许你指定默认选择哪些选项。如果使用了多个attribute
,那么selected
属性可以被添加到多个option
元素中。multiple
:该属性允许您指定用户可以从列表中选择多个选项。这是通过在选择每个所需选项时按住 Ctrl 键来完成的。设置此属性后,列表显示为列表框,而不是下拉列表。size
:这个属性允许你指定列表的大小,用不需要滚动就可以看到多少选项来衡量。
在清单 5-20 中,您可以修改之前的下拉列表,以允许多重选择并一次显示五个项目。
清单 5-20。 添加允许多选的下拉列表控件
<select id="mySelect" multiple size="5">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
<option selected="selected" value="option4">Option 4</option>
<option value="option5">Option 5</option>
<option value="option6">Option 6</option>
<option value="option7">Option 7</option>
<option value="option8">Option 8</option>
<option value="option9">Option 9</option>
<option value="option10">Option 10</option>
</select>
当你运行应用时,你会看到一个列表,类似于图 5-21 。您可以通过在选择选项时按住 Ctrl 键来选择多个项目(因为有了multiple
属性)。
图 5-21 。下拉列表,调整大小并允许多重选择
复选框
当你需要问用户一个“是或否”的问题时,或者当有许多选项并且允许用户选择多个时,复选框是一个很好的选择。在清单 5-21 的中,你可以给htmlcontrols.html
添加一个复选框。
清单 5-21。 添加复选框控件
<input type="checkbox" id="myCheckbox" />
<label for="myCheckbox">Check the box</label>
图 5-22 。复选框,未选中
注意标签的使用,当它的内容被点击或触摸时,切换复选框。默认情况下,该复选框是未选中的,如图图 5-22 所示。
该控件有几个属性,可以设置这些属性来更改复选框控件的行为。最常见的是checked
属性(见清单 5-22 )。
清单 5-22。 添加复选框控件
<input type="checkbox" id="myCheckbox" checked />
<label for="myCheckbox">Check the box</label>
该属性允许您指定默认情况下复选框是否被选中(参见图 5-23 )。
图 5-23 。复选框,已选中
单选按钮
当用户需要从可能选项的简短列表中选择一个选项时,单选按钮非常有用,例如回答选择题或回答调查。在清单 5-23 中,我给htmlcontrols.html
添加了两个单选按钮。每个单选按钮由一个 HTML input
元素表示,其type
属性设置为radio
。为了更好的可用性,我再次为每个单选按钮添加了一个标签控件。
清单 5-23。 添加单选按钮
<input type="radio" id="myRadio1" name="myRadioButtonGroup" value="radio1" />
<label for="myRadio1">Option 1</label>
<input type="radio" id="myRadio2" name="myRadioButtonGroup" value="radio2" checked />
<label for="myRadio2">Option 2</label>
当您运行您的应用时,您会看到两个单选按钮(参见图 5-24 )。由于checked
属性,最初选择了选项 2,您应该能够在这两个选项之间切换。每个单选按钮都有一个唯一的id
属性,但是name
属性是相同的。name
属性用于将相关的单选按钮组合在一起,页面上的每组单选按钮都需要不同的name
属性。
图 5-24 。单选按钮
选择选择控件
在决定使用哪种类型的选择控件时,应该考虑许多因素。用户是从一长串选项中选择一个选项吗?使用下拉列表。用户是从三到四个选项中选择一个选项吗?也许一系列单选按钮会更好。用户是否在回答“是或否”的问题?使用复选框。
显然,做出这个决定不仅仅是这三个简单场景中的例子。Microsoft 提供了有关如何确定控件是否适合在多种情况下使用的指南。
- 下拉列表 :
http://msdn.microsoft.com/en-us/library/windows/desktop/aa511458.aspx
- 复选框 :
http://msdn.microsoft.com/en-us/library/windows/desktop/aa511452.aspx
- 单选按钮 :
http://msdn.microsoft.com/en-us/library/windows/desktop/aa511488.aspx
按钮
当用户必须启动某种操作时,比如保存数据或执行搜索,您应该提供一个按钮控件。HTML 提供了三种不同类型的按钮控件,其中两种是为满足非常特殊的需求而设计的:重置按钮,用于将表单中的所有字段重置为默认值;提交按钮,触发表单的submit
事件;和用于任何其他用途的标准按钮。将清单 5-24 中的代码添加到htmlcontrols.html
中。
清单 5-24。 添加按钮控件
<button type="button" id="myButton">Button</button>
<button type="reset" id="myReset">Reset</button>
<button type="submit" id="mySubmit">Submit</button>
运行应用查看按钮,如图 5-25 所示。
图 5-25 。小跟班
虽然您的按钮通常只包含纯文本,但也允许按钮包含其他更丰富的内容类型,如图像或格式化文本。例如,在清单 5-25 中,你可以创建一个包含松鼠图像和一些格式化文本的按钮。
清单 5-25。 添加更丰富的按钮
<button type="button" id="myButton">
<img src="/img/60/Squirrel.png" /><br />
Click the <em>squirrel</em>!
</button>
你的松鼠按钮可以在图 5-26 中看到。
图 5-26 。富按钮
注意松鼠图像可以在 WinJSControlsSample 项目中找到,该项目包含在本书附带的源代码中。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
复位按钮
重置按钮是最简单的,所以我从它开始。当按钮控件的type
属性设置为reset
时,按钮被赋予默认功能,该功能会将表单中所有其他字段的值更改为初始值。因为这个行为是自动分配的,所以不需要处理重置按钮的click
事件;但是,表单中必须有一个重置按钮,此默认行为才能起作用。在这一章的开始,在清单 5-3 (这里在清单 5-26 中重复)中,我给htmlcontrols.html
添加了一个form
来允许这个例子工作。
清单 5-26。 添加表单
<body>
<form id="myForm">
<!-- SNIPPED -->
</form>
</body>
运行应用,在前面添加的文本输入控件中输入一些文本。现在单击 reset 按钮,文本将被清除。
提交按钮
按钮控件被赋予默认行为的另一个例子是当它的type
属性被设置为submit
时。在这种情况下,点击或触摸按钮将通过触发表单的submit
事件来提交表单。提交按钮在默认情况下也有不同的样式,如图 5-25 所示。此外,提交按钮是页面上的默认按钮,例如,如果在光标位于单行文本输入字段时按下 Enter 键,则表单被提交。与 reset 按钮一样,submit 按钮必须在表单中,默认行为才能起作用,并且您不需要处理它的click
事件;但是,您可以选择处理表单的submit
事件。
实际上,这正是你之前在清单 5-4 中所做的(在清单 5-27 中重复)。在这种情况下,您只需通过调用preventDefault
函数来取消表单提交。在本书的后面,我将向submit
事件添加更多的功能,以便在提交表单时做更多有趣的事情。
清单 5-27。 防止默认表单提交
ready: function (element, options) {
document.getElementById("myForm").addEventListener("submit", function (e) {
e.preventDefault();
});
},
标准按钮
标准按钮是当你将按钮控件的type
属性设置为button
时 HTML 所提供的,它用于启动某个动作。虽然“重置”按钮和“提交”按钮各有其特定的用途,但“标准”按钮则用于其他用途。事实上,您也可以使用标准按钮来提交或重置表单。但是,您将失去使用这些专用按钮所带来的默认行为的好处。当在你的页面上使用一个标准按钮时,你将把你的功能添加到按钮的click
事件中。这就是你在清单 5-6 中看到的,我在home.html
的按钮上添加了一个click
事件处理程序。
进度指标
进度指示器控件为用户提供有关某个操作或进程状态的视觉反馈。它可用于指示用户正处于向导中五个步骤的第三步,或者提供关于多文件下载的反馈。Windows 8 应用可以用三种不同的方式显示进度指示器:确定条、不确定条和不确定环。
确定条
当剩余工作量或剩余时间已知时,使用确定的条形图。这个指示器让用户看到你的应用取得了多大的进展。它显示为一个条形,当工作完成时,颜色从左向右变化,直到工作完成,条形被填满。要添加一个确定的进度条,添加清单 5-28 中的代码到htmlcontrols.html
。
清单 5-28。 添加确定的进度条控件
<progress id="myProgressDeterminate" value="75" max="100" />
指定value
和max
属性会添加一个填充了 75%的进度条。你可以在图 5-27 中看到这一点。
图 5-27 。进度指示器,确定条
不定杆
当剩余工时或时间量未知时,使用不确定条形图。该指示器显示为点从左向右滚动的重复动画。当操作或流程正在进行时,用户仍然可以与您的应用交互时,通常会使用此指示器。要添加一个不确定的进度条,将清单 5-29 中的代码添加到htmlcontrols.html
中。
清单 5-29。 添加一个不确定的进度条控件
<progress id="myProgressIndeterminateBar" />
因为没有指定value
或max
属性,这增加了在图 5-28 中看到的动画进度条。
图 5-28 。进度指示器,不确定条形图
不定环
当剩余工作量或时间未知时,使用不定环。该指示器显示为圆点在圆圈中旋转的重复动画。当操作或流程正在进行时,如果不允许用户与您的应用进行交互,通常会使用此指示器。要添加一个不确定的进度环,添加清单 5-30 中的代码到htmlcontrols.html
。
清单 5-30。 添加一个不确定进度环控件
<progress id="myProgressIndeterminateRing" class="win-ring" />
就像不确定条一样,没有指定value
或max
属性。在这种情况下,增加了 Windows 8 提供的特殊的“win-ring
”CSS 类,这样就增加了图 5-29 中看到的动画进度环。
图 5-29 。进度指示器,不定环
结论
在这一章中,你学习了 HTML 提供的一些更常见的控件。如果您是 web 开发人员,那么您可能已经在 web 应用中见过所有这些控件。多年来,这些控件一直被用于构建 web 应用,许多有用的 Windows 8 应用都可以仅使用这些控件来构建。在第六章和第七章,我将讨论 Windows 8 提供的额外控制,使你的应用看起来和感觉起来像 Windows 8 应用。
六、WinJS 控件
在第五章的中,我介绍了一些常见的 HTML 控件,它们可以在用 HTML 和 JavaScript 构建的 Windows 8 应用中使用。如果你有 web 开发的背景,你可能对我在第五章中提到的内容很熟悉。在接下来的几章中,我将介绍 WinJS 中包含的各种控件。
在这一章中,我将介绍你可能在任何你可能构建的应用中使用的最常见的 WinJS 控件。本章中的控件将允许您以与 Windows 8 其余部分一致的方式构建导航、用户输入和用户反馈机制等内容。
在第七章的中,我将介绍ListView
、FlipView
和SemanticZoom
控件,它们是用来处理对象集合的。这些控件允许您定义用于显示集合中每一项的模板。如果你有一个复杂的用户界面,你想在你的应用中多次使用,那么第八章就是为你准备的。您将发现如何使用 WinJS 构建自己的自定义控件。
但是在我深入研究 WinJS 和它为控件创建的所有选项之前,我将首先介绍 WinJS 实际上是什么。
WinJS、WinRT、Windows RT 和 Windows 8
2012 年,微软发布了名为 Windows 8 的新操作系统。你当然知道,否则你不会读这本书。在发布 Windows 8 的同时,微软发布了另一款新的操作系统,名为 Windows RT. Windows RT 专门设计用于基于 ARM 的设备,如平板电脑。
Windows 8 和 Windows RT 有一些关键的区别,最显著的两个区别是
- Windows RT 无法运行为早期版本的 Windows 构建的应用。
- Windows RT 运行在采用 ARM 处理器的设备上,而 Windows 8 运行在支持 Windows 7 的相同的熟悉硬件架构上。
抛开差异不谈,Windows 8 和 Windows RT 确实有很多共同点。Windows 8 和 Windows RT 的一些共享功能包括
- 带有磁贴布局的开始屏幕
- 使用“动态磁贴”向用户呈现丰富的个性化信息
- 微软设计语言的使用,在第二章中讨论
- 运行 Windows 应用商店应用的能力
因为这本书是关于创建 Windows Store 应用的,所以你会很高兴地知道,你使用这些概念构建的任何应用都可以在 Windows 8 和 Windows RT 设备上工作。事实上,我展示的所有代码都经过了两种操作系统的测试:我笔记本电脑上的 Windows 8 和 Surface 平板电脑上的 Windows RT。因此,除非特别指出,否则,每当我在本书中提到 Windows 操作系统时,我指的是 Windows 8 和 Windows RT。
Windows 提供了两个不同的库,用于使用 HTML 和 JavaScript 构建 Windows 应用商店应用。第一个是 Windows 运行时,通常称为 WinRT。Windows 运行时是 Windows 8 和 Windows RT 所共有的,它可以用于构建多种语言的 Windows 应用商店应用。例如,除了 HTML 和 JavaScript 之外,Windows 运行时还支持用 C#、VB.NET 和 C++构建应用。WinRT 提供了构建 Windows 应用商店应用的所有核心功能,例如应用模型、对设备和设备上的传感器的访问、网络、安全性和存储。
Windows 提供的第二个库是用于构建 HTML 和 JavaScript 的 Windows 商店应用的 Windows 库,即 WinJS。WinJS 是 JavaScript 和 CSS 代码的集合,用于简化 Windows 应用商店应用开发。所提供的 CSS 确保,默认情况下,您的应用看起来和感觉起来都像 Windows 应用商店应用,而不是网站。JavaScript 提供了许多 UI 控件,我将在本章后面讨论,还有动画和导航类、DOM 操作、事件以及创建自定义类和控件的能力。
不幸的是,在我看来,给产品命名不是微软的强项之一。正如我将在本书中介绍的,Windows 运行时通常被称为 WinRT,这个名字与 Windows RT 操作系统非常相似。更令人困惑的是,WinRT 提供的类和功能在Windows
名称空间中,尽管 WinJS 提供的功能很方便地在WinJS
名称空间中。虽然对于不太熟悉的用户来说,这可能会令人沮丧和困惑,但实际上,这并不是一个问题。
控制器
WinJS 提供了许多用于在 Windows 8 应用中显示和编辑信息的控件。与第五章的中讨论的 HTML 控件类似,WinJS 控件通常是通过在 HTML 文件中添加一些标记来创建的,并且控件在添加到 HTML 中后能够在 JavaScript 中操作。因为 WinJS 控件不是 HTML 的一部分,所以它们没有可以添加到 HTML 文件中的相应 HTML 元素。相反,您使用一个div
元素的data-win-control
属性来指定您想要创建的 WinJS 控件的类型。此外,因为 WinJS 控件没有专用的 HTML 元素,所以它们没有用于在 HTML 文件中设置属性的专用属性。相反,如果某些属性需要初始值,可以使用data-win-options
属性。在接下来的几页中,当我讨论不同的 WinJS 控件时,您将看到这一切是如何工作的。
在开始之前,您需要一个地方来放置本章中的代码。从第五章中打开 WinJSControlsSample 项目。在pages
文件夹中创建一个名为windowscontrols
的新文件夹,然后添加另一个名为windowscontrols.html
的页面控件,遵循在第五章中添加htmlcontrols.html
页面控件时使用的步骤。当你完成后,你的解决方案浏览器应该看起来像图 6-1 。
图 6-1 。添加一个名为 windowscontrols.html 的新页面控件
确保将windowscontrols.html
中的 CSS 引用从ui-dark.css
更改为ui-light.css
,并为home.html
添加一个按钮和相应的点击事件处理程序,以导航到这个新页面。完成后,运行应用以确保目前为止一切看起来和运行起来都像预期的那样(参见图 6-2 )。
图 6-2 。带有新按钮的主页
注意如果你正在执行本章中的所有示例,请确保将所有示例代码放在
windowscontrols.html,
的<section aria-label="Main content" role="main">
和</section>
元素之间,除非特别指定了不同的位置。一个经验法则是,如果控件在屏幕上总是可见的,比如一个DatePicker
,你可以把它添加到主section
中。如果它只在用户执行一个动作时显示,比如从屏幕边缘滑动以看到一个AppBar
或SettingsFlyout
,它可能需要被定义为页面的body
元素中的一个顶级元素,或者可能在一个完全独立的文件中。
当你完成本章的练习时,你会看到 WinJS 控件被添加到使用 HTML 元素的页面中,通常是一个div
元素,用属性定义控件的类型(data-win-control
)和它的初始状态(data-win-options
)。默认情况下,在屏幕上显示应用的 HTML 呈现引擎不知道任何有关 WinJS 控件的信息。它只理解 HTML、CSS 和 JavaScript。只有在调用 WinJS 方法WinJS.UI.processAll
时,才会将 HTML 转换成 WinJS 控件。该方法在 HTML 文件中查找任何 WinJS 控件,并实例化它们,以便 HTML 呈现引擎可以理解它们。
您必须确保在您的应用中调用了WinJS.UI.processAll
方法 。因为PageControl
自动调用processAll
,并且您在这些例子中使用了PageControl
,您将不必手动添加这个方法调用。如果你添加任何 WinJS 控件到任何其他不是PageControl
的 HTML 页面,你将不得不在你的 JavaScript 代码中显式调用该方法。
AppBar 和 AppBar 命令和
AppBar
控件 用于将应用工具栏或应用栏添加到您的页面中。默认情况下,应用栏是隐藏的,直到用户通过以下方法之一激活它:
- 用鼠标右键单击
- 从触摸屏底部向上滑动或从触摸屏顶部向下滑动
- 按下键盘上的 Windows 徽标键+Z
也可以通过编程激活AppBar
控件,例如,通过单击应用中提供的按钮。
应用栏包含命令按钮,用户可以单击这些按钮来执行不同的操作。它分为两个部分:“全局”部分和“选择”部分。“选择”部分中的命令适用于当前选定或活动的一个项目或一组项目,如列表中的选中项目,而“全局”部分适用于当前页面。对于从左向右阅读的语言,如英语,全局部分在应用栏的右侧,选择部分在左侧。对于从右向左阅读的语言,情况正好相反。让我们给你的新页面添加一个AppBar
控件。添加该控件的代码必须直接包含在页面的body
元素中,所以将来自清单 6-1 的代码添加到windowscontrols.html
中的开始body
元素之后。
清单 6-1。 添加一个 AppBar
<body>
<div id="myAppBar" class="win-ui-dark" data-win-control="WinJS.UI.AppBar">
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'myAddCommand',label:'Add',icon:'add',
section:'global',tooltip:'Add item'}">
</button>
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'myRemoveCommand',label:'Done',icon:'accept',
section:'selection',tooltip:'Mark item done'}">
</button>
<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:'myDeleteCommand',label:'Delete',icon:'delete',
section:'selection',tooltip:'Delete item'}">
</button>
</div>
<!-- SNIPPED -->
</body>
让我们深入了解一下这是怎么回事。由于data-win-control
属性,WinJS 库将div
元素识别为AppBar
控件,并且将class
属性设置为win-ui-dark
将会以深色显示AppBar
,默认为黑色,使其在应用的浅色背景下非常突出。我将在第九章中说明如何用颜色来设计你的应用。
接下来,向AppBar
添加几个按钮和一个分隔符,将data-win-control
属性设置为WinJS.UI.AppBarCommand
。此外,因为 HTML 中没有AppBarCommand
元素,因此也没有与之对应的标准 HTML 属性,所以我使用data-win-options
属性来设置按钮的初始属性。在第一个按钮中,我将icon
属性设置为add
,它显示一个加号(见图 6-3 ),我将section
属性设置为global
,使其显示在AppBar
的右侧。想象一个任务列表应用。在这种情况下,第一个按钮会向列表中添加一个新任务。我在选择部分添加了第二个和第三个按钮。在任务列表应用中,这些按钮将应用于当前选定的任务。
图 6-3 。一个应用栏,有两个选择命令和一个全局命令 ??
注意虽然没有什么可以阻止你向选择部分添加一个全局命令,反之亦然,但随着你的用户获得 Windows 8 应用的专业知识,他们会希望按钮以这种方式组织。
你可能想知道AppBarCommand
的图标是在哪里定义的。默认情况下,可用图标是使用 Segoe UI 符号字体 ?? 显示的不同字符。WinJS 在WinJS.UI.AppBarIcon
枚举中提供了一个可用图标列表。通过指定其中一个作为icon
属性的值,您可以显示您选择的图标。这个列表中有将近 200 个图标,完整的列表可以在微软的 MSDN 网站 ( http://msdn.microsoft.com/en-us/library/windows/apps/hh770557.aspx
)上看到。有了这么多可用的图标,您很可能会在列表中找到一个符合您需要的图标。然而,有时你需要一些不同的东西。在这些情况下,可以提供自定义图像来代替基于字体的图像。我们将在第十二章中了解如何做到这一点。
AppBar
和AppBarCommand
类有许多不同的属性和可用。一些比较常见的是
layout
:这个AppBar
属性允许你指定你正在提供一个应用栏的自定义布局,并且 WinJS 不应该期望显示一系列的AppBarCommand
对象。placement
:这个AppBar
属性允许你指定它应该放在页面的顶部还是底部。在屏幕顶部放置带有自定义布局的应用栏是在应用中提供导航选项的常用方式。sticky
:这个AppBar
属性可以用来表示当用户点击或触摸应用中的其他地方时,应用栏仍然可见。用户仍然可以使用通常会激活它的相同方法隐藏应用栏,例如从屏幕顶部滑动或用鼠标右键单击。disabled
:这个AppBarCommand
属性可以用来使一个命令对用户不可用。例如,如果在前面的任务列表示例中没有选择任何项目,则应该禁用删除命令。type
:该AppBarCommand
属性用于改变用户将看到的命令类型。您在代码示例中简要地看到了这一点,其中您将一个命令的type
属性设置为separator
。该属性的有效选项有button
(如果未指定,则为默认值)、separator
、toggle
和flyout
。切换命令是当用户重复选择它时在两种状态之间交替的命令按钮,例如电子邮件应用中的“标记为已读”/“标记为未读”按钮。一个弹出命令被用来显示一个Flyout
控件,我将在本章后面介绍它。
在 MSDN 上可以找到AppBar
( http://msdn.microsoft.com/en-us/library/windows/apps/br229670.aspx
)和AppBarCommand
( http://msdn.microsoft.com/en-us/library/windows/apps/hh700497.aspx
)酒店的完整列表。
工具提示
我在第五章中简单提到了工具提示。如果您不熟悉工具提示,它们是当您将鼠标悬停在页面上的另一个控件上时显示的小文本框。它们通常包含鼠标悬停项目的描述或如何处理该项目的说明。对于大多数 HTML 控件,可以通过将title
属性的值设置为想要在工具提示中显示的文本来添加简单的工具提示。将清单 6-2 中的代码添加到windowscontrols.html
中。这段代码将产生一个简单的纯文本工具提示,如图 6-4 所示。
清单 6-2。 给标签添加简单的工具提示
<label title="This is a simple, text only tooltip">This label has a simple tooltip.</label>
图 6-4 。您的示例应用(正如它将在本章末尾看到的那样),突出显示了一个带有简单工具提示的标签
像这样简单的工具提示对改善用户体验大有帮助,因为他们开始了解你的应用。但是,如果您想提供更丰富的内容,WinJS 提供了一个可以包含格式化内容的Tooltip
控件。将清单 6-3 中的代码添加到windowscontrols.html
中,并再次运行应用。
清单 6-3。 给标签添加内容丰富的工具提示
<label id="myRichTooltip" data-win-control="WinJS.UI.Tooltip"
data-win-options="{infotip: true,
innerHTML: 'Here is a <strong>richer</strong> <em>tooltip</em>.'}">
This label has a rich tooltip.
</label>
注意工具提示(见图 6-5 )实际上包含了格式化的 HTML 内容。此外,infotip
属性用于指示这个可能包含大量信息的Tooltip
应该比常规的Tooltip
显示更长的时间。
图 6-5 。带有丰富工具提示的标签
如果您需要一个更丰富的工具提示,包含图像或其他控件,您可以利用contentElement
属性创建一个工具提示,使用一个单独的 HTML 元素的内容作为您的Tooltip
的内容。在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br229763.aspx
)上可以找到Tooltip
控件的完整属性列表,包括contentElement
属性的文档。
日期选择器 和时间选择器
如果您正在构建一个需要用户提供日期的应用,比如安排约会,您首先想到的可能是使用文本输入控件。您当然可以这样做,提供一个正则表达式模式,只允许输入格式正确的日期。在许多情况下,这可能就足够了,甚至是更好的选择。但是,至少有两个原因可以考虑使用专用的日期控件。
- 随着触摸屏设备的日益普及,您的用户可能更喜欢用手指选择日期,而不是在键盘上键入日期。
- 如果您的应用要在国际上使用,您必须为应用的每个本地化版本提供不同的正则表达式。
如果您有一个使用文本输入的好理由,那么前面的两个要求都不是您无法克服的障碍。然而,WinJS 提供了一个DatePicker
控件来为您处理这些需求。将清单 6-4 中的代码添加到windowscontrols.html
的主要内容section
中。
清单 6-4。 添加日期选择器
<div id="myDatePicker" data-win-control="WinJS.UI.DatePicker"></div>
如果你的用户除了日期之外还必须输入约会的时间,你可能不会惊讶地发现 WinJS 也为这些目的提供了一个TimePicker
控件,而且添加起来也很简单。将清单 6-5 中的代码添加到windowscontrols.html
中。
清单 6-5。 添加时间选择器
<div id="myTimePicker" data-win-control="WinJS.UI.TimePicker"></div>
现在,当您运行您的应用时,您应该会看到如图图 6-6 所示的控件。
图 6-6 。DatePicker 控件和 TimePicker 控件
DatePicker
和TimePicker
类有许多不同的属性可用。一些更常见的DatePicker
属性有
current
:这个属性允许你指定默认日期,当DatePicker
控件最初加载时显示。如果没有为此属性指定值,默认日期将是当前日期。minYear
、maxYear
:这些属性允许您指定用户在选择日期时可以指定的最早和最晚年份。minYear
的默认值是当前年前 100 年,maxYear
的默认值是当前年后 100 年。yearPattern
、monthPattern
、datePattern
:这些属性允许你控制日期不同部分的格式。例如,将datePattern
属性设置为"{day.integer(2)} - {dayofweek.full}"
将导致日期下拉列表将每个日期显示为两位数的日期,以零开头,后跟星期几。
在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br211681.aspx
)上可以找到DatePicker
控件属性的完整列表,包括yearPattern
、monthPattern
和datePattern
属性的选项列表。
一些更常见的TimePicker
属性是
current
:这个属性允许你指定TimePicker
控件最初加载时显示的默认时间。如果没有为此属性指定值,默认时间将是当前时间。clock
:该属性允许您指定时间以 12 小时格式(12HourClock
)或 24 小时格式(24HourClock
)显示。如果使用 24 小时制,则不会显示用于选择时间段(上午或下午)的下拉列表。minuteIncrement
:该属性允许您限制时间的分钟部分的选择。例如,如果您正在编写一个安排牙科预约的应用,您可以将该值设置为 15,使分钟下拉列表只提供 00、15、30 和 45 作为选择。hourPattern
、minutePattern
、periodPattern
:这些属性允许你控制时间不同部分的格式。例如,将hourPattern
属性设置为“{hour.integer(2)}
”将导致小时被格式化为两位数,如果需要的话,带有前导零。
在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br229736.aspx
)上可以找到TimePicker
控件属性的完整列表。
切换开关
如果你使用 Windows 8 有一段时间,你可能会看到一个ToggleSwitch
(见图 6-7 )。
图 6-7 。基本的拨动开关
很像一个复选框,它被用来在两种可能的状态之间切换,允许你的用户做出诸如“开和关”、“是或否”、“真或假”、“喜欢它或讨厌它”等选择。在清单 6-6 的中可以看到ToggleSwitch
的最简单用法。
清单 6-6。 添加 ToggleSwitch
<div id="myToggle" data-win-control="WinJS.UI.ToggleSwitch"></div>
ToggleSwitch
类有许多不同的属性可用。一些比较常见的是
title
:该属性允许您指定在ToggleSwitch
上方显示的文本提示。如果未指定title
,则不显示提示。labelOn
,labelOff
:这个属性允许你指定当用户打开和关闭控件时显示的文本。默认情况下,labelOn
设置为On,
,而labelOff
设置为Off
。checked
:该属性允许您指定页面加载时ToggleSwitch
的初始状态。默认情况下,checked
为假,ToggleSwitch
关闭。
在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh701411.aspx
)上可以找到ToggleSwitch
控件属性的完整列表。为了查看这些属性如何影响ToggleSwitch
,让我们用下面的清单 6-7 中的代码替换清单 6-6 中的代码。
清单 6-7。 增加一个更有价值的 ToggleSwitch
<div id="myToggle" data-win-control="WinJS.UI.ToggleSwitch"
data-win-options="{title:'What do you think about pizza?',
labelOn:'Love it',
labelOff:'Hate it',
checked:true}"></div>
看完代码后,您可能不会惊讶地发现您的应用现在看起来像图 6-8 。
图 6-8 。您定制的 ToggleSwitch
正如我提到的,一个ToggleSwitch
的行为非常像一个复选框。两者都允许你打开或关闭某些东西。微软提供了关于ToggleSwitch
用法(http://msdn.microsoft.com/en-us/library/windows/apps/hh465475.aspx
)的指导,包括以下关于在ToggleSwitch
和复选框之间选择的建议:
- 当更改将立即生效时,使用
ToggleSwitch
,例如在SettingsFlyout
中。 - 如果在用户执行其他操作(如单击提交按钮)之前,更改不会生效,请使用复选框。
- 当用户可以从列表中选择多个项目时,使用复选框。
基于这个指导,虽然它说明了一些不同属性的用法,但是图 6-8 中的例子可能不是ToggleSwitch
控件的最佳用法。用户可以通过Rating
控件得到更好的服务。
评级
使用Rating
控件,您可以提供一个熟悉的机制来对事物进行评级。显示为熟悉的一系列星星,Rating
控件是收集关于某样东西质量的反馈的直观方式。将清单 6-8 中的代码添加到windowscontrols.html
中,然后运行您的应用。
清单 6-8。 添加一个评级控件
<div id="myRating" data-win-control="WinJS.UI.Rating"></div>
当页面第一次加载时,你会看到一系列类似于图 6-9 的五星。除了这个视图之外,Rating
控件的另外两个视图也是现成的:暂定评级视图和用户评级视图。当您将鼠标悬停在每个评级星上方时,将显示暂定评级视图。当你将鼠标悬停在每个星星上时,星星的颜色会动态变化,工具提示会显示你选择该选项时所设置的值(参见图 6-10 )。当您最终选择其中一颗星星时,颜色变化被设置,工具提示被隐藏(参见图 6-11 )。
图 6-9 。标准分级控制
图 6-10 。当用户的鼠标悬停在第四颗星星上时的暂定评级
图 6-11 。用户选择的评级
Rating
类有许多不同的属性可用。一些比较常见的是
maxRating
:该属性允许您指定用户可以选择的最大可能等级。默认情况下,maxRating
设置为 5。averageRating
:此属性允许您指定用户对该物品的平均评分。确定该平均值的方法由您决定,而不是由Rating
控件提供。tooltipStrings
:该属性允许您指定当用户将鼠标悬停在Rating
控件中的每个选项上时显示的工具提示。该属性接受一个字符串数组,数组中的项数必须与maxRating
属性的值相匹配。默认情况下,tooltipStrings
为空,数字显示在每个选项的工具提示中。disabled
:该属性允许你指定Rating
控件处于只读状态。当disabled
属性设置为true
时,用户不能添加或更改等级。
在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br211895.aspx
)上可以找到Rating
控件属性的完整列表。为了看看这些属性是如何影响Rating
的,让我们用下面清单 6-9 中的代码替换清单 6-8 中的代码。
清单 6-9。 更好的披萨评分界面
<div id="myRating" data-win-control="WinJS.UI.Rating"
data-win-options="{averageRating:3.6,
tooltipStrings:['Hate it','Dislike it','It\'s Ok','Like it','Love it']}"></div>
现在,当您运行应用时,您会看到一个更适合测量人们对比萨饼的喜爱程度的控件。同样的五颗星也在那里,但是它们将被突出显示以指示averageRating
属性的当前值。在这个例子中,3.6 颗星星被高亮显示(见图 6-12 )。
图 6-12 。具有平均评级的评级控制
当您将鼠标悬停在不同的星星上时,您可以看到为tooltipStrings
属性指定的不同值(参见图 6-13 )。此外,当您将鼠标悬停在星星上时,它们会以不同的颜色突出显示,默认情况下为蓝色。
图 6-13 。每个选项的自定义工具提示
消息对话框
如果你已经编写或使用软件一段时间了,你肯定对消息框很熟悉。通常显示为模式窗口,即应用前面的窗口,它阻止应用在被寻址之前使用,消息框用于向用户显示紧急信息,或者请求应用继续使用所需的信息。对于 Windows Store 应用,您可以使用MessageDialog
控件来实现这一目的。
注意
MessageDialog
控件实际上是由 Windows 运行时 WinRT 提供的,并不是 WinJS 的一部分。
添加一个对话框比我之前讨论的一些控件要复杂一些,但是仍然非常简单。对于您的示例,您将向页面添加一个按钮,用于打开MessageDialog
控件,尽管在实践中,您可以基于任意数量的标准创建MessageDialog
。首先将清单 6-10 中的代码添加到windowscontrols.html
中。
清单 6-10。 添加一个按钮显示消息对话框和一个占位符显示结果
<button id="myShowDialogButton">Show message</button>
<span id="myDialogResult"></span>
现在,您需要让按钮在用户单击它时做一些事情。将清单 6-11 中高亮显示的代码添加到windowscontrols.js
中的ready
函数中。这段代码将导致一个名为myShowDialogButton_Click
的函数,当有人点击这个按钮时,这个函数就会被执行。
清单 6-11。 为按钮点击接线起事件处理程序
ready: function (element, options) {
// message dialog event binding
document.getElementById("myShowDialogButton")
.addEventListener("click", myShowDialogButton_Click, false);
},
现在让我们创建一个函数,它将实际处理按钮点击并向用户显示MessageDialog
。清单 6-12 包含前面提到的myShowDialogButton_Click
函数的函数定义。务必注意这段代码在windowscontrols.js
中的位置。该功能应在PageControl
定义后定义。为了帮助澄清这一点,清单 6-12 还包括了PageControl
的定义,但是为了简单起见,删除了其中的内容。
清单 6-12。 在 PageControl 定义后添加事件处理程序
"use strict";
WinJS.UI.Pages.define("/pages/windowscontrols/windowscontrols.html", {
// SNIPPED
});
function myShowDialogButton_Click() {
// Create the message dialog and set its content
var msg = new Windows.UI.Popups.MessageDialog("Some irreversible process is about to "
+ "begin. Do you wish to continue?", "Message dialog sample");
// Add commands and set their command handlers
msg.commands.append(new Windows.UI.Popups.UICommand("Continue", function (command) {
myDialogResult.textContent = "You chose 'Continue'";
}));
msg.commands.append(new Windows.UI.Popups.UICommand("Cancel", function (command) {
myDialogResult.textContent = "You chose 'Cancel'";
}));
// Set the command that will be invoked by default
msg.defaultCommandIndex = 0;
msg.cancelCommandIndex = 1;
// Show the message dialog
msg.showAsync();
}
清单 6-12 中定义的事件处理器创建一个MessageDialog
对象。在向用户显示之前,您创建两个UICommand
对象并将它们添加到您的MessageDialog
中。当显示MessageDialog
时,这些命令是用户可以选择的按钮。在这种情况下,您可以通过设置清单 6-10 中占位符的textContent
属性来处理每个按钮的点击。
注意如果你的事件处理程序很短,你可以省去一个单独的事件处理函数。相反,您可以使用匿名函数在对
addEventListener
的调用中声明click
事件处理程序。你可以在清单 6-12 中看到这种技术,这里我定义了MessageDialog
对象的命令按钮。虽然匿名函数在这个例子中也可以工作,但是长的处理程序会使代码更难阅读。
一个MessageDialog
可以定义一个默认命令和一个取消命令。默认命令是当用户按下 ENTER 键时将被单击的按钮。“取消”命令是用户按下 ESC 键时将被单击的按钮。将MessageDialog
的defaultCommandIndex
属性设置为 0,表示您的第一个按钮 Continue 按钮将是默认按钮。同样,使用cancelCommandIndex
表示另一个按钮将是取消按钮。最后,调用MessageDialog
对象的showAsync
函数向用户显示对话框,并要求用户在继续使用应用之前采取一些行动。
运行应用并单击“显示消息”按钮。应用的内容将变暗,对话框将显示,如图图 6-14 所示。当点击继续或取消按钮时,将显示您选择的结果,如图图 6-15 。
图 6-14 。向用户显示消息对话框的页面
图 6-15 。消息对话框的结果
弹出窗口
介于工具提示和消息对话框之间,WinJS 提供了一个Flyout
控件,用于向用户显示上下文内容和其他控件。这些内容出现在应用主页上方的浮动框中,在视觉上类似于工具提示;然而,当用户的鼠标悬停在某个触发器上时,工具提示会提供描述或指令,而Flyout
控件通常会在用户采取更谨慎的动作时显示,比如单击某个东西。例如,您可以使用弹出按钮让您的用户确认他们想要删除一些数据。
与添加消息对话框类似,向应用中添加弹出型按钮包括许多步骤。和AppBar
的例子一样,Flyout
控件的代码必须直接包含在页面的body
元素中,所以在windowscontrols.html
中的 app bar 声明之后立即添加来自清单 6-13 的代码。
清单 6-13。 添加弹出型按钮
<div id="myFlyout" data-win-control="WinJS.UI.Flyout">
<div>This is a flyout.</div>
<button id="myCloseFlyoutButton">Close flyout</button>
</div>
如果您现在运行应用,它是不可见的,但是您已经创建了一个带有一些文本和一个按钮的Flyout
。为了看到Flyout
,您需要一些用户可以用来显示它的控件。正如您对MessageDialog
所做的那样,您将为此在页面上添加一个按钮。将清单 6-14 中的代码添加到用于显示MessageDialog
的按钮之后的windowscontrols.html
中。
清单 6-14。 添加按钮以显示弹出型按钮
<button id="myShowFlyoutButton">Show flyout</button>
现在,您将通过将清单 6-15 中高亮显示的代码添加到windowscontrols.js
中ready
函数的末尾来连接按钮。您必须为两个按钮连接事件处理程序——一个显示弹出按钮,一个关闭它。
清单 6-15。 连接按钮点击的事件处理器
ready: function (element, options) {
// SNIPPED
// flyout event binding
document.getElementById("myShowFlyoutButton")
.addEventListener("click", myShowFlyoutButton_Click, false);
document.getElementById("myCloseFlyoutButton")
.addEventListener("click", myCloseFlyoutButton_Click, false);
},
最后一步是定义您在清单 6-15 中连接的两个事件处理程序。清单 6-16 包含了在ready
函数中引用的两个函数定义。与清单 6-12 中的事件处理程序一样,注意我已经在PageControl
定义之外定义了这些函数。将清单 6-16 中的代码添加到windowscontrols.js
中的MessageDialog
部分的myShowDialogButton_Click
函数定义之后。
清单 6-16。 在 PageControl 定义后添加事件处理程序
function myShowFlyoutButton_Click() {
myFlyout.winControl.show(myShowFlyoutButton, "top");
}
function myCloseFlyoutButton_Click() {
myFlyout.winControl.hide();
}
现在您已经准备好了所有的代码,运行您的应用并单击“显示弹出”按钮。因为您在调用show
函数时指定了top
作为placement
参数的值,所以您在清单 6-13 中创建的弹出按钮显示在myShowFlyoutButton
上方(参见图 6-16 )。其他有效选项有"bottom"
、"left"
或"right"
。
图 6-16 。您的弹出按钮可见
任何时候弹出按钮可见,用户都可以通过按 ESC 键或点击关闭它来消除它,这种交互被称为光消除。此外,单击弹出按钮可以隐藏它。使用您可能添加的任何其他控件(如ToggleSwitch
或Rating
控件)引起的更改应该会立即生效,但不会隐藏Flyout
控件。
设置弹出按钮
SettingsFlyout
控件用于给你的应用添加一个小窗口,包含应用某些方面的设置。您的应用可以有几个SettingsFlyout
控件,为了与其他 Windows 应用保持一致,每个控件都应该在设置窗格中列出。图 6-17 显示了 Bing 财务应用 ,其“关于设置”弹出按钮打开。
图 6-17 。设置弹出按钮已打开
默认情况下,“设置”面板是隐藏的,直到用户激活它。激活后,设置窗格会列出用户可用的每个SettingsFlyout
。用户可以通过以下任何一种方法激活设置窗格 :
- 将鼠标移动到屏幕的右上角,然后单击“设置”按钮,打开 Windows charms 栏。
- 通过从触摸屏的右侧向内滑动,然后点击设置按钮来打开 Windows charms 栏;
- 在键盘上按下 Windows 徽标键+I。
设置面板,甚至是一个特定的SettingsFlyout
,也可以通过编程激活,例如,通过点击应用中提供的一个按钮。您将向您的应用添加一个打开设置面板的按钮,然后您可以选择要打开的SettingsFlyout
。将清单 6-17 中的代码添加到windowscontrols.html
中。
清单 6-17。 添加按钮显示设置弹出按钮
<button id="myShowSettingsButton">Show Settings</button>
定义SettingsFlyout
不同于我到目前为止讨论过的任何其他控件。这是因为它是在自己的 HTML 文件中定义的。在 Visual Studio 的解决方案资源管理器中,向pages
文件夹添加一个名为settingsflyout
的新文件夹。右键单击这个新文件夹,并选择添加新项目的选项。Visual Studio 的添加新项对话框打开(参见图 6-18 )。这次没有选择页面控件项,而是选择 HTML 页面项,并将页面命名为settingsflyout.html
。
图 6-18 。添加 HTML 页面
然后打开settingsflyout.html
文件,用清单 6-18 中的代码替换它的内容。
清单 6-18。 设置弹出页面
<!DOCTYPE html>
<html>
<head>
<title>Settings Flyout Sample</title>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
</head>
<body>
<div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout"
aria-label="Settings flyout sample"
data-win-options="{settingsCommandId:'settingsflyout',width:'wide'}">
<div class="win-ui-dark win-header" style="background-color: #464646">
<button type="button" class="win-backbutton"
onclick="WinJS.UI.SettingsFlyout.show()"></button>
<div class="win-label">Settings Flyout Sample</div>
</div>
<div class="win-content">
Put your settings here.
</div>
</div>
</body>
</html>
注意第一个div
元素,它的id
是settingsDiv
。您已经使用data-win-control
属性指出它是一个 WinJS SettingsFlyout
控件。您还设置了settingsCommandId
属性 ,稍后在将“设置”弹出按钮添加到“设置”窗格时会用到该属性。您还将width
属性设置为wide
,而不是默认的narrow
。在SettingsFlyout
控件中,有一个div
元素用于您的标题,另一个包含您希望看到的实际内容。
现在您已经创建了将用于您的SettingsFlyout
的页面,您必须连接一些事件。将清单 6-19 中高亮显示的代码添加到windowscontrols.js
中ready
函数的末尾。
清单 6-19。 为按钮点击连线事件处理程序
ready: function (element, options) {
// SNIPPED
// show the Settings pane when our button is clicked
document.getElementById("myShowSettingsButton").addEventListener("click", function () {
WinJS.UI.SettingsFlyout.show();
}, false);
},
这段代码是按钮的事件处理程序代码,它显示了调用WinJS.UI.SettingsFlyout.show
的设置面板。如果你想直接跳到你的SettingsFlyout
而不是显示设置面板,你可以使用WinJS.UI.SettingsFlyout.showSettings
方法 。
最后一步是让您的应用让 Windows 知道您想要在设置窗格中显示什么设置。清单 6-20 中突出显示的代码应该添加到位于js
文件夹中的default.js
文件中的processAll
调用之前。这段代码定义了一个与SettingsFlyout
控件相关联的命令,并将其添加到设置窗格中。将该命令命名为settingsflyout,
,以匹配您在settingsflyout.html
中设置的settingsCommandId
属性。title
属性决定该命令在设置窗格中显示的文本。
清单 6-20。 连接事件处理程序来填充设置命令列表
app.addEventListener("activated", function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
// SNIPPED
// add our SettingsFlyout to the list when the Settings pane is shown
WinJS.Application.onsettings = function (e) {
e.detail.applicationcommands = {
"settingsflyout": {
title: "Settings Flyout Sample",
href: "/pages/settingsflyout/settingsflyout.html"
}
};
WinJS.UI.SettingsFlyout.populateSettings(e);
};
args.setPromise(WinJS.UI.processAll().then(function () {
// SNIPPED
}));
}
});
注意在
default.js
中添加这段代码允许设置弹出按钮在你的应用加载到default.html
的任何PageControl
中可用。通过将该代码移动到每个PageControl
的就绪功能,可以从应用的每个页面获得不同的设置。但是,微软的指导建议在整个应用中显示相同的设置列表,禁用任何在特定情况下不适用的设置。
现在,如果你运行你的应用并通过点击显示设置按钮(或任何其他方法)打开设置面板,你现在有一个按钮来打开你的SettingsFlyout,
,如图 6-19 中的所示。
图 6-19 。应用的设置面板
点击此按钮打开你的宽格式SettingsFlyout,
,如图 6-20 所示。
图 6-20 。“设置”弹出按钮
在SettingsFlyout
中所做的任何设置更改都会立即生效,用户不必点击保存按钮。当用户完成设置弹出按钮时,无论是否进行了任何更改,他或她都可以单击 Back 按钮返回到设置窗格,或者通过单击SettingsFlyout
之外的某个地方将其关闭。
结论
在本章中,您了解了 Windows Library for JavaScript 或 WinJS 提供的许多常见控件。这些控件超越了 HTML 提供的基本控件,允许您以符合 Windows 8 全新现代界面的外观和行为的方式与用户进行交互。在第七章的中,我将讨论为处理项目集合而构建的 WinJS 控件:ListView
、FlipView
和SemanticZoom
。
七、WinJS 集合控件
在前两章中,我介绍了一些可以用来构建 Windows 应用商店应用的控件。除了DropDownList
控件之外,到目前为止我介绍的所有控件都是为处理单个值而设计的。每个文本输入控件都与单个字符串相关联;每个按钮控件都与一个操作相关联;每个工具提示显示一个项目的描述或说明。事实上,尽管DropDownList
控件包含许多可供选择的项目,但它在逻辑上与单个设置相关的选项相关联。
在这一章中,我将介绍 WinJS 提供的基于集合的控件。当您有想要使用模板显示的项目列表时,可以使用这些控件。如果你用过 Windows 8,那么最明显的例子就是 Windows 开始屏幕,如图 7-1 所示。这个开始屏幕显示了我已经安装的应用列表。每一个,都有一个标题;小的或大的平铺图像;以及可选地,显示在标题上的一些实况内容。如你所见,我现在正处于热浪之中。
图 7-1 。Windows 8 开始屏幕
虽然在本章中我不打算演示如何复制 Windows 开始屏幕,但是您将看到如何通过将集合控件绑定到数据列表来构建类似的功能。但是在绑定到数据列表之前,您首先需要理解拥有数据列表意味着什么。
收集
如果您从事编程已经有一段时间了,那么您无疑已经熟悉了项目集合。数组是一种常见的集合类型,数据库查询的结果、驱动器上的文件列表或您喜爱的博客的 RSS 提要中的项目也是如此。WinJS 提供了WinJS.UI.IListDataSource
接口,用于在 Windows 应用商店应用中处理这些类型的收藏。此接口可用于向集合中添加新项,编辑或移除集合中的现有项,或将集合绑定到控件以在应用中显示。你将在本章中看到的 WinJS 集合控件、ListView
和FlipView
,都常用于显示来自IListDataSource
的数据。
在某些情况下,您必须创建自己的实现IListDataSource
的 JavaScript 类,比如当您想要将ListView
控件直接绑定到 web 服务调用的结果时。然而,在许多情况下,您可以利用WinJS.Binding.List
类从一组数据中创建一个IListDataSource
。这就是我将在本章中解释的。事实上,在大多数情况下,我推荐这种方法,即使是在处理来自 web 服务的数据时。将 web 服务数据加载到一个数组中,然后将它们包装在一个List
对象中,这样可以很好地将用户界面与底层数据源分离开来,并为您提供更多的灵活性和控制。我将在第十一章更详细地讨论数据绑定。现在,我将保持它非常简单,因为本章的主要焦点是用来显示这些数据的控件。
让我们从第五章和第六章开始,对你的 WinJSControlsSample 项目做一些修改,这将允许你在本章后面使用ListView
和FlipView
控件。
项目设置
本章中你将看到的所有例子都将使用相同的项目集合。在这种情况下,它是一组动物。每个动物的定义都有动物的名称、科学分类以及大小不同的图片。让我们将这些数据添加到您的项目中。
-
打开 WinJSControlsSample 项目。
-
Right-click on the
js
folder in the Solution Explorer and add a new item (see Figure 7-2).图 7-2 。向 js 文件夹添加新项目
-
Select the JavaScript File item (see Figure 7-3).
图 7-3 。创建 JavaScript 文件
-
将文件命名为
data.js
,然后单击 Add 按钮。 -
将清单 7-1 中的代码输入到您的新
data.js
文件中,并保存该文件。
清单 7-1。 添加数据
var animals = new WinJS.Binding.List([
{
name: "Ant",
classification: "Formicidae",
pic_sm: "/img/60/Ant.png",
pic_lg: "/img/400/Ant.png"
},
{
name: "Bat",
classification: "Chiroptera",
pic_sm: "/img/60/Bat.png",
pic_lg: "/img/400/Bat.png"
},
{
name: "Bee",
classification: "Anthophila",
pic_sm: "/img/60/Bee.png",
pic_lg: "/img/400/Bee.png"
},
// SNIPPED
{
name: "Squirrel",
classification: "Sciuridae",
pic_sm: "/img/60/Squirrel.png",
pic_lg: "/img/400/Squirrel.png"
},
{
name: "Turtle",
classification: "Chelonii",
pic_sm: "/img/60/Turtle.png",
pic_lg: "/img/400/Turtle.png"
}
]);
这段代码创建了一个由五只动物组成的数组,每只动物都有一个name
、classification
、pic_sm
和pic_lg
属性。然后,这个数组被包装在一个名为animals
的WinJS.Binding.List
对象中,这个对象将作为本章其余例子的数据源。
您还必须将每种动物引用的图像添加到项目中的适当目录中:一个 60×60 大小的小图像和一个 400×400 大小的大图像。创建图像的教程超出了本书的范围;但是,您可以使用您喜欢的任何方法。也就是说,Syncfusion 已经创建了 Metro Studio,这是一个包含数百个免费和免版税图标的伟大资源。我用它创作了整本书的许多图标和图像。可以从 Syncfusion 的网站这里下载:http://www.syncfusion.com/downloads/metrostudio
.
注本书附带的源代码包括一个名为 WinJSControlsSample 的完整项目,其中包括第 5、6、7、8 章中使用的样本代码。它包括这些示例中使用的所有图像文件,以及 data.js 文件中更长的动物列表。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
现在您已经有了所需的数据,让我们来看看显示项目集合的不同控件。
控制器
WinJS 提供了两个控件来显示 Windows 应用商店应用中的项目集合:ListView
控件和FlipView
控件。正如您将看到的,它们在许多方面是相似的,但是它们有一个显著的区别。ListView
控件可以一次显示多个项目,用你的项目填满可用空间。另一方面,FlipView
控件一次突出显示一个项目,用户可以像翻书一样滚动浏览您的收藏。
WinJS 还提供了几个其他的类,用于ListView
或FlipView
。两者都支持使用一个WinJS.Binding.Template
对象来格式化你的集合中的单个项目。ListView
还支持两种布局模式,WinJS.UI.GridLayout
和WinJS.UI.ListLayout
,这两种模式决定了你的每个项目显示在哪里。最后,您可以使用带有两个ListView
控件的WinJS.UI.SemanticZoom
类来允许您的用户选择他或她正在查看的数据的分辨率,缩小时是摘要或组级别的信息,放大时是项目级别的信息。您将在下面看一下这些控件和类,从下一节的ListView
控件的一些基础开始。
当您学习本章中的示例时,应该将它们添加到前面章节中的 WinJSControlsSample 项目中。对于下面的每一个部分,你都必须按照第五章中添加 htmlcontrols 页面控件的步骤,向项目添加另一个页面控件。确保将所有示例代码放在页面控件的<section aria-label="Main content" role="main">
和</section>
元素之间。您还必须为 home.html 的每个页面控件添加一个导航按钮。最后,每个页面控件都必须引用你在本章前面创建的data.js
文件。你可以在本章剩余部分中创建的每个页面控件的head
部分添加这个引用(见清单 7-2 )。
清单 7-2。 引用您的动物数据对每个页面进行控制
<head>
<!-- SNIPPED -->
<script src="/js/data.js"></script>
</head>
ListView 基础
几乎在每个应用中,你都需要显示一个项目列表,使用ListView
控件是最常见的方式。在当前示例中,您将显示清单 7-1 中定义的动物数据列表。首先创建一个名为listViewBasics.html
的新页面控件,然后将清单 7-3 中的代码添加到主部分,这将为您的页面添加一个ListView
,并将其绑定到您之前在data.js
中创建的animals
列表。不要忘记引用data.js
,如清单 7-2 中的所示。
清单 7-3。 添加列表视图
<div id="listView"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: animals.dataSource,
selectionMode: 'none',
tapBehavior: 'none',
swipeBehavior: 'none'
}">
</div>
注意要显示在 HTML 中添加的
ListView
或任何其他 WinJS 控件,必须在页面的 JavaScript 代码中添加对WinJS.UI.processAll
的调用。但是,因为我们的示例使用了页面控件,所以这是在幕后为您处理的。但是,如果您曾经以不同的方式构建页面,请记住这一点。
我还设置了一些其他选项。通过将class
属性设置为win-selectionstylefilled
,ListView
将用纯色背景高亮显示项目。如果没有添加该类,默认情况下将在该项周围绘制一个矩形。此外,我已经将selectionMode
、tapBehavior,
和swipeBehavior
属性设置为none
,这实质上是将您的列表置于只读模式,在这种模式下不能选择任何项目,当您单击一个项目时也不会发生任何事情。现在运行应用。你应该会看到类似于图 7-4 的东西。
图 7-4 。一个无用的列表视图
你所有的数据都在那里,但这不是一个非常有用的观点。事实上,这很令人困惑,坦白说,还有点难看。在下一节,我将解释如何使用模板使它更实用。关于selectionMode
、tapBehavior
、swipeBehavior
和其他ListView
属性的文档可在http://msdn.microsoft.com/en-us/library/windows/apps/br211837.aspx
的 MSDN 上获得。我鼓励您在继续之前花几分钟时间探索这些属性的其他值组合。例如,将selectionMode
设置为multi
并将swipeBehavior
设置为select
,您可以看到如何开始创建一个界面,其行为类似于 Windows 自带的邮件应用中的电子邮件列表。
模板
你可能在清单 7-1 中注意到,集合中的条目可能比单个值更复杂,比如一个字符串。在当前情况下,每个项目有四个不同的属性:name
、classification
、pic_sm
和pic_lg
。没有进一步的指示,ListView
简单地显示集合中每个项目的文本表示,如图图 7-4 所示。使用模板,您可以指定如何显示项目的每个属性。通过将高亮显示的行添加到您的代码中,修改您添加到listViewBasics.html
中的代码以匹配清单 7-4 。
清单 7-4。 给你的 ListView 添加模板
<div id="listViewTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="listViewItem">
<img src="#" class="listViewItemImage" data-win-bind="src: pic_sm" />
<div class="listViewItemText">
<h4 data-win-bind="innerText: name"></h4>
<h6 data-win-bind="innerText: classification"></h6>
</div>
</div>
</div>
<div id="listView"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: animals.dataSource,
itemTemplate: select('#listViewTemplate'),
selectionMode: 'none',
tapBehavior: 'none',
swipeBehavior: 'none'
}">
</div>
这段代码创建了一个Template
控件,它定义了数据源中每个项目的属性应该如何显示。您将在整个Template
中使用data-win-bind
属性来设置绑定,这些绑定将用于将项目的值分配给正确的 HTML 属性。通过给id
属性赋值,您就可以设置ListView
的itemTemplate
属性,以便在显示时使用这个Template
。我将在第十一章中更详细地介绍数据绑定。当你运行应用时,你可以看到,如图图 7-5 所示,你离目标越来越近了,但仍有差距。
图 7-5 。您的 ListView 就快完成了
你必须给你的页面控件添加一些 CSS 来润色一下。Visual Studio 在您创建页面控件时添加了一个名为listViewBasics.css
的文件。打开文件并添加来自清单 7-5 的 CSS 代码。这段代码设置适当的宽度、高度、边距和其他样式属性,以使页面按预期显示。
清单 7-5。 CSS 为你的基本 ListView 示例
.listViewItem {
width: 250px;
height: 75px;
padding: 5px;
overflow: hidden;
display: -ms-grid;
}
.listViewItem img.listViewItemImage {
width: 60px;
height: 60px;
margin: 5px;
-ms-grid-column: 1;
}
.listViewItem .listViewItemText {
margin: 5px;
-ms-grid-column: 2;
}
#listView {
height: 400px;
width: 100%;
-ms-grid-column-span: 2;
}
现在已经添加了所有代码,您可以再次运行应用。这一次,您终于看到了您所期望看到的:一个格式良好的网格,包含来自您的animals
数据源的数据(参见图 7-6 )。每个项目都显示彩色图标、动物名称及其分类,这在本章前面的data.js
中已经定义。
图 7-6 。一个漂亮的列表视图
布局
你可能注意到了在一个ListView
控件中项目的默认显示是网格模式。从左上角开始(在从左到右的语言中,如英语),项在一列中从上到下加载。当到达ListView
的底部时,从第一列的右侧开始新的一列。如果与 ListView 控件所占用的空间相比,所显示的每个项目的可视大小较小,这通常是首选显示。但是,如果项模板需要更多的空间,或者如果 ListView 显示在狭窄的空间中,例如当您的应用处于对齐模式时,更紧凑的垂直布局可能是首选。这可以通过使用ListView
的layout
属性来完成。
添加一个名为listViewLayouts.html
的新页面控件并引用data.js
。然后将清单 7-6 中的代码添加到主部分。这段代码非常类似于您在前面几节中添加的内容。有一个Template
控制和两个ListView
控制。两个ListView
控件绑定到同一个数据源并引用同一个Template
控件。唯一的区别是第一个ListView
的布局属性设置为WinJS.UI.GridLayout
,而第二个ListView
的布局属性设置为WinJS.UI.ListLayout
。
注意我有意忽略了一些用于布局你的示例应用屏幕的 HTML 和 CSS。我将涵盖与所描述的功能相关的所有代码,但是因为一些支持代码片段没有包含在代码清单中,所以您的应用看起来可能与这里包含的图略有不同。您可以通过引用名为 WinJSControlsSample 的完整项目来查看我使用的所有代码,该项目包含在本书附带的源代码中。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
清单 7-6。 添加不同布局的 ListView 控件
<div id="listViewTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="listViewItem">
<img src="#" class="listViewItemImage" data-win-bind="src: pic_sm" />
<div class="listViewItemText">
<h4 data-win-bind="innerText: name"></h4>
<h6 data-win-bind="innerText: classification"></h6>
</div>
</div>
</div>
<div id="col1">
<div id="listViewGridLayout"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: animals.dataSource,
itemTemplate: select('#listViewTemplate'),
selectionMode: 'none',
tapBehavior: 'none',
swipeBehavior: 'none',
layout: { type: WinJS.UI.GridLayout }
}">
</div>
</div>
<div id="col2">
<div id="listViewListLayout"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: animals.dataSource,
itemTemplate: select('#listViewTemplate'),
selectionMode: 'none',
tapBehavior: 'none',
swipeBehavior: 'none',
layout: { type: WinJS.UI.ListLayout }
}">
</div>
</div>
接下来,将清单 7-7 中的代码从添加到到listViewLayouts.css
,然后运行应用。您应该会看到两个ListView
控件,如图 7-7 中的所示。除了更窄以允许两个ListView
控件适合屏幕之外,第一个控件与上一节的ListView
相同。第二个ListView
在垂直列表中显示它的项目。每个单独的项目在两个控件中显示相同,说明一个Template
可以在许多控件中重用,并且多个控件可以绑定到同一个数据源。
清单 7-7。 CSS 为您的列表视图布局示例
.listViewItem {
width: 250px;
height: 75px;
padding: 5px;
overflow: hidden;
display: -ms-grid;
}
.listViewItem img.listViewItemImage {
width: 60px;
height: 60px;
margin: 5px;
-ms-grid-column: 1;
}
.listViewItem .listViewItemText {
margin: 5px;
-ms-grid-column: 2;
}
#listViewGridLayout {
height: 400px;
width: calc(100% - 50px);
border: 1px solid #464646; /* make it easier to distinguish */
}
#listViewListLayout {
height: 400px;
width: 300px;
border: 1px solid #464646; /* make it easier to distinguish */
}
图 7-7 。使用不同布局的两个 ListView 控件
WinJS 只包括这两个布局选项:GridLayout
和ListLayout
。如果您需要一个不同的布局,比如一个以行而不是列来定位数据的网格布局,您可以通过实现WinJS.UI.ILayout
接口来选择自己实现一个新的布局选项。这超出了本书的范围,但是关于这个接口的更多信息可以在 MSDN 的http://msdn.microsoft.com/en-us/library/windows/apps/jj712247.aspx
上找到。
分组和语义缩放
通常,长长的项目列表非常有意义。但是,有时,尤其是当列表很长时,可以通过对数据进行分组来改善用户体验。例如,一个城市列表可以按国家分组,一个电子邮件列表可以按发件人的电子邮件地址分组,或者,正如您在我们的示例中所做的,一个动物列表可以按字母顺序分组。
添加一个名为listViewGrouping.html
的新页面控件并引用data.js
。然后将清单 7-8 中的代码添加到主部分。这段代码类似于我们的第一个ListView
示例,除了在这个示例中,我添加了另一个Template
控件,用于每个组的标题。我还在ListView
控件上设置了一些属性,以指示哪些文本应该显示在组标题中,以及标题应该使用我们新的Template
。
清单 7-8。 添加带有分组的 ListView】
<div id="listViewHeaderTemplate"
data-win-control="WinJS.Binding.Template"
style="display: none">
<div class="listViewHeader">
<h1 data-win-bind="innerText: name"></h1>
</div>
</div>
<div id="listViewTemplate"
data-win-control="WinJS.Binding.Template"
style="display: none">
<div class="listViewItem">
<img src="#" class="listViewItemImage" data-win-bind="src: pic_sm" />
<div class="listViewItemText">
<h4 data-win-bind="innerText: name"></h4>
<h6 data-win-bind="innerText: classification"></h6>
</div>
</div>
</div>
<div id="listView"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: groupedAnimals.dataSource,
itemTemplate: select('#listViewTemplate'),
groupDataSource: groupedAnimals.groups.dataSource,
groupHeaderTemplate: select('#listViewHeaderTemplate'),
selectionMode: 'none',
tapBehavior: 'none',
swipeBehavior: 'none'
}">
</div>
在清单 7-8 中,我将ListView
的itemDataSource
属性设置为groupedAnimals.dataSource
,将ListView
的groupDataSource
属性设置为groupedAnimals.groups.dataSource
,但是groupedAnimals
还不存在。您可以通过将清单 7-9 中的代码添加到之前创建的data.js
JavaScript 文件的末尾来定义它。这段代码定义了一个用于对组进行排序的函数(compareGroups
)、一个指示集合中的哪些项目应该分组在一起的函数(getGroupKey
)以及一个指示应该使用哪些数据来填充在groupHeaderTemplate
属性中指定的Template
的函数(getGroupData
)。然后代码使用这三个函数基于您的animals
集合创建一个新的WinJS.Binding.List
。有关创建分组列表的更多信息,请访问 MSDN 的http://msdn.microsoft.com/en-us/library/windows/apps/hh700742.aspx
.
清单 7-9。 将你的数据分组
function compareGroups(left, right) {
return left.toUpperCase().charCodeAt(0) - right.toUpperCase().charCodeAt(0);
}
function getGroupKey(dataItem) {
return dataItem.name.toUpperCase().charAt(0);
}
function getGroupData(dataItem) {
return {
name: dataItem.name.toUpperCase().charAt(0)
};
}
var groupedAnimals = animals.createGrouped(getGroupKey, getGroupData, compareGroups);
在这个例子中,我使用了动物名称的第一个字母作为分组关键字和分组标题。在一个更复杂的例子中,您可以按国家对城市进行分组,getGroupKey
函数可以返回该国家的 ISO 国家代码。同样,因为在groupHeaderTemplate
属性中指定的模板不限于简单的文本,所以getGroupData
可以返回一个带有国家名称、其大洲和国旗图像的对象。在这种情况下,您还可以修改compareGroups
函数,按照国家名称、洲、甚至国家的面积(平方英里)进行排序。
现在剩下的工作就是样式化这个ListView
,所以让我们通过添加清单 7-10 中的代码到用页面控件创建的listViewGrouping.ccs
文件中。同样,这类似于在清单 7-5 中添加的 CSS 代码,除了这个代码包含了你的组标题的样式。运行应用,查看类似于图 7-8 中的分组ListView
。
清单 7-10。 用于列表视图分组的样式
.listViewHeader
{
width: 50px;
height: 50px;
padding: 8px;
}
.listViewItem {
width: 250px;
height: 75px;
padding: 5px;
overflow: hidden;
display: -ms-grid;
}
.listViewItem img.listViewItemImage {
width: 60px;
height: 60px;
margin: 5px;
-ms-grid-column: 1;
}
.listViewItem .listViewItemText {
margin: 5px;
-ms-grid-column: 2;
}
#listView {
height: 400px;
width: 100%;
-ms-grid-column-span: 2;
}
图 7-8 。项目按字母顺序分组的列表视图
还不错,但是想象一下一个包含数百种动物数据的ListView
。这将是很好的能够看到你的动物群体作为一个索引列表,然后能够使用该索引找到你正在寻找的动物。WinJS 为此提供了SemanticZoom
控件。如果你不熟悉语义变焦的概念以及它与光学变焦的区别,我将尝试描述它们。光学缩放是以不同的放大级别查看同一项目,而语义缩放允许您查看每个项目的少量数据,以换取查看更多项目的数据。我们来看几个类比。
光学变焦 是照片编辑软件的常用功能。你可以放大照片中的一片草,也可以缩小来看整片草坪。在这两种情况下,你看到的是同一张照片,只是放大倍数不同。另一方面,语义缩放是在线绘图软件的常见功能。你可以放大看到你居住的街道,但当你缩小时,你的街道消失了,你看到的是城市。继续缩小,你会看到州,然后是国家,然后是大陆。与照片中草叶的细节仍然是照片的一部分不同,缩小地图会导致街道的细节被其他东西的细节所取代。
语义缩放的另一个常见案例是日历。在最底层,您可以看到您在某一天参加的所有会议,包括时间、地点和其他与会者的详细信息。缩小时,您可能会看到整个星期,只列出会议标题和时间。进一步缩小,你可能看不到任何关于你的会议的信息,而只能看到年份和月份。
注意
SemanticZoom
控件只支持一个缩放级别。如果你的应用有一个更深的层次,我鼓励你遵循第四章中介绍的网格应用项目模板,为层次的每一层导航到一个新的页面。
SemanticZoom
控件允许您指定数据的不同视图,包含不同数量的细节。如果你的列表中有数百种动物,缩小到只看到组标题会很有帮助。然后你可以选择一个直接跳到你的动物列表的那一部分。您可以通过几个额外的步骤来添加这些功能。首先,将清单 7-11 中突出显示的代码从添加到listViewGrouping.html
。我已经添加了另一个Template
控件和ListView
控件,它们将在你缩小时使用。我还添加了SemanticZoom
控件,它首先包含用户放大到最大细节级别时看到的ListView
,其次包含用户缩小以查看更多项目的更少细节时看到的ListView
。请注意,这些控件的顺序很重要,更详细的放大视图必须放在不太详细的缩小视图之前。
清单 7-11。 添加语义缩放及相关控件
<div id="listViewHeaderTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="listViewHeader">
<h1 data-win-bind="innerText: name"></h1>
</div>
</div>
<div id="listViewTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="listViewItem">
<img src="#" class="listViewItemImage" data-win-bind="src: pic_sm" />
<div class="listViewItemText">
<h4 data-win-bind="innerText: name"></h4>
<h6 data-win-bind="innerText: classification"></h6>
</div>
</div>
</div>
<div id="semanticZoomTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="semanticZoomItem">
<h2 class="semanticZoomItemText" data-win-bind="innerText: name"></h2>
</div>
</div>
<div id="semanticZoom" data-win-control="WinJS.UI.SemanticZoom">
<!-- zoomed in view -->
<div id="listView"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: groupedAnimals.dataSource,
itemTemplate: select('#listViewTemplate'),
groupDataSource: groupedAnimals.groups.dataSource,
groupHeaderTemplate: select('#listViewHeaderTemplate'),
selectionMode: 'none',
tapBehavior: 'none',
swipeBehavior: 'none'
}">
</div>
<!-- zoomed out view -->
<div id="zoomedOutListView"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource: groupedAnimals.groups.dataSource,
itemTemplate: select('#semanticZoomTemplate'),
selectionMode: 'none',
tapBehavior: 'invoke',
swipeBehavior: 'none'
}">
</div>
</div>
你可以停在那里,有一个工作的例子,但稍加设计,你可以有一个工作的例子,也很好看。将清单 7-12 中的代码添加到listViewGrouping.css
中,并运行应用。
清单 7-12。 语义缩放的附加样式
#zoomedOutListView {
height: 400px;
width: 100%;
-ms-grid-column-span: 2;
}
#semanticZoom {
height: 400px;
width: 100%;
-ms-grid-column-span: 2;
}
.semanticZoomItem
{
color: #ffffff;
background-color: #464646;
width: 150px;
height: 40px;
padding: 5px 15px;
}
起初,ListView
看起来和你添加分组时差不多,显示了所有动物的详细列表(见图 7-9 )。然而,您会注意到的一个不同之处是在ListView
的右下角增加了一个小减号(“-”)按钮。该按钮是激活SemanticZoom
和缩小的几种方式之一。
图 7-9 。添加了 SemanticZoom 的 ListView
要缩小并查看图 7-10 ,用户可以
- 单击减号按钮。
- 按住键盘上的 Ctrl 键,并使用鼠标上的滚轮向下滚动。
- 使用第一章中描述的捏手势。当然,这只适用于触摸屏。
图 7-10 。缩小的列表视图
在ListView
中确定所需位置后,用户可以执行以下操作之一进行放大:
- 通过单击或直接点击来选择群组。
- 将鼠标悬停在所需的组上,然后按住 Ctrl 键并在鼠标滚轮上向上滚动。
- 使用第一章中描述的拉伸手势。同样,这需要触摸屏。
注意开箱即用,唯一能与
SemanticZoom
控件一起工作的 WinJS 控件是ListView
。然而,通过实现WinJS.UI.IZoomableView
接口,可以创建你自己的控件,或者修改另一个控件。关于此界面的更多信息,请访问MSDN at
http://msdn.microsoft.com/en-us/library/windows/apps/br229794.aspx
。
FlipView
到目前为止,您已经看到了许多查看项目列表的技术,但是每种技术都一次向用户显示几个项目。虽然这是一个非常常见的场景,但有时,您希望将列表中的项目一次一个地呈现给用户,WinJS 为此任务提供了FlipView
控件。FlipView
控件的一个常见例子是一个相册,用户可以看到一张照片和一个标题,然后点击一个按钮前进到下一张。因此,让我们建立一个这样的例子。
添加一个名为flipView.html
的新页面控件并引用data.js
。然后将清单 7-13 中的代码添加到主部分。你会再次注意到这段代码与之前的ListView
例子非常相似。事实上,如果您将data-win-control
属性从WinJS.UI.FlipView
更改为WinJS.UI.ListView
,您仍然可以使用应用。这是因为FlipView
和ListView
都绑定到相同类型的数据源,并且都用相同类型的模板显示它们的项目。
清单 7-13。 添加一个动画视图
<div id="flipViewTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="imageWithOverlay">
<img class="image" data-win-bind="src: pic_lg; alt: name" />
<div class="overlay">
<h2 class="title" data-win-bind="innerText: name"></h2>
</div>
</div>
</div>
<div id="flipView"
data-win-control="WinJS.UI.FlipView"
data-win-options="{
itemDataSource: animals.dataSource,
itemTemplate: select('#flipViewTemplate')
}">
</div>
让我们通过添加来自清单 7-14 到flipView.css
的 CSS 代码来设置FlipView
的样式。在这个例子中有相当多的 CSS 代码,但这主要是因为我希望图像标题显示在覆盖在图像底部的半透明块中。你可以在图 7-11 中看到结果。用户可以通过点击箭头或在触摸屏上滑动来从一幅图像滚动到下一幅图像。
清单 7-14。 设计您的动画视图
#flipView
{
width: 400px;
height: 400px;
border: solid 1px black;
}
.flipViewContent
{
width: 400px;
height: 400px;
}
.imageWithOverlay
{
display: -ms-grid;
-ms-grid-columns: 1fr;
-ms-grid-rows: 1fr;
width: 400px;
height: 400px;
}
.imageWithOverlay img
{
width: 100%;
height: 100%;
}
.imageWithOverlay .overlay
{
position: relative;
-ms-grid-row-align: end;
background-color: rgba(0,0,0,0.65);
height: 40px;
padding: 20px 15px;
overflow: hidden;
}
.imageWithOverlay .overlay .title
{
color: rgba(255, 255, 255, 0.8);
}
图 7-11 。你的动物的全景
一个常见的改进是在你的FlipView,
下面的ListView
中提供图像的缩略图,两个控件绑定到同一个数据源。然而,FlipView
并不局限于显示带标题的图片。例如,如果您正在构建一个定制的文档阅读应用,您可以使用一个FlipView
来显示文档的每一页。
结论
在本章中,您看到了几种不同的向用户显示项目集合的技术。这里介绍的控件——ListView
及其布局选项,Template
、SemanticZoom,
和FlipView
—无疑会在你的 Windows 应用商店中找到它们的位置。虽然我只触及了这些控件的表面,但是它们提供了丰富的功能,并且有许多定制。我将在后面的章节中探讨其中的一些功能。
八、WinJS 自定义控件
使用我在最后三章中提到的控件,你可以构建一个应用来满足几乎所有的需求,并且当我们深入研究这些控件和其他概念的更多细节时,你将会看到,这正是你从第九章开始要做的事情。也就是说,有时您可能会发现自己希望获得一些额外的控件,而定制控件可能正是您所需要的。
在这一章中,我将研究在你的应用中需要一些时钟功能的情况。虽然使用一些基本的 HTML 和 JavaScript 构建一个时钟并不困难,但是您虚构的应用需求表明您在整个应用的多个地方都有时钟。无论是现成的控件还是自定义控件,使用控件的最大好处之一就是它们很容易重用。如果生成一个表示时钟的控件,就可以多次使用它,就像可以向应用中添加多个文本输入框一样。
自定义控件
简而言之,控件可以被认为是用户界面的任何一部分,以某种方式捆绑在一起,允许你将它添加到你的应用中。至少就本章而言,自定义控件是指在开发 Windows 应用商店应用时没有提供的现成控件。通常,当开发人员提到自定义控件时,他们指的是使用这些控件的应用的开发人员编写的控件。这当然没有错,但该术语也可以指从第三方控件供应商处购买的控件。不缺少第三方控件,例如,可以用来显示图表或图形,您可以在您最喜欢的 web 搜索引擎的帮助下找到许多这样的控件。
也就是说,本章的重点不是那些第三方控件。相反,我将介绍构建您自己的控件的步骤,特别是 WinJS 控件,以满足您自己的特定需求。我将讨论构建自定义控件的两种常用方法。
- 我将用本章的大部分时间讨论如何用 JavaScript 构建自定义 WinJS 控件。
- 我将简要介绍如何使用 WinJS 提供的
PageControl
构建定制的 WinJS 控件,您已经在前几章中看到了。
您当然可以不使用 WinJS 库来创建自定义控件。如果您是一名经验丰富的 JavaScript 开发人员,这是一个完全合理的选择,尽管这超出了本书的范围。因为 Windows Store 应用是用 HTML 和 JavaScript 构建的,所以您可以在应用中使用许多流行的 JavaScript 库和几乎所有常见的 JavaScript 技术。但是,使用 WinJS 的功能来构建自定义控件也有好处。WinJS 库提供了一种模式,可以帮助您维护代码的一致性并避免与 JavaScript 语言相关的常见缺陷,从而使您能够将更多的精力放在业务需求上,而不是控制构建的内部工作。像任何软件抽象一样,它并不总是正确的选择,但我发现几乎总是,我更喜欢构建自定义 WinJS 控件,而不是纯粹用 JavaScript 构建控件。
用 JavaScript 定制 WinJS 控件
正如我上面提到的,我们将用本章的大部分时间用 JavaScript 和 WinJS 库构建一个自定义时钟控件。让我们首先确定这种控制的一些要求。
- 时钟可以以 12 小时或 24 小时格式显示当前本地时间。
- 当用于显示当前时间时,用户可以选择显示或隐藏秒。
- 该时钟可以用作倒计时或“向上计数”计时器,以测量经过的时间。
- 当用作倒计时或递增计时时,可以指定初始时间。
- 当用作倒计时或计数计时器时,计时器可以在创建时自动启动。
- 时钟可以通过编程启动、停止或复位。
- 包含时钟控件的页面可以通过事件得到通知,例如时钟开始、停止或重置的时间,以及倒计时结束的时间。
注本书附带的源代码包括一个名为 WinJSControlsSample 的完整项目,其中包含了在第五章、第六章、第七章和第八章中使用的样本代码。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
在前几章中,您将在现有的 WinJSControlsSample 项目中构建此功能。在我进入构建这个控件的细节之前,您必须做一些项目设置。在 Visual Studio 中打开项目后,按照第五章中的步骤添加一个名为customcontrols.html
的页面控件。确保将所有的示例代码放在页面控件的<section aria-label="Main content" role="main">
和</section>
元素之间。您还必须为这个页面控件添加一个导航按钮到home.html
。
接下来,添加一个名为controls
的新文件夹到你的项目的根目录,并在其中添加另一个名为js
的文件夹(见图 8-1 )。这是您将要创建自定义控件的地方,所以右键单击该文件夹,并选择向项目添加新项的选项。添加一个名为clockControl.js
的新 JavaScript 文件(参见图 8-2 )。
图 8-1 。JavaScript 控件的主页
图 8-2 。在项目中创建 JavaScript 控件
JavaScript 自定义 WinJS 控件的剖析
WinJS JavaScript 控件只需创建一个新的类,遵循一些特定的约定。虽然面向对象的开发任务,比如创建类,可以用 JavaScript 来完成,但是与使用许多其他语言(比如 C#)相比,这样做要冗长得多。对面向对象概念的全面介绍,更不用说 JavaScript 了,已经超出了本书的范围。(如果你想探索这个话题,请参阅罗斯·哈梅斯和达斯汀·迪亚兹的《Pro JavaScript 设计模式》。)幸运的是,WinJS 抽象掉了这种冗长的麻烦,允许您以更简单的方式创建类和控件。清单 8-1 展示了一个简单的模板,当你在应用中定义控件时,你可以使用它。
清单 8-1。 定义控件的模板
(function () {
"use strict";
var controlDefinition = WinJS.Class.define(
function Control_ctor(element, options) {
this.element = element || document.createElement("div");
this.element.winControl = this;
// control initialization
},
{
// instance members
},
{
// static members
}
);
WinJS.Namespace.define("WinJSControlsSample.UI", {
HelloWorld: controlDefinition
});
})();
如果你有相当多的 JavaScript 经验,你可能会自己弄清楚清单 8-1 中的代码在做什么,如果是这样的话,我邀请你跳到下一节。但是,如果您是 JavaScript 新手,或者接触的内容有限,我将逐步介绍这段代码,让您大致了解一下发生了什么。让我们从顶部开始……从底部开始。
第一行(function () {
和最后一行})();
构成了通常所说的自执行功能。如果这对你来说看起来有点奇怪,你并不孤单。第一次看的时候,我也有点不知所措。用技术术语来说,这段代码定义了一个匿名 JavaScript 函数,并立即执行它。从最后一行的末尾开始,向后看,你会看到左括号和右括号,后面跟着一个分号,就像你在一行代码的末尾看到你调用了你写的函数一样。这就是“自执行功能”的“执行”部分那么,正在执行的功能是什么?在右括号后面再后退一个字符。这与清单 8-1 第一行的第一个左括号匹配。这两个括号之间的所有内容都是匿名函数,正如它听起来的那样,是一个没有名字或不需要名字的函数。一旦定义了这个函数,这段代码就会立即执行它。在清单 8-2 和清单 8-3 中,你会看到两种手段殊途同归。这两个代码块做同样的事情,并向用户显示一个对话框。
清单 8-2。 示例函数定义和函数调用
function myFunction() {
alert("foo");
}
myFunction();
清单 8-3。 样本自动执行功能
(function() {
alert("foo");
})();
我们为什么要这么做?有几个原因,网上有很多关于这个主题的详细文章可以阅读,但简单地说,我们这样做是为了界定范围。函数内定义的变量对该函数外的任何代码都不可用。这意味着我们可以安全地创建自己的成员——变量、属性和函数——而不用担心与另一个同名成员冲突。在清单 8-1 中,名为controlDefinition
的变量不会对我们应用中名为controlDefinition
的任何其他变量产生任何影响,也不会受其影响。通过以这种方式保护我们的函数成员,我们可以有选择地以一种有意义的方式公开它们。如果您熟悉 C#之类的语言,这是一个类似于定义私有变量和函数的概念,这些变量和函数在定义它们的类之外不可用,并且提供公共成员,以便其他代码仍然可以以有意义的方式使用该类。你会在整本书中看到更多这样的内容。如果现在有点雾,不用担心。等你看了几遍,就更清楚了。现在,重要的一点是记住在自执行函数中定义控件。
注对于自执行函数应该叫什么,不同的人有不同的看法。其他常见的名字还有立即调用函数表达式(IIFE)、立即执行函数、或自执行匿名函数。只要你知道不同的名字,并且理解这个概念,任何名字都可以。为了保持一致,在本书的其余部分,我将把它们称为自执行函数。
清单 8-1 中代码的下一行是字符串文字"use strict";
。这声明该范围内的代码处于严格模式,这允许更好的错误检查。在严格模式下,某些做法是不允许的,通常是那些可能导致歧义或其他意外行为的做法。这使您能够更快地发现代码中的潜在错误,而不是部署一个在测试中似乎可以工作的应用,结果却导致问题。它有助于减少“它在我的机器上工作”的错误。
到目前为止,在本节中,我已经讨论了自执行函数和"use strict"
指令。这两个概念都是常见的现代 JavaScript 开发实践,关于它们的文章层出不穷。在清单 8-1 中,接下来是 WinJS 开发特有的东西。我们使用WinJS.Class.define
方法定义一个类,并将该类定义赋给一个名为controlDefinition
的私有变量。
注意如果你的背景是 C#这样的静态类型语言,那么给一个变量赋值函数和类定义可能看起来很奇怪,尽管在 C#中类似的模式越来越常见。这里需要注意的是,JavaScript 中的类和函数与任何其他值一样,可以作为变量值赋值。描述这一点的常用短语是说函数是 JavaScript 中的一级对象。
用三个参数调用WinJS.Class.define
方法。第一个是构造函数,在这里命名为Control_ctor
。每次创建该类的新实例时,都会调用该函数。定义控件时,此构造函数需要两个参数。我们的例子,以及通常的约定,将这些参数命名为element
和options
。第一个是对用于将控件放置在页面上的 HTML 元素的引用。正如你在第六章中看到的,第二个用于在你的 HTML 标记中使用data-win-options
属性为你的控件提供特定的选项。
第二个参数是描述类的实例成员的对象,第三个参数是定义类的静态成员的对象。示例场景可能是描述实例和静态成员的最简单的方法。
假设您正在创建一个Person
类。在这种情况下,您可能有一个名为firstName
的实例属性。它被称为实例属性,因为firstName
是描述一个Person
的单个实例的东西。您的Person
类也可能有一个名为search
的静态方法,您可以调用它来查找符合某些标准的单个Person
或一组Person
对象。清单 8-4 展示了一个如何引用这些属性的例子。注意,firstName
属性需要一个Person
类的实例,对于这个类,search
函数可以直接从Person
类获得,而不需要该类的任何特定实例。随着本章的进行,您将看到如何为实例成员构造传递给WinJS.Class.define
方法的对象的例子。我们在本章中的例子不需要任何静态成员,但是指定它们是以同样的方式完成的。
清单 8-4。 你的虚构人物类的属性和方法
var myPerson = new Person();
myPerson.firstName = "Scott";
var searchResults = Person.search("Scott");
清单 8-1 中剩余的代码用于定义一个名称空间,其中包括你的类定义。这就是如何向应用中需要引用它的其他代码公开私有的controlDefinition
类。您的代码调用了带有两个参数的WinJS.Namespace.define
方法。第一个是名称空间本身的名称,它用于对相关功能进行分组并防止命名冲突(两个类可以各有一个同名的属性)。第二个参数是一个对象,描述您向其他代码公开的不同内容。在这种情况下,您将您的私有controlDefinition
类暴露给应用的其余部分,并在WinJSControlsSample.UI
名称空间中给它一个公共名称HelloWorld
。这样,您的基本控件定义就完成了,您可以在应用的其他地方使用它,要么以声明方式将控件添加到您的 HTM 页面(您将在下一节看到),要么在 JavaScript 中使用类似于清单 8-5 的代码。
清单 8-5。 创建你的控件类的实例
var myControl = new WinJSControlsSample.UI.HelloWorld();
版本 1:一个简单的 12 小时时钟
好了,你已经在前面的部分制作了一个控件,但是它实际上并不做任何事情。让我们在这个例子的基础上创建一些真正的功能。打开clockControl.js
JavaScript 文件,添加来自清单 8-6 的代码。你会认出我在清单 8-1 中描述的模式,有一些不同。首先,我没有将你的类公开为HelloWorld
,而是将名字改为Clock,
来表示这个控件将提供的功能。其次,也是最值得注意的,我已经为WinJS.Class.define
的实例成员参数提供了相当多的定义。
清单 8-6。 你的第一个“真实”自定义控件
(function () {
"use strict";
var controlDefinition = WinJS.Class.define(
function Control_ctor(element, options) {
this.element = element || document.createElement("div");
this.element.winControl = this;
this._init();
},
{
// instance members
_init: function () {
this.start();
},
start: function () {
setInterval(this._refreshTime.bind(this), 500);
},
_refreshTime: function () {
var dt = new Date();
var hr = dt.getHours();
var min = dt.getMinutes();
var sec = dt.getSeconds();
var ampm = (hr >= 12) ? " PM" : " AM";
hr = hr % 12;
hr = (hr === 0) ? 12 : hr;
min = ((min < 10) ? "0" : "") + min;
sec = ((sec < 10) ? "0" : "") + sec;
var formattedTime = new String();
formattedTime = hr + ":" + min + ":" + sec + ampm;
this.element.textContent = formattedTime;
},
}
);
WinJS.Namespace.define("WinJSControlsSample.UI", {
Clock: controlDefinition,
});
})();
这个实例成员对象是使用 JavaScript 对象表示法(JSON)语法定义的。JSON 允许您直接指定一个对象,就地定义它,而不是创建一个类定义,然后设置许多属性。它也是一种广泛使用的格式,用于在应用之间传输数据,因为它很容易与任何编程语言可读的文本表示进行相互转换。在 JSON 中,对象的成员用逗号分隔,每个成员都用模式memberName: memberDefinition
定义。您可以在我们的示例中看到这一点,其中您的类的实例成员的对象有三个自己的成员:_init
函数、start
函数和_refreshTime
函数。
注意使用 WinJS 实用程序定义类时使用的惯例是,私有成员(仅在类本身中可用的变量和函数)的名称以下划线字符(_)开头,而公共成员(使用类的代码中可用的变量和函数)的名称以字母开头。
我作为构造函数的最后一行调用的_init
函数非常简单:它只是调用了start
函数。反过来,start
函数创建一个间隔,这是一个 JavaScript 特性,允许代码在一定时间后重复执行,在本例中是 500 毫秒。_refreshTime
函数获取当前时间,对其进行格式化,然后通过设置用于将该控件添加到页面的 HTML 元素的textElement
属性,在页面上显示该时间。
说到那个元素,是时候看看怎么做这个了。第一步是将自定义控件 JavaScript 文件的引用添加到 HTML 页面中。将清单 8-7 中的代码添加到customcontrols.html
的head
部分的末尾。然后将清单 8-8 中的代码添加到主部分。
清单 8-7。 给 HTML 文件添加脚本引用
<head>
<!-- SNIPPED -->
<script src="/controls/js/clockControl.js"></script>
</head>
清单 8-8。 添加您的自定义控件
<div id="myClock12" data-win-control="WinJSControlsSample.UI.Clock"></div>
如果你仔细看看你刚刚在customcontrols.html
中做了什么,你会发现,一旦定义了一个自定义控件,将它添加到页面就像添加你在第六章和第七章中看到的 WinJS 控件一样。添加一个对定义控件的脚本文件的引用——本例中为clockControl.js
,前面章节中为base.js
或ui.js
——然后添加一个div
元素,并将data-win-control
属性设置为控件的全名。当您运行应用时,您应该会看到一个显示当前本地时间的时钟(参见图 8-3 ),并且它会随着时间的流逝而更新。
图 8-3 。自定义时钟控件
版本 2:增加 24 小时时钟 选项
到目前为止,您已经完成了我在本章开始时为您的自定义控件定义的需求的一半。您制作了一个时钟,它以 12 小时制显示本地时间,但不是 24 小时制。在本节中,您将完成该需求的其余部分,并添加可选地显示或隐藏当前时间的秒数的功能。
让我们从描述你的时钟控制可以使用什么模式开始。在清单 8-9 中高亮显示的代码定义了一个包含两种不同时钟模式的变量,这是你当前开发任务的一部分。您将使用Object.freeze
JavaScript 函数来防止clockModes
变量被更改。这与在名称空间中公开clockModes
一起,有效地允许您使用WinJSControlsSample.UI.ClockModes
作为值的枚举。这将允许您以后通过变量名来指定模式,而不是使用数字或字符串,这样更容易被开发人员在将控件添加到页面时键入错误。
清单 8-9。 定义不同时钟模式的选项
(function () {
"use strict";
var controlDefinition = WinJS.Class.define(
// SNIPPED
);
// clockModes is an enum(eration) of the different ways our clock control can behave
var clockModes = Object.freeze({
CurrentTime12: "currenttime12",
CurrentTime24: "currenttime24",
});
WinJS.Namespace.define("WinJSControlsSample.UI", {
Clock: controlDefinition,
ClockModes: clockModes,
});
})();
既然您已经定义了可供时钟选择的不同模式,那么您需要一种方法来设置您的时钟的每个实例所需的单独模式。现在,让我们添加一种方法来显示或隐藏当前时间的秒数。这样做有三个步骤,如清单 8-10 中的所示。
- 为 模式和 showClockSeconds 添加实例属性定义:在我们的示例中,
mode
属性同时定义了get
和set
函数,因此它对于使用控件的代码是可读和可写的。然而,showClockSeconds
属性只是可写的,因为它只定义了一个set
函数。在本章的后面,您将创建一个只有get
函数的属性,使它成为一个只读属性。 - 在我们的构造函数中为这些属性设置默认值:默认情况下,您的时钟将处于 12 小时模式,并将显示当前时间的秒数。
- 调用
WinJS.UI.setOptions
方法,该方法将options
传递给我们的构造函数,并设置mode, showClockSeconds
和任何其他属性:您将在本节的后面看到这是如何工作的。
清单 8-10。 给你的时钟控件添加一个模式属性
var controlDefinition = WinJS.Class.define(
function Control_ctor(element, options) {
this.element = element || document.createElement("div");
this.element.winControl = this;
// Set option defaults
this._mode = clockModes.CurrentTime12;
this._showClockSeconds = true;
// Set user-defined options
WinJS.UI.setOptions(this, options);
this._init();
},
{
// instance members
mode: {
get: function () {
return this._mode;
},
set: function (value) {
this._mode = value;
}
},
showClockSeconds: {
set: function (value) {
this._showClockSeconds = value;
}
},
_init: function () {
this.start();
},
// SNIPPED
}
);
现在让我们向您的页面添加另一个时钟控件。将清单 8-11 中的代码添加到customcontrols.html
中。请注意,我将模式设置为显示当前时间,24 小时格式,并且我已经决定隐藏该时钟的秒数。
清单 8-11。 添加 24 小时时钟
<div id="myClock24"
data-win-control="WinJSControlsSample.UI.Clock"
data-win-options="{
mode: WinJSControlsSample.UI.ClockModes.CurrentTime24,
showClockSeconds: false
}">
</div>
继续运行应用。不是你所期望的?到目前为止,您已经描述了时钟可以使用的模式,并且在新的时钟控件上设置了属性,以选择 24 小时时钟模式并隐藏秒。然而,您还没有更改实际呈现时间的代码。用清单 8-12 中的高亮代码更新_refreshTime
函数后,再次运行应用。这一次,你应该看到一个新的 24 小时时钟,没有秒(见图 8-4 )。尽管我没有为您最初的时钟控制指定mode
或showClockSeconds
的值,但它仍然表现相同,因为我在清单 8-10 中的构造函数中设置了默认值。
清单 8-12 。 根据属性值渲染不同的时钟
_refreshTime: function () {
var dt = new Date();
var hr = dt.getHours();
var min = dt.getMinutes();
var sec = dt.getSeconds();
var ampm = (hr >= 12) ? " PM" : " AM";
if (this._mode === clockModes.CurrentTime12) {
hr = hr % 12;
hr = (hr === 0) ? 12 : hr;
} else {
ampm = "";
}
min = ((min < 10) ? "0" : "") + min;
sec = ((sec < 10) ? "0" : "") + sec;
var formattedTime = new String();
formattedTime = hr + ":" + min
+ ((this._showClockSeconds) ? ":" + sec : "") + ampm;
this.element.textContent = formattedTime;
},
图 8-4 。自定义时钟控件的两个实例
版本 3:增加定时器选项和引发事件
完成了两个要求,还有五个。你有一个完美的工作控件来显示当前时间。剩下的需求将让你选择使用这个控件作为一个定时器。对于本教程来说,更重要的是,您将添加对控件上调用方法的支持,并从控件中引发事件,这些事件可以在显示控件的页面上处理。
让我们首先将清单 8-13 中的代码添加到clockControl.js
中。这个清单包括该文件的完整源代码,突出显示了不同的部分。我将在本节的剩余部分介绍这些变化。它有几页长,但我建议你在继续之前通读一遍。当您通读它时,您会注意到向您的控件添加新模式的更改,以允许它用作计时器,并提供为计时器设置初始值的能力。此外,您将看到新的代码,它将使您能够启动、停止和重置计时器,并根据控件的状态引发事件。
清单 8-13。clock control . js 的完整来源
(function () {
"use strict";
var controlDefinition = WinJS.Class.define(
function Control_ctor(element, options) {
this.element = element || document.createElement("div");
this.element.winControl = this;
// Set option defaults
this._mode = clockModes.CurrentTime12;
this._showClockSeconds = true;
this._initialCounterValue = [0, 0, 0];
this._autoStartCounter = false;
// Set user-defined options
WinJS.UI.setOptions(this, options);
this._init();
},
{
// instance members
_intervalId: 0,
_counterValue: 0,
isRunning: {
get: function () {
return (this._intervalId != 0);
}
},
mode: {
get: function () {
return this._mode;
},
set: function (value) {
this._mode = value;
}
},
autoStartCounter: {
get: function () {
return this._autoStartCounter;
},
set: function (value) {
this._autoStartCounter = value;
}
},
initialCounterValue: {
set: function (value) {
if (isNaN(value)) {
// if not a number, value is an array of hours minutes and seconds
this._counterValue = (value[0] * 3600) + (value[1] * 60) + (value[2]);
this._initialCounterValue = value;
} else {
this._counterValue = value;
this._initialCounterValue = [0, 0, value];
}
}
},
showClockSeconds: {
set: function (value) {
this._showClockSeconds = value;
}
},
_init: function () {
if (this._mode === clockModes.CurrentTime12
|| this._mode === clockModes.CurrentTime24) {
this.start();
} else {
this._updateCounter();
if (this._autoStartCounter) {
this.start();
}
}
},
start: function () {
if (!this.isRunning) {
if (this._mode === clockModes.CurrentTime12
|| this._mode === clockModes.CurrentTime24) {
this._intervalId =
setInterval(this._refreshTime.bind(this), 500);
} else {
this._intervalId =
setInterval(this._refreshCounterValue.bind(this), 1000);
}
this.dispatchEvent("start", {});
}
},
stop: function () {
if (this.isRunning) {
clearInterval(this._intervalId);
this._intervalId = 0;
this.dispatchEvent("stop", {});
}
},
reset: function () {
this.initialCounterValue = this._initialCounterValue;
this._updateCounter();
this.dispatchEvent("reset", {});
},
_refreshTime: function () {
var dt = new Date();
var hr = dt.getHours();
var min = dt.getMinutes();
var sec = dt.getSeconds();
var ampm = (hr >= 12) ? " PM" : " AM";
if (this._mode === clockModes.CurrentTime12) {
hr = hr % 12;
hr = (hr === 0) ? 12 : hr;
} else {
ampm = "";
}
min = ((min < 10) ? "0" : "") + min;
sec = ((sec < 10) ? "0" : "") + sec;
var formattedTime = new String();
formattedTime = hr + ":" + min
+ ((this._showClockSeconds) ? ":" + sec : "") + ampm;
this.element.textContent = formattedTime;
},
_refreshCounterValue: function () {
if (this._mode === clockModes.CountDown) {
this._counterValue--;
if (this._counterValue <= 0) {
this._counterValue = 0;
this.stop();
this.dispatchEvent("countdownComplete", {});
}
} else {
this._counterValue++;
}
this._updateCounter();
this.dispatchEvent("counterTick", {
value: this._counterValue
});
},
_updateCounter: function () {
var sec = this._counterValue % 60;
var min = ((this._counterValue - sec) / 60) % 60;
var hr = ((this._counterValue - sec - (60 * min)) / 3600);
min = ((min < 10) ? "0" : "") + min;
sec = ((sec < 10) ? "0" : "") + sec;
var formattedTime = new String();
formattedTime = hr + ":" + min + ":" + sec;
this.element.textContent = formattedTime;
},
}
);
// clockModes is an enum(eration) of the different ways our clock control can behave
var clockModes = Object.freeze({
CurrentTime12: "currenttime12",
CurrentTime24: "currenttime24",
CountDown: "countdown",
CountUp: "countup",
});
WinJS.Namespace.define("WinJSControlsSample.UI", {
Clock: controlDefinition,
ClockModes: clockModes,
});
WinJS.Class.mix(WinJSControlsSample.UI.Clock,
WinJS.Utilities.createEventProperties("counterTick"),
WinJS.Utilities.createEventProperties("countdownComplete"),
WinJS.Utilities.createEventProperties("start"),
WinJS.Utilities.createEventProperties("stop"),
WinJS.Utilities.createEventProperties("reset"),
WinJS.UI.DOMEventMixin);
})();
你都明白了吗?从技术的角度来看,清单 8-13 的大部分内容与上一节相似,设置默认值并添加实例成员。让我们在这里快速浏览一下这些变化。
- 我们在构造函数中为两个私有属性
_initialCounterValue
和_autoStartCounter
设置默认值。然后我们将这些作为实例属性公开,分别命名为initialCounterValue
和autoStartCounter
。 - 在我们对
initialCounterValue
的定义中,我们利用了 JavaScript 是一种动态类型语言这一事实,允许将值设置为整数秒或表示小时、分钟和秒的三个数字的数组。 - 我们添加了一个名为
_counterValue
的私有属性来跟踪计数器的当前值,以秒为单位。一个名为reset
的新方法被用来(出人意料地)将_counterValue
重置为_initialCounterValue
。 - 我们添加了名为
_intervalId
的属性来跟踪让计数器滴答作响的 JavaScript 间隔。根据是否设置了_intervalId
属性,isRunning
属性指示计数器当前是否正在运行。属性在我们修改后的start
方法中被设置,并在新的stop
方法中被清除。 - 我们修改了
start
和_init
方法,使得当控件处于我们现有的时钟模式之一时,它们的行为不变,但是当控件处于新添加的计数器模式之一CountDown
和CountUp
时,它们的行为会有所不同。 - 我们添加了一个新的
_updateCounter
方法来格式化和显示计数器的当前值。同样,每隔 1000 毫秒,start
函数调用_refreshCounterValue
方法来递增或递减计数器值。
虽然所有这些更改确实为您的控件添加了相当多的新功能,但实现它们的过程与我在上一节中讨论的过程非常相似。然而,在清单 8-13 中有一些新概念,我想更详细地介绍一下。在文件末尾,我定义了五个新的事件混合,并用WinJS.Class.mix
方法将它们附加或合并到您的控制中:counterTick
、countdownComplete
、start
、stop,
和reset
。清单 8-14 是一个例子,摘录自清单 8-13 中的,它创建事件并将它们附加到你的控件中。WinJS.Class.mix
方法将您的控件作为第一个参数,后面是我们想要附加的 mixin 对象列表。
清单 8-14。 定义一个事件,摘自清单 8-13
WinJS.Class.mix(WinJSControlsSample.UI.Clock,
WinJS.Utilities.createEventProperties("counterTick"),
WinJS.Utilities.createEventProperties("countdownComplete"),
WinJS.Utilities.createEventProperties("start"),
WinJS.Utilities.createEventProperties("stop"),
WinJS.Utilities.createEventProperties("reset"),
WinJS.UI.DOMEventMixin);
注意mixin 是定义可重用功能的对象,在这里,定义事件。然后,可以将这些 mixins 附加到其他类,以添加这种可重用的功能。如果您熟悉面向对象编程,这有点类似于在您的类中实现接口。主要区别在于 mixin 包括可重用功能的实现,而接口描述了您的类必须实现的功能。
这些事件使您能够在类中发生某些事情时通知调用代码。然而,简单地定义这些事件没有任何作用。您必须决定在您的控制范围内何处引发这些事件。理论上,您可以在_init
方法中引发countdownComplete
事件,但是,当然,这没有意义。相反,你使用从WinJS.UI.DOMEventMixin
mixin 添加到你的类中的dispatchEvent
方法来引发start
方法中的start
事件(参见清单 8-15 )和stop
方法中的stop
事件。
清单 8-15。 引发开始事件
this.dispatchEvent("start", {});
在_refreshCounterValue
方法中引发了countdownComplete
事件和counterTick
事件。请特别关注一下counterTick
。对dispatchEvent
的调用带有两个参数:要引发的事件的名称和要传递给事件处理程序的特定于事件的数据参数的对象。对于所有其他事件,没有任何数据传递给事件处理程序,但是在counterTick
的情况下,事件处理程序知道私有_counterValue
属性的当前值可能是有用的(参见清单 8-16 )。在本节的后面,您将看到如何利用这个论点。
清单 8-16。 用数据参数引发 counterTick 事件
this.dispatchEvent("counterTick", {
value: this._counterValue
});
随着您的控制最终完成,让我们看看如何利用这些新功能。将清单 8-17 和清单 8-18 中的代码添加到customcontrols.html
中。请注意,在清单 8-17 中,计数器将从 10 秒的初始值开始倒数,而在清单 8-18 中,计数器将自动开始向上计数,测量经过的时间,从 10 小时 59 分 50 秒开始。运行应用,看看发生了什么(参见图 8-5 )。
清单 8-17。 增加一个“倒计时”计时器
<div id="myCountDown"
data-win-control="WinJSControlsSample.UI.Clock"
data-win-options="{
mode: WinJSControlsSample.UI.ClockModes.CountDown,
initialCounterValue: 10 }">
</div>
<button id="downStart">Start</button>
<button id="downStop" disabled>Stop</button>
<button id="downReset" disabled>Reset</button>
<span id="downEventStatus"></span>
清单 8-18。 增加一个“倒计时”计时器
<div id="myCountUp"
data-win-control="WinJSControlsSample.UI.Clock"
data-win-options="{
mode: WinJSControlsSample.UI.ClockModes.CountUp,
initialCounterValue: [10, 59, 50],
autoStartCounter: true }">
</div>
<div id="upEventStatus">10 second ticks: </div>
图 8-5 。增加了计时器
此时,您的 12 小时时钟、24 小时时钟和“countup”计时器都在更新。倒计时器不算了,因为还没开始。你有按钮,但是它们还不做任何事情,所以让我们将清单 8-19 中突出显示的代码添加到customcontrols.js
中的ready
函数。因为myCountDown
是我们在清单 8-17 中添加的div
元素的 id,我们使用winControl
属性来引用该元素表示的控件。有了那个引用,我们可以调用你的控件的start
、stop,
和reset
方法。
清单 8-19。 添加按钮点击处理程序
ready: function (element, options) {
downStart.addEventListener("click", function (e) {
myCountDown.winControl.start();
}, false);
downStop.addEventListener("click", function (e) {
myCountDown.winControl.stop();
}, false);
downReset.addEventListener("click", function (e) {
myCountDown.winControl.reset();
downReset.disabled=true;
}, false);
},
既然您可以控制“倒计时”计时器,那么我要介绍的最后一件事就是处理您的控件引发的事件。你会注意到在清单 8-17 和清单 8-18 中占位符分别被命名为downEventStatus
和upEventStatus
。您将使用它们来显示控件引发的每个事件的结果。检查清单 8-20 中更新的ready
函数,你会注意到的第一个变化是一个名为handleCountDownEvent
的函数。在一个真实的应用中,你可以用自己的逻辑来处理每一个事件,但是我现在想保持事情简单。因此,handleCountDownEvent
函数将事件的名称作为参数,并将其显示在downEventStatus
占位符中,然后根据正在处理的事件切换按钮的状态。在myCountDown
上处理的四个事件中的每一个都简单地调用handleCountDownEvent
,将事件类型作为参数传入。最后,当我们处理myCountUp
控件的counterTick
事件时,我们检查我们在清单 8-16 中设置的值数据参数,并且每当另一个 10 秒过去时,向upEventStatus
占位符添加一个“tick”字符。
清单 8-20 。?? 为你的定时器事件添加事件处理程序
ready: function (element, options) {
downStart.addEventListener("click", function (e) {
myCountDown.winControl.start()
}, false);
downStop.addEventListener("click", function (e) {
myCountDown.winControl.stop()
}, false);
downReset.addEventListener("click", function (e) {
myCountDown.winControl.reset()
}, false);
var handleCountDownEvent = function (eventName) {
downEventStatus.textContent = eventName;
var enableStart = (eventName === "start") ? false : true;
downStart.disabled = !enableStart;
downStop.disabled = enableStart;
downReset.disabled = !enableStart;
};
myCountDown.addEventListener("countdownComplete", function (e) {
handleCountDownEvent(e.type);
}, false);
myCountDown.addEventListener("start", function (e) {
handleCountDownEvent(e.type);
}, false);
myCountDown.addEventListener("stop", function (e) {
handleCountDownEvent(e.type);
}, false);
myCountDown.addEventListener("reset", function (e) {
handleCountDownEvent(e.type);
}, false);
myCountUp.addEventListener("counterTick", function (e) {
if (e.value % 10 === 0) upEventStatus.textContent += "'";
}, false);
},
至此,您的Clock
控件的所有四个实例现在都完全正常了。您可以为每个控件设置属性、调用方法和处理事件。再次运行应用,看看它们是如何组合在一起的(见图 8-6 )。
图 8-6 。您的全功能时钟控制
使用 PageControl 自定义 WinJS 控件
之前用 JavaScript 创建 WinJS 控件的方法可能非常强大。事实上,它本质上是用来创建我在第六章中讨论的所有控件的过程。然而,正如我在本章开始时提到的,在 JavaScript 中还有其他创建自定义控件的方法。我将在这里简要介绍的另一种方法是使用PageControl
创建控件。在前几章中,您已经使用了PageControl
向您的应用添加新的屏幕,但是在这里,您将看到一个如何创建一个简单的Contact
控件的示例,您可以在您的应用的页面上显示这个控件,将一个PageControl
嵌套在另一个PageControl
中。我们的Contact
控制不了多少。这个版本的控件的唯一要求是显示一个人的名字、姓氏和生日,但这可以扩展到包括更多关于这个人的数据,甚至包括编辑一个人的联系信息的控件。
您将按照前几章中使用的相同步骤添加一个PageControl
,但在此之前,让我们为控件创建一个更符合逻辑的 home。实际上,您几乎可以在 Visual Studio 项目中的任何位置添加该控件,但是像任何项目一样,将文件组织到适当的文件夹中有助于保持有序,尤其是当您的项目变大并且有许多文件时。让我们从在现有的controls
文件夹中创建一个名为pages
的文件夹开始。然后,在该文件夹中,创建一个contactControl
文件夹,并在该文件夹中添加一个名为contactControl.html
的新PageControl
。当你完成后,你的解决方案资源管理器应该看起来像图 8-7 。现在,用清单 8-21 中的代码替换contactControl.js
的内容。
清单 8-21。 新页面控件的代码隐藏文件
(function () {
"use strict";
var controlDefinition = WinJS.UI.Pages.define(
"/controls/pages/contactControl/contactControl.html",
{
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
options = options || {};
this._first = "";
this._last = "";
this._birthday = "";
// Set user-defined options
WinJS.UI.setOptions(this, options);
firstContent.textContent = this.first;
lastContent.textContent = this.last;
birthdayContent.textContent = this.birthday;
},
first: {
get: function () { return this._first; },
set: function (value) {
this._first = value;
}
},
last: {
get: function () { return this._last; },
set: function (value) { this._last = value; }
},
birthday: {
get: function () { return this._birthday; },
set: function (value) { this._birthday = value; }
},
unload: function () {
// TODO: Respond to navigations away from this page.
},
updateLayout: function (element, viewState, lastViewState) {
/// <param name="element" domElement="true" />
// TODO: Respond to changes in viewState.
}
}
);
WinJS.Namespace.define("WinJSControlsSample.UI", {
Contact: controlDefinition,
});
})();
图 8-7 。您的新页面控件
浏览这个文件,你会发现它与我们在清单 8-13 中创建的 JavaScript 控件有些不同,但是有一些相似的概念。要注意的第一个区别是,我们不是定义一个类,而是使用WinJS.UI.Pages.define
方法来定义一个PageControl
。然而,我们仍然将该控件公开作为我们的WinJSControlsSample.UI
名称空间的一部分。另一个区别是PageControl
没有构造函数;然而,它有一个ready
函数,用来初始化你的控件属性。最后,虽然实例成员没有对象,但是您的first
、last
和birthday
属性是在ready
函数旁边声明的。
JavaScript 控件和PageControl
的显著区别在于PageControl
还包括一个 HTML(和 CSS)文件,允许您在标记中定义更多的控件布局和设计。清单 8-22 包含了您需要为您的PageControl
添加到contactControl.html
中的代码。如果你熟悉使用 ASP.NET 进行 web 开发,JavaScript 控件在概念上类似于 ASP.NET 服务器控件,而PageControl
在概念上类似于 ASP.NET 用户控件。像 ASP.NET 服务器控件一样,JavaScript 控件可以并且经常被定义在一个代码文件中。这使得项目之间的可重用性非常简单。另一方面,PageControl
的定义分布在多种类型的多个文件中,通常一个文件包含表示逻辑——HTML 用于PageControl
和 ASCX 用于 ASP.NET 用户控件——另一个文件包含行为逻辑——JavaScript 用于PageControl
和. NET 语言,如 C#或 VB.NET,用于 ASP.NET 用户控件。虽然这提供了一种更简单的方法来控制控件的表示,但它确实使项目之间的可重用性稍显不便。
清单 8-22。 页面控件的标记
<script src="/controls/pages/contactControl/contactControl.js"></script>
<body>
<div class="contactControl">
<p class="contactControl-first">
<strong>First name:</strong>
<span id="firstContent">First name goes here</span>
</p>
<p class="contactControl-last">
<strong>Last name:</strong>
<span id="lastContent">Last name goes here</span>
</p>
<p class="contactControl-birthday">
<strong>Birthday:</strong>
<span id="birthdayContent">Birthday goes here</span>
</p>
</div>
</body>
一旦创建了页面控件,向页面添加控件并设置选项的过程与添加自定义 JavaScript 控件或 WinJS 提供的现成控件没有任何不同。将清单 8-23 中的代码添加到customcontrols.html
中。在本例中,您在标记中以声明方式设置了first
、last,
和birthday
属性。实际上,你可能会在 JavaScript 中设置这些值,可能是在数据绑定的过程中,这个主题我将在第十一章中更全面地讨论。
清单 8-23。 将页面控件添加到 customcontrols.html
<div id="myContact"
data-win-control="WinJSControlsSample.UI.Contact"
data-win-options="{
first: 'Scott',
last: 'Isaacs',
birthday: 'December 1' }">
</div>
当然,这个PageControl
例子非常简单,但是您可以看到开发一个PageControl
与开发一个定制的 JavaScript 控件是多么相似。当您需要一个可重用的复合控件(一个本身包含多个控件的控件)时,这尤其有用。如果你想一想,这正是我们从《??》第五章开始对《??》所做的事情。您一直使用它来包含示例应用的整个屏幕的内容,其中包含几个子控件。您将控件视为独立的单元,彼此独立。然而,通过定义一些额外的属性,您可以看到如何使用清单 8-21 中的PageControl
和清单 8-22 中的来显示从其自身定义之外提供的数据(参见图 8-8 )。
图 8-8 。你的页面控件和你的自定义 JavaScript 控件一起显示
结论
在本章中,我介绍了两种在应用中创建可重用控件的技术。通过将一些用户界面和行为封装到一个包中,自定义控件提供了方便的可重用性,无论是在您的项目中还是在您将来可能开发的其他项目中。每当您发现自己多次构建同一个界面时,都应该考虑自定义控件。这一章的大部分时间我都在构建一个自定义的 JavaScript WinJS 控件,您将在本书的后面再次使用它,并简要介绍了PageControl
,您已经在几章中使用了它。
九、构建用户界面
现在,我已经介绍了触摸概念、Microsoft 设计风格的原则、创建 Visual Studio 项目以及使用 Windows 应用商店应用的许多可用控件,是时候做一些更有趣的事情了。在本章中,您将开始构建一个真实世界的应用,并在本书的其余部分继续构建。我们将构建一个面向软件顾问、设计师、自由职业者和任何其他执行基于项目的工作的人的计时应用。
当我不写书的时候,我会花时间做软件开发顾问。像许多咨询公司一样,我工作的公司使用第三方时间和费用跟踪系统来支持它与各种客户合作的所有顾问。从任何地方都可以使用 web 浏览器访问它,这是一个非常完整的系统,具有各种功能和配置选项。不幸的是,这不是最容易使用的系统。所有的附加功能和可配置选项都需要大量的点击和导航来输入我每天的计费时间。因此,像许多必须记录项目花费时间的人一样,我发现自己使用穷人的通用数据库:Microsoft Excel 来记录我所有的时间。每月一次,我在一个窗口打开我们的时间和费用系统,在另一个窗口打开 Excel,将时间从一个窗口复制到另一个窗口。还不算太糟,但是有时我会在同一天接到多个客户的账单。我可以通过调整我在 Excel 中记录时间的方式来解决这个问题,或者我可以写一个应用。因为我刚好在写一本关于构建 Windows Store 应用的书,所以我选择了后者,这样我们就可以一起构建了。
介绍克洛克
在本书的剩余章节中,我们将构建 Clok,一个 Windows 商店的时间表应用。为了防止这本书长达 1500 页,我将保持基本的特性集。以下是顾问可以使用 Clok 执行的高级功能:
- 通过启动和停止计时器来跟踪项目的时间
- 将照片和文稿添加到项目
- 管理项目列表
- 管理以前跟踪的时间条目
这听起来可能不多,但是您会发现,有许多技术对于使用 HTML 和 JavaScript 构建 Windows Store 应用非常有用,这些技术可以组合在一起构建一个真实但简单的应用。在图 9-1 中,你可以看到当你完成这一章时,我们应用的主屏幕将会是什么样子。
图 9-1 。完整的 Clok 仪表板
我定义的四个高级功能都有一个按钮。最大的按钮用于开启和关闭计时器。在图 9-1 中,计时器当前正在运行,我已经指出了我正在进行的项目,并添加了一些注释。此外,还添加了徽标和当前时间。在接下来的几页中,您将看到这个屏幕是如何创建的。
注本书附带的源代码包括一个完整的项目,其中包含本章使用的所有源代码和图像文件。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
创建项目外壳
因此,让我们卷起袖子,开始使用导航应用模板创建一个新的 Visual Studio 项目。如果图 9-2 还不够,你可以参考第五章并遵循同样的步骤。将新项目命名为 Clok。我将在本书的剩余部分构建这个项目中应用的所有功能。
图 9-2 。“新建项目”对话框
正如您在第五章中看到的,导航应用项目模板为我们创建了许多文件。在这一章中,我们编辑这些文件中的大部分,并创建一些新文件,以实现如图图 9-1 所示的应用设计。除了我们在本章中开发的主屏幕,我们将在应用中使用这种设计,因为我们将在后面的章节中添加更多的屏幕。
实现设计
当我们在第五章中创建项目时,我们配置应用使用ui-light.css
中定义的主题,而不是ui-dark.css
中定义的默认主题。然而,对于 Clok,我们将保留默认的黑暗主题。不要求使用这两个主题中的任何一个;但是,它们提供了一种简单的方法来确保您的应用的外观和行为与您的用户已经安装和使用的许多其他应用一样。
在ui-dark.css
中定义的主题具有深色背景和浅色文本。默认背景是深灰色,文本是白色的。相反,ui-light.css
中定义的主题有浅色背景和深色文本,默认为白色背景上的黑色文本。虽然我们确实想使用深色主题,但我们希望在屏幕右下角有一个蓝色背景和一个模糊版本的应用徽标。将清单 9-1 中突出显示的代码添加到default.css
中。一定要将background.png
(可以在本书附带的源代码中找到)添加到images
文件夹中。
清单 9-1。 改变我们应用的背景
#contenthost {
height: 100%;
width: 100%;
background-color: #3399aa;
background-image: url('/img/background.png');
background-repeat: no-repeat;
background-attachment: fixed;
background-position: 100% calc(100% - 85px);
}
详细的 CSS 教程超出了本书的范围,但简单地说,这段代码将应用的背景颜色设置为蓝色,用#3399aa
表示,并设置一个显示一次的背景图像(no-repeat
),当用户滚动应用窗口(fixed
)时保持不动,并位于屏幕底部最右侧 85 像素处。如果你正在寻找关于 CSS 的更深入的报道,除了数百本书之外,你可以用你最喜欢的网络搜索引擎找到大量的信息,比如大卫·鲍尔斯的开始 CSS 3(a press,2012)。现在运行应用将显示如图 9-3 所示的应用。我们还没到那一步,但已经开始成形了。
图 9-3 。我们的新应用背景在一个分辨率为 1366×768 的屏幕上
它看起来很好,但是 Clok 标志比图 9-1 中的大了很多。根据您的屏幕分辨率,它可能会比您想要的占用更多的屏幕空间。比如很多 Windows RT 平板的屏幕分辨率都是 1366×768;不过我的笔记本电脑屏幕是 1920×1080 的屏幕分辨率。图 9-4 显示了同一个应用在这个更大的屏幕上的样子。
图 9-4 。我们相同的新应用背景显示在分辨率为 1920×1080 的大屏幕上
我们真的不希望我们的时间输入表单覆盖我们的应用徽标。我们希望用户可以看到这个标志,尤其是在 Clok 应用的主屏幕上。我们可以将徽标变小,这样它就不会被平板电脑等较小屏幕上的表单隐藏,但在较大的屏幕分辨率下,徽标可能会显得太小。解决方案是使用 CSS 媒体查询来定义仅在满足特定条件时才应用的 CSS 规则。在default.css
的末尾添加来自清单 9-2 的代码。当应用窗口宽度小于 1400 像素时,这将导致背景徽标调整大小。在下一节中,我将介绍测试这种行为的技术。
清单 9-2。 在较小的屏幕上改变背景图像的大小
@media screen and (max-width: 1400px) {
#contenthost {
background-size: 40%;
}
}
实现 Clok 设计的下一步是将当前时间添加到屏幕底部。我们将使用我们在第八章中构建的时钟控件的修改版本。修改包括几个简单的助手函数,这些函数将在以后派上用场,同时还更改了名称空间。修改后的版本包含在本章的源代码管理中。清单 9-3 突出显示了对default.html
添加控件所做的修改。
清单 9-3。 添加当前时间
<!DOCTYPE html>
<html>
<head>
<title>Clok</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<!-- Clok references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
<script src="/js/navigator.js"></script>
<script src="/controls/js/clockControl.js"></script>
</head>
<body>
<div id="contenthost"
data-win-control="Application.PageControlNavigator"
data-win-options="{home: '/pages/home/home.html'}"></div>
<div id="currentTime" data-win-control="Clok.UI.Clock"></div>
</body>
</html>
控件的默认配置是显示当前时间,包括秒。你会从第八章中记起,你可以用data-win-options
将showClockSeconds
设置为false
。您也可以在 JavaScript 中设置相同的值,方法是添加清单 9-4 到default.js
中突出显示的代码。
清单 9-4。 在 JavaScript 中设置控件选项
args.setPromise(WinJS.UI.processAll().then(function () {
currentTime.winControl.showClockSeconds = false;
if (nav.location) {
nav.history.current.initialPlaceholder = true;
return nav.navigate(nav.location, nav.state);
} else {
return nav.navigate(Application.navigator.home);
}
}));
最后,将清单 9-5 中的 CSS 规则添加到default.css
中。此规则设置字体大小和粗细,将颜色设置为半透明白色,并将时间放在屏幕的左下角。
清单 9-5。 造型当前时间
#currentTime {
font-size: 60pt;
font-weight: 200;
letter-spacing: 0;
line-height: 1.15;
color: rgba(255, 255, 255, 0.2);
position: fixed;
top: calc(100% - 85px);
left: 10px;
}
实现我们的设计的最后一步是添加一些增量 CSS 规则来覆盖各种控件的颜色,以便它们与我们想要的设计相匹配。Microsoft 提供了大量示例应用来说明构建 Windows 应用商店应用的不同方面。您可以单独下载许多示例,但我建议从 MSDN 下载整个示例应用包:http://msdn.microsoft.com/en-US/windows/apps/br229516
。虽然这个包中的所有示例都有助于理解如何在您的代码中实现不同的功能,但主题 Roller 示例实际上是开发人员的一个有用软件。它允许你选择一个亮或暗的主题,并指定一些你想在应用中使用的不同颜色。然后它会生成并预览一些 CSS 规则来添加到你自己的应用代码中(见图 9-5 )。
`
图 9-5 。主题滚轮示例应用
注主题滚轮应用包含在示例 app 包中,也可以单独下载:
http://code.msdn.microsoft.com/windowsapps/Theme-roller-sample-64b679f2
。
本章附带的示例代码包括一个名为themeroller.css
的文件,该文件包含由 Theme Roller 示例应用生成的增量 CSS 规则。您可以将该文件复制到您的 Visual Studio 项目中,或者您可以使用主题 Roller 示例应用自己生成 CSS 规则。你必须将这个 CSS 文件的引用添加到default.html
(参见清单 9-6 )。
清单 9-6。 引用新的 CSS 文件
<!-- SNIPPED -->
<link href="/css/default.css" rel="stylesheet" />
<link href="/css/themeroller.css" rel="stylesheet" />
<!-- SNIPPED -->
因为 Clok 目前是相当空的,所以您实际上还看不到我们应用中的任何变化,但是生成的样式覆盖了ui-light.css
和ui-dark.css
中的一些默认样式,所以我们稍后添加的控件匹配我们已经定义的主题。例如,清单 9-7 包含了来自themeroller.js
的一个片段,它将改变你添加的任何下拉列表控件的颜色,比如在图 9-1 中用于选择一个项目的控件。
清单 9-7。 用增量 CSS 规则覆盖默认 CSS 规则
/*
Text selection color
*/
::selection, select:focus::-ms-value {
background-color: rgb(0, 0, 70);
color: rgb(255, 255, 255);
}
/*
Option control color
*/
option:checked {
background-color: rgb(0, 0, 70);
color: rgb(255, 255, 255);
}
option:checked:hover, select:focus option:checked:hover {
background-color: rgb(33, 33, 94);
color: rgb(255, 255, 255);
}
至此,我们已经完成了 Clok 的外壳。类似于我们在第五章到第八章中所做的工作,除了我们将在本章后面添加的SettingsFlyout
控件之外,这个应用中的大多数功能都将使用页面控件加载到这个 shell 中,在我看来,这使得导航应用模板非常方便。
使用模拟器进行调试
基于图 9-3 和图 9-4 之间的差异,你可以明白为什么用不同类型和尺寸的设备定期测试你的应用是一个好的实践。在尽可能多的真实设备(台式机、笔记本电脑和平板电脑)上测试您的应用是无可替代的。但是,有时您不容易访问这些设备,或者您可能正在积极开发功能,还没有准备好部署到多台机器上进行更严格的测试。如果你没有各种硬件来测试,微软视窗模拟器可以帮助你。模拟器是你在开发机器上运行的软件,允许你在不同的设备上模拟运行你的应用。
最重要的是,您已经有了模拟器,因为它是随 Visual Studio 一起安装的。你可能已经注意到了 Visual Studio 中调试按钮旁边的一个小菜单指示器(见图 9-6 )。此菜单允许您设置调试目标。默认情况下,选择本地机器,但是有两个附加选项:远程机器和模拟器。选择模拟器,然后运行您的应用。模拟器将打开,您的应用将启动(参见图 9-7 )。
图 9-6 。使用模拟器进行调试
图 9-7 。在模拟器中完成版本的 Clok
注意模拟器不是虚拟机,也不是和你的开发机器隔离的。模拟器只是运行与您的开发计算机上安装的相同的 Windows 安装,并且使用您用来启动 Visual Studio 的相同凭据运行。因此,我发现在我的开发机器的后台运行的一些程序在我启动模拟器时偶尔会表现异常,通常是因为该程序的两个实例——一个在我的开发机器上,一个在模拟器上——试图访问同一个锁定的资源。
模拟器提供了许多特性来促进基本测试。在模拟器工具栏的顶部,您会发现一个图钉图标,您可以切换该图标以将模拟器保持在所有其他窗口的顶部(参见图 9-8 )。在它的正下方是四个图标,允许您选择交互模式。您可以在鼠标模式、基本触摸模式、挤压/缩放触摸模式和旋转触摸模式之间进行选择。在鼠标模式下,您的应用在模拟器中的行为与在开发机器上的行为相同。
图 9-8 。选择交互模式(左);触摸模式下的鼠标光标(中间)和按下时的鼠标光标(右侧)
在基本触摸模式下,鼠标光标被替换为单目标图标,而在捏合/缩放或旋转触摸模式下,它是双目标图标。这些目标指示当你点击鼠标时,你的虚拟手指将触摸屏幕的位置。当您在其中一种触摸模式下单击鼠标时,光标会再次改变,以指示触摸交互正在进行中。在基本触摸模式下,您的鼠标的行为可能与您预期的一样:单击鼠标以点击,然后单击并按住鼠标以拖动或打开上下文菜单。
捏/缩放和旋转触摸模式一开始使用起来有点棘手,但只需几次尝试就能习惯。你滚动鼠标滚轮来移动目标。在挤压/缩放触摸模式下,这将使目标靠近或远离,而在旋转触摸模式下,一个目标将围绕另一个目标旋转。一旦你的目标被设置成你想要的样子,点击并按住你的鼠标按钮,同时用你的鼠标滚轮滚动来实际执行手势。我的主要开发机器是一台没有触摸屏的笔记本电脑。对于更精确的手势,我发现用两只手更容易。我使用笔记本电脑内置的触摸板按钮来点击,同时使用鼠标上的滚轮来挤压、缩放或旋转。不过,你的手指可能比我的更协调。
下一组图标,如图 9-9 所示,允许您测试对模拟设备的更改。前两个选项允许您顺时针或逆时针旋转设备 90 度,在横向、纵向、横向翻转和纵向翻转方向之间循环。图 9-9 中的第三个图标提供了改变模拟器屏幕分辨率的方法。我在创建图 9-3 和图 9-4 中的图像时使用了这个功能。这组按钮中的最后一个图标提供了更改模拟器位置的方法,这在测试使用地理定位来确定应用用户位置的功能时非常有用。
图 9-9 。更改设备设置
其余的工具栏图标如图 9-10 所示。相机图标将获取模拟器的截图,齿轮图标允许您指定截图将存储在开发机器的驱动器上的什么位置。最后,熟悉的问号图标将为您提供模拟器所提供的各种功能的更多帮助。
图 9-10 。更改设备设置
我鼓励你花点时间在模拟器上玩一玩。打开您已经安装的其他应用,观察这些应用在屏幕旋转或调整大小时的行为。熟悉不同的交互模式。虽然在开发 Clok 时,我们不太需要使用收缩/缩放和旋转交互模式,但在构建更复杂的应用时,它们肯定会派上用场。尽管模拟器很有帮助,但建议您在将应用发布到 Windows 应用商店之前,在尽可能多的真实设备上测试应用。
注意如果你在网络上有第二个测试设备,远程机器调试非常方便,我经常用它来测试我平板电脑上的应用。为调试配置远程目标既快速又简单。MSDN 上有一个很好的攻略:
http://msdn.microsoft.com/en-us/library/windows/apps/hh441469.aspx
。
添加设置弹出按钮
随着我们在接下来的几章中构建 Clok,我们将添加一些应用级别的设置,用户可以利用这些设置来定制应用的行为。在这一节,我们将添加一个SettingsFlyout
,我们将在本书的其余部分继续添加设置。我们还将在另一个SettingsFlyout
中添加一些关于 Clok 的信息,给潜在用户一个应用的概述和一个找到更多信息的方法。
我们已经在第六章的中看到了如何添加一个空的SettingsFlyout
到我们的应用中,我们将在本章中遵循同样的基本步骤。然而,我们将在 Visual Studio 中以稍微不同的方式组织我们的文件,以最小化混乱。让我们从在 Visual Studio 中添加一个settings
文件夹到我们项目的根目录开始(见图 9-11 )。
图 9-11 。在解决方案资源管理器中添加设置文件夹
注意我们用来构建应用的文件只是普通的 HTML、CSS 和 JavaScript 文件。我们可以按照我们认为合适的方式来组织我们的项目,只要所有的部分都与正确的路径相链接。例如,我们可以选择将每个
PageControl
的 CSS 文件放到项目的css
文件夹中。或者,如果我们计划添加大量的页面,我们可以将它们组织到子文件夹中。
锁定选项设置弹出按钮
将名为options.html
的 HTML 文件添加到settings
文件夹中。用清单 9-8 中的代码替换该文件的默认内容。这将是我们的用户想要改变 Clok 默认设置时打开的SettingsFlyout
。我们将在整本书中添加更多内容。
清单 9-8。options.html 的 HTML 代码
<!DOCTYPE html>
<html>
<head>
<title>Options</title>
</head>
<body>
<div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout"
aria-label="Options"
data-win-options="{settingsCommandId:'options',width:'narrow'}">
<div class="win-ui-dark win-header" style="background-color: #000046;">
<button type="button" class="win-backbutton"
onclick="WinJS.UI.SettingsFlyout.show()"></button>
<div class="win-label clok-logo">Options</div>
</div>
<div class="win-content">
<div class="win-settings-section">
<h3>Settings Section Header</h3>
<p>Put your settings here.</p>
</div>
<div class="win-settings-section">
<h3>Settings Section Header</h3>
<p>Put your settings here.</p>
</div>
</div>
</div>
</body>
</html>
当您查看这段代码时,您会发现它与清单 6-18 中的非常相似。除了为标题指定窄的宽度和不同的颜色,最显著的区别是我们用 CSS 类win-settings-section
在div
元素中添加了一些占位符内容。这个类是由 WinJS 提供的,并且是一个简单的方法来应用一个样式到我们的SettingsFlyout
上,这个样式和其他的 Windows 应用是一致的。您可以在一个名为ui-dark.css
的文件中看到这个 CSS 规则,以及 WinJS 提供的任何其他 CSS 规则。为了在解决方案资源管理器中找到这个文件,展开References
文件夹,然后是Windows Library for JavaScript 1.0
文件夹,最后是css
文件夹(参见图 9-12 )。您不能编辑这些文件,但您可以通过添加类似于我们之前使用主题滚轮的增量样式来更改样式。
图 9-12 。WinJS 提供的 CSS 文件
关于锁定设置弹出按钮
您现在使用的大多数应用都有一个包含应用信息的屏幕。通常情况下,会打开一个弹出窗口,显示应用的描述和链接,以了解有关应用或构建它的公司的更多信息。这一概念延续到了 Windows Store 应用,弹出窗口被一个SettingsFlyout
所取代。要在 Clok 中添加这个特性,需要在settings
文件夹中添加一个名为about.html
的 HTML 文件。用清单 9-9 中的代码替换该文件的默认内容。
清单 9-9。about.html 的 HTML 代码
<!DOCTYPE html>
<html>
<head>
<title>About Clok</title>
</head>
<body>
<div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout"
aria-label="About Clok"
data-win-options="{settingsCommandId:'about',width:'narrow'}">
<div class="win-ui-dark win-header" style="background-color: #000046;">
<button type="button" class="win-backbutton"
onclick="WinJS.UI.SettingsFlyout.show()">
</button>
<div class="win-label clok-logo">About Clok</div>
</div>
<div class="win-content">
<div class="win-settings-section">
<h3>About Clok</h3>
<p>
Clok is a sample application being developed in conjunction with
<em>Beginning Windows Store Application Development: HTML and JavaScript
Edition</em>, an upcoming title about building Windows Store applications
with HTML, JavaScript and CSS using the WinJS and WinRT libraries. It is
written by <a href="http://www.tapmymind.com">Scott Isaacs</a> and
<a href="http://apress.com/">Apress Media LLC</a> will publish the
title in Summer 2013.
</p>
<p>
For more information, please visit:
<a href="http://clok.us/">http://clok.us/</a>.
</p>
</div>
</div>
</div>
</body>
</html>
正如您所看到的,在这种情况下使用SettingsFlyout
是一种合适的技术,尽管它实际上并不用于修改任何应用设置,正如其名称所暗示的那样。
将设置弹出按钮添加到设置窗格
最后一步是注册我们的两个SettingsFlyout
控件,以便 Windows 在设置面板上显示它们。打开default.js
并添加清单 9-10 中突出显示的代码。
清单 9-10。 注册我们的设置弹出按钮
// SNIPPED
if (app.sessionState.history) {
nav.history = app.sessionState.history;
}
// add our SettingsFlyout to the list when the Settings charm is shown
WinJS.Application.onsettings = function (e) {
e.detail.applicationcommands = {
"options": {
title: "Clok Options",
href: "/settings/options.html"
},
"about": {
title: "About Clok",
href: "/settings/about.html"
}
};
WinJS.UI.SettingsFlyout.populateSettings(e);
};
args.setPromise(WinJS.UI.processAll().then(function () {
currentTime.winControl.showClockSeconds = false;
if (nav.location) {
nav.history.current.initialPlaceholder = true;
return nav.navigate(nav.location, nav.state);
} else {
return nav.navigate(Application.navigator.home);
}
}));
// SNIPPED
运行应用并打开“设置”面板。您可以使用以下方法之一打开“设置”面板:
- 将鼠标移动到屏幕的右上角以显示 Windows charms,然后单击设置按钮
- 从触摸屏右侧向内滑动以显示 Windows charms,然后点击设置按钮
- 在键盘上按下 Windows 徽标键+I
一旦你这样做了,你会看到我们的两个SettingsFlyout
控件在图 9-13 中列出。
图 9-13 。“设置”面板中列出了我们的设置弹出控件
构建仪表板
到目前为止,在这一章中,我们已经将重点放在实现应用的通用设计元素上——这些元素将在 Clok 的每个屏幕上可见。在这一部分,我们将开始构建仪表板的用户界面,如图 9-14 所示。
图 9-14 。仪表盘上的 UI 元素
当然,您可以在您的应用中使用任何您希望的布局技术,因为我们只是使用 HTML 和 CSS 来布局我们的界面元素。我将借此机会讨论 CSS3 中两种不同的布局选项,以 Clok dashboard 为例。我将介绍 flexbox 布局和网格布局,这两者都是万维网联盟(W3C)的工作草案。换句话说,这些 CSS3 布局很可能会成为跨不同浏览器工作的标准。
注意用 HTML 和 JavaScript 构建的 Windows Store 应用使用了与 Internet Explorer (IE) 10 相同的渲染引擎。因此,IE 10 中支持的 CSS 和 JavaScript 在您的应用中也受支持。
Flexbox 布局
flexbox 或 flexible box 布局是一个新的 CSS 布局选项,通过将元素的display
属性设置为-ms-flexbox
来启用。它提供了一种简单的方法来指示该元素的子元素的大小是灵活的,通过指定它们如何增长或收缩来填充可用空间。flexbox 布局有很多选项,关于它可以写几十页。我不会深入探讨这个布局选项,我将只介绍两个用例作为例子。首先,我们将通过定义灵活的区域来创建仪表板的整体页面布局。然后我们将再次使用 flexbox 布局来定位四个菜单选项,以匹配图 9-14 。
使用 Flexbox 定义页面布局
图 9-14 中的仪表板有两个内容区域。在左边,我们有四个菜单选项,右边是一个时间输入表单。打开home.html
并用清单 9-11 中突出显示的代码替换 main section 元素的内容。
清单 9-11。 仪表盘上的两个内容区
<section aria-label="Main content" role="main">
<div id="mainMenu"></div>
<div id="timeEntry"></div>
</section>
在本节的后面,我们将向mainMenu
元素添加菜单选项,向timeEntry
元素添加大的计时器显示和表单字段。在我们添加所有这些控件之前,让我们先来看看这两个区域的布局。将清单 9-12 中突出显示的代码添加到home.css
中,将 CSS 规则添加到三个元素中:主要部分和两个新内容区域中的每一个。
清单 9-12。 在 CSS 中设置 Flexbox
.homepage section[role=main] {
margin-left: 120px;
width: calc(100% - 120px);
display: -ms-flexbox;
-ms-flex-direction: row;
-ms-flex-align: start;
-ms-flex-pack: start;
-ms-flex-wrap: nowrap;
}
.homepage #mainMenu {
width: 424px;
-ms-flex: 0 auto;
border: 2px solid yellow; /* temporary */
height: 500px; /* temporary */
}
.homepage #timeEntry {
margin-left: 20px;
margin-right: 20px;
-ms-flex: 1 auto;
border: 2px solid yellow; /* temporary */
height: 500px; /* temporary */
}
@media screen and (-ms-view-state: snapped) {
.homepage section[role=main] {
margin-left: 20px;
}
}
@media screen and (-ms-view-state: portrait) {
.homepage section[role=main] {
margin-left: 100px;
}
}
将display
属性设置为-ms-flexbox
表示主部分应该被视为 flexbox 容器。将-ms-flex-direction
属性设置为row
会导致该 flexbox 的子对象的水平布局。该属性的其他选项包括column
,用于垂直定向,以及row-reverse
和column-reverse
,用于以与定义时相反的顺序显示子项。
-ms-flex-align
属性用于指定子元素如何垂直于-ms-flex-direction
对齐。也就是说,当使用row
指定水平布局时,-ms-flex-align
属性指定如何垂直显示子元素,当使用column
时,它指定如何水平显示子元素。因为我们的例子有一个水平布局,将-ms-flex-align
设置为start
将会在容器顶部对齐我们的两个内容区域。该属性的其他选项包括end
、center
、stretch
和baseline
。
当-ms-flex-align
属性控制垂直于布局方向的显示时,-ms-flex-pack
属性控制平行于布局方向的布局。因为我们已经将这个属性设置为start
,这个 flexbox 的子对象将会向左对齐。该属性的其他选项包括end
、center
和justify
。
既然我们的 flexbox 容器已经定义好了,让我们看看我们为两个内容区域添加的规则。添加一个width
设置将会使菜单选项保持在左边,给timeEntry
元素添加边距将会防止定时器和表单与菜单选项冲突。-ms-flex
属性是 flexbox 的最后一块魔力。为mainMenu
元素指定0
将阻止它增长或收缩以适应可用空间,但为timeEntry
元素指定1
将导致该元素增长以填充剩余空间。如果我们有两个非零值的元素,剩余的空间将按比例分割成这个值,这意味着如果一个被设置为1
而另一个被设置为2
,第二个元素将伸缩两倍于第一个元素。
注意关于 flexbox 布局的更深入的讨论,包括本章使用的所有属性,可以在 MSDN:
http://msdn.microsoft.com/en-us/library/ie/hh673531.aspx
找到。
对于mainMenu
和timeEntry
元素,我临时添加了一个height
和一个border
,这样我们可以很容易地看到 flexbox 实际上是如何布局其子元素的。你可以在图 9-15 中看到结果。
图 9-15 。我们最初的 flexbox 布局
用 Flexbox 定位菜单选项
到目前为止,我们已经看到了一个使用 flexbox 布局在页面中创建区域的例子。它还可以用于在这些区域中的一个区域内布置内容。我们还将使用 flexbox 在图 9-14 的左侧布置菜单选项。首先,让我们添加 HTML。打开home.html
并用清单 9-13 中突出显示的代码替换主节元素的内容。
清单 9-13。Clok 仪表盘的内容
<section aria-label="Main content" role="main">
<div id="mainMenu">
<div id="toggleTimerMenuItem" class="mainMenuItem primaryMenuItem"></div>
<div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem"></div>
<div id="projectsMenuItem" class="mainMenuItem secondaryMenuItem"></div>
<div id="timesheetMenuItem" class="mainMenuItem secondaryMenuItem"></div>
</div>
<div id="timeEntry">
<div id="elapsedTime">
<h2 id="elapsedTimeClock"
data-win-control="Clok.UI.Clock"
data-win-options="{ mode: Clok.UI.ClockModes.CountUp }"></h2>
</div>
<div>
<label for="project">Project</label>
<select id="project">
<option value="">Choose a project</option>
<option value="1">Website Redesign (ABC Telecom)</option>
<option value="2">Windows Store App (ABC Telecom)</option>
</select>
</div>
<div>
<label for="timeNotes">Notes</label>
<textarea id="timeNotes"></textarea>
</div>
<div>
<button id="saveTimeButton">Save</button>
<button id="discardTimeButton">Discard</button>
</div>
</div>
</section>
我在mainMenu
区域添加了四个空的div
元素,作为菜单选项的占位符。我将在下一节添加实际的内容——图标和文本。我还在timeEntry
区域添加了计时器和时间输入表单字段。清单 9-14 包含了home.css
的代码。
清单 9-14。 新 CSS 规则布局菜单选项和时间录入表单字段
.homepage section[role=main] {
margin-left: 120px;
width: calc(100% - 120px);
display: -ms-flexbox;
-ms-flex-direction: row;
-ms-flex-align: start;
-ms-flex-pack: start;
-ms-flex-wrap: nowrap;
}
.homepage #mainMenu {
width: 424px;
-ms-flex: 0 auto;
display: -ms-flexbox;
-ms-flex-direction: row;
-ms-flex-align: start;
-ms-flex-wrap: wrap;
}
/* all menu buttons */
.homepage .mainMenuItem {
border: 2px solid transparent;
margin: 4px;
background: rgba(0,0,50,0.65);
}
/* just the big menu button */
.homepage .primaryMenuItem {
height: 408px;
width: 408px;
}
/* the smaller menu buttons */
.homepage .secondaryMenuItem {
height: 128px;
width: 128px;
}
.homepage #timeEntry {
margin-left: 20px;
margin-right: 20px;
-ms-flex: 1 auto;
}
.homepage #timeEntry label {
display: block;
font-size: 2em;
}
.homepage #elapsedTime {
padding-bottom: 30px;
}
.homepage #elapsedTimeClock {
font-size: 8em;
}
.homepage #project {
width: 400px;
}
.homepage #timeNotes {
width: 400px;
height: 75px;
}
@media screen and (-ms-view-state: snapped) {
.homepage section[role=main] {
margin-left: 20px;
}
}
@media screen and (-ms-view-state: portrait) {
.homepage section[role=main] {
margin-left: 100px;
}
}
我去掉了向两个主要内容区域添加黄色边框和高度的规则,但是我还添加了许多其他规则。我们对timeEntry div
及其包含的控件所做的更改非常简单,所以我不会在这里讨论它们。不过,你会注意到,我们通过将display
属性设置为-ms-flexbox
来声明mainMenu div
是另一个 flexbox 容器。与主部分一样,该容器也是通过将-ms-flex-direction
属性设置为row
以水平布局排列的,并且通过将-ms-flex-align
设置为start
其内容在顶部对齐。然而,这一次,我们已经通过将-ms-flex-wrap
属性设置为wrap
来指示不适合第一行的项目应该换行到下一行。运行应用,查看我们目前的进度(图 9-16 )。
图 9-16 。菜单选项的占位符,以及我们的时间输入表单
我们离图 9-1 中的目标越来越近了。我们将一个 flexbox 容器嵌套在另一个中。这是一种构建复杂布局的强大方法,正如您将在下一节中看到的,可以在这个 flexbox 容器中进一步嵌套网格布局。
网格布局
与 flexbox 类似,网格布局是另一个新的 CSS 布局选项,通过将元素的display
属性设置为-ms-grid
来启用。顾名思义,这种布局允许您指示该元素的子元素排列在一个网格中。
如果你是十年或更久以前的 web 开发人员,你可能熟悉使用 HTML table
元素来布局 web 页面。在使用 CSS 进行网页布局变得突出之前,这是一种常见的做法。虽然它允许对布局进行简单的控制,但由于几个原因,它不再受欢迎,特别是因为它严重地将表示逻辑与页面内容混合在一起。大约在从使用 HTML table
元素布局到使用 CSS 布局的转变开始的同时,我开始花更多的时间在 web 应用的后端,而不是在布局上。因此,我仍然对基于table
的布局情有独钟,尽管我知道有更好的选择。
幸运的是,网格布局现在已经可用。我和像我一样的其他人现在可以使用熟悉的基于表格的概念来实现我们想要的布局,但仍然保持我们的内容和表示逻辑之间的分离,因为网格现在是在 CSS 中指定的,而不是用table
元素指定的。我们将使用网格布局向每个菜单选项添加图标和文本。将清单 9-15 中突出显示的代码添加到home.html
中。另外,一定要添加这里引用的图像文件;它们包含在本书附带的源代码中。
清单 9-15。Clok 仪表盘的菜单选项
<div id="mainMenu">
<div id="toggleTimerMenuItem" class="mainMenuItem primaryMenuItem">
<img class="mainMenuItem-image" id="timerImage" src="/img/Clock-Stopped.png" />
<div class="mainMenuItem-overlay">
<h4 class="mainMenuItem-title" id="timerTitle">Start Clok</h4>
</div>
</div>
<div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem">
<img class="mainMenuItem-image" src="/img/Camera.png" />
<div class="mainMenuItem-overlay">
<h4 class="mainMenuItem-title">Camera</h4>
</div>
</div>
<div id="projectsMenuItem" class="mainMenuItem secondaryMenuItem">
<img class="mainMenuItem-image" src="/img/Projects.png" />
<div class="mainMenuItem-overlay">
<h4 class="mainMenuItem-title">Projects</h4>
</div>
</div>
<div id="timesheetMenuItem" class="mainMenuItem secondaryMenuItem">
<img class="mainMenuItem-image" src="/img/Timesheet.png" />
<div class="mainMenuItem-overlay">
<h4 class="mainMenuItem-title">Time Sheets</h4>
</div>
</div>
</div>
对于我们之前创建为空的div
元素的每个菜单选项,我们现在添加了一个图标和一个标签。四个菜单选项中的每一个所使用的代码模式都是相同的,除了第一个被分配了一个 CSS 类primaryMenuItem
,而其他三个使用了secondaryMenuItem
CSS 类。无论大小,所有四个都分配了mainMenuItem
CSS 类。将多个类分配给一个元素的能力允许我们指定适用于所有元素的 CSS 规则,以及仅适用于某些元素的单独规则。清单 9-16 中突出显示了home.css
所需的 CSS 变化。
清单 9-16。 CSS 实现菜单选项 的网格布局
/* SNIPPED */
.homepage #mainMenu {
-ms-flex: 0 auto;
width: 424px;
display: -ms-flexbox;
-ms-flex-align: center;
-ms-flex-direction: row;
-ms-flex-wrap: wrap;
}
/* all menu buttons */
.homepage .mainMenuItem {
border: 2px solid transparent;
margin: 4px;
background: rgba(0,0,50,0.65);
display: -ms-grid;
-ms-grid-columns: 1fr;
}
.homepage .mainMenuItem:hover {
cursor: pointer;
border: 2px solid #ffffff;
}
.homepage .mainMenuItem .mainMenuItem-image {
-ms-grid-row-span: 2;
}
.homepage .mainMenuItem .mainMenuItem-overlay {
-ms-grid-row: 2;
padding: 6px 15px;
background: rgba(0,0,35,0.65);
}
/* just the big menu button */
.homepage .primaryMenuItem {
height: 408px;
width: 408px;
-ms-grid-rows: 1fr 70px;
}
.homepage .primaryMenuItem .mainMenuItem-image {
height: 382px;
width: 382px;
margin: 10px;
}
.homepage .primaryMenuItem .mainMenuItem-overlay .mainMenuItem-title {
font-size: 2.5em;
}
/* the smaller menu buttons */
.homepage .secondaryMenuItem {
height: 128px;
width: 128px;
-ms-grid-rows: 1fr 32px;
}
.homepage .secondaryMenuItem .mainMenuItem-image {
height: 128px;
width: 128px;
padding: 0;
}
.homepage .secondaryMenuItem .mainMenuItem-overlay .mainMenuItem-title {
font-size: 1em;
}
.homepage #timeEntry {
margin-left: 20px;
margin-right: 20px;
-ms-flex: 1 auto;
}
/* SNIPPED */
从清单 9-16 的顶部开始,我们做的第一个改变是通过将display
属性设置为-ms-grid
来表明我们的任何mainMenuItem
元素都将是网格布局容器。我们在-ms-grid-columns
属性中定义了一个单列。-ms-grid-columns
属性和-ms-grid-rows
属性(我们稍后将介绍)都可以采用各种不同的值,包括以下内容:
- 一个或多个带单位的指定尺寸,如
3px
或1.5em
- 一个或多个百分比值
- 剩余空间的一个或多个部分,如
1fr
或1fr 2fr
- 这些值的任意组合,比如
150px 1fr 2fr 150px
。本例将定义四列(-ms-grid-columns
)或四行(-ms-grid-rows
),其中第一列和第四列各为 150 像素,剩余空间的三分之一将分配给第二列或第二行,三分之二分配给第三列或第三行。
对于-ms-grid-columns
属性和-ms-grid-rows
属性还有一些其他的选项,我不会在这里介绍,但是你可以在http://msdn.microsoft.com/en-us/library/windows/apps/hh466340.aspx
和http://msdn.microsoft.com/en-us/library/windows/apps/hh466350.aspx
阅读更多关于这些 CSS 属性的内容。
接下来,我们添加了 CSS 规则来将鼠标光标变为指针,并在用户的鼠标悬停在其中一个元素上时显示白色边框。因为我们用-ms-grid-row-span
属性指定图像将跨越两行,并使用-ms-grid-row
属性将覆盖图放在第二行,所以覆盖图将放在图像底部的顶部。当我们在这里将背景颜色设置为半透明的深蓝色时,当各种primaryMenuItem
和secondaryMenuItem
CSS 规则被定义后,覆盖的确切大小和位置将被确定。
注意注意这个例子中
-ms-grid-rows
和-ms-grid-row
的区别。前者应用于网格容器,以定义网格将有多少行。后者应用于网格中的一个项目,以指示该项目的放置行。同样的建议也适用于-ms-grid-columns
和-ms-grid-column
CSS 属性。
我之前提到过,虽然我们所有的菜单项都应用了mainMenuItem
CSS 类,但是它们也有primaryMenuItem
类或者secondaryMenuItem
类。到目前为止,我在本节中介绍的所有 CSS 规则都适用于我们所有的菜单选项,不管它们是大还是小。您会注意到,我们还没有为任何菜单选项指定任何大小。为了解决这个问题,我们为primaryMenuItem
类和secondaryMenuItem
类添加了一组相似的 CSS 规则。清单 9-17 包含一段 CSS 代码,取自清单 9-16 ,它定义了大菜单选项的各种尺寸。
清单 9-17。 CSS 覆盖为大菜单选项
/* just the big menu button */
.homepage .primaryMenuItem {
height: 408px;
width: 408px;
-ms-grid-rows: 1fr 70px;
}
.homepage .primaryMenuItem .mainMenuItem-image {
height: 382px;
width: 382px;
margin: 10px;
}
.homepage .primaryMenuItem .mainMenuItem-overlay .mainMenuItem-title {
font-size: 2.5em;
}
您可以看到,这是我们指定网格行大小的地方,表明第二行是70px
高,第一行填充剩余的空间。图像和标题文本的大小也被设置为适合大菜单选项的值。回头参考清单 9-16 ,你会看到小菜单选项非常相似的 CSS 规则。我不会在这里详细介绍它们,因为除了定义更小的维度之外,它们与清单 9-17 中的几乎相同。
如果您再次运行 Clok,您将会看到我们所有的菜单选项都已就位(参见图 9-17 )。当然,没有一个菜单选项可以使用,但是我们会在第十章中看到如何开始添加一些功能。
图 9-17 。我们的菜单选项,每个都用 CSS 网格布局定义
注意你可能认为向项目添加文档和照片并不完全符合时间跟踪功能。你说得对,但它是一个有用的功能,原因有很多,比如为费用报告记录收据。更重要的是,它给了我们一个在应用中处理文件的理由。我们将在第十六章和第二十二章中进一步探讨这个问题。
结论
我们在这一章中涉及了很多内容。我们为 Clok 创建了一些高级需求,Clok 是我们将在本书剩余部分构建的示例应用。我们创建了应用的整体外观,每当我们向 Clok 添加一个新的PageControl
时,它就会自动应用。我们添加了两个SettingsFlyout
控件,当我们添加用户可以修改的选项时,它们将在整本书中更新。最后,我们使用新的 CSS flexbox 和网格布局添加了应用主页的所有用户界面元素 Clok 仪表板。Clok 还不做任何事情,但它开始看起来像一个真正的应用。我们将在第十章中开始添加一些基本功能,包括一些有用的动画,以便在用户执行某些任务时为他们提供视觉反馈。`
十、过渡和动画
微妙的动画存在于整个 Windows 8。当切换到开始屏幕时,磁贴会放大以填充熟悉的网格,它们会对被点击或触摸做出反应。激活的搜索或设置窗格像抽屉一样从屏幕一侧滑出。同样,AppBar
控件从屏幕的顶部或底部滑入。我用了微妙这个词来描述这些动画。我的意思是,在日常使用中,你更有可能注意到动画是否已经被删除,而不是它们最初的存在。
我们当然可以构建一个没有动画的应用,但是许多好的应用利用动画为用户提供关于正在发生的变化或他或她刚刚发起的动作的直观反馈。好的动画很短,发生的很快。此外,它们经常模拟一些真实世界的运动,例如当按钮被按下时,它看起来会移开。在应用中添加动画时要记住的一点是,动画不应该分散用户对应用主要目的的注意力。
转场和动画:有什么不同
到目前为止,我在一般意义上使用单词 animation 来表示“屏幕上正在移动的东西”,但是这个术语在技术上是不正确的。实际上,Windows Store 应用可以定义两种不同类型的动作:CSS 过渡和 CSS 动画,或者简单地说,过渡和动画。这两者在很多方面都很相似。转场和动画都会在一段时间内在屏幕上产生运动,因为它们会修改应用中 HTML 元素的 CSS 属性,如大小、颜色、旋转、位置、透视和透明度。
不过,转场和动画在一些方面有所不同。最显著的区别是动画可以定义关键帧,这使您可以更好地控制动画元素的 CSS 属性如何随时间变化。例如,通过定义关键帧,单个动画可以将元素的颜色从白色更改为黄色,然后再更改为红色,最后重置回白色。在本章的后面你会看到一个类似的例子。
动画允许您在动画的不同点指定各种 CSS 属性的值。另一方面,转换不定义 CSS 属性的值,但是定义 CSS 属性如何在原始值和更改后的值之间转换。例如,我们可以使用一个过渡来指示任何时候我们改变一个元素的位置,它应该缓慢地进入和退出(开始缓慢移动,然后在减速到停止之前加速)或者以恒定的速度从开始位置移动到新的位置。
实际上,动画通常用于提供一些反馈,在完成时将 CSS 属性重置为原始状态。另一方面,过渡不会自动将 CSS 属性重置为其原始状态。因此,如果我们使用一个过渡将一个元素的颜色从白色变为黄色再变为红色,那么这个元素将一直保持红色,直到我们将它变回白色。
动画(和过渡)的方法
那么,我们如何使我们的应用的元素动画化呢?正如软件开发中常见的那样,有许多方法可以实现这一点。我将在本章中介绍四种不同的技术,如下所示:
- 纯粹在 CSS 中
- 在我们的 JavaScript 代码中使用内置于 WinJS 动画库中的动画
- 使用 JavaScript 以编程方式操作我们的 CSS
- 用 JavaScript 定义我们自己的过渡和动画,并在我们的 JavaScript 代码中执行它们
纯 CSS 动画
最后,我将在本章中介绍的所有动画和过渡都是 CSS 动画和 CSS 过渡。屏幕上的运动或变化是更改 CSS 属性并允许客户端以平滑的方式呈现从一个值到另一个值的更改的结果。虽然我将介绍一些从 JavaScript 代码中启动这些动画和过渡的技术,但是一些简单但有用的动画可以直接在我们的 CSS 中定义。
我们将通过在home.css
中定义关键帧,为我们在第九章的中用 Clok 创建的计时器添加一个动画。关键帧允许我们在一个特殊的@keyframes
CSS 规则中定义动画中许多中间步骤的 CSS 规则,它包含我们正在定义的每个步骤或帧的规则。将清单 10-1 中的代码从添加到home.css
的末尾。
清单 10-1。 在 CSS 中定义关键帧
@keyframes animateTimeIn {
from, to {
color: rgba(255, 255, 255, 1);
}
50% {
color: rgba(255, 255, 0, 0.5);
}
}
首先要注意的是,我们已经将我们的@keyframes
规则命名为animateTimeIn
,以便我们以后可以引用它。我们的初始状态在from
规则中定义,我们的最终状态在to
规则中定义。因为这两者是相同的,我们可以声明一次规则,用逗号分隔规则名。这正是我们在清单 10-1 中所做的,在那些情况下设置前景色为白色。我们已经创建了另一个规则,它将在动画进行到一半时将颜色更改为半透明的黄色。
注意类似于我们在
50%
定义动画中点的方式,你也可以用0%
定义初始状态,用100%
定义最终状态。这些值分别相当于from
和to
。
到目前为止,我们已经定义了当动画出现时什么值会改变,但是在我们的 CSS 中还没有提到我们的计时器。为了将动画附加到我们的计时器上,我们必须将清单 10-2 中突出显示的代码添加到home.css
中。
清单 10-2。 将我们的关键帧动画应用到计时器
.homepage section[role=main] #timeEntry #elapsedTime #elapsedTimeClock {
font-size: 8em;
animation: animateTimeIn 750ms ease-in-out 1s 2 normal;
}
通过指定我们在清单 10-1 中定义的名称,将animation
CSS 属性添加到该规则中允许我们确定我们想要将哪个动画应用到elapsedTimeClock
元素。我们的动画将在 750 毫秒的时间内渐入渐出。它将在 1 秒钟的延迟后启动,并将重复两次。这个快捷语法允许我们在一行中定义动画属性。我们也可以单独设置这些属性(见清单 10-3 )。
清单 10-3。 长格式相当于清单 10-2 中的
.homepage section[role=main] #timeEntry #elapsedTime #elapsedTimeClock {
font-size: 8em;
animation-name: animateTimeIn;
animation-duration: 750ms;
animation-timing-function: ease-in-out;
animation-delay: 1s;
animation-iteration-count: 2;
animation-direction: normal;
}
注意关于这些和其他 CSS 动画属性的更完整的描述可以在 MSDN 的
http://msdn.microsoft.com/en-us/library/hh673530.aspx
找到。
当您现在启动应用时,定时器控件将在白色和黄色之间交替两次(参见图 10-1 )。目前,这不是很有用,因为我们在每次应用启动时都显示动画。然而,想象一个场景,我们启动 Clok,计时器已经在运行,或者计时器没有运行,但是有一个值还没有保存。在那些独特的情况下,使用这个动画或类似的动画,可能是提醒用户应用当前处于“进行中”状态的一种微妙方式。
图 10-1 。我们的计时器处于初始状态(上图)和修改后的颜色(下图)。底部图像中的颜色与我们应用中的颜色不同。它已经变暗,以增加印刷书籍中的对比度
纯 CSS 过渡
除了在 CSS 中定义动画,您还可以定义过渡。值得注意的是,CSS 转场本身不会在屏幕上产生运动。例如,您不能使用transition
属性指定新的颜色。转换实际上做的是定义目标元素如何从它的当前样式改变到它的新样式。这意味着您必须为目标元素定义两套 CSS 规则:转换的开始和结束。
因为我们有三个尚未实现的菜单选项,我们将添加一个当用户将鼠标悬停在它们上面时改变它们的过渡。我们要做的第一件事是向我们还没有实现的菜单选项添加另一个 CSS 类。通过将notImplemented
添加到三个较小菜单选项的class
属性来修改home.html
(参见清单 10-4 中突出显示的代码)。
清单 10-4。 标记菜单选项为未实现
<div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem notImplemented">
<img class="mainMenuItem-image" src="/img/Camera.png" />
<div class="mainMenuItem-overlay">
<h4 class="mainMenuItem-title">Camera</h4>
</div>
</div>
<div id="projectsMenuItem" class="mainMenuItem secondaryMenuItem notImplemented">
<img class="mainMenuItem-image" src="/img/Projects.png" />
<div class="mainMenuItem-overlay">
<h4 class="mainMenuItem-title">Projects</h4>
</div>
</div>
<div id="timesheetMenuItem" class="mainMenuItem secondaryMenuItem notImplemented">
<img class="mainMenuItem-image" src="/img/Timesheet.png" />
<div class="mainMenuItem-overlay">
<h4 class="mainMenuItem-title">Time Sheets</h4>
</div>
</div>
既然我们已经指出了哪些菜单选项应该应用转换,我们必须定义转换完成后将生效的 CSS 规则。将清单 10-5 中的 CSS 代码添加到home.css
中。
清单 10-5。 为未实现的菜单选项添加 CSS
/* buttons that haven't been implemented yet */
.homepage .mainMenuItem.notImplemented:hover {
cursor: default;
border: 2px solid transparent;
background: rgba(50,50,50,0.65);
background-image: url('/img/Thumb-Down.png');
}
.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-image {
visibility: hidden;
}
.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-overlay {
background: rgba(35,35,35,0.65);
}
.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-overlay .mainMenuItem-title {
display: none;
}
.homepage .mainMenuItem.notImplemented:hover .mainMenuItem-overlay::after {
content: 'Coming Soon';
}
如果你现在运行 Clok,任何时候你将鼠标悬停在三个小菜单选项中的一个上,背景色就会变成灰色,图标就会变成“拇指朝下”图标。此外,描述性文本将变为“即将推出”一旦您移开鼠标,菜单选项将恢复正常状态。图 10-2 显示了时间表按钮的两种状态。
图 10-2 。未实现的菜单选项的正常状态(左)和同一菜单选项的悬停状态(右)
当您运行应用并将鼠标移到 Time Sheets 选项上时,您可能会注意到,虽然样式发生了变化,但这是一个突然的变化。这是因为,到目前为止,我们只定义了 CSS 的最终状态,但是我们还没有指出 CSS 应该如何从初始状态转换到最终状态。让我们给home.css
再加一行代码。清单 10-6 中突出显示的代码行将使背景颜色从默认的蓝色逐渐过渡到灰色。
清单 10-6。 设置初始 CSS 规则和悬停状态规则之间的短暂转换
.homepage .mainMenuItem.notImplemented:hover {
cursor: default;
border: 2px solid transparent;
background: rgba(50,50,50,0.65);
background-image: url('/img/Thumb-Down.png');
transition: background 500ms ease-in-out 0s;
}
我鼓励您在继续之前先尝试一下 CSS。例如,您可能还想给覆盖图的背景色添加一个过渡,因为目前,它仍然会立即从深蓝变为深灰。CSS transition
属性和其他相关属性的文档可以在http://msdn.microsoft.com/en-us/library/hh673535.aspx
找到。
WinJS 动画库
当你使用 Windows 8 时,你会发现有许多常见的动画。WinJS 通过一个动画库提供了其中的许多内容,我们可以在我们的应用中使用它们来提供与其他 Windows 应用商店应用以及 Windows 本身一致的视觉反馈。例如,有一些预定义的动画,用于向ListView
控件添加一个项目,用于淡入或淡出视图,以及用于让一个元素对被点击或触摸做出反应。当用户按下一个按钮时,我们将使用这个库来激活菜单选项。用来自清单 10-7 的代码替换home.js
中的页面定义。
清单 10-7。 添加指针动画
WinJS.UI.Pages.define("/pages/home/home.html", {
ready: function (element, options) {
this.initializeMenuPointerAnimations();
},
initializeMenuPointerAnimations: function () {
var buttons = WinJS.Utilities.query(".mainMenuItem");
buttons.listen("MSPointerDown", this.pointer_down, false);
buttons.listen("MSPointerUp", this.pointer_up, false);
buttons.listen("MSPointerOut", this.pointer_up, false);
},
pointer_down: function (e) {
WinJS.UI.Animation.pointerDown(this);
e.preventDefault();
},
pointer_up: function (e) {
WinJS.UI.Animation.pointerUp(this);
e.preventDefault();
},
});
在新的initializeMenuPointerAnimations
方法中,我们找到了所有的菜单选项按钮——那些带有mainMenuItem
CSS 类的 HTML 元素。对于我们找到的每个项目,我们监听MSPointerDown
和MSPointerUp
事件,它们代表鼠标或触摸交互。我们创建了两个名为pointer_down
和pointer_up
的函数,通过调用 WinJS 动画库中的适当方法WinJS.UI.Animation.pointerDown
或WinJS.UI.Animation.pointerUp
来激活被点击或触摸的项目,从而处理这些事件。如果你仔细观察图 10-3 ,你可以看到当按钮被按下时,它的尺寸会稍微缩小,给人一种被推开的感觉。在与应用交互时,效果会明显得多。
图 10-3 。推开我们的按钮
此外,我们正在处理MSPointerOut
事件,就像它是一个MSPointerUp
事件一样。如果我们忽略该事件,很容易使菜单选项停留在按下状态,例如,单击它并在释放鼠标按钮之前将鼠标滑离它。
我们在这里只介绍了动画库中的两个动画。还有其他几个可用的,你可以在http://msdn.microsoft.com/en-us/library/windows/apps/br229780.aspx
的 MSDN 上了解更多。
使用 JavaScript 操作 CSS
正如我们在前面几节中看到的,直接在 CSS 中配置动画和过渡很简单,使用 WinJS 动画库也一样简单。但是,有时候你需要更多一点的控制。例如,您可能希望让用户指定在上面的动画中使用的颜色。他们可能会选择橙色,而不是黄色。或者他们可以选择#E3A238
。或者,您可能希望在动画或过渡完成后运行一些代码,这正是我们在本节中要做的。
我们将向 Clok 添加一个 CSS 转换,当用户保存他们的时间条目时,就会触发这个转换。在这种情况下,动画可能是不必要的,但它有助于向用户提供反馈,尤其是那些通过触摸与 Clok 交互的用户。如果我们简单地保存数据并重置表单,用户可能不会确信他们的数据已经保存,因为这在视觉上看起来与他们放弃时间输入时一样。为了提供清晰的反馈,我们将使用 CSS 转换来缩小表单,并使其向时间表菜单选项移动,以表明他们的条目已经保存到他们的时间表中。图 10-4 显示了用户输入一些注释并按下保存按钮后的时间输入表单。让我们看看在他或她按下保存按钮后,我们需要添加什么来使有趣的事情发生。
图 10-4 。一个 Clok 用户正在保存她的时间条目
首先要做的事情:让计时器滴答作响
在我们添加代码来添加这个转换之前,我们有一些设置工作要做。用清单 10-8 中突出显示的代码更新home.js
中的页面定义。这段代码并不特定于我们将要添加的转换,所以我不会详细讨论它,但是它是配置基本表单行为所必需的。当你滚动这段代码时,你会看到熟悉的处理事件和改变控件的值和状态的概念,我在第八章的中讨论过,当时我们创建了自定义时钟控件。您将看到当 Clok 用户按下 Start Clok 菜单选项时启动计时器的代码,然后当他按下 Stop Clok 菜单选项时停止计时器。有一些逻辑可以防止用户在没有选择项目的情况下保存条目,以及防止用户保存或丢弃没有经过时间的计时器。目前,保存按钮和放弃按钮都只是重置表单。虽然这是丢弃按钮的正确动作,但我们还是给自己留了一个注释,记录用户按下保存按钮时的时间输入。我将在第十二章中介绍这一点。
清单 10-8。 准备我们的 JavaScript 来处理时间输入表单事件
WinJS.UI.Pages.define("/pages/home/home.html", {
ready: function (element, options) {
this.initializeMenuPointerAnimations();
toggleTimerMenuItem.onclick = this.toggleTimerMenuItem_click.bind(this);
project.onchange = this.project_change.bind(this);
saveTimeButton.onclick = this.saveTimeButton_click.bind(this);
discardTimeButton.onclick = this.discardTimeButton_click.bind(this);
this.setupTimerRelatedControls();
},
initializeMenuPointerAnimations: function () {
var buttons = WinJS.Utilities.query(".mainMenuItem");
buttons.listen("MSPointerDown", this.pointer_down, false);
buttons.listen("MSPointerUp", this.pointer_up, false);
buttons.listen("MSPointerOut", this.pointer_up, false);
},
pointer_down: function (e) {
WinJS.UI.Animation.pointerDown(e.srcElement);
e.preventDefault();
},
pointer_up: function (e) {
WinJS.UI.Animation.pointerUp(e.srcElement);
e.preventDefault();
},
timerIsRunning: false,
toggleTimerMenuItem_click: function (e) {
this.toggleTimer();
},
project_change: function (e) {
this.enableOrDisableButtons();
},
discardTimeButton_click: function (e) {
this.discard();
},
saveTimeButton_click: function (e) {
this.save();
},
save: function () {
// TODO: save the time entry
this.resetTimer()
},
discard: function () {
this.resetTimer()
},
toggleTimer: function () {
this.timerIsRunning = !this.timerIsRunning;
this.setupTimerRelatedControls();
},
resetTimer: function () {
this.timerIsRunning = false;
elapsedTimeClock.winControl.reset();
project.selectedIndex = 0;
timeNotes.value = "";
this.setupTimerRelatedControls();
},
setupTimerRelatedControls: function () {
if (this.timerIsRunning) {
elapsedTimeClock.winControl.start();
timerImage.src = "/img/Clock-Running.png";
timerTitle.innerText = "Stop Clok";
} else {
elapsedTimeClock.winControl.stop();
timerImage.src = "/img/Clock-Stopped.png";
timerTitle.innerText = "Start Clok";
}
this.enableOrDisableButtons();
},
enableOrDisableButtons: function () {
if ((project.value !== "")
&& (!this.timerIsRunning)
&& (elapsedTimeClock.winControl.counterValue > 0)) {
saveTimeButton.disabled = false;
} else {
saveTimeButton.disabled = true;
}
discardTimeButton.disabled = (this.timerIsRunning)
|| (elapsedTimeClock.winControl.counterValue <= 0);
},
});
用 JavaScript 添加 CSS 过渡
如果你现在运行 Clok,这个应用可能会像你预期的那样运行。您可以启动和停止计时器,并保存有效的时间条目。现在让我们看看清单 10-9 中的来找到我们需要添加到我们的save
方法中来触发转换的代码。
清单 10-9。 我们更新了保存方法
save: function () {
// TODO: save the time entry
timeEntry.style.transition = 'color 5ms ease 0s, '
+ 'transform 500ms ease 0s, opacity 500ms ease 0s';
timeEntry.style.transform = 'scale3d(0,0,0)';
timeEntry.style.opacity = '0';
timeEntry.style.color = '#00ff00';
timeEntry.style.transformOrigin = "-130px 480px";
var self = this;
var transitionend = function (e1) {
if (e1.propertyName === "transform") {
timeEntry.removeEventListener('transitionend', transitionend);
self.resetTimer();
}
};
timeEntry.addEventListener('transitionend', transitionend, false);
},
我们在这里添加的第一行代码是为了定义我们的转换。这一行将动画显示对color
、transform
或opacity
CSS 属性的任何更改,在指定的时间内逐渐改变每个属性、5ms
的属性、500ms
的属性、transform
和opacity
的属性。接下来,我们为这些属性中的每一个指定新的值,指示我们的时间输入表单应该收缩和褪色,同时将文本颜色更改为绿色,以表示成功。transformOrigin
属性允许我们指出转换发生的点。在这种情况下,我们已经指出过渡的中心在时间输入表单左上角的左侧 130 像素和下方 480 像素处。这些数字是根据我们之前为菜单选项定义的大小选择的,并将在时间表按钮的顶部设置过渡的原点。
接下来,我们创建一个名为transitionend
的内嵌函数来处理同名事件。正如您可能猜到的,当转换完成时会引发此事件。我们有三个同时发生的转换,color
、transform
和opacity
转换在完成时都会引发这个事件,每个都在不同的时间发生。我们的处理函数忽略了color
和opacity
完成事件,但是当transform
转换完成时,我们的处理函数重置表单并停止监听后续的转换完成事件。因为opacity
转换的持续时间与transform
转换的持续时间相同,所以监听那个转换是否完成是等效的。
注意如果这个转换是我们要添加到 Clok 中的唯一一个转换,我们就不需要像这样担心删除事件监听器。然而,如果不这样做,任何后续的转换也会触发
transitionend
事件处理程序,这可能会导致意想不到的结果。
现在,当我们运行 Clok 并保存我们的时间条目时,我们可以清楚地看到我们的时间条目被保存到我们的时间表中(见图 10-5 )。
图 10-5 。成功完成的时间输入表保存到我们的时间表
这一转变相当顺利,Clok 开始成为一个有用的小应用。不过,我们现在有一个小问题。一旦条目被保存,表单就消失了。如果用户想为另一个项目记录时间,我们可以让他关闭 Clok 并重新启动它,但这将是一个非常糟糕的体验。幸运的是,就像我们将时间输入表单动画化一样容易,我们可以将表单重置为初始状态。清单 10-10 就是这么做的。我添加了一个新的resetTimerStyles
方法来将所有的样式重置回它们的初始值,并清除过渡。然后我从现有的resetTimer
方法中调用这个方法。
清单 10-10。 把东西放回原处
resetTimer: function () {
this.timerIsRunning = false;
elapsedTimeClock.winControl.reset();
project.selectedIndex = 0;
timeNotes.value = "";
this.resetTimerStyles();
this.setupTimerRelatedControls();
},
resetTimerStyles: function () {
timeEntry.style.transition = 'none';
timeEntry.style.transformOrigin = "50% 50%";
timeEntry.style.transform = 'scale3d(1,1,1)';
timeEntry.style.opacity = '1';
timeEntry.style.color = '#ffffff';
},
executeTransition 和 executeAnimation 方法
在上一节中,我们看到了如何通过修改想要制作动画的元素的各种 CSS 样式属性来创建过渡。这非常方便和简单。然而,还有一些事情需要记住。在易访问控制面板中,Windows 8 允许用户禁用不必要的动画(参见图 10-6 )。有些用户可能会关闭动画,因为启用动画时,他们使用的计算机会变慢。其他人这样做可能只是因为他们不想被动画分散注意力。不管是什么原因,如果动画(或过渡)对应用的功能并不重要,你应该尊重用户的选择,不要启动动画。
图 10-6 。禁用不必要的动画
那么我们如何检查这个值呢?Windows 运行时(WinRT)定义了一个我们可以使用的类。Windows.UI.ViewManagement.UISettings
类提供了一种简单的方法来访问一些常见的用户界面设置(参见清单 10-11 )。一旦我们有了这个类的实例,我们可以检查一个名为animationsEnabled
的属性,它直接对应于图 10-6 中的设置。
清单 10-11。 检查用户偏好的例子
var uiSettings = new Windows.UI.ViewManagement.UISettings();
if (uiSettings.animationsEnabled) {
// perform animation or transition
myElement.style.transition = "opacity 500ms ease 0s";
timeEntry.style.opacity = "0.5";
}
此外,WinJS 提供了一个函数WinJS.UI.isAnimationEnabled
,它检查该设置,并结合一些其他标准,确定是否应该出现动画。确定isAnimationEnabled
值的标准的描述可以在 MSDN: http://msdn.microsoft.com/en-us/library/windows/apps/hh779793.aspx
上找到。isAnimationEnabled
函数由动画库和ListView
控件在内部使用,它为您提供保持动画一致性所需的信息。虽然纯粹用 CSS 声明动画是不可能的,清单 10-12 给出了一个假设的例子,说明我们如何修改清单 10-9 中的代码,在开始转换之前检查这个函数。
清单 10-12。 假想改变我们的保存方法
save: function () {
if (WinJS.UI.isAnimationEnabled()) {
// SNIPPED
}
},
这种做法的缺点是,每当使用 JavaScript 定义自定义 CSS 过渡或 CSS 动画时,您都必须检查该函数。我提到过动画库在内部检查这个方法,所以我们之前创建的pointerDown
和pointerUp
动画在创建动画时会自动考虑控制面板设置。如果能够为我们声明自动检查isAnimationEnabled
功能的自定义动画和过渡,将会非常方便。
幸运的是,这是可能的。动画库在内部使用两个 WinJS 方法来执行过渡和动画。对isAnimationEnabled
函数的检查发生在这两个方法中,并且它们也被公开,供我们在自己的应用中使用。我们可以使用WinJS.UI.executeTransition
和WinJS.UI.executeAnimation
分别设置一个或多个过渡和动画,在页面中的特定元素上执行。
我们来看一个例子。在上一节中,我们添加了一个转换,以便在用户保存时间条目时向她提供反馈。在这一节中,我们还将为丢弃按钮添加一个过渡。将表格动画条目保存到时间表菜单选项中。当我想到丢弃一些东西,比如一个空水瓶,我会想象自己把它扔进回收站。在 Clok 中我们没有回收站的概念;然而,我们可以创建一个扔掉某物的类比。与保存动画一样,我们将让我们的丢弃动画缩小表单,但不是向菜单选项显示动画,我们只是让它在缩小到背景中时旋转,而不是变成绿色来指示成功,我们将让文本变成红色来指示我们正在删除该条目。用清单 10-13 中突出显示的代码更新home.js
中的discard
方法。
清单 10-13。 一种新的丢弃方法
discard: function () {
var self = this;
var slideTransition = WinJS.UI.executeTransition(
timeEntry,
[
{
property: "transform",
delay: 0,
duration: 500,
timing: "ease",
from: "rotate(0deg) scale3d(1,1,1)",
to: "rotate(720deg) scale3d(0,0,0)"
},
{
property: "opacity",
delay: 0,
duration: 500,
timing: "ease",
from: 1,
to: 0
},
{
property: "color",
delay: 0,
duration: 5,
timing: "ease",
from: '#ffffff',
to: '#ff0000'
}
]).done(function () { self.resetTimer(); });
},
这里我们使用executeTransition
方法在我们的timeEntry
元素上执行三种不同的转换。
- 我们通过将旋转角度从 0 度转换到 720 度来旋转表单两次。
- 我们正在通过改变它的不透明度来淡化表单。
- 我们将文本颜色从白色改为红色。
然后,一旦转换完成,计时器和表单被重置。结果是,Clok 现在给用户反馈,确认当她按下放弃按钮时,我们已经有意清除了表单(见图 10-7 )。
图 10-7 。丢弃动画
结论
在这一章中,你已经看到了向用户提供可视化、动画反馈的多种方式。虽然过多的动画会分散注意力,但是代表用户已经执行的动作的微妙的动画可以向我们的用户提供信心,我们的应用已经如预期的那样运行了。
我们选择的动画技术包括纯粹用 CSS 定义的简单动画,使用 WinJS 动画库中几个预定义的动画之一,使用 JavaScript 以编程方式修改与元素相关联的 CSS 样式,或者使用 WinJS 中的低级executeTransition
或executeAnimation
方法。虽然在我们的应用中,每一个都有它的位置,但是我们应该意识到用户可能因为这样或那样的原因不喜欢看到不必要的动画,我们应该让这个事实影响我们决定使用哪种动画方法。
十一、数据绑定概念
不言而喻,数据是几乎所有有用应用不可或缺的一部分。我们的示例应用 Clok 也不例外。本章将介绍在应用中显示数据的各种技术——一个叫做数据绑定的过程。第十四章、第十五章和第十六章将涵盖处理各种不同来源的数据,以及保存数据的不同方式。然而,为了使本章简单,我们将使用存储在内存中的数据。这有一个副作用,就是在我们关闭应用后,阻止我们的更改持续,但是我会在第十四章中解决这个问题。
我们将把我们的数据模型构建成 JavaScript 中的几个类。这些类将定义我们的数据结构,以及如何使用它。让我们开始构建数据模型,我们将在整本书中继续扩展。
我们的数据模型
在这一章中,我们将构建一个类来建模项目和客户数据,以及一个单独的存储类来处理我们的内存数据库。这个存储类将允许我们创建、读取、更新和删除数据——这些概念统称为 CRUD 操作。尽管我们创建这些类是为了管理内存中的数据,但在升级 Clok 以利用其他数据源时,我们将继续使用它们。
随着我们继续构建 Clok,我们将向 Visual Studio 项目添加许多文件。因此,作为一种更有条理的方法,让我们为数据模型代码创建一个新文件夹。如果尚未打开,请在 Visual Studio 中打开 Clok 项目。然后在解决方案浏览器中添加一个名为data
的文件夹(参见图 11-1 )。我们将把这个文件夹用于我们在本章中创建的类,以及我们添加的任何其他文件,以便在整本书中执行不同的数据访问功能。
图 11-1 。将数据文件夹添加到我们的项目中
注意一个名为
data
的文件夹没有什么特别的,你没有理由不把我们将要处理的文件放在其他地方,只要你在本章后面的主题中正确引用它们。因此,如果您或您所在的团队有其他约定,您将能够在构建 Windows 应用商店应用时遵循这些约定。这些文件可以放在 Visual Studio 项目中的任何地方,但是对于本书的其余部分,我们将它们放在这个新的data
文件夹中。
项目类别
我们的计时应用将有两种主要类型的对象:项目和时间条目。我不会在这一章中讨论时间条目,但是我们将定义一个类来表示项目。我们会让它非常简单地开始。在data
文件夹中添加一个新的 JavaScript 文件,命名为project.js
(参见图 11-2 )。
图 11-2 。为我们的项目数据模型添加一个类
在我建立或使用的其他项目管理和时间记录系统中,项目是一个非常复杂的概念。一个项目属于一个客户,这个客户有一个或多个联系人和一个或多个地址。一个项目由多个任务组成,每个任务可以分配给一个或多个开发人员。虽然这是一个非常健壮的项目管理应用,但它造成了一定程度的复杂性和重复的概念,不适合本书。因此,我们将把所有的功能整合到我们单一的简单的Project
类中,我们通过添加来自清单 11-1 的代码在project.js
中定义了这个类。
清单 11-1。 定义我们的项目类
(function () {
"use strict";
var projectClass = WinJS.Class.define(
function constructor() {
// define and initialize properties
this.id = (new Date()).getTime();
this.name = "";
this.projectNumber = "";
this.status = "active";
this.description = "";
this.startDate = new Date();
this.dueDate = new Date();
this.clientName = "";
this.contactName = "";
this.address1 = "";
this.address2 = "";
this.city = "";
this.region = "";
this.postalCode = "";
this.email = "";
this.phone = "";
},
{
// instance members
},
{
// static members
}
);
WinJS.Namespace.define("Clok.Data", {
Project: projectClass,
});
})();
如前几章所述,我们在自执行函数中使用WinJS.Class.define
方法定义了一个类,然后使用WinJS.Namespace.define
方法将其公开为Clok.Data.Project,
用于我们的应用。该类本身非常简单,定义了许多属性及其初始值,大部分是空字符串。我想特别解释一下这段代码。
传统的关系数据库引擎,如 Microsoft SQL Server 或 Oracle,允许开发人员创建标识字段作为对象的唯一标识符。这些通常表示为一个整数,对于数据库中的每个新记录自动递增,或者表示为一个全局唯一标识符(GUID) ,例如C9EFEADF-A6BB-455A-8A9A-CD0BC5A588CB
。我将在第十四章中介绍的 IndexedDB 也支持自动递增的标识字段。但是,因为 Clok 数据最终可能需要在多台计算机之间同步,所以最好使用 GUID 来防止标识冲突。虽然 JavaScript 可以创建看起来像 GUID 的随机字符串,但它并不是真正的全局唯一的,并且可能会出现重复值。在我们的内存示例中缺少真实身份功能的情况下,我使用了属于 JavaScript 的Date
类的getTime
方法。此方法返回自 1970 年 1 月 1 日以来经过的毫秒数。虽然它不是 identity 字段的完美替代品,但只要我们不在同一毫秒内创建多个项目,它就足够好了。我将在第十八章中再次讨论这个问题,在那里你将看到如何使用一个外部库来为我们生成一个 GUID。
存储类
您可能已经注意到我们的Project
类不包含任何 CRUD 操作的方法。它只是一个简单的类,定义了一些属性。因为我们需要某种方式来存储和检索这些数据,所以我们将创建一个Storage
类来为我们处理这些操作。将清单 11-2 中的代码添加到本章前面创建的data
文件夹中一个名为storage.js
的新 JavaScript 文件中。
清单 11-2。 定义我们的存储类
(function () {
"use strict";
var storage = WinJS.Class.define(
function constructor() {
},
{
// instance members
},
{
// static members
projects: new WinJS.Binding.List([]),
}
);
WinJS.Namespace.define("Clok.Data", {
Storage: storage,
});
})();
在这一点上,我们的新Clok.Data.Storage
类是相当轻的。除了用 WinJS 创建类的样板代码之外,我们只定义了一个静态属性,名为projects,
作为一个空的WinJS.Binding.List
,它将存储用户将创建的所有Project
对象。当你第一次看到ListView
控件时,你已经在第七章的中看到过这个List
类。虽然我们可以使用常规的 JavaScript 数组来存储这些数据,但是List
提供了许多额外的好处。我将在本章中介绍的一些好处包括对分组的支持(我也在第七章中介绍过)和在List
中创建数据的实时过滤和排序视图的能力。它还实现了必要的功能来支持 Windows 应用商店应用中的绑定。当我们构建 Clok 时,无论是在本章还是在后续章节,我们都会回到这个类来添加更多的功能。
引用我们的数据类
所以,我们有一个Project
类和一个Storage
类。现在我们必须让这些类对 Clok 的其他部分可用。因为我们创建的每个屏幕都必须访问这些类,所以最快最简单的方法就是添加对default.html
的脚本引用。记住default.html
是我们的应用启动时加载的第一个页面,我们创建的所有其他页面都在该页面中加载。如果您熟悉 ASP.NET 开发中母版页的概念,您可以将default.html
视为我们应用的母版页。重要的是,因为我们使用 Clok 的导航应用模板,default.html
总是被加载,所以我们引用的任何脚本中的代码总是可用的。将清单 11-3 中突出显示的代码添加到default.html
的head
部分。
清单 11-3。 引用我们 default.html 的新班级
<head>
<meta charset="utf-8" />
<title>Clok</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<script src="/js/extensions.js"></script>
<script src="/js/debug.js"></script>
<!-- Clok references -->
<link href="/css/default.css" rel="stylesheet" />
<link href="/css/themeroller.css" rel="stylesheet" />
<script src="/js/default.js"></script>
<script src="/js/navigator.js"></script>
<script src="/controls/js/clockControl.js"></script>
<script src="/data/project.js"></script>
<script src="/data/storage.js"></script>
</head>
注意如果您决定使用一个文件夹而不是
data
来存储您的数据模型脚本,一定要相应地修改清单 11-3 。
临时数据
为了使本章中关于将数据绑定到用户界面的内容更简单,我们实际上不会在任何类型的持久存储中读写数据。我会在第十四章和第十五章的中谈到这一点。然而,我们确实需要一些数据来帮助我们理解数据绑定是如何工作的。为了实现这一点,我们将通过在storage.js
的最后添加来自清单 11-4 的代码来硬编码一些项目。
清单 11-4。 临时添加硬编码数据
// add temp data
(function () {
var createProject = function (name, projectNumber, clientName, id, status) {
var newProject = new Clok.Data.Project();
newProject.id = id;
newProject.name = name;
newProject.projectNumber = projectNumber;
newProject.clientName = clientName;
newProject.status = status || newProject.status;
return newProject;
}
var projects = Clok.Data.Storage.projects;
var name1 = "Windows Store App";
var name2 = "Mobile Website";
var name3 = "Website Redesign";
var name4 = "Employee Portal";
var client1 = "Northwind Traders";
var client2 = "Contoso Ltd.";
var client3 = "AdventureWorks Cycles";
var client4 = "TailSpin Toys";
var client5 = "A. Datum Corporation";
var client6 = "Woodgrove Bank";
var client7 = "Fabrikam, Inc.";
projects.push(createProject(name1, "2012-0003", client1, 1368296808745, "inactive"));
projects.push(createProject(name2, "2012-0008", client2, 1368296808746, "inactive"));
projects.push(createProject(name3, "2012-0011", client1, 1368296808747, "inactive"));
projects.push(createProject(name1, "2012-0017", client3, 1368296808748));
projects.push(createProject(name3, "2012-0018", client4, 1368296808749, "deleted"));
projects.push(createProject(name1, "2012-0023", client5, 1368296808750, "deleted"));
projects.push(createProject(name3, "2012-0027", client6, 1368296808751, "inactive"));
projects.push(createProject(name3, "2012-0030", client7, 1368296808752, "inactive"));
projects.push(createProject(name3, "2012-0033", client3, 1368296808753));
projects.push(createProject(name2, "2012-0039", client1, 1368296808754, "inactive"));
projects.push(createProject(name4, "2012-0042", client3, 1368296808755, "inactive"));
projects.push(createProject(name3, "2012-0050", client5, 1368296808756, "inactive"));
projects.push(createProject(name1, "2012-0053", client4, 1368296808757, "inactive"));
projects.push(createProject(name2, "2013-0012", client5, 1368296808758));
projects.push(createProject(name2, "2013-0013", client7, 1368296808759));
projects.push(createProject(name4, "2013-0016", client1, 1368296808760, "deleted"));
projects.push(createProject(name4, "2013-0017", client6, 1368296808761));
projects.push(createProject(name3, "2013-0018", client2, 1368296808762));
})();
总之,createProject
方法是一种使用一行代码创建临时项目变得更容易的方法。所有的name
和client
变量只是为了确保一致性,让每一行更短,更容易阅读。push
方法将一个新的Project
添加到我们在Storage
类中创建的List
的末尾。
手动绑定下拉列表
现在我们已经定义了数据类和一些实际数据,让我们在屏幕上显示一些数据。我们将从做一些手工数据绑定开始。我的意思是,我们将显式设置控件的值,而不是使用 WinJS 中包含的绑定功能来为我们设置控件的值。虽然使用 WinJS 绑定功能通常比显式设置控件的值更容易,尤其是当您有多个控件时,有时手动设置也是合理的。一个特别的例子是将下拉列表中的项目绑定到您的数据。
虽然您将使用 HTML 和 JavaScript 来构建 Windows 应用商店应用的许多控件都支持 WinJS 绑定功能,但有一个特别的控件不支持下拉列表中的项目列表。为了将下拉列表中的选项绑定到我们的数据,我们将以更加手动的方式来完成。首先,让我们看看我们在做什么。在第九章 ( 清单 9-13 )中,我们用option
元素硬编码了home.html
中 Clok 仪表板上的project
下拉列表,以包含图 11-3 中所示的两个项目。
图 11-3 。home.html 的硬编码项目
对于某些下拉列表,将数据硬编码是合适的。例如,如果您的下拉列表是一个月的列表,或者是美国的州,或者是某本书的章节,或者是其他很少改变的内容,那么硬编码下拉列表选项是完全有效的,就像我们到目前为止所做的那样。然而,很明显,对于用户当前正在进行的项目列表,硬编码并不是一个好的选择。
在我们开始将数据绑定到project
下拉列表之前,我想描述一个我已经解释过几次的技术,以帮助减少一些不必要的代码重复。一般来说,我更喜欢描述性的名称空间和类名,因为它们使代码的目的更加清晰,无论是对其他开发人员还是对我来说。这就是为什么我在前面的例子中使用了名字Clok.Data.Storage
和Clok.Data.Project
。但是,我不想每次都键入全名,所以我借鉴了 Visual Studio 附带的项目模板中别名这些命名空间或类的做法。如果你打开default.js
,你会在靠近文件顶部的清单 11-5 中看到代码。
清单 11-5。 我们项目模板中一些常见的别名
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
虽然这一步不是必需的,但它确实使我们代码的其余部分更容易阅读和编写。当我们在default.js
工作时,我们现在可以输入nav.navigate
而不是输入WinJS.Navigation.navigate
。我们也可以将这个概念引入到我们的home.js
文件中,在靠近文件顶部的清单 11-6 中添加突出显示的代码。
清单 11-6。 在 home.js 中添加别名
(function () {
"use strict";
var nav = WinJS.Navigation;
var storage = Clok.Data.Storage;
WinJS.UI.Pages.define("/pages/home/home.html", {
// SNIPPED
现在,让我们在添加一些功能时利用新的storage
别名,在这个下拉列表中只显示那些将status
设置为active
的项目。将清单 11-7 中定义的bindListOfProjects
函数添加到home.js
中。
清单 11-7。 新增功能填充项目下拉列表
bindListOfProjects: function () {
project.options.length = 1;
var activeProjects = storage.projects.filter(
function (p) { return p.status === "active"; }
);
activeProjects.forEach(function (item) {
var option = document.createElement("option");
option.text = item.name + " (" + item.projectNumber + ")";
option.title = item.clientName;
option.value = item.id;
project.appendChild(option);
});
},
第一行删除了来自projects
的所有选项,只留下“选择一个项目”选项。我们使用WinJS.Binding.List
的filter
方法来获取一系列具有活动状态的项目。对于每个活动项目,我们创建一个新的option
,并将其添加到project
下拉列表中。现在,我们只需要调用这个方法。为此,将清单 11-8 中突出显示的代码添加到home.js
中的ready
函数中。
清单 11-8。 调用新功能
ready: function (element, options) {
this.initializeMenuPointerAnimations();
toggleTimerMenuItem.onclick = this.toggleTimerMenuItem_click.bind(this);
this.bindListOfProjects();
project.onchange = this.project_change.bind(this);
saveTimeButton.onclick = this.saveTimeButton_click.bind(this);
discardTimeButton.onclick = this.discardTimeButton_click.bind(this);
this.setupTimerRelatedControls();
},
现在,当您运行 Clok 时,您应该会在下拉列表中看到我们在清单 11-4 中添加的一些项目(参见图 11-4 )。尽管我们在本章前面将数据硬编码到了storage.js
中,但是这个下拉列表是动态加载的。当我们稍后将 Clok 更改为使用持久存储并删除硬编码项目时,该功能仍将继续工作。
图 11-4 。从我们的数据源添加的项目
将分组数据绑定到列表视图
为了使 Clok 成为一个有用的应用,用户需要一种方法来查看他或她的所有项目的列表。这是使用 WinJS 的数据绑定功能的绝佳机会。事实上,因为我们设计了我们的Project
类和Storage
类来使用WinJS.Binding.List
存储我们的项目,所以添加一个ListView
来显示它们将非常类似于第七章中的ListView
示例。
创建和连接页面控件
首先向pages
文件夹添加一个名为projects
的新文件夹,然后向projects
文件夹添加一个名为list.html
的新页面控件。当你完成后,你的解决方案浏览器应该看起来类似于图 11-5 。
图 11-5 。为项目列表添加页面控件
现在,我们必须修改 Clok 仪表板,以便项目菜单选项(图 11-6 )将导航到我们的新页面控件。
图 11-6 。项目菜单选项
打开home.js
,在enableOrDisableButtons
函数的定义后添加清单 11-9 中的projectsMenuItem_click
函数。
清单 11-9。 定义事件处理程序
projectsMenuItem_click: function (e) {
nav.navigate("/pages/projects/list.html");
},
接下来,修改ready
函数,添加清单 11-10 中突出显示的代码行。
清单 11-10。 将事件处理程序连接到菜单项目选项
ready: function (element, options) {
this.initializeMenuPointerAnimations();
toggleTimerMenuItem.onclick = this.toggleTimerMenuItem_click.bind(this);
this.bindListOfProjects();
project.onchange = this.project_change.bind(this);
editProjectButton.onclick = this.editProjectButton_click.bind(this);
saveTimeButton.onclick = this.saveTimeButton_click.bind(this);B
discardTimeButton.onclick = this.discardTimeButton_click.bind(this);
projectsMenuItem.onclick = this.projectsMenuItem_click.bind(this);
this.setupTimerRelatedControls();
},
最后要记住的是,我们应该从home.html
中的projectsMenuItem div
中移除notImplemented
CSS 类(在清单 11-11 中突出显示)。保留这个不会阻止导航工作,但是如果我们不能删除它,我们在第十章中添加的鼠标悬停动画将会保留,并可能给我们的用户造成混乱。
清单 11-11。 应该删除的 CSS 类
<div id="projectsMenuItem" class="mainMenuItem secondaryMenuItem notImplemented">
添加 ListView 并绑定数据
此时,我们可以导航到项目页面,但没有太多可看的。为了使这个页面有用,我们将添加一个ListView,
来显示数据中每个Clok.Data.Project
对象的一个项目。同时,我们还将按照项目的clientName
属性的第一个字母对项目进行分组,并添加一个SemanticZoom
控件,当用户有很多项目时,这将会派上用场。我们必须重新访问storage.js,
来添加对项目数据分组的支持。打开该文件并添加清单 11-12 中突出显示的代码。
清单 11-12。 添加对分组项目的支持
// SNIPPED
{
// static members
projects: new WinJS.Binding.List([]),
compareProjectGroups: function (left, right) {
return left.toUpperCase().charCodeAt(0) - right.toUpperCase().charCodeAt(0);
},
getProjectGroupKey: function (dataItem) {
return dataItem.clientName.toUpperCase().charAt(0);
},
getProjectGroupData: function (dataItem) {
return {
name: dataItem.clientName.toUpperCase().charAt(0)
}
},
groupedProjects: {
get: function () {
var grouped = storage.projects.createGrouped(
storage.getProjectGroupKey,
storage.getProjectGroupData,
storage.compareProjectGroups);
return grouped;
}
},
}
// SNIPPED
这段代码应该看起来很熟悉,因为我们在第七章中使用了非常相似的代码来为那一章中的动物数据添加分组支持。事实上,任何时候你想要将一个WinJS.Binding.List
中的数据分组到一个ListView
中,你都必须调用List
的createGrouped
方法。该函数返回您的List
中数据的实时视图,这意味着,通过添加或修改底层数据,分组视图也会发生变化。有关createGrouped
功能的更多信息,请访问 MSDN: http://msdn.microsoft.com/en-us/library/windows/apps/hh700742.aspx
。
同样熟悉的是用SemanticZoom
显示ListView
所需的 HTML 代码。用清单 11-13 中的代码替换list.html
中的body
元素。
清单 11-13。 添加 ListView 来显示项目
<body>
<div class="list fragment">
<header aria-label="Header content" role="banner">
<button class="win-backbutton" aria-label="Back" disabled type="button"></button>
<h1 class="titlearea win-type-ellipsis">
<span class="pagetitle">Projects</span>
</h1>
</header>
<section aria-label="Main content" role="main">
<div id="listViewHeaderTemplate"
data-win-control="WinJS.Binding.Template"
style="display: none">
<div class="listViewHeader">
<h1 data-win-bind="innerText: name"></h1>
</div>
</div>
<div id="listViewTemplate"
data-win-control="WinJS.Binding.Template"
style="display: none">
<div class="listViewItem">
<h4 data-win-bind="innerText: name"></h4>
<h6>
<span data-win-bind="innerText: projectNumber"></span>
(<span data-win-bind="innerText: status"></span>)
</h6>
<h6 data-win-bind="innerText: clientName"></h6>
</div>
</div>
<div id="semanticZoomTemplate"
data-win-control="WinJS.Binding.Template"
style="display: none">
<div class="semanticZoomItem">
<h2 class="semanticZoomItemText" data-win-bind="innerText: name"></h2>
</div>
</div>
<div id="semanticZoom" data-win-control="WinJS.UI.SemanticZoom">
<!-- zoomed in -->
<div id="listView"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemTemplate: select('#listViewTemplate'),
groupHeaderTemplate: select('#listViewHeaderTemplate'),
selectionMode: 'none',
tapBehavior: 'invoke',
swipeBehavior: 'none',
itemDataSource: Clok.Data.Storage.groupedProjects.dataSource,
groupDataSource: Clok.Data.Storage.groupedProjects.groups.dataSource
}">
</div>
<!-- zoomed out -->
<div id="zoomedOutListView"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemTemplate: select('#semanticZoomTemplate'),
selectionMode: 'none',
tapBehavior: 'invoke',
swipeBehavior: 'none',
itemDataSource: Clok.Data.Storage.groupedProjects.groups.dataSource
}">
</div>
</div>
</section>
</div>
</body>
最后,我们必须样式化ListView
,通过添加清单 11-14 中的代码到list.css
。
清单 11-14。 CSS 来样式 ListView
.listViewHeader {
width: 50px;
height: 50px;
padding: 8px;
}
.listViewItem {
width: 250px;
height: 75px;
padding: 5px;
overflow: hidden;
}
#listView {
height: 400px;
width: 100%;
}
#zoomedOutListView {
height: 400px;
width: 100%;
}
#semanticZoom {
height: calc(100% - 30px);
width: calc(100% - 120px);
}
.semanticZoomItem {
color: #ffffff;
background-color: #000046;
width: 50px;
height: 40px;
padding: 5px 15px;
}
如果你现在在 Clok 中查看这个页面,你会看到一个类似于图 11-7 的列表视图。挺好的但是不算伟大。最大的问题是,它会显示已被删除的项目,但我们可以解决这个问题。
图 11-7 。包含所有项目的 ListView
将筛选的数据绑定到 ListView
在 Clok 中,我们实际上不能删除项目,因为,一旦我们构建了功能,我们就可能拥有与项目相关联的时间条目。相反,我们给了项目一个已删除的状态,并通过代码中的逻辑不让它显示。此外,项目可以是活动的或不活动的,一旦我们的用户有几个项目,一个有用的特性将允许他或她按状态过滤项目。
显示过滤的数据
WinJS.Binding.List
类有一个createFiltered
方法,它返回满足 JavaScript 中定义的过滤条件的List,
中的数据子集。像createGrouped
方法一样,createFiltered
返回List,
的实时视图,这意味着添加或修改底层数据时,过滤后的视图也会改变。关于createFiltered
功能的更多信息可从 MSDN: http://msdn.microsoft.com/en-us/library/windows/apps/hh700741.aspx
获得。通过添加清单 11-15 中突出显示的代码来修改storage.js
,注意把它放在类定义之外。
清单 11-15。 添加分组和过滤方法
var storage = WinJS.Class.define(
// SNIPPED
);
storage.projects.getGroupedProjectsByStatus = function (statuses) {
var filtered = this
.createFiltered(function (p) {
return statuses.indexOf(p.status) >= 0;
});
var grouped = filtered
.createGrouped(
storage.getProjectGroupKey,
storage.getProjectGroupData,
storage.compareProjectGroups);
return grouped;
};
WinJS.Namespace.define("Clok.Data", {
Storage: storage,
});
感谢 JavaScript 的动态特性,我们能够定义一个名为getGroupedProjectsByStatus
的新函数,并将其添加到现有的projects
变量中。该函数接受用户想要查看的一系列项目状态。因为getGroupedProjectsByStatus
被添加到了projects
,所以this
JavaScript 关键字指的是项目的当前List
。我们使用createFiltered
方法返回与我们的过滤函数匹配的当前项目的子集——那些具有在statuses
参数中指定的状态的项目。一旦我们有了一个过滤列表,我们就使用上一节中使用的相同的createGrouped
方法对它们进行分组。
为了将我们的ListView
绑定到这个函数的结果,我们必须更改我们现有的代码,以便在我们的 JavaScript 代码中设置数据源。用清单 11-16 中的代码修改list.js
中的ready
函数来完成。
清单 11-16。 在 JavaScript 中设置 ListView 数据源
ready: function (element, options) {
this.filteredProjects =
storage.projects.getGroupedProjectsByStatus(["active", "inactive"]);
listView.winControl.itemDataSource = this.filteredProjects.dataSource;
listView.winControl.groupDataSource = this.filteredProjects.groups.dataSource;
zoomedOutListView.winControl.itemDataSource = this.filteredProjects.groups.dataSource;
},
注意记得在
list.js
中定义storage
别名,就像在清单 11-6 中的home.js
中一样。
因为我们在代码中设置了ListView
数据源,所以我们可以将它们从 HTML 标记中移除。从list.html
中删除清单 11-17 中高亮显示的代码。
清单 11-17。 从 list.html 删除数据源
<!-- zoomed in -->
<div id="listView"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemTemplate: select('#listViewTemplate'),
groupHeaderTemplate: select('#listViewHeaderTemplate'),
selectionMode: 'none',
tapBehavior: 'invoke',
swipeBehavior: 'none',
itemDataSource: Clok.Data.Storage.groupedProjects.dataSource,
groupDataSource: Clok.Data.Storage.groupedProjects.groups.dataSource
}">
</div>
<!-- zoomed out -->
<div id="zoomedOutListView"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemTemplate: select('#semanticZoomTemplate'),
selectionMode: 'none',
tapBehavior: 'invoke',
swipeBehavior: 'none',
itemDataSource: Clok.Data.Storage.groupedProjects.groups.dataSource
}">
</div>
注意不要忘记从
data-win-options
属性的最后一项中删除尾部逗号——在本例中,是在swipeBehavior
选项后的逗号。
绑定其他控件属性
此时,您的项目页面应该只显示活动的和非活动的项目,并且您不应该看到任何已删除的项目,但是我们可以通过对每个项目进行不同的样式化来改善用户体验。WinJS 数据绑定非常强大,可以帮助我们非常容易地完成这个任务。除了绑定屏幕上显示的数据之外,还可以将控件的其他属性绑定到数据源。例如,我们可以将ListView
模板的className
属性绑定到Project
类的status
属性。假设您已经为.active
和.inactive
创建了 CSS 规则来改变项目的背景颜色,您可以通过添加清单 11-18 中突出显示的代码来修改list.html
。
清单 11-18。 通过 className 属性绑定 CSS 类
<div class="listViewItem" data-win-bind="className: status">
然而,当我们现在运行 Clok 时,我们并没有完全看到我们所期望的。正如您在图 11-8 中看到的,我们丢失了之前在清单 11-14 中定义的样式,该样式设置了 CSS 属性,如width
和height
。这是因为通过绑定className
属性,我们用active
或inactive
类替换了listViewItem
类。
图 11-8 。这不是我们想要的
此时,我们有几个选择。最直接的选择是向我们的active
和inactive
CSS 类添加额外的 CSS 定义。在这种情况下,这并不坏,因为只有两个类,但是我更喜欢不必复制这些设置。此外,如果我们这样做了,那么我就不能举例说明下一个选项。
为了实现我们想要的格式,我们必须确保当我们将一个值绑定到className
属性时,listViewItem
类中的原始 CSS 设置不会被覆盖。我将在这里演示的方法利用了 HTML5 中支持的data-
属性。从第五章开始,我们就一直在使用这些属性,那时我们开始将data-win-control
添加到 HTML 元素中,让 WinJS 将它们视为控件而不是div
元素。此外,我们还使用了data-win-options
来用默认设置初始化我们的控件,使用data-win-bind
来配置绑定。您可能知道也可能不知道的是,您可以在 HTML5 中创建自己的data-
属性,我们将利用这一点来解决我们的ListView
样式问题。用清单 11-19 中高亮显示的代码再次修改list.html
。
清单 11-19。 通过 className 属性绑定 CSS 类
<div data-class="listViewItem" data-win-bind="className: status">
与清单 11-18 中的的唯一区别是将class
属性改为我们的自定义data-class
属性。现在,我们只需要相应地修改我们的 CSS,这是一个分为两部分的过程。首先,从list.css
中删除清单 11-20 中的代码。
清单 11-20。 从 list.css 中移除此
.listViewItem {
width: 250px;
height: 75px;
padding: 5px;
overflow: hidden;
}
将清单 11-21 中的代码添加到list.css
中,以代替删除的代码。
清单 11-21。 新的 CSS 规则为我们的 data-class 属性
[data-class=listViewItem] {
width: 250px;
height: 75px;
padding: 5px;
overflow: hidden;
}
[data-class=listViewItem].active {
background-color: #000046;
}
[data-class=listViewItem].inactive {
background-color: #464646;
color: #cccccc;
}
注意您可以使用
[attributeName=attributeValue]
CSS 选择器语法根据属性来设计元素的样式。
您将看到[data-class=listViewItem]
的新规则与我们删除的.listViewItem
规则相同。设置这个自定义属性并从我们的 CSS 中定位它意味着className
属性不再有定义的值。在这种情况下,这使我们不必担心删除我们希望保留的 CSS 规则。然后,我们的新规则根据数据绑定过程中className
属性是设置为active
还是inactive
来添加适当的样式。参见图 11-9 中的结果。
图 11-9 。基于状态过滤和设计项目
使用 WinJS。Binding.as
我们的项目页面现在可以正确隐藏已删除的项目,只显示活动和非活动项目。它还根据它们的状态对它们进行不同的样式化。然而,随着用户项目列表的增长,这个页面可能会变得不必要的长。为了补救这一点,让我们添加一些功能,允许用户指定他或她是否希望只查看活动项目,只查看非活动项目,或者同时查看活动项目和非活动项目。首先,我们需要一些按钮,用户可以点击这些按钮来指示他们想要如何过滤他们的项目。将清单 11-22 中突出显示的代码添加到list.html
中。
清单 11-22。 添加按钮过滤列表中的项目
<div id="semanticZoomTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="semanticZoomItem">
<h2 class="semanticZoomItemText" data-win-bind="innerText: name"></h2>
</div>
</div>
<div id="filters">
<button id="allProjectsButton" class="selected">All projects</button>
<button id="activeProjectsButton">Only active projects</button>
<button id="inactiveProjectsButton">Only inactive projects</button>
</div>
<div id="semanticZoom" data-win-control="WinJS.UI.SemanticZoom">
<!-- SNIPPED -->
</div>
我们可以保留这些过滤按钮的默认样式,但是更好的用户体验(至少在我看来)是去掉按钮的边框,将当前选中的过滤按钮的文本加粗。因此,将清单 11-23 中的代码添加到list.css
中。
清单 11-23。 CSS 来样式过滤按钮
#filters {
height: 30px;
}
#filters button {
border: 0px;
font-weight: normal;
}
#filters button.selected {
font-weight: bold;
}
现在我们已经有了 UI,我们需要在 JavaScript 中做一些工作和返工,将所有的部分连接在一起。用清单 11-24 中的代码替换list.js
中的整个页面定义。
清单 11-24。 我们的新页面定义
WinJS.UI.Pages.define("/pages/projects/list.html", {
ready: function (element, options) {
allProjectsButton.onclick = this.allStatusFilter_click.bind(this);
activeProjectsButton.onclick = this.activeStatusFilter_click.bind(this);
inactiveProjectsButton.onclick = this.inactiveStatusFilter_click.bind(this);
this.filter = WinJS.Binding.as({ value: ["active", "inactive"] });
this.filter.bind("value", this.filter_value_changed.bind(this));
},
allStatusFilter_click: function (e) {
this.filter.value = ["active", "inactive"];
this.setSelectedButton(allProjectsButton);
},
activeStatusFilter_click: function (e) {
this.filter.value = ["active"];
this.setSelectedButton(activeProjectsButton);
},
inactiveStatusFilter_click: function (e) {
this.filter.value = ["inactive"];
this.setSelectedButton(inactiveProjectsButton);
},
setSelectedButton: function (btnToSelect) {
WinJS.Utilities.query("#filters button").removeClass("selected");
WinJS.Utilities.addClass(btnToSelect, "selected");
},
filter_value_changed: function (e) {
this.filteredProjects = storage.projects.getGroupedProjectsByStatus(this.filter.value);
listView.winControl.itemDataSource = this.filteredProjects.dataSource;
listView.winControl.groupDataSource = this.filteredProjects.groups.dataSource;
zoomedOutListView.winControl.itemDataSource = this.filteredProjects.groups.dataSource;
},
});
首先要注意的是,我们添加了函数来处理每个新过滤器按钮的click
事件。它们非常简单明了:它们设置一个名为filter
的变量的值,我稍后会讨论这个变量,然后它们使用WinJS.Utilities
名称空间中的一些方法将被点击的按钮设置为当前选中的按钮。你还会看到一个名为filter_value_changed,
的新函数,它看起来非常类似于我们在清单 11-16 中添加的代码,除了它还引用了filter
变量。这里有趣的部分是filter
的定义。我已经在清单 11-25 中复制了这个有趣的部分。
清单 11-25。清单 11-24 最有趣的部分
this.filter = WinJS.Binding.as({ value: ["active", "inactive"] });
this.filter.bind("value", this.filter_value_changed.bind(this));
我们已经将filter
定义为一个新变量,用它来获取一个具有包含状态数组的value
属性的对象,并通过对WinJS.Binding.as
的调用来包装它。as
方法接受它收到的任何参数,并返回该参数的一个可观察版本。这是什么意思?简而言之,当某个东西是可观察的,那么任何正在观察它的东西,比如绑定到它的控件,都会在可观察对象改变时得到通知。在我们的例子中,我们将过滤器的value
属性绑定到filter_value_changed
,每当value
改变时,就会调用filter_value_changed
函数。
因此,当我们的按钮点击处理程序改变filter.value
时,就会调用filter_value_changed
并将ListView
数据源重新绑定到一个经过过滤的项目列表中。更多关于as
方法的信息可以在 MSDN: http://msdn.microsoft.com/en-us/library/windows/apps/br229801.aspx
上找到。
注意清单 11-25 的第二行包含对两个不同
bind
方法的调用。第一个是this.filter.bind
,为value
属性配置一个观察者作为filter_value_changed
函数的地址。第二个,this.filter_value_changed.bind
,标识了this
JavaScript 关键字将在该方法中引用什么,在本例中,是页面定义本身,这是 ready 函数中this
的值。
现在,当您运行 Clok 并导航到项目页面时,您最初会看到一个类似于图 11-10 的屏幕,显示所有活动和非活动的项目。这和我们在图 11-9 中看到的一样,只是增加了我们新的过滤按钮。
图 11-10 。“项目”页面,带有新的过滤器按钮
现在,点击第二个过滤器按钮,只查看活动项目。页面将更新以匹配图 11-11 ,选择第二个按钮并从ListView
中移除不活动的项目。
图 11-11 。仅显示活动项目
设置数据上下文
既然我们已经有了一个很好的工作项目列表,下一个合乎逻辑的步骤就是添加一个项目细节页面。该页面将允许用户查看和编辑现有项目,以及创建新项目。然而,HTML 表单没有数据源属性。因此,我们将看看如何通过设置数据上下文来有效地设置表单的数据源,或者任何任意的 HTML 元素。但是,首先,我们需要完成一些快速设置工作。
添加项目详细信息页面
将名为detail.html
的新页面控件添加到我们在本章前面创建的同一个projects
文件夹中。当你完成后,你的解决方案浏览器应该看起来类似于图 11-12 。
图 11-12 。项目文件夹中的两个页面控件
用户可以通过两种方式访问项目详细信息页面:编辑现有项目和添加新项目。我们必须将这两个选项的导航添加到项目列表页面中。为了与 Windows Store 应用指南保持一致,我们将添加一个带有AppBarCommand
按钮的AppBar
,以便向项目列表页面添加一个新项目。将清单 11-26 中高亮显示的代码添加到list.html
中的body
元素之后。
清单 11-26。 向项目列表页面添加 AppBar
<body>
<div id="projectListAppBar"
class="win-ui-dark"
data-win-control="WinJS.UI.AppBar">
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'addProjectCommand',
label:'Add',
icon:'add',
section:'global',
tooltip:'Add'}">
</button>
</div>
<!-- SNIPPED -->
</body>
注意记住应用栏可以用鼠标右键点击屏幕或者从触摸屏底边向上滑动打开。
为这个按钮添加一个 click-event 处理程序,当用户点击(调用)一个ListView
中的项目时添加另一个处理程序,现在应该很熟悉了。将清单 11-27 中突出显示的代码添加到list.js
中的ready
函数中。
清单 11-27。 布线起事件处理程序
ready: function (element, options) {
allProjectsButton.onclick = this.allStatusFilter_click.bind(this);
activeProjectsButton.onclick = this.activeStatusFilter_click.bind(this);
inactiveProjectsButton.onclick = this.inactiveStatusFilter_click.bind(this);
this.filter = WinJS.Binding.as({ value: ["active", "inactive"] });
this.filter.bind("value", this.filter_value_changed.bind(this));
addProjectCommand.onclick = this.addProjectCommand_click.bind(this);
listView.winControl.oniteminvoked = this.listView_itemInvoked.bind(this);
},
?? 注意当用户点击
ListView
中的一个项目时ListView
会引发itemInvoked
事件。这个事件非常类似于click
事件,但是也包括关于哪个项目被点击的信息。
定义这些事件处理程序也非常简单。将清单 11-28 中的代码添加到list.js
中的就绪函数之后。
清单 11-28。 定义导航到项目详情页面的事件处理程序
addProjectCommand_click: function (e) {
WinJS.Navigation.navigate("/pages/projects/detail.html");
},
listView_itemInvoked: function (e) {
var item = this.filteredProjects.getAt(e.detail.itemIndex);
WinJS.Navigation.navigate("/pages/projects/detail.html", { id: item.id });
},
当点击addProjectCommand
时,Clok 执行一个简单的导航到项目细节页面。当调用ListView
中的一个项目时,我们使用itemIndex
属性来确定选择了哪个项目,然后导航到项目详细信息页面,其初始状态包括所选Project
对象的id
。
向项目详细信息页面添加表单
现在,我们可以导航到项目详细信息页面,是时候向该页面添加一个表单了。我们将使用这个表单来查看、编辑和添加项目到 Clok。用清单 11-29 中的代码替换detail.html
中的body
元素。
清单 11-29。 项目明细表
<body>
<div id="projectDetailAppBar"
class="win-ui-dark"
data-win-control="WinJS.UI.AppBar"
data-win-options="{ sticky: true }">
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'saveProjectCommand',
label:'Save',
icon:'save',
section:'selection',
tooltip:'Save'}">
</button>
<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:'deleteProjectCommand',
label:'Delete',
icon:'delete',
section:'selection',
tooltip:'Delete',
disabled: true}">
</button>
</div>
<div class="detail fragment">
<header aria-label="Header content" role="banner">
<button class="win-backbutton" aria-label="Back" disabled type="button"></button>
<h1 class="titlearea win-type-ellipsis">
<span class="pagetitle">Project Detail</span>
</h1>
</header>
<section aria-label="Main content" role="main">
<form id="projectDetailForm" onsubmit="return false;">
<div class="formField" style="-ms-grid-row: 1; -ms-grid-column: 1;">
<label for="projectName">Project Name</label><br />
<input type="text" id="projectName"
maxlength="75"
required
autofocus
data-win-bind="value: name">
</div>
<div class="formField" style="-ms-grid-row: 1; -ms-grid-column: 3;">
<label for="projectNumber">Project Number</label><br />
<input type="text" id="projectNumber"
maxlength="25"
required
data-win-bind="value: projectNumber">
</div>
<div class="formField" style="-ms-grid-row: 1; -ms-grid-column: 5;">
<label for="projectStatus">Status</label><br />
<div id="projectStatus"
data-win-control="WinJS.UI.ToggleSwitch"
data-win-options="{
labelOn: 'Active',
labelOff: 'Inactive'
}"></div>
</div>
<div class="formField"
style="-ms-grid-row: 2; -ms-grid-column: 1; -ms-grid-column-span: 3;">
<label for="projectDescription">Description</label><br />
<textarea id="projectDescription"
data-win-bind="value: description"></textarea>
</div>
<div class="formField" style="-ms-grid-row: 3; -ms-grid-column: 1;">
<label for="startDate">Start Date</label><br />
<div id="startDate"
data-win-control="WinJS.UI.DatePicker"
data-win-bind="winControl.current: startDate"></div>
</div>
<div class="formField" style="-ms-grid-row: 3; -ms-grid-column: 3;">
<label for="dueDate">Due Date</label><br />
<div id="dueDate"
data-win-control="WinJS.UI.DatePicker"
data-win-bind="winControl.current: dueDate"></div>
</div>
<div class="formField" style="-ms-grid-row: 4; -ms-grid-column: 1;">
<label for="clientName">Client Name</label><br />
<input type="text" id="clientName"
maxlength="50"
required
data-win-bind="value: clientName">
</div>
<div class="formField" style="-ms-grid-row: 4; -ms-grid-column: 3;">
<label for="contactName">Contact Name</label><br />
<input type="text" id="contactName"
maxlength="50"
data-win-bind="value: contactName">
</div>
<div class="formField"
style="-ms-grid-row: 5; -ms-grid-column: 1; -ms-grid-column-span: 3;">
<label for="address1">Address</label><br />
<input type="text" id="address1"
maxlength="60"
placeholder="Line 1 (e.g., 1234 Maple St.)"
data-win-bind="value: address1"><br />
<input type="text" id="address2"
maxlength="60"
placeholder="Line 2 (e.g., Suite A)"
data-win-bind="value: address2"><br />
<input type="text" id="city"
maxlength="35"
placeholder="City"
data-win-bind="value: city">
<input type="text" id="region"
maxlength="25"
placeholder="State/Region"
data-win-bind="value: region">
<input type="text" id="postalCode"
maxlength="12"
placeholder="Postal Code"
data-win-bind="value: postalCode">
</div>
<div class="formField" style="-ms-grid-row: 6; -ms-grid-column: 1;">
<label for="contactEmail">Email</label><br />
<input type="email" id="contactEmail"
maxlength="75"
data-win-bind="value: email">
</div>
<div class="formField" style="-ms-grid-row: 6; -ms-grid-column: 3;">
<label for="phone">Phone</label><br />
<input type="tel" id="phone"
maxlength="25"
data-win-bind="value: phone">
</div>
</form>
</section>
</div>
</body>
注意在各种地址字段中使用
placeholder
属性为这些字段提供了很好的水印效果。占位符文本显示在空字段中,并在用户单击该字段时消失。这是向用户提供简单说明或示例输入的一种简单、紧凑的方式。
那有很多代码,但是正如你所看到的,很多都是不言自明的。我们创建一个带有两个AppBarCommand
控件的AppBar
,然后创建一个带有多个字段的表单。我们添加了一些 CSS 类,这样我们就可以在 CSS 中设置表单字段的样式,我们很快就会这样做。这很好,但这并不是什么新东西。在这一节中,我想特别说明两个主题,这两个主题都在清单 11-30 中进行了说明。
清单 11-30。 项目名称字段为必填项
<input type="text" id="projectName"
maxlength="75"
required
autofocus
data-win-bind="value: name" >
除了 Status 字段(我将在下一节中讨论)之外,页面上的每个字段都在其data-win-bind
属性中定义了一个绑定。语法和我们在清单 11-13 中看到的完全一样。此外,我们用required
属性标记了三个字段——项目名称、项目编号和客户名称——我们将在验证用户输入的数据时利用这个属性。
我稍后将回到这两个属性。然而,在我开始之前,让我们用一些 CSS 使这个表单看起来更好。将清单 11-31 中的代码添加到detail.css
中。
清单 11-31。 造型项目明细表
#projectDetailForm {
display: -ms-grid;
-ms-grid-columns: 2fr 20px 1fr 20px 1fr;
max-width: 900px;
}
#projectDetailForm .formField {
padding-bottom: 10px;
}
#projectDetailForm .formField input:invalid,
#projectDetailForm .formField textarea:invalid,
#projectDetailForm .formField select:invalid {
border: 5px solid red;
background-color: #EE9090;
}
#projectName, #clientName, #contactName, #contactEmail {
width: 430px;
}
#projectDescription {
height: 60px;
width: calc(90vw - 120px);
}
#projectNumber, #phone {
width: 200px;
}
#address1, #address2 {
width: 600px;
}
#city {
width: 265px;
}
#region {
width: 175px;
}
#postalCode {
width: 140px;
}
同样,这是几行代码,但是非常简单。我们使用网格布局在表单中创建五列,其中三列用于表单内容,另外两列用于增加列间距。剩余的大部分 CSS 用于设置不同字段的尺寸,通过其id
属性识别每个字段。
使用 CSS 样式化必填和无效字段
在清单 11-31 中,你可能不熟悉的一件事是:invalid
CSS 伪类的使用。我们使用:invalid
创建的 CSS 规则将用红色粗边框突出显示任何无效字段。因为我们按照要求标记了三个字段,HTML5 自动为我们验证这些字段,并在这些字段为空时添加:invalid
伪类。此外,因为我们将它的type
属性设置为email
,HTML5 会自动验证我们的电子邮件字段包含的任何文本是否是有效的电子邮件地址。参见图 11-13 中的示例。
图 11-13 。因为项目名称和项目编号是必需的,所以它们是无效的
这对我们的用户来说是一个很好的指示,他们输入或没有输入的东西是不允许的。对于用户体验来说不幸的是,当添加一个新项目时,只要项目详细信息页面一加载,字段就已经出现错误。虽然从技术上来说,项目必须有一个名字,但这对于用户来说是一种突兀的体验。因此,我们将做一个小的修改来达到两个目的:指示哪些字段是必需的,并且在用户与表单交互之前不指示它们是无效的。看看清单 11-32 中修改过的detail.html
文件的一个片段。
清单 11-32。 修改项目名称字段
<div class="formField required " style="-ms-grid-row: 1; -ms-grid-column: 1; ">
<label for="projectName">Project Name</label><br />
<input type="text" id="projectName"
maxlength="75"
autofocus
data-win-bind="value: name">
</div>
看到区别了吗?我们从input
控件中移除了required
属性,并向包含该控件的div
元素添加了一个名为required
的 CSS 类。对“项目编号”和“客户名称”字段进行同样的更改。然后通过添加清单 11-33 中突出显示的代码来修改detail.css
。
清单 11-33。 必填字段的新 CSS 规则
#projectDetailForm .formField {
padding-bottom: 10px;
}
#projectDetailForm .formField.required input,
#projectDetailForm .formField.required textarea,
#projectDetailForm .formField.required select {
border: solid green;
background-color: lightgreen;
}
#projectDetailForm .formField input:invalid,
#projectDetailForm .formField textarea:invalid,
#projectDetailForm .formField select:invalid {
border: 5px solid red;
background-color: #EE9090;
}
现在,当我们运行 Clok 并添加一个新项目时,必填字段会以较细的绿色边框和绿色背景突出显示(参见图 11-14 )。
图 11-14 。这些字段是必需的,但尚未被视为无效
然而,CSS 类并不特殊。当你使用这个表单时,你可能会注意到,无论你现在做什么,这些字段都不会出现无效,就像图 11-13 中的一样。这是因为应用名为required
的 CSS 类实际上并没有使字段成为必需的。应该发生的是,必填字段在有效时为绿色,在无效时为红色。用清单 11-34 中的代码替换detail.js
中的页面定义。
清单 11-34。 配置 AppBarCommand 控件
ready: function (element, options) {
this.configureAppBar(options && options.id);
saveProjectCommand.onclick = this.saveProjectCommand_click.bind(this);
},
saveProjectCommand_click: function (e) {
WinJS.Utilities
.query(".required input, .required textarea, .required select")
.setAttribute("required", "required");
},
configureAppBar: function (existingId) {
var fields = WinJS.Utilities.query("#projectDetailForm input, "
+ "#projectDetailForm textarea, "
+ "#projectDetailForm select");
fields.listen("focus", function (e) {
projectDetailAppBar.winControl.show();
}, false);
if (existingId) {
deleteProjectCommand.winControl.disabled = false;
}
},
这里采用的方法是,只有在用户第一次尝试保存表单之后,才将required
属性添加到我们的必填字段。在此之前,我们不会通过强调任何空的必填字段都是无效的来打扰他们。我们通过查询具有required
CSS 类的字段,并在用户单击保存按钮时向它们添加required
属性来实现这一点。此外,为了更好地衡量,我们添加了一些代码来显示当用户将焦点放在一个字段上时的AppBar
,并且只有当我们正在编辑一个现有的项目时,我们才启用删除按钮。
现在,当我们运行 Clok 并添加一个新项目时,必填字段的样式与预期一致。当字段有效时,或者在用户第一次尝试保存表单之前,字段是绿色的。第一次尝试保存表单后,任何无效字段都以红色突出显示,并给出一个较粗的边框(见图 11-15 )。
图 11-15 。一个有效字段和一个无效字段
设置表单的数据上下文
回到清单 11-28 ,我们指出如果我们想添加一个新项目,我们只需导航到detail.html
。然而,如果我们想要编辑一个现有的项目,我们将使用一个包含我们想要编辑的Project
对象的id
的对象来初始化页面。目前,我们没有一种简单的方法来为特定的id
检索一个Project
对象,所以让我们来解决这个问题。将清单 11-35 中高亮显示的代码添加到storage.js
。
清单 11-35。 通过 id 获取项目的新方法
storage.projects.getGroupedProjectsByStatus = function (statuses) {
// SNIPPED
};
storage.projects.getById = function (id) {
if (id) {
var matches = this.filter(function (p) { return p.id === id; });
if (matches && matches.length === 1) {
return matches[0];
}
}
return undefined;
};
WinJS.Namespace.define("Clok.Data", {
Storage: storage,
});
回到清单 11-29 ,当我们在detail.html
中定义表单时,我们为所有字段指定了一个data-win-bind
属性。定义了新的getById
函数后,我们现在可以对detail.js,
做一些小的修改,以完成现有项目的数据绑定。首先,正如我们在清单 11-6 中所做的,让我们给detail.js
添加一个Clok.Data.Storage
的别名(参见清单 11-36 )。
清单 11-36。 添加别名来简化我们的代码
var storage = Clok.Data.Storage;
WinJS.UI.Pages.define("/pages/projects/detail.html", {
// SNIPPED
});
接下来,将清单 11-37 中突出显示的代码添加到detail.js
中的ready
函数中。
清单 11-37。 用 processAll 设置数据绑定上下文
ready: function (element, options) {
this.currProject = storage.projects.getById(options && options.id)
|| new Clok.Data.Project();
this.configureAppBar(options && options.id);
saveProjectCommand.onclick = this.saveProjectCommand_click.bind(this);
var form = document.getElementById("projectDetailForm");
WinJS.Binding.processAll(form, this.currProject);
},
虽然与ListView
一起使用的模板继承了数据上下文,但是由于包含在数据绑定的ListView
中,我们的表单没有绑定到任何数据。我们添加的代码创建了一个名为currProject
的属性。如果被请求的Project
存在,currProject
被设置为该对象。否则,它被设置为一个新的空的Project
对象。然而,神奇之处在于对WinJS.Binding.processAll
的调用。这个方法将我们的currProject
属性配置为表单的数据上下文。因此,projectDetailForm
中任何设置了data-win-bind
属性的控件都被绑定到currProject
。
结果,例如,因为项目名称字段的data-win-bind
属性被设置为"value: name"
,输入字段的value
属性将被映射到currProject
的name
属性。现在,当我们运行 Clok 并点击一个已存在的项目时,表单被填充(见图 11-16 )。我们在本章前面的临时数据中设置的项目的任何属性都被填充到表单中。
图 11-16 。编辑现有项目
保存和删除项目
在我们解决 Status 字段没有绑定到任何东西的事实之前,让我们通过允许用户保存和删除项目来结束这一部分。第一步是通过添加清单 11-38 中突出显示的代码,在storage.js
中定义两个新函数。
清单 11-38。 功能保存和删除项目
storage.projects.getById = function (id) {
if (id) {
var matches = this.filter(function (p) { return p.id === id; });
if (matches && matches.length === 1) {
return matches[0];
}
}
return undefined;
};
storage.projects.save = function (p) {
if (p && p.id) {
var existing = storage.projects.getById(p.id);
if (!existing) {
storage.projects.push(p);
}
}
};
storage.projects.delete = function (p) {
if (p && p.id) {
var existing = storage.projects.getById(p.id);
if (existing) {
existing.status = "deleted";
storage.projects.save(existing);
}
}
};
WinJS.Namespace.define("Clok.Data", {
Storage: storage,
});
save
和delete
函数都将一个Project
对象作为参数。现在,因为我们所有的数据都存储在内存中,当保存一个Project,
时,我们只需要担心将新的Project
对象添加到我们的List
中。没有必要保存现有的Project
对象。当我们将 Clok 更改为使用不同类型的存储时,我们也将实现必要的代码来保存这些更改。删除一个Project
就像把它的status
改成deleted
然后保存一样简单。将清单 11-39 中突出显示的代码添加到detail.js
中,以完成该过程。
清单 11-39。 实现保存和删除功能
ready: function (element, options) {
this.currProject = storage.projects.getById(options && options.id)
|| new Clok.Data.Project();
this.configureAppBar(options && options.id);
saveProjectCommand.onclick = this.saveProjectCommand_click.bind(this);
deleteProjectCommand.onclick = this.deleteProjectCommand_click.bind(this);
var form = document.getElementById("projectDetailForm");
WinJS.Binding.processAll(form, this.currProject);
},
saveProjectCommand_click: function (e) {
WinJS.Utilities
.query(".required input, .required textarea, .required select")
.setAttribute("required", "required");
if (projectDetailForm.checkValidity()) {
this.populateProjectFromForm();
storage.projects.save(this.currProject);
WinJS.Navigation.back();
}
},
deleteProjectCommand_click: function (e) {
storage.projects.delete(this.currProject);
WinJS.Navigation.back();
},
populateProjectFromForm: function () {
this.currProject.name = document.getElementById("projectName").value;
this.currProject.projectNumber = document.getElementById("projectNumber").value;
this.currProject.status = (projectStatus.winControl.checked) ? "active" : "inactive";
this.currProject.description = document.getElementById("projectDescription").value;
this.currProject.startDate = startDate.winControl.current;
this.currProject.dueDate = dueDate.winControl.current;
this.currProject.clientName = document.getElementById("clientName").value;
this.currProject.contactName = document.getElementById("contactName").value;
this.currProject.address1 = document.getElementById("address1").value;
this.currProject.address2 = document.getElementById("address2").value;
this.currProject.city = document.getElementById("city").value;
this.currProject.region = document.getElementById("region").value;
this.currProject.postalCode = document.getElementById("postalCode").value;
this.currProject.email = document.getElementById("contactEmail").value;
this.currProject.phone = document.getElementById("phone").value;
},
因为 WinJS 绑定不是双向的——也就是说,对数据绑定表单字段的更改不会自动反映在底层数据上下文中——我们使用populateProjectFromForm
函数根据用户在每个表单字段中提供的内容来设置我们的currProject
值。保存和删除Project
对象的点击事件处理程序调用我们刚刚在清单 11-38 中定义的适当方法,然后将用户返回到他或她之前查看的页面。这里最值得注意的一项是对 projectDetailForm.checkValidity
的调用。该方法检查projectDetailForm,
中每个字段的状态,以确保它们都是有效的。在这种情况下,它检查以确保必填字段有值,并且电子邮件地址的格式正确。然后,如果每个字段都有效,它只保存Project
对象。
运行 Clok,对一些项目进行更改,并删除一些其他项目。除了 Status 字段(我们将在接下来讨论)之外,一切都应该按预期运行。对比图 11-17 和图 11-10 ,你会发现我改了一个项目的名字,删除了另一个。
图 11-17 。一个项目已被编辑,另一个已被删除
因为我们已经将所有的数据访问封装到了Clok.Data.Storage
中,所以当我们开始持久化我们的数据,而不是将它保存在内存中时,我们应该只需要对storage.js
进行更改,应用的其余部分应该或多或少保持不变。
绑定转换器
您肯定已经注意到 Status 字段还没有绑定到任何东西。当用户单击“保存”按钮时,我们已经保存了他们为此字段选择的任何值,但是当他们选择编辑现有项目时,我们没有显示正确的值。我们把这个留到最后的原因是为了说明绑定转换器。状态有三种可能的值(active
、inactive
和deleted
),但是我们用来显示该字段的ToggleSwitch
控件只有两种可能的状态。绑定转换器允许我们绑定到一些数据,但是在绑定发生之前对其应用一些逻辑。简而言之,它们将我们绑定的值转换成绑定控件需要的格式。在这种情况下,我们将把一个status
转换成一个布尔值,其中一个活动状态是true
,其他任何状态都是false
。
定义一个绑定转换器与定义一个函数没有太大的不同。事实上,这可以通过简单地将函数定义包装在对project.js
中的WinJS.Binding.converter
的调用中来实现(参见清单 11-40 )。
清单 11-40。 我们绑定转换器和新枚举
var statuses = Object.freeze({
Active: "active",
Inactive: "inactive",
Deleted: "deleted",
});
var projectStatusToBoolConverter = WinJS.Binding.converter(function (status) {
return (status === Clok.Data.ProjectStatuses.Active);
});
WinJS.Namespace.define("Clok.Data", {
Project: projectClass,
ProjectStatuses: statuses,
ProjectStatusToBoolConverter: projectStatusToBoolConverter,
});
在这段代码中,我们还创建了一个ProjectStatuses
枚举,来定义一个Project
对象可能拥有的状态。然后,我们将这个新的枚举和转换器添加到Clok.Data
名称空间中。
注意在本书附带的示例项目中,我已经将整个项目中所有硬编码的状态字符串改为使用
ProjectStatuses
枚举。因为枚举值与硬编码的字符串相同,所以不需要进行这种更改。然而,我更喜欢它的一致性,以及防止由输入错误引起的奇怪行为。
现在,我们可以将projectStatus ToggleSwitch
绑定到当前数据上下文的status
属性。将清单 11-41 中突出显示的代码添加到detail.html
中。
清单 11-41。 与转换器绑定
<div id="projectStatus"
data-win-control="WinJS.UI.ToggleSwitch"
data-win-options="{ labelOn: 'Active', labelOff: 'Inactive' }"
data-win-bind="winControl.checked: status Clok.Data.ProjectStatusToBoolConverter" ></div>
通过在数据绑定属性后指定一个绑定转换器,WinJS 将使用status
作为参数调用Clok.Data.ProjectStatusToBoolConverter
,并将结果绑定到ToggleSwitch
控件的checked
属性。
现在,当您运行 Clok 并从项目列表中选择不同的项目时,状态ToggleSwitch
将被正确填充。因为我们已经实现了保存项目变更的功能,所以您可以将项目从活动状态切换到非活动状态,反之亦然,并在项目列表中查看变更。
一个简单的 MapReduce 例子
在这一章中我已经介绍了很多,但是在结束之前,我还想在项目细节界面中添加一个特性。因为与其他项目管理系统相比,我们有一个简化的项目概念,所以在添加或编辑项目时,我们可以在客户端名称字段中输入任何值。虽然这对于我们定义新的客户端很重要,但是在输入客户端名称字段时给用户提供提示会更好。我们希望尽可能容易地防止拼写错误或拼写不同的客户名称。
HTML5 有一个datalist
控件。它类似于一个用select
元素创建的下拉列表,你可以向它添加option
元素,除了它本身不显示任何东西。它用于定义一个值列表,然后将该列表附加到另一个元素。添加从清单 11-42 到detail.html
中突出显示的代码。
清单 11-42。 添加一个数据列表在客户端名称字段提供提示
<input type="text" id="clientName"
maxlength="50"
list="clientList"
data-win-bind="value: clientName">
<datalist id="clientList"></datalist>
这段代码将获取我们添加到clientList
中的任何项目,并在用户输入客户端名称的值时向他们提供提示。此时,datalist
中没有任何项目,因此不会显示任何提示。理想的情况是用我们已经在 Clok 中定义的所有客户端填充clientList
。我们可以使用WinJS.Binding.List
类的map
方法来创建一个包含用户客户端的数组。添加清单 11-43 中突出显示的代码到storage.js
。
清单 11-43。 运用地图法
projects: new WinJS.Binding.List([]),
clients: {
get: function () {
return new WinJS.Binding.List(storage.projects
.map(function (p) { return p.clientName; })
);
}
},
compareProjectGroups: function (left, right) {
return left.toUpperCase().charCodeAt(0) - right.toUpperCase().charCodeAt(0);
},
现在我们有了一个clients
属性,我们只需要用它来填充我们在清单 11-42 中创建的datalist
控件。将清单 11-44 中突出显示的代码添加到detail.js
中。
清单 11-44。 绑定客户端到 Datalist
ready: function (element, options) {
this.currProject = storage.projects.getById(options && options.id)
|| new Clok.Data.Project();
this.configureAppBar(options && options.id);
var form = document.getElementById("projectDetailForm");
WinJS.Binding.processAll(form, this.currProject);
this.bindClients();
saveProjectCommand.onclick = this.saveProjectCommand_click.bind(this);
deleteProjectCommand.onclick = this.deleteProjectCommand_click.bind(this);
},
bindClients: function () {
storage.clients.forEach(function (item) {
var option = document.createElement("option");
option.textContent = item;
option.value.textContent = item;
clientList.appendChild(option);
});
},
saveProjectCommand_click: function (e) {
// SNIPPED
},
正如我提到的,datalist
控件非常类似于从select
元素创建的下拉列表控件。datalist
和select
都不支持 WinJS 数据绑定,这在我看来很不幸,所以在这两种情况下,我们都通过为数据源中的每一项手动创建一个option
元素来绑定数据。注意清单 11-44 中的和清单 11-7 中的之间的相似之处。
这样,运行 Clok 并添加一个新项目。它像你期望的那样工作吗?这不是我想要的。我们的datalist
对于用户的每个项目都有一个客户端,但是如果一个客户端有不止一个项目,那么这个客户端就不止一次出现在列表中。例如,Northwind Traders 有四个项目,所以在客户名称字段中键入字母N
会产生一个datalist
,如图 11-18 中的所示。
图 11-18 。客户端名称,带有过度填充的数据列表
虽然这本身可能是有帮助的,但是随着越来越多的项目被添加进来,datalist
将变得毫无用处。一个更好的选择是在datalist
中只显示每个客户一次,不管用户有多少项目给那个客户。
如果你熟悉 MapReduce,你肯定知道接下来会发生什么。如果不是,一个简短的、非技术性的描述是,它将一组可能非常大的数据转换成该数据的有意义的摘要。例如,给定一所大学所有学生的几个考试分数,MapReduce 可能用于获取所有学生的子集,包括他们在该学期的 GPA。虽然概念是一致的,但是 MapReduce 实现在不同的语言和平台之间是不同的。关于 MapReduce 概念的更多详细信息可以在网上找到。特别是,WinJS.Binding.List
类的map
和reduce
功能在 MSDN 的http://msdn.microsoft.com/en-us/library/windows/apps/hh700766.aspx
和http://msdn.microsoft.com/en-us/library/windows/apps/hh700784.aspx
有记录。无论如何,到目前为止我们只做了映射部分。我们想减少我们的客户列表,每个客户只包括一个项目。添加清单 11-45 中突出显示的代码到storage.js
。
清单 11-45。 排序然后减少我们映射的客户端
clients: {
get: function () {
return new WinJS.Binding.List(storage.projects
.map(function (p) { return p.clientName; })
.sort()
.reduce(function (accumulated, current) {
if (current !== accumulated[accumulated.length - 1]) {
accumulated[accumulated.length] = current;
}
return accumulated;
}, [])
);
}
},
在将我们的项目映射到客户列表之后,我们首先对结果进行排序,然后调用reduce
方法。reduce
方法为映射结果中的每一项调用指定的回调函数。在本例中,它将当前正在评估的客户端(变量current
)添加到一个不断增长的客户端数组(变量accumulated
)中,但前提是该客户端不在accumulated
中。reduce
方法也为accumulated
结果取一个初始值,在我们的例子中,它是一个空数组[]
。
运行 Clok 并添加一个新项目。当您第一次点击客户端名称字段时,会显示用户定义的所有客户端的列表,以防您的新项目是针对这些客户端中的一个(见图 11-19 )。
图 11-19 。现有客户
当您开始在客户端名称字段中输入时,数据列表会自动过滤,仅显示与您目前输入的文本相匹配的提示(参见图 11-20 )。
图 11-20 。过滤和唯一提示
注意本节代码有意包含了 Clok 中所有项目的客户端,包括已删除的项目。如果您希望只包含有活动或不活动项目的客户,那么您必须改变您的映射方法,只返回
p.clientName
,如果p.status
有一个合适的值。
结论
这一章我已经讲了很多。将数据绑定到用户界面的技术有很多,我在这里已经介绍了几种。虽然我们目前使用内存中的数据作为 Clok,但是我们在本章中创建的所有数据绑定代码将继续工作,因为我们升级了我们的Clok.Data.Storage
类,以更有意义的方式保存数据。数据绑定可能很简单,但它是一个庞大的主题,有很多信息不适合在本书中介绍。如果您对更多信息感兴趣,请致电http://msdn.microsoft.com/en-us/library/windows/apps/br229775.aspx
了解更多关于 MSDN 的信息。
十二、承诺
当您正在使用的应用在执行某项任务时暂时停止运行时,您是否曾感到沮丧?也许当你搜索它的数据或者执行一个长时间的计算时,它冻结了。有许多因素可能导致应用变得无响应,但这通常是在负责更新用户界面(UI)的同一线程上执行某些密集型操作的结果。这意味着当这个长时间运行的代码执行时,用户界面无法更新,导致应用冻结。
显然,这不是我们在应用中想要的。幸运的是,有一些技巧可以让您在代码中最大限度地减少这种情况。在这一章中,我将讲述承诺,在第十三章,我将讲述 Web 工作器。承诺允许你写代码来做一些你还不知道的事情。Web workers 允许您创建新的线程来执行长时间运行的操作。
为了保持应用的响应性,WinJS 库大量使用了承诺。事实上,只要在 Visual Studio 中使用第四章中的项目模板创建一个新项目,你的项目就会包含处理承诺的代码。在 Clok 的上下文中,我们在第九章中开始构建的示例时间跟踪应用,我们将探索承诺,因为我们添加了允许使用来保存和查看时间条目的功能。我将给出承诺的概述,包括如何处理从您可能调用的函数返回的承诺,以及如何在您自己的代码中创建它们。不过,首先,先介绍一下背景。
什么是承诺
简而言之,承诺代表了一种可能尚不存在的价值。这是什么意思?如果您有一个异步运行的操作,比如通过 HTTP 请求远程数据,而不是阻塞并等待该操作完成,那么您可以使用 promise 来表示您最终将从该 HTTP 请求中接收到的数据。如果您这样做了,那么您的代码的其余部分可以继续执行,并且当承诺已经实现时,一个指定的函数执行来处理异步(async)操作的结果。清单 12-1 展示了一个通过 HTTP 获得远程数据的潜在实现。
清单 12-1。 一个伪码诺言的例子
var myPromise = getJsonDataAsync();
myPromise.then(
function doSomethingWithTheData(data) { /* */ },
function logError(e) { /* */ }
);
显然,这段代码缺少一些上下文,但它确实说明了如何处理承诺。假设函数getJsonDataAsync
已经被定义并返回一个 promise 对象。一旦收到数据,promise 就执行它的then
函数,将数据作为参数传递,以便在doSomethingWithTheData
中进行处理。或者,如果承诺中有错误,则执行logError
功能。
需要指出的是,在清单 12-1 的中,函数getJsonDataAsync
没有返回我们请求的数据。它回报一个承诺。承诺不是异步操作的实际结果。它是一个独立的对象,可以以三种状态之一存在:未实现、实现或拒绝。未实现的承诺意味着异步工作尚未完成。兑现的承诺意味着工作完成无误。被拒绝的承诺是完成了,但有错误的承诺。
承诺可能是一件棘手的事情。不同的库中有许多不同的 JavaScript 实现,比如 jQuery 或 Dojo,每一个都可能有稍微不同的实现,这并没有什么帮助。幸运的是,虽然 CommonJS Promises/A 规范(http://wiki.commonjs.org/wiki/Promises/A
)不是一个标准,但它很流行,并且已经被许多不同的 Promises 库实现。事实上,promises 的 WinJS 实现遵循了这个规范。
正如我提到的,清单 12-1 中的代码缺少一些上下文,所以让我们通过定义getJsonDataAsync
函数来添加一些,如清单 12-2 所示。这是创建返回Promise
对象的函数的常见模式。
清单 12-2。 延续前人的诺言的例子
function getJsonDataAsync() {
return new WinJS.Promise(function init(oncomplete, onerror) {
setTimeout(function () {
try {
var data = getTheData();
oncomplete(data);
}
catch (e) {
onerror(e);
}
}, 1000);
});
}
这是怎么回事?嗯,我已经将清单 12-2 中的一些语句合并成一个return
语句。虽然这是定义Promise
时的一种常见技术,但当引入主题时可能会有点复杂。在这个例子中,实际获取我需要的数据的任务发生在对getTheData
的调用中,但是我需要getJsonDataAsync
返回一个Promise
对象。Promise
构造函数将初始化函数init
作为它的第一个参数。这个init
函数接受两个函数作为参数。第一个是oncomplete
,是在Promise
成功完成时调用的函数,传递我需要的数据。第二个init
参数onerror
,是出错时调用的函数。我已经将init
函数的内容包装在对setTimeout
的调用中,因此它会在一秒钟后异步执行。
注意
Promise
构造函数可以选择另一个函数作为第二个参数,如果承诺被取消,这个函数将被执行。此外,init
函数可以选择接受第三个参数,该参数可用于向调用代码报告Promise
的进度。有关这些可选参数的更多详细信息,请参见 MSDN:http://msdn.microsoft.com/en-us/library/windows/apps/br211866.aspx
。
Promise
类定义了两个函数来处理异步方法的结果:then
和done
。这两个功能非常相似。它们都可以接受多达三个函数作为参数:一个在成功完成承诺时调用的函数,一个在出现错误时调用的函数,以及一个报告异步代码进度的函数。事实上,在某些情况下,你可以用then
来代替done
。它们之间有两个主要区别。
第一个是then
函数返回另一个Promise
,允许您将多个then
语句相互链接。另一方面,done
函数不返回任何东西。清单 12-3 展示了一个例子,其中我们的Promise
的结果被传递给第一个then
函数,然后它的结果被传递给第二个then
函数,最后传递给done
函数。在这个例子中,只有处理承诺完成的函数被提供给then
和done
。
清单 12-3。 承诺链的例子
getRemoteData()
.then(function processTheData(data) {
// ...
})
.then(function postProcessedDataToServer(data) {
// ...
})
.done(function updateUserInterfaceWithPostStatus(data) {
// ...
});
then
和done
的第二个显著区别与错误处理有关。如果没有向then
提供错误处理函数,任何错误都将传递给链中的下一条语句。如果没有向done
提供错误处理功能,任何错误都会导致应用中出现异常。
回到我们在清单 12-1 和清单 12-2 中的例子,从清单 12-2 中的getTheData
返回的数据被传递给oncomplete
函数,它映射到then
函数的第一个参数doSomethingWithTheData
,在清单 12-1 中。任何错误都被传递给onerror
函数,该函数映射到清单 12-1 中then
函数的第二个参数logError
。结果是,我可以请求我需要的数据,并在实际拥有它之前指定我想用它做什么。在这一章的后面,你会看到一些完全实现承诺的例子。
在处理承诺时要记住的一点是,JavaScript 在默认情况下不是多线程语言。虽然 promises 的目的是处理异步函数的结果,但我想说明的是,仅仅拥有 promise 对象并不意味着您的代码在不同的线程上执行。换句话说,异步代码不一定运行在不同的线程上。它只是在不同的时间运行。异步代码在不同的线程上执行是很常见的,但是正如你将在本章后面看到的,即使使用 promises,你的 UI 仍然可能变得没有响应,因为异步代码是在 UI 线程上执行的。
在本章的其余部分,我将向您介绍 Clok 上下文中的承诺,这是我们在过去几章中组装的示例应用。虽然这一章的目的是涵盖WinJS.Promise
类,但这一章中有大量的代码本身并不与承诺有内在的联系。但是,要构建一个有意义的示例,这段代码是必需的。在本章结束时,你会看到一些不同的处理承诺的技术,Clok 将会在成为一个有用的 Windows 应用商店的道路上走得更远。
记录时间条目
我们的示例应用 Clok 需要保存时间条目的能力。您可能还记得,我们在 Clok dashboard 屏幕上创建的时间输入表单实际上还没有做任何事情。它只会移动和重置。我们必须添加功能来保存这些时间条目,幸运的是,对于第十二章(本章)来说,这是一个利用承诺的好机会。为了允许用户记录时间条目,我们必须采取三个步骤。
- 我们必须用一个新的计时器控件替换 Clok 仪表板屏幕上的当前时钟。
- 除了项目之外,我们还必须更新我们的数据模型来支持时间条目。
- 我们必须添加代码,以便在用户单击 save 按钮时实际保存新的时间条目。
新的定时器控制
在保存时间条目之前,我们必须对 Clok dashboard 屏幕上的计时器进行一些更改。虽然当前的Clok.UI.Clock
控件有助于说明如何创建自定义控件,但是让一个控件具有多种功能并不太实际:显示当前时间和显示经过的时间。我们将把这个控件分成两个不同的控件,每个控件都有其特定的用途。当前控件仍将存在,但将删除与计时器相关的功能,并将创建一个新的Clok.UI.Timer
控件来计算和显示经过的时间。
这个新的Timer
控件将跟踪它何时开始和停止,并根据这些值计算经过的时间,而不是存储一个经过的时间值。稍后,这将允许我们的用户启动计时器,然后关闭 Clok。当他们稍后返回时,计时器仍将显示为正在运行,显示自用户启动计时器以来所经过的总时间,即使在此期间应用没有运行。我会在第十七章中介绍这一点。同时,让我们创建新的Timer
控件。将名为timerControl.js
的新 JavaScript 文件添加到 Visual Studio 项目的controls/js
文件夹中。将清单 12-4 中的代码添加到这个新文件中。
清单 12-4。 我们的新定时器控件
(function () {
"use strict";
var controlDefinition = WinJS.Class.define(
function Control_ctor(element, options) {
this.element = element || document.createElement("div");
this.element.winControl = this;
// Set option defaults
this._startStops = [];
// Set user-defined options
WinJS.UI.setOptions(this, options);
this._init();
},
{
_intervalId: 0,
isRunning: {
get: function () {
return (this.startStops.length > 0
&& this.startStops[this.startStops.length - 1]
&& this.startStops[this.startStops.length - 1].startTime
&& !this.startStops[this.startStops.length - 1].stopTime);
}
},
startStops: {
get: function () {
return this._startStops;
},
set: function (value) {
this._startStops = value;
}
},
timerValue: {
get: function () {
if (this.startStops.length <= 0) {
return 0;
} else {
var val = 0;
for (var i = 0; i < this.startStops.length; i++) {
var startStop = this.startStops[i];
if (startStop.stopTime) {
val += (startStop.stopTime - startStop.startTime);
} else {
val += ((new Date()).getTime() - startStop.startTime);
}
}
return Math.round(val / 1000);
}
}
},
timerValueAsTimeSpan: {
get: function () {
return Clok.Utilities.SecondsToTimeSpan(this.timerValue);
}
},
_init: function () {
this._updateTimer();
},
start: function () {
if (!this.isRunning) {
this._intervalId = setInterval(this._updateTimer.bind(this), 250);
this.startStops[this.startStops.length] = {
startTime: (new Date()).getTime()
};
this.dispatchEvent("start", {});
}
},
stop: function () {
if (this.isRunning) {
clearInterval(this._intervalId);
this._intervalId = 0;
this.startStops[this.startStops.length - 1]
.stopTime = (new Date()).getTime();
this._updateTimer();
this.dispatchEvent("stop", {});
}
},
reset: function () {
this._startStops = [];
this._updateTimer();
this.dispatchEvent("reset", {});
},
_updateTimer: function () {
var ts = this.timerValueAsTimeSpan;
var sec = ts[2];
var min = ts[1];
var hr = ts[0];
min = ((min < 10) ? "0" : "") + min;
sec = ((sec < 10) ? "0" : "") + sec;
var formattedTime = new String();
formattedTime = hr + ":" + min + ":" + sec;
this.element.textContent = formattedTime;
},
}
);
WinJS.Namespace.define("Clok.UI", {
Timer: controlDefinition,
});
WinJS.Class.mix(Clok.UI.Timer,
WinJS.Utilities.createEventProperties("start"),
WinJS.Utilities.createEventProperties("stop"),
WinJS.Utilities.createEventProperties("reset"),
WinJS.UI.DOMEventMixin);
})();
这是相当多的代码,但是您已经看到了其中的大部分,所以我不会详细讨论它。我将只指出有一个名为startStops
的新属性,它包含计时器开始和停止的时间数组。每次定时器启动时,一个新的项目被添加到startStops
中,仅定义了一个startTime
。当定时器停止时,为数组中最近的项目定义stopTime
。然后使用startStops
属性来确定isRunning
和timerValue
属性的值。请注意,因为Date
对象的getTime
方法返回毫秒数,所以在返回之前,我在timerValue
中将差值除以 1000。
您还会看到对一个新函数Clok.Utilities.SecondsToTimeSpan
的调用。将名为utilities.js
的文件添加到js
文件夹中,并添加来自清单 12-5 的代码。
清单 12-5。 我们的公用事业类
(function () {
"use strict";
var utilClass = WinJS.Class.define(
function constructor() { },
{ /* no instance members */ },
{
// static members
SecondsToTimeSpan: function (totalSec) {
if (!isNaN(totalSec)) {
var sec = totalSec % 60;
var min = ((totalSec - sec) / 60) % 60;
var hr = ((totalSec - sec - (60 * min)) / 3600);
return [hr, min, sec];
}
return [0, 0, 0];
},
TimeSpanToSeconds: function (timespan) {
if (isNaN(timespan)) {
return (timespan[0] * 3600) + (timespan[1] * 60) + (timespan[2]);
}
return 0;
},
}
);
WinJS.Namespace.define("Clok", {
Utilities: utilClass,
});
})();
只剩下一些小的变化,用新的Timer
控件替换旧的Clock
控件。将清单 12-6 中突出显示的代码添加到default.html
中,这样我们的新类在整个应用中都可用。
清单 12-6。 在 default.html 添加脚本引用
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
<script src="/js/navigator.js"></script>
<script src="/js/utilities.js"></script>
<script src="/controls/js/timerControl.js"></script>
<script src="/controls/js/clockControl.js"></script>
<script src="/data/project.js"></script>
<script src="/data/storage.js"></script>
接下来,打开home.html
,用我们新的Timer
控件替换Clock
控件。清单 12-7 中的突出了这一变化。
清单 12-7。 切换到我们新的控制
<div id="elapsedTime">
<h2 id="elapsedTimeClock"
data-win-control=" Clok.UI.Timer "></h2>
</div>
最后,我们要对home.js
做两个小改动。当将计时器功能分解到它自己的类中时,我将counterValue
属性的名称改为timerValue
。因此,清单 12-8 突出了我们必须在home.js
的enableOrDisableButtons
函数中改变的两个地方。
清单 12-8。 用新的属性名更新 home.js
enableOrDisableButtons: function () {
if ((project.value !== "")
&& (!this.timerIsRunning)
&& (elapsedTimeClock.winControl. timerValue > 0)) {
saveTimeButton.disabled = false;
} else {
saveTimeButton.disabled = true;
}
discardTimeButton.disabled = (this.timerIsRunning)
|| (elapsedTimeClock.winControl. timerValue <= 0);
editProjectButton.disabled =
(project.options[project.selectedIndex].value === "");
},
现在,如果运行 Clok,它的外观和行为应该和以前完全一样。虽然这一变化似乎在这一点上没有任何区别,但Timer
控件功能现在反映了用户的行为。用户可以启动和停止计时器,而Timer
控件现在可以跟踪每次发生的时间。此外,这一变化对于你将在第十五章中构建的功能至关重要,该功能允许用户关闭 Clok 并在稍后返回时保持计时器不变。
注意在这一章中,我已经说明了新
Clok.UI.Timer
控件的代码,但是没有显示修改过的Clok.UI.Clock
控件。在这个控件中保留不必要的与计时器相关的功能不会阻止 Clok 按预期运行,但是保持您的代码库整洁是一个很好的实践,可以在将来提高可维护性。因此,如果您有兴趣查看修改后的代码,本书提供的源代码包含一个删除了与计时器相关的功能的版本。你可以在该书的 press 产品页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790
)。
更新我们的数据模型
为了节省时间条目,需要更新 Clok 数据模型。需要一个新的timeEntry
类,以及对storage
类的修改,来处理timeEntry
对象。在 Visual Studio 项目的data
文件夹中创建一个名为timeEntry.js
的新文件。将清单 12-9 中的代码添加到timeEntry.js
。
清单 12-9。 一类为时间条目
(function () {
"use strict";
var timeEntryClass = WinJS.Class.define(
function constructor() {
// define and initialize properties
this.id = (new Date()).getTime();
this._projectId = -1;
this._dateWorked = (new Date()).removeTimePart();
this.elapsedSeconds = 0;
this.notes = "";
},
{
// instance members
projectId: {
get: function () {
return this._projectId;
},
set: function (value) {
this._projectId =
(value && !isNaN(value) && Number(value))
|| this._projectId;
}
},
dateWorked: {
get: function () {
return this._dateWorked;
},
set: function (value) {
this._dateWorked = value.removeTimePart();
}
},
project: {
get: function () {
var p = Clok.Data.Storage.projects.getById(this.projectId);
return p;
}
},
},
{
// static members
}
);
WinJS.Namespace.define("Clok.Data", {
TimeEntry: timeEntryClass,
});
})();
注意一定要在
default.html
中添加一个对/data/timeEntry.js
的脚本引用。
你可能已经注意到,我在第十一章的中构造了与project
略有不同的timeEntry
类。我已经为一些timeEntry
属性明确定义了get
和set
函数。这允许验证分配给这些属性的值。具体来说,在设置projectId
属性的值之前,我验证了为projectId
提供的值是一个数字。此外,我在Date
类上使用了一个名为removeTimePart
的方法,这样保存的日期没有时间。例如,保存“2013 年 12 月 1 日”而不是“2013 年 12 月 1 日下午 4:05”,可以简化时间条目的过滤,我将在本章的后面介绍这一点。
不幸的是,Date
类实际上没有名为removeTimePart
的方法。为了让它工作,我给Date.prototype
增加了一些功能。如果你熟悉 JavaScript,你可能已经知道如何使用prototype
。如果你来自不同的背景,我来总结一下。因为 JavaScript 是一种动态语言,所以我们可以在创建一个对象后为其添加成员(属性和方法)。我从未调用过它,但这正是我在定义数据模型类的属性时所做的,比如当我初始化清单 12-9 中this
对象的notes
属性时,尽管它没有明确的定义。
除了将成员添加到对象的实例中,我们还可以在创建类定义之后,将成员添加到类定义本身中。完成后,该类的任何实例也包含该新成员。在 Visual Studio 项目的js
文件夹中创建一个名为extensions.js
的文件。将清单 12-10 中的代码添加到extensions.js
中。
清单 12-10。 扩展日期类
Date.prototype.removeTimePart = function () {
var year = this.getFullYear();
var month = this.getMonth();
var date = this.getDate();
return new Date(year, month, date);
}
注意如果你熟悉 C#,这和扩展方法有相似的感觉。这不是一回事,因为在 JavaScript 中我们改变了类的定义。然而,对于 C#扩展方法,编译器会变一些魔法,让我们的扩展方法看起来像是类的成员,而实际上并没有改变类的定义。
我们的Clok.Data.Storage
类也需要一些更新,以允许我们保存timeEntry
对象。打开storage.js
,在groupedProjects
函数定义后添加清单 12-11 中高亮显示的代码。
清单 12-11。 添加一个空列表来存储时间条目
groupedProjects: {
// SNIPPED
},
timeEntries: new WinJS.Binding.List([]),
我们还需要一个save
方法,比如我们为第十一章中的项目添加的方法。在storage.projects.delete
函数定义之后,添加清单 12-12 到storage.js
中突出显示的代码。
清单 12-12。 向我们的存储类添加获取和保存时间条目的方法
storage.projects.delete = function (p) {
// SNIPPED
};
storage.timeEntries.getById = function (id) {
if (id) {
var matches = this.filter(function (te) { return te.id === id; });
if (matches && matches.length === 1) {
return matches[0];
}
}
return undefined;
};
storage.timeEntries.save = function (te) {
if (te && te.id) {
var existing = storage.timeEntries.getById(te.id);
if (!existing) {
storage.timeEntries.push(te);
}
}
};
从 Clok 仪表板保存时间条目
虽然我们仍然没有将数据持久化到任何地方,但是我们现在已经有了代码,可以像保存项目对象一样将timeEntry
对象保存到内存中。我将在第十四章中讨论更多持久存储选项。然而,我们终于准备好向 Clok 实际介绍承诺了。
实际上,我们已经有了。看一下home.js
中的discard
函数。对WinJS.UI.executeTransition
的调用实际上返回了一个Promise
对象(参见清单 12-13 )。
清单 12-13。 我们已经看到了承诺
var slideTransition = WinJS.UI.executeTransition(
// SNIPPED
]).done(function () { self.resetTimer(); });
当由WinJS.UI.executeTransition
返回的Promise
成功完成时,我们传递给done
函数的匿名函数重置计时器。这是一个非常基本的承诺用例。当我们实现保存时间条目的能力时,我们可以以不同的方式利用承诺,因为我们对home.js
中的save
函数做了一些更改。用清单 12-14 中的代码替换save
函数。
清单 12-14。 修改保存功能
save: function () {
var self = this;
var transitionPromise = new WinJS.Promise(function (comp, err, prog) {
timeEntry.style.transition = 'color 5ms ease 0s, '
+ 'transform 500ms ease 0s, opacity 500ms ease 0s';
timeEntry.style.transformOrigin = "-130px 480px";
timeEntry.style.transform = 'scale3d(0,0,0)';
timeEntry.style.opacity = '0';
timeEntry.style.color = '#00ff00';
var self = this;
var transitionend = function (e1) {
if (e1.propertyName === "transform") {
timeEntry.removeEventListener('transitionend', transitionend);
comp();
}
};
timeEntry.addEventListener('transitionend', transitionend, false);
});
var savePromise = new WinJS.Promise(function (comp, err, prog) {
var timeEntry = new Clok.Data.TimeEntry();
timeEntry.projectId = Number(project.options[project.selectedIndex].value);
timeEntry.dateWorked = new Date(elapsedTimeClock.winControl.startStops[0].startTime);
timeEntry.elapsedSeconds = elapsedTimeClock.winControl.timerValue;
timeEntry.notes = timeNotes.value;
storage.timeEntries.save(timeEntry);
comp();
});
WinJS.Promise.join([transitionPromise, savePromise]).done(function () {
self.resetTimer();
});
},
您将注意到的第一件事是,我们将执行 CSS 转换的代码包装在名为transitionPromise
的Promise
对象中。我们调用承诺初始化器的comp
处理程序,而不是调用transitionend
事件处理程序中的self.resetTimer
。接下来,我们有一些代码来实际保存时间条目。创建和保存一个新的timeEntry
对象本身非常简单。在清单 12-14 中,我们还将该功能包装在一个名为savePromise
的Promise
对象中。
现在我们有两个Promise
对象。当两者都完成时——动画结束且时间条目已保存——计时器应复位。Promise
类有一个join
方法正好提供了这个功能。您可以将一个由Promise
对象组成的数组传递给join
方法,当数组中的每个Promise
都成功完成时,join
方法将返回一个新的Promise
。因此,对self.resetTimer
的调用不会发生,直到动画已经完成并且时间条目已经保存。
和
join
一样,any
接受一组Promise
对象作为它的参数。
到目前为止,我们在这一章中已经做了很多有益的工作。我们更改了计时器控件,更新了数据模型,并添加了代码来保存时间条目,充分利用了流程中的承诺。然而,当此时运行 Clok 时,表面上看起来一切都与第十一章结束时完全一样。当用户单击“开始”菜单选项时,计时器仍然开始计时,当用户单击“停止”菜单选项时,计时器停止计时。当用户单击“保存”或“放弃”按钮时,表单仍然会显示动画,然后重置。尽管我们添加了保存时间条目的功能,但从用户的角度来看,没有办法看出有什么不同。让我们解决这个问题。
查看时间条目
现在,用户可以从 Clok dashboard 屏幕保存时间条目,他们需要一种查看它们的方式。在这一节中,我们将添加一个页面来查看保存的时间条目列表,以及一些不同的过滤列表的方法。
临时数据
虽然我们可以使用刚刚添加的功能来加载一些timeEntry
对象,但每次测试应用时都必须这样做,会很快变得过时。因此,在我们构建查看时间条目的新页面之前,让我们通过在storage.js
的最后添加来自清单 12-15 的代码来硬编码一些timeEntry
对象。
清单 12-15。 临时添加硬编码数据
(function () {
var createTime = function (id, projectId, dateWorked, elapsedSeconds, notes) {
var newTimeEntry = new Clok.Data.TimeEntry();
newTimeEntry.id = id;
newTimeEntry.projectId = projectId;
newTimeEntry.dateWorked = dateWorked;
newTimeEntry.elapsedSeconds = elapsedSeconds;
newTimeEntry.notes = notes;
return newTimeEntry;
}
var time = Clok.Data.Storage.timeEntries;
var date1 = (new Date()).addMonths(-1).addDays(1);
var date2 = (new Date()).addMonths(-1).addDays(2);
var date3 = (new Date()).addMonths(-1).addDays(3);
var timeId = 1369623987766;
time.push(createTime(timeId++, 1368296808757, date1, 10800, "Lorem ipsum dolor sit."));
time.push(createTime(timeId++, 1368296808757, date2, 7200, "Amet, consectetur euismod."));
time.push(createTime(timeId++, 1368296808757, date3, 7200, "Praesent congue diam."));
time.push(createTime(timeId++, 1368296808760, date2, 7200, "Curabitur euismod mollis."));
time.push(createTime(timeId++, 1368296808759, date1, 7200, "Donec sit amet porttitor."));
time.push(createTime(timeId++, 1368296808758, date3, 8100, "Praesent congue euismod."));
time.push(createTime(timeId++, 1368296808758, date2, 14400, "Curabitur euismod mollis."));
time.push(createTime(timeId++, 1368296808761, date1, 7200, "Donec sit amet porttitor."));
time.push(createTime(timeId++, 1368296808748, date3, 7200, "Praesent euismod diam."));
time.push(createTime(timeId++, 1368296808748, date2, 7200, "Curabitur euismod mollis."));
time.push(createTime(timeId++, 1368296808748, date1, 7200, "Donec sit amet porttitor."));
time.push(createTime(timeId++, 1368296808746, date2, 8100, "Congue euismod diam."));
time.push(createTime(timeId++, 1368296808753, date2, 14400, "Curabitur euismod mollis."));
time.push(createTime(timeId++, 1368296808753, date1, 7200, "Donec sit amet porttitor."));
time.push(createTime(timeId++, 1368296808761, date2, 10800, "Donec semper risus nec."));
})();
一旦我们开始持久化我们的数据,我们将不得不删除这些代码,但同时,随着我们的开发,在应用中有一些测试数据是很好的。这个自执行函数非常类似于第十一章中添加的代码,用于硬编码临时项目到 Clok。
然而,有一点你可能已经注意到了,那就是我添加到Date
类的prototype
中的两个额外函数的使用:addMonths
和addDays
。在创建临时数据时使用这些函数将确保我们总是有最近的测试数据,不管我们什么时候测试。让我们定义这些函数,以及一个名为addYears
的函数。添加清单 12-16 中的代码到extensions.js
。
清单 12-16。 进一步扩展日期类
Date.prototype.addDays = function (n) {
var year = this.getFullYear();
var month = this.getMonth();
var date = this.getDate();
date += n;
return new Date(year, month, date);
}
Date.prototype.addMonths = function (n) {
var year = this.getFullYear();
var month = this.getMonth();
var date = this.getDate();
month += n;
return new Date(year, month, date);
}
Date.prototype.addYears = function (n) {
var year = this.getFullYear();
var month = this.getMonth();
var date = this.getDate();
year += n;
return new Date(year, month, date);
}
当我第一次看到这种通过向日期的一部分添加一个值来改变日期的技术时,我开始担心如果我向 5 月 24 日添加 30 天会发生什么。没有 5 月 54 日这个日期。幸运的是,JavaScript Date
构造函数能够处理这个问题,在这个实例中会返回 6 月 23 日。
注意许多编程语言和框架都提供了像这样轻松处理日期和时间的方法。默认情况下,JavaScript 没有处理日期的便捷方法。这些简单的方法满足了我们在 Clok 中的一些常见需求。此外,WinRT 提供了一些通过
Windows.Globalization.DateTimeFormatting
名称空间格式化日期的能力。如果您正在寻找额外的功能,许多库是可用的。比较流行的一个是 Moment.js 库,可以在网上www.momentjs.com
找到。
列出时间条目
现在我们已经加载了一些临时数据,让我们定义当用户查看时间条目列表时 UI 应该如何显示。Windows 8 附带的邮件应用使用“拆分布局”,在结构上非常类似于 Visual Studio 在您选择我们在第四章中介绍的拆分应用模板时创建的项目。在邮件应用中,消息列表在屏幕左侧可见,消息细节在屏幕右侧可见(参见图 12-1 )。
图 12-1 。Windows Mail 应用
我想使用类似的布局来查看 Clok 中的时间条目。当我计划这本书的内容时,我制作了 Clok 中各种屏幕可能出现的模型。图 12-2 是我们将要构建的屏幕的早期模型。此后,我对 Clok 做了一些更新,与这个模型略有不同,但还是相当接近。
图 12-2 。Clok 时间条目屏幕的早期模型
注意为了创建我的 Clok 模型,我使用了一个流行的线框图工具,叫做 Balsamiq 样机。关于这个伟大工具的更多信息可以从 Balsamiq 网站获得,网址是
www.balsamiq.com/products/mockups
。使用线框或模型是一种很好的快速方法,可以确保你知道你将要构建什么。我强烈建议从模型开始工作,即使它们是白板上的图纸照片,巧合的是,这是我们将在第二十二章中添加到 Clok 的一个功能。
在左侧,用户将看到以前的时间条目列表,他们可以使用过滤器应用栏命令进行过滤。在右边,他们将能够编辑现有的一个。删除时间条目是一个场景,我将在本章稍后介绍,但我不会在本书的文本中实际介绍编辑现有时间条目或从该屏幕添加时间条目。这样做的代码类似于允许用户编辑或添加项目的代码。这本书附带的源代码确实有一个完整版本的时间表屏幕,包括从这个屏幕编辑时间条目或添加新条目的能力。(见该书的 Apress 产品页[ www.apress.com/9781430257790
]的源代码/下载标签)。)
创建和连接页面控件
首先要做的是在 Visual Studio 项目的pages
文件夹中创建一个名为timeEntries
的文件夹。在timeEntries
文件夹中,添加一个名为list.html
的新页面控件(参见图 12-3 )。
图 12-3 。具有新的时间条目页面控件的解决方案资源管理器
现在我们必须修改 Clok dashboard 屏幕,以便时间表菜单选项(图 12-4 )将导航到我们的新页面控件。参考第十一章,特别是清单 11-9 、清单 11-10 和清单 11-11 ,如果你需要一个如何连接导航的快速提示。
图 12-4 。时间表菜单选项
对于这一部分,我们将从屏幕左侧唯一的时间条目列表开始。稍后,我们将添加用于过滤和删除时间条目的应用栏命令。首先,用来自清单 12-17 的代码替换list.html
中body
元素的全部内容。
清单 12-17。 新增内容为 list.html
<div class="timeEntryListPage fragment">
<header aria-label="Header content" role="banner">
<button class="win-backbutton" aria-label="Back" disabled type="button"></button>
<h1 class="titlearea win-type-ellipsis">
<span class="pagetitle">Time Sheets</span>
</h1>
</header>
<section aria-label="Main content" role="main">
<div id="timeEntryTemplate"
data-win-control="WinJS.Binding.Template"
style="display: none">
<div class="timeEntryItem">
<div class="timeEntryItem-dateWorked">
<h5 class="timeEntryItem-dateWorked-mon"
data-win-bind="textContent: dateWorked "></h5>
<h2 class="timeEntryItem-dateWorked-day"
data-win-bind="textContent: dateWorked"></h2>
<h5 class="timeEntryItem-dateWorked-year"
data-win-bind="textContent: dateWorked"></h5>
</div>
<div class="timeEntryItem-projectInfo">
<h3 class="timeEntryItem-projectName win-type-ellipsis"
data-win-bind="textContent: project.name"></h3>
<h6 class="timeEntryItem-clientName win-type-ellipsis"
data-win-bind="textContent: project.clientName"></h6>
<h6 class="timeEntryItem-projectNumber"
data-win-bind="textContent: project.projectNumber"></h6>
</div>
<div class="timeEntryItem-timeWorked">
<h2 class="timeEntryItem-timeWorked-elapsed"
data-win-bind="textContent: elapsedSeconds"></h2>
<h5>hours</h5>
</div>
</div>
</div>
<div id="timeEntriesContainer">
<div id="timeEntriesListViewPane">
<div
id="timeEntriesListView"
class="itemlist win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemTemplate: select('#timeEntryTemplate'),
selectionMode: 'multi',
swipeBehavior: 'select',
tapBehavior: 'directSelect'
}">
</div>
<div id="noMatchesFound">No data found. Try adjusting the filters.</div>
<div id="searchInProgress">
Searching for time entries...<br />
<progress />
</div>
<div id="searchError">There was an error searching for time entries.</div>
</div>
<div id="timeEntryDetailPane">
<!-- edit form goes here -->
</div>
</div>
</section>
</div>
至此,您可能已经非常熟悉这段代码在做什么了。我添加了一个名为timeEntryTemplate
的WinJS.Binding.Template
,并在页面上定义了两个区域。第二个是timeEntryDetailPane div
元素,目前为空。我将在本章的后面回到这一点。我们在本节中使用的区域是timeEntriesListViewPane div
元素。在那个区域中,我添加了一个ListView
和一些其他的div
元素,用于向用户显示不同的状态。这次我在ListView
上设置了一些不同的属性。突出显示的代码显示了用于使ListView
按预期运行的设置。在这种情况下,我们允许用户选择多个项目,通过在触摸屏上滑动项目或用鼠标右键单击。此外,如果用户点击或单击单个项目,该项目将被选中,其他任何项目都将被取消选中。接下来,用清单 12-18 中的代码替换list.css
的全部内容。
清单 12-18。 新内容为 list.css
.timeEntryListPage section[role=main] {
margin-left: 120px;
width: calc(100% - 120px);
}
.hidden {
display: none;
}
#timeEntriesContainer {
display: -ms-flexbox;
-ms-flex-align: start;
-ms-flex-pack: start;
-ms-flex-direction: row;
-ms-flex-wrap: nowrap;
height: 100%;
}
#timeEntriesListViewPane {
-ms-flex: 0 auto;
width: 600px;
height: 100%;
}
#timeEntriesListView {
height: 100%;
}
#timeEntriesListView .win-container {
background-color: #46468C;
}
#timeEntriesListView .timeEntryItem {
display: -ms-grid;
-ms-grid-columns: auto 1fr 150px;
}
#timeEntriesListView .timeEntryItem-dateWorked {
-ms-grid-column: 1;
margin: 5px;
width: 75px;
height: 75px;
text-align: center;
background-color: #8C8CD2;
}
#timeEntriesListView .timeEntryItem-dateWorked-day {
font-weight: bold;
}
#timeEntriesListView .timeEntryItem-projectInfo {
-ms-grid-column: 2;
margin: 5px;
}
#timeEntriesListView .timeEntryItem-projectName {
font-size: 1.25em;
}
#timeEntriesListView .timeEntryItem-timeWorked {
-ms-grid-column: 3;
height: 100%;
margin-left: 5px;
margin-right: 10px;
text-align: right;
display: -ms-flexbox;
-ms-flex-pack: center;
-ms-flex-direction: column;
}
#timeEntriesListView .timeEntryItem-timeWorked-elapsed {
font-weight: bold;
}
@media screen and (-ms-view-state: snapped) {
.timeEntryListPage section[role=main] {
margin-left: 20px;
margin-right: 20px;
}
}
@media screen and (-ms-view-state: fullscreen-portrait) {
.timeEntryListPage section[role=main] {
margin-left: 100px;
margin-right: 100px;
}
}
再说一次,这里没有你没见过的东西。我只是添加了 CSS,使用 flexbox 布局显示页面,并添加了一些规则来设计timeEntryTemplate
的各个部分。
获取时间输入数据
到目前为止,还是真的没什么好看的。如果你现在运行 Clok,你会看到一个空的时间表页面,类似于图 12-5 。
图 12-5 。我们的工作正在进行中。还没什么可看的
注意在我们所有的代码和技术讨论中,我都使用术语时间条目来指代应用的这一部分。然而,时间表对用户来说是一个更有意义的名字。请记住:虽然您应该在与开发人员交流时使用对开发人员有意义的名称和术语,但是您应该确保在您的用户界面和任何非技术文档中使用对您的用户有意义的名称和术语。如果有充分的理由让它们不同,这些术语不必匹配。
在第十一章中,我们给storage.js
添加了函数,允许我们为项目指定一个过滤器。这些函数最终返回一个经过过滤和排序的项目列表,我们在项目页面上将一个ListView
绑定到这个列表。现在,我们将为时间条目做一些类似的事情。在timeEntries
的定义之后,将清单 12-19 中高亮显示的代码添加到storage.js
中。
清单 12-19。 一个针对 storage.js 的时间条目比较器
timeEntries: new WinJS.Binding.List([]),
compareTimeEntries: function (left, right) {
// first sort by date worked...
var dateCompare = left.dateWorked.getTime() - right.dateWorked.getTime();
if (dateCompare !== 0) {
return dateCompare;
}
// then sort by client name...
if (left.project.clientName !== right.project.clientName) {
return (left.project.clientName > right.project.clientName) ? 1 : -1;
}
// then sort by project name...
if (left.project.name !== right.project.name) {
return (left.project.name > right.project.name) ? 1 : -1;
}
return 0;
},
注意比较函数应该返回-1、0 或 1。如果比较的两个值相等,函数应该返回 0。如果第一个值大于第二个函数,则返回 1。如果第二个函数大于第一个函数,则返回-1。
在这种情况下,我们决定使用更复杂的排序定义。时间条目将首先按日期排序,然后按客户名称排序,最后按项目本身的名称排序。接下来,在storage.js
中,在timeEntries.getById
的定义之前,添加来自清单 12-20 的高亮代码。
清单 12-20。 时间录入搜索功能
storage.timeEntries.getSortedFilteredTimeEntriesAsync = function (begin, end, projectId) {
var filtered = this
.createFiltered(function (te) {
if (begin) {
if (te.dateWorked < begin) return false;
}
if (end) {
if (te.dateWorked >= end.addDays(1)) return false;
}
if (projectId && !isNaN(projectId) && Number(projectId) > 0) {
if (te.projectId !== Number(projectId)) return false;
}
if (te.project.status !== Clok.Data.ProjectStatuses.Active) return false;
return true;
});
var sorted = filtered.createSorted(storage.compareTimeEntries);
return sorted;
};
storage.timeEntries.getById = function (id) {
if (id) {
var matches = this.filter(function (te) { return te.id === id; });
if (matches && matches.length === 1) {
return matches[0];
}
}
return undefined;
};
虽然我们只允许用户按状态过滤项目列表,但允许他们按日期和项目过滤时间条目是有意义的。因此,getSortedFilteredTimeEntriesAsync
接受三个参数:begin
和end
,用于定义日期范围,以及projectId
,用于将时间条目列表限制为单个项目的时间条目。对于第一次迭代,我们不会使用那些过滤器,所有的时间条目都会显示出来。一旦我们得到了正确显示的未过滤列表,我们将在本章的下一节添加过滤。为此,添加清单 12-21 中突出显示的代码到list.js
。
清单 12-21。 添加别名使后续代码更容易编写
(function () {
"use strict";
var data = Clok.Data;
var storage = data.Storage;
WinJS.UI.Pages.define("/pages/timeEntries/list.html", {
// SNIPPED
});
})();
用清单 12-22 中的代码替换list.js
中页面定义的默认内容。
清单 12-22。 页面定义为我们的第一次迭代
ready: function (element, options) {
this.setupListViewBinding(options);
timeEntriesListView.winControl.layout = new WinJS.UI.ListLayout();
},
setupListViewBinding: function (options) {
var results = storage.timeEntries.getSortedFilteredTimeEntriesAsync();
if (results.length <= 0) {
this.updateResultsArea(noMatchesFound);
} else {
this.updateResultsArea(timeEntriesListView);
}
timeEntriesListView.winControl.itemDataSource = results.dataSource;
},
updateResultsArea : function (div) {
var allDivs = WinJS.Utilities.query("#timeEntriesListView, "
+ "#noMatchesFound, "
+ "#searchError, "
+ "#searchInProgress");
allDivs.addClass("hidden");
if (div) {
WinJS.Utilities.removeClass(div, "hidden");
}
timeEntriesListView.winControl.forceLayout();
},
因为这在很大程度上类似于我已经在第七章和第十一章中解释的内容,所以我将快速指出几件事。我已经将ListView
控件设置为使用ListLayout
,而不是默认的GridLayout
。顾名思义,我们的ListView
中的项目将从上到下显示在一个列表中,而不是像项目页面上那样从左到右显示在一个网格中。我还添加了一个名为updateResultsArea
的函数,它将通过添加或删除一个名为hidden
的 CSS 类来显示或隐藏ListView
和在清单 12-17 中添加的各种状态div
元素,这个 CSS 类在清单 12-18 中定义。最后,在updateResultsArea
函数中,我调用了ListView
的forceLayout
方法。当一个ListView
被隐藏时,WinJS 停止跟踪布局信息,所以当你改变一个ListView
的显示时,你应该调用这个方法。更多关于forceLayout
的信息可以在http://msdn.microsoft.com/en-us/library/windows/apps/hh758352.aspx
的 MSDN 上找到。
运行 Clok 并导航至“时间表”页面。你应该看到我们之前添加的所有临时数据,在一个格式良好的列表中(见图 12-6 )。大部分是。我们还需要添加一些内容来适当地格式化日期和时间。
图 12-6 。需要一点点工作,但差不多了
为了解决日期和时间格式问题,我们将再次使用绑定转换器。让我们通过添加清单 12-23 中突出显示的代码到timeEntry.js
来定义一些转换器。
清单 12-23。 为日期和时间绑定转换器
var secondsToHoursConverter = WinJS.Binding.converter(function (s) {
return (s / 3600).toFixed(2);
});
var dateToDayConverter = WinJS.Binding.converter(function (dt) {
return formatDate("day", dt);
});
var dateToMonthConverter = WinJS.Binding.converter(function (dt) {
return formatDate("month.abbreviated", dt);
});
var dateToYearConverter = WinJS.Binding.converter(function (dt) {
return formatDate("year", dt);
});
var formatDate = function (format, dt) {
var formatting = Windows.Globalization.DateTimeFormatting;
var formatter = new formatting.DateTimeFormatter(format)
return formatter.format(dt);
}
WinJS.Namespace.define("Clok.Data", {
TimeEntry: timeEntryClass,
DateToDayConverter: dateToDayConverter,
DateToMonthConverter: dateToMonthConverter,
DateToYearConverter: dateToYearConverter,
SecondsToHoursConverter: secondsToHoursConverter,
});
接下来,为list.html
中的每个数据绑定元素指定要使用的转换器(参见清单 12-24 中突出显示的代码)。
清单 12-24。 使用新转换器
<div class="timeEntryItem">
<div class="timeEntryItem-dateWorked">
<h5 class="timeEntryItem-dateWorked-mon"
data-win-bind="textContent: dateWorked Clok.Data.DateToMonthConverter "></h5>
<h2 class="timeEntryItem-dateWorked-day"
data-win-bind="textContent: dateWorked Clok.Data.DateToDayConverter "></h2>
<h5 class="timeEntryItem-dateWorked-year"
data-win-bind="textContent: dateWorked Clok.Data.DateToYearConverter "></h5>
</div>
<div class="timeEntryItem-projectInfo">
<h3 class="timeEntryItem-projectName win-type-ellipsis"
data-win-bind="textContent: project.name"></h3>
<h6 class="timeEntryItem-clientName win-type-ellipsis"
data-win-bind="textContent: project.clientName"></h6>
<h6 class="timeEntryItem-projectNumber"
data-win-bind="textContent: project.projectNumber"></h6>
</div>
<div class="timeEntryItem-timeWorked">
<h2 class="timeEntryItem-timeWorked-elapsed"
data-win-bind="textContent: elapsedSeconds Clok.Data.SecondsToHoursConverter "></h2>
<h5>hours</h5>
</div>
</div>
如果您再次运行 Clok,时间条目列表的格式应该与图 12-2 中的模型略有不同,但是图 12-7 中的当前状态看起来相当不错。
图 12-7 。一个格式良好的时间条目列表
获取时间输入数据,这次是有承诺的
应用的当前状态工作得很好,就像它过滤一些已经在内存中的时间条目记录一样。然而,实际上,您的应用可能正在从远程数据源检索数据,这需要几秒钟或更长的时间来响应。我们将通过从getSortedFilteredTimeEntriesAsync
函数返回一个WinJS.Promise
对象来处理这个问题。更改storage.js
中的getSortedFilteredTimeEntriesAsync
函数,使其与清单 12-25 中的代码匹配,注意突出显示的代码。
清单 12-25。 一诺千金
storage.timeEntries.getSortedFilteredTimeEntriesAsync = function (begin, end, projectId) {
return new WinJS.Promise(function (complete, error) {
setTimeout(function () {
try {
var filtered = this
.createFiltered(function (te) {
if (begin) {
if (te.dateWorked < begin) return false;
}
if (end) {
if (te.dateWorked >= end.addDays(1)) return false;
}
if (projectId && !isNaN(projectId) && Number(projectId) > 0) {
if (te.projectId !== Number(projectId)) return false;
}
if (te.project.status !== Clok.Data.ProjectStatuses.Active) {
return false;
}
return true;
});
var sorted = filtered.createSorted(storage.compareTimeEntries);
//// simulate a delay
//for (var i = 1; i <= 50000000; i++) { }
//// simulate an error
//throw 0;
complete(sorted);
} catch (e) {
error(e);
}
}.bind(this), 10);
}.bind(this));
};
storage.timeEntries.getById = function (id) {
// SNIPPED
};
方法的主体大部分和以前一样。我只是将以前的方法体包装在一个新的Promise
中。我没有返回sorted
,而是将sorted
传递给Promise
构造函数中提供的complete
方法。对setTimeout
的调用导致这段代码立即返回,然后异步执行。
完整的处理程序
你可能已经注意到清单 12-25 中的一些注释语句。一个用于模拟获得结果的延迟,另一个用于模拟错误。现在,取消对模拟延迟的注释(参见清单 12-26 )。在完成本节中的其余代码后,您需要回到这行代码,重新注释或删除它。
清单 12-26。 模拟一次延迟
//// simulate a delay
for (var i = 1; i <= 50000000 ; i++) { }
注意这是一种不精确的模拟延迟的方式。根据你的电脑,你可能要调整循环的上端:太小,你看不到进度条;太大,你会因为看到进度条的时间太长而感到沮丧。
既然getSortedFilteredTimeEntriesAsync
函数返回了一个Promise
,我们必须改变我们在list.js
中进行数据绑定的方式。用清单 12-27 中的代码更新setupListViewBinding
。
清单 12-27。 履行诺言时具有约束力
setupListViewBinding: function (options) {
this.updateResultsArea(searchInProgress);
storage.timeEntries.getSortedFilteredTimeEntriesAsync()
.then(
function complete(results) {
if (results.length <= 0) {
this.updateResultsArea(noMatchesFound);
} else {
this.updateResultsArea(timeEntriesListView);
}
timeEntriesListView.winControl.itemDataSource = results.dataSource;
}.bind(this),
function error(results) {
this.updateResultsArea(searchError);
}.bind(this)
);
},
我们来走一遍这个版本的setupListViewBinding
。第一条语句将向用户显示一条消息,表明正在进行搜索。调用getSortedFilteredTimeEntriesAsync
返回一个Promise
,而不是数据,所以我在then
方法的一个参数中处理了数据绑定。第一个参数是当承诺已经实现并且结果可用时要调用的函数。如果有结果,我显示ListView
,如果没有,我向用户显示适当的消息。
注意在这个例子中,我将两个处理函数命名为
complete
和error
。实际上,你可以给这些起任何对你有意义的名字。名字无关紧要。其实完全可以省略名字。例如,我可以写function (results) {...}
而不是function complete(results) {...}
。在这种情况下,名称是为了更容易理解。
现在运行 Clok。假设你已经在清单 12-26 的中将模拟延迟设置为一个合适的数字,你将会看到一个“进行中”的信息和进度条(见图 12-8 ),最终,你会看到之前在图 12-7 中看到的相同的时间条目列表。
图 12-8 。搜索时间条目—模拟延迟
此时,我们已经看到时间表页面处于“进行中”状态和“找到结果”状态。让我们做一个快速、临时的更改,这样我们就可以确保“没有找到结果”状态也能像预期的那样工作。对于这个测试,改变对list.js
中getSortedFilteredTimeEntriesAsync
的调用,以便它搜索遥远未来某个日期之后的时间条目(参见清单 12-28 中突出显示的代码)。
清单 12-28。 为 Begin 参数指定一个值到遥远未来的某个日期
storage.timeEntries.getSortedFilteredTimeEntriesAsync( new Date("1/1/2500") )
从getSortedFilteredTimeEntriesAsync
返回的Promise
对象仍将被实现,并将成功完成。然而,因为我们没有在我们的临时数据中提前定义任何条目,所以不会找到任何结果(见图 12-9 )。
图 12-9 。找不到 2500 年 1 月 1 日之后的时间条目
错误处理程序
目前为止,一切顺利。我们尚未处理的唯一情况是从getSortedFilteredTimeEntriesAsync
返回的Promise
对象处于错误状态。因为你不再需要在检索时间输入数据时模拟延迟,你现在可以删除,或者重新注释清单 12-26 中的代码。另外,一定要撤销在清单 12-28 中所做的更改,测试返回一个没有结果的承诺。现在,我们想模拟一个错误,所以切换回storage.js
,取消清单 12-29 中代码的注释。
清单 12-29。 模拟错误
//// simulate an error
throw 0;
现在,当你运行 Clok 时,在清单 12-27 中定义的error
处理程序将被调用,而不是complete
处理程序。在 Clok 中,我们简单地向用户显示一条友好的消息(见图 12-10 )。在其他应用中,我们可能会使用它来记录错误,或者将错误信息报告给我们的开发团队。
图 12-10 。有一个错误
此时,您已经实现了处理时间表页面可能处于的各种状态的代码:搜索、找到并显示结果、没有找到结果以及错误状态。继续删除或重新注释清单 12-29 中的代码,因为不再需要模拟错误。
过滤时间条目
我已经提到过getSortedFilteredTimeEntriesAsync
可以按日期或项目过滤时间条目。事实上,我们在清单 12-28 中使用了这个特性来做一些测试。现在是时候向 Clok 添加特性了,这样用户就可以以一种有意义的方式过滤时间条目。在本节中,我们将为用户提供两种过滤数据的方法。首先,我们将向项目详细信息页面添加一个按钮,以查看所选项目的时间条目。然后,我们将向时间表页面添加一些过滤控件,以便用户可以在查看时间条目时指定日期范围或项目。
从项目详细信息中筛选
您可能想知道为什么我要将页面的options
参数传递给清单 12-27 中的setupListViewBinding
函数。当我们从项目详细信息页面导航到时间表页面时,必须提供当前选择的项目,并且它将在这个options
对象中传递。在我们修改项目细节页面之前,让我们对timeEntries
文件夹中的list.js
文件做一些必要的修改。对清单 12-30 至list.js
中突出显示的内容进行更改。
清单 12-30。 对 list.js 的修改
setupListViewBinding: function (options) {
this.filter = WinJS.Binding.as({
startDate: (options && options.startDate) || (new Date()).addMonths(-1),
endDate: (options && options.endDate) || new Date().removeTimePart(),
projectId: (options&&
**options.projectId) || -1**
**});**
**this.filter.bind("startDate", this.filter_changed.bind(this));**
**this.filter.bind("endDate", this.filter_changed.bind(this));**
**this.filter.bind("projectId", this.filter_changed.bind(this));**
**},**
**filter_changed** `: function (e) {`
`this.updateResultsArea(searchInProgress);`
**storage.timeEntries.getSortedFilteredTimeEntriesAsync(**
**this.filter.startDate,**
**this.filter.endDate,**
**this.filter.projectId)**
`.then(`
`function complete(results) {`
`if (results.length <= 0) {`
`this.updateResultsArea(noMatchesFound);`
`} else {`
`this.updateResultsArea(timeEntriesListView);`
`}`
`timeEntriesListView.winControl.itemDataSource = results.dataSource;`
`}.bind(this),`
`function error(results) {`
`this.updateResultsArea(searchError);`
`}.bind(this)`
`);`
`},`
这段代码为
setupListViewBinding指定了一个新的定义,它声明了一个可观察的
filter属性。另外,先前对
setupListViewBinding函数的定义已经转移到一个名为
filter_changed的新处理函数中。这与我们在第十一章中为过滤项目所做的非常相似。不同的是,在这个页面上,
filter可观察对象有三个属性可以改变:
startDate、
endDate和
projectId。当这些属性中的任何一个发生变化时,就会调用新的
filter_changed函数,并检索一组新的时间条目。新的
filter_changed函数和以前的
setupListViewBinding函数之间的唯一区别是各种
filter属性作为参数被提供给
getSortedFilteredTimeEntriesAsync`的调用。
注意如果我不知道我们将在下一节中添加的功能需求,那么清单 12-30 中的代码可能会简单一点。然而,因为我们将在下一节中利用所有这些变化,所以我现在添加了它们。
因此,我们的时间表页面现在被配置为支持从 Clok 中的不同位置进行过滤。现在,我们只需添加允许用户请求时间条目过滤列表的功能。打开projects
文件夹中的detail.html
文件,将清单 12-31 中高亮显示的代码添加到projectDetailAppBar
中。
清单 12-31。 向应用栏添加考勤表按钮
<div id="projectDetailAppBar"
class="win-ui-dark"
data-win-control="WinJS.UI.AppBar"
data-win-options="{ sticky: true }">
<!-- SNIPPED -->
<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:'goToTimeEntriesCommand',
label:'Time Sheet',
icon:'url(/img/Timesheet-small-sprites.png)',
section:'selection',
tooltip:'Time Entries',
disabled: true}">
</button>
</div>
注意,我为这个新的AppBarCommand
的icon
属性指定了一个图像的路径。除了在WinJS.UI.AppBarIcon
枚举中定义的所有图标,AppBarCommand
对象可以使用自定义图像。对于 Clok,我创建了一个名为Timesheet-small-sprites.png
的特殊格式的图像(见图 12-11 )。
图 12-11 。用于 AppBarCommand 图标的图像
这个图像有一个透明的背景,由两行组成,每行有四个版本的图标。当AppBarCommand
处于正常状态时使用第一行,当其处于切换状态时使用第二行。因为这个AppBarCommand
不需要切换状态,所以我没有在第二行创建图像。每个图标为 40×40 像素,整个图像为 160×80。从左到右,每行的四个图像分别用于按钮的默认状态、悬停状态、活动状态(当它被单击时)和禁用状态。因为 WinJS 会自动在图像周围添加圆环,所以只需要图标本身。AppBarCommand.icon
遗产的文件可在http://msdn.microsoft.com/en-us/library/windows/apps/hh700483.aspx
的 MSDN 上获得。此外,当我第一次研究这个问题时,我发现这篇博客文章特别有帮助:http://blogs.msdn.com/b/shawnste/archive/2012/06/16/custom-appbar-sprite-icons-for-your-windows-8-metro-style-html-app.aspx
。
最后一步是在detail.js
中连线goToTimeEntriesCommand
。用清单 12-32 中突出显示的代码更新detail.js
。
清单 12-32。 接线新的 AppBarCommand
ready: function (element, options) {
// SNIPPED
saveProjectCommand.onclick = this.saveProjectCommand_click.bind(this);
deleteProjectCommand.onclick = this.deleteProjectCommand_click.bind(this);
goToTimeEntriesCommand.onclick = this.goToTimeEntriesCommand_click.bind(this);
},
// SNIPPED
deleteProjectCommand_click: function (e) {
storage.projects.delete(this.currProject);
WinJS.Navigation.back();
},
goToTimeEntriesCommand_click: function (e) {
if (this.currProject &&
**this.currProject.id) {**
**WinJS.Navigation.navigate("/pages/timeEntries/list.html",**
**{ projectId: this.currProject.id });**
**}**
**},**
`// SNIPPED`
`configureAppBar: function (existingId) {`
`// SNIPPED`
`if (existingId) {`
`deleteProjectCommand.winControl.disabled = false;`
**goToTimeEntriesCommand.winControl.disabled = false;**
`}`
`},`
`// SNIPPED`
`我们终于准备好测试这些变化了。立即运行 Clok 并转到现有项目的项目详细信息页面。如果你查看应用栏,你应该看到时间表命令被激活(见图 12-12 )。
图 12-12 。项目详细信息页面上的新 appbar 命令
我们在清单 12-31 和清单 12-32 中添加的代码将导致该按钮在添加新项目时被禁用,而在查看现有项目的详细信息时被启用。正如您可能期望的那样,点击 new 按钮将导航到时间表页面,为当前选择的项目提供projectId
作为导航选项(参见图 12-13 )。
图 12-13 。所选项目的时间条目
使用应用栏过滤
能够看到单个项目的时间条目列表非常方便。作为一个 Clok 用户,我可以看到自己经常使用这个功能。但是,目前没有办法按日期过滤时间条目列表,为了查看不同项目的条目,您必须导航回项目页面,从列表中选择不同的项目,然后单击时间表按钮。这是很好的功能,但还不够。我们必须添加一种方法来过滤时间表页面上的时间条目。将清单 12-33 中的代码添加到list.html
中的开始body
元素之后。
清单 12-33。 添加应用栏和弹出按钮
<div id="filterFlyout"
data-win-control="WinJS.UI.Flyout">
<label for="filterStartDate">From</label><br />
<div id="filterStartDate" data-win-control="WinJS.UI.DatePicker"></div>
<br />
<label for="filterEndDate">To</label><br />
<div id="filterEndDate" data-win-control="WinJS.UI.DatePicker"></div>
<hr />
<label for="filterProjectId">Project</label><br />
<select id="filterProjectId">
<option value="-1">All projects</option>
</select>
<br />
<button id="clearFilterButton"> Clear Filter</button>
</div>
<div id="timeEntryAppBar"
class="win-ui-dark"
data-win-control="WinJS.UI.AppBar"
data-win-options="{ sticky: true }">
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'filterTimeEntriesCommand',
label:'Filter',
icon:'filter',
type: 'flyout',
flyout: 'filterFlyout',
section:'global',
tooltip:'Filter'}">
</button>
</div>
我已经向时间表页面添加了一个AppBar
,它包含一个过滤器按钮。单击过滤器按钮将打开一个弹出菜单,其中有几个选项用于过滤ListView
中显示的时间条目。现在,将清单 12-34 中的简单 CSS 添加到list.css
中。
清单 12-34。 样式清晰滤镜按钮
#clearFilterButton {
border: 0px;
background-color: transparent;
float: right;
}
#clearFilterButton:active {
color: #666666;
}
有了这些控件,它们的初始值必须设置为与filter
的当前值相匹配。此外,当这些控件的值发生变化时,我们必须更新filter
。这两项任务都将通过将清单 12-35 中高亮显示的代码添加到list.js
中的setupListViewBinding
函数中来完成。
清单 12-35。 连接用于过滤的控件
setupListViewBinding: function (options) {
this.filter = WinJS.Binding.as({
startDate: (options && options.startDate) || (new Date()).addMonths(-1),
endDate: (options && options.endDate) || new Date().removeTimePart(),
projectId: (options && options.projectId) || -1
});
this.filter.bind("startDate", this.filter_changed.bind(this));
this.filter.bind("endDate", this.filter_changed.bind(this));
this.filter.bind("projectId", this.filter_changed.bind(this));
filterStartDate.winControl.current = this.filter.startDate;
filterEndDate.winControl.current = this.filter.endDate;
filterProjectId.value = this.filter.projectId;
filterStartDate.winControl.onchange = this.filterStartDate_change.bind(this);
filterEndDate.winControl.onchange = this.filterEndDate_change.bind(this);
filterProjectId.onchange = this.filterProjectId_change.bind(this);
clearFilterButton.onclick = this.clearFilterButton_click.bind(this);
},
接下来,我们必须定义清单 12-35 中引用的事件处理程序。此外,我们必须用当前活动的项目填充项目过滤器下拉列表,就像 Clok 仪表板屏幕上的下拉列表一样。在定义了filter_changed
之后,添加新的事件处理程序和新的bindListOfProjects
函数,如清单 12-36 所示。
清单 12-36。list . js 中的新功能
filter_changed: function (e) {
// SNIPPED
},
filterStartDate_change: function (e) {
this.filter.startDate = filterStartDate.winControl.current;
},
filterEndDate_change: function (e) {
this.filter.endDate = filterEndDate.winControl.current;
},
filterProjectId_change: function (e) {
this.filter.projectId = filterProjectId.value;
},
clearFilterButton_click: function (e) {
filterStartDate.winControl.current = new Date().addMonths(-1);
filterEndDate.winControl.current = new Date().removeTimePart();
filterProjectId.value = -1;
this.filterStartDate_change();
this.filterEndDate_change();
this.filterProjectId_change();
},
bindListOfProjects: function (selectControl) {
selectControl.options.length = 1;
var activeProjects = storage.projects.filter(function (p) {
return p.status === Clok.Data.ProjectStatuses.Active;
});
activeProjects.forEach(function (item) {
var option = document.createElement("option");
option.text = item.name + " (" + item.projectNumber + ")";
option.title = item.clientName;
option.value = item.id;
selectControl.appendChild(option);
});
},
当任何一个过滤器控件被改变时,当前filter
对象上的相应属性被设置,这将触发filter_change
方法。单击清除滤镜按钮,简单地将滤镜控件重置为默认值,并更新当前的filter
对象。bindListOfProjects
功能与home.js
中的同名功能几乎相同。唯一的区别是提供了一个下拉列表控件作为函数的参数,这允许在实现编辑现有时间条目的功能时重用bindListOfProjects
,这可以在本书附带的源代码中看到。最后需要做的改变是在ready
函数中调用bindListOfProjects
函数。将清单 12-37 中突出显示的代码添加到list.js
中的ready
函数中。
清单 12-37。 向 filterProjectId 添加项目
ready: function (element, options) {
this.bindListOfProjects(filterProjectId);
this.setupListViewBinding(options);
timeEntriesListView.winControl.layout = new WinJS.UI.ListLayout();
},
这样,您现在在时间表页面上就有了一个全功能的过滤器。运行 Clok 并尝试一下。选择不同的日期,选择一个项目,然后单击“清除过滤器”按钮。一旦您对其中一个过滤器控件进行了更改,ListView
会立即更新匹配结果(参见图 12-14 )。
图 12-14 。“时间表”页面上使用的过滤器弹出按钮
删除时间条目
我们可以创建,也可以阅读时间条目;我们已经实现了一半的 CRUD 操作。现在让我们给用户删除的能力。回到清单 12-17 中的,我们配置了ListView
来支持多重选择。在本节中,我们将利用这一点,允许用户选择一个或多个要删除的时间条目。让我们从在应用栏中添加一个删除按钮开始。在list.html
的timeEntryAppBar
控件中添加清单 12-38 中突出显示的代码。
清单 12-38。 向应用栏添加删除按钮
<div id="timeEntryAppBar"
class="win-ui-dark"
data-win-control="WinJS.UI.AppBar"
data-win-options="{ sticky: true }">
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'deleteTimeEntriesCommand',
label:'Delete',
icon:'delete',
section:'selection',
tooltip:'Delete',
disabled: true}">
</button>
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'filterTimeEntriesCommand',
label:'Filter',
icon:'filter',
type: 'flyout',
flyout: 'filterFlyout',
section:'global',
tooltip:'Filter'}">
</button>
</div>
想一想 Windows 8 开始屏幕是如何工作的。当您在开始屏幕上选择一个或多个互动程序时,会出现一个应用栏,其中包含适合您选择的命令。我们对 Clok 的要求也差不多。默认情况下,我们刚刚添加的删除按钮是禁用的,但是当选择一个或多个时间条目时,应该显示应用栏,并且应该启用删除按钮。将清单 12-39 中的代码添加到list.js
中。
清单 12-39。 配置应用栏,基于当前选择
timeEntriesListView_selectionChanged: function (e) {
var selectionCount = timeEntriesListView.winControl.selection.count();
if (selectionCount <= 0) {
deleteTimeEntriesCommand.winControl.disabled = true;
timeEntryAppBar.winControl.hide();
} else {
deleteTimeEntriesCommand.winControl.disabled = false;
timeEntryAppBar.winControl.show();
}
},
每次用户选择或取消选择ListView
中的项目时,该功能将检查当前选择了多少个项目。如果选择了一个或多个项目,将显示应用栏,并启用删除按钮(参见图 12-15 )。
图 12-15 。任何选择都会显示应用栏并启用删除按钮
当用户点击删除按钮时,我们当然希望从列表中删除时间条目。在我们定义Storage
类中的delete
函数之前,让我们看看当用户点击删除按钮时将执行的函数。将清单 12-40 中的代码添加到list.js
中。
清单 12-40。 点击处理删除按钮
deleteTimeEntriesCommand_click: function (e) {
timeEntriesListView.winControl.selection.getItems()
.then(function complete(selectedItems) {
var deletePromises = selectedItems.map(function (item) {
return storage.timeEntries.delete(item.data);
});
return WinJS.Promise.join(deletePromises);
})
.then(null, function error(result) {
new Windows.UI.Popups
.MessageDialog("Could not delete all selected records.", "An error occurred. ")
.showAsync();
});
},
ListView
类有一个selection
属性,该属性又有一个getItems
函数,用于标识ListView
中的哪些项目当前被选中。然而,getItems
函数实际上并不返回被选中的项目。相反,它返回一个Promise
对象。当 promise 完成时,它的then
方法被调用,选择的项目作为参数传递给complete
函数,即then
的第一个参数。我们将要添加到Storage
类中的delete
函数也将返回一个Promise
(我们将很快定义它),并且,使用map
方法,用户的选择被映射到一个表示删除操作的Promise
对象的数组。第一个then
调用返回一个用join
方法从该数组创建的新的Promise
。当所有输入的Promise
对象被满足时,从join
创建的Promise
被满足。如果成功删除所有选定的时间条目,第二个then
函数不做任何事情,因为它没有complete
函数(第一个参数是null
)。但是,如果有任何错误,将向用户显示一条消息。
将清单 12-41 中高亮显示的代码添加到list.js
中的ready
函数中,以连接timeEntriesListView_selectionChanged
和deleteTimeEntriesCommand_click
处理程序。
清单 12-41。 连接起事件处理程序
ready: function (element, options) {
this.bindListOfProjects(filterProjectId);
this.setupListViewBinding(options);
timeEntriesListView.winControl.onselectionchanged =
this.timeEntriesListView_selectionChanged.bind(this);
timeEntriesListView.winControl.layout = new WinJS.UI.ListLayout();
deleteTimeEntriesCommand.winControl.onclick =
this.deleteTimeEntriesCommand_click.bind(this);
},
现在,我们需要在Storage
类中定义delete
函数。将清单 12-42 中的代码添加到storage.timeEntries.save
定义后的storage.js
中。
清单 12-42。 有删除时间条目的功能
storage.timeEntries.delete = function (te) {
var canceled = false;
var delPromise = new WinJS.Promise(function (complete, error) {
setTimeout(function () {
try {
if (te && te.id) {
var index = this.indexOf(te);
if (!canceled && index >= 0) {
this.splice(index, 1);
}
}
complete();
} catch (e) {
error(e);
}
}.bind(this), 100);
}.bind(this),
function oncancel(arg) {
canceled = true;
}.bind(this));
return WinJS.Promise.timeout(20, delPromise);
};
当然,在我们使用内存数据的简单例子中,这段代码是多余的。我们可以删除除了突出显示的代码之外的所有内容,然后就到此为止。然而,有两个原因使我不打算那样做。首先,我们不会总是像这样使用内存中的数据。其次,也是更重要的,我正在写关于承诺的一章,所以我必须创建一个例子来说明如何创建一个Promise
并取消它。
让我们浏览一下清单 12-42 中的代码。我已经创建了一个新的Promise
,在setTimeout
中定义的 100 毫秒延迟之后,它实际上删除了数据,假设canceled
变量仍然是false
。如果成功,调用complete
方法,将成功信号返回给我们在清单 12-40 中的代码。如果有任何错误,Promise
被置于错误状态,最终导致对话框显示给用户。在这个例子中,我在Promise
构造函数中加入了第二个参数。如果Promise
由于某种原因需要取消任何剩余的工作,就会调用oncancel
函数。
有两种方法可以使用WinJS.Promise.timeout
功能。第一种情况是只提供第一个参数,并且该参数是数值型的。在这种情况下,成功完成的Promise
在指定的延迟后返回。我在清单 12-42 中使用的WinJS.Promise.timeout
的第二个版本有两个参数。第一个还是一个数字,第二个是另一个Promise
。如果在指定的毫秒数过去之前Promise
没有完成,则Promise
被取消。在本例中,我取消了Promise
,它在 20 毫秒后删除数据。因为在那个Promise
开始执行之前有 100 毫秒的延迟,所以每次都会被取消。运行 Clok 并选择一些时间条目,然后单击删除按钮。因为超时发生在Promise
执行之前,所以Promise
被取消,这被捕获为一个错误。然后,向用户显示一条消息(见图 12-16 )。
图 12-16 。通知用户删除操作失败的错误消息
我希望大家清楚,这种被迫的失败只是为了展示这是如何工作的。在我们的delete
函数中有代码来处理需要取消Promise
的情况是很好的,在我们的删除按钮的点击处理程序中有代码来处理错误也是很好的。对依赖于外部资源的代码设置超时可能是一个好的做法,如果执行时间太长,可能必须取消这些外部资源。然而,拥有一个WinJS.Promise
并不太实际。timeout
在其有机会执行之前取消删除操作。因此,用清单 12-43 中突出显示的代码更新storage.js
中的delete
函数。
清单 12-43。 更新删除功能
storage.timeEntries.delete = function (te) {
// SNIPPED
return delPromise;
};
现在这个函数返回未取消的Promise
,您可以运行 Clok 并删除一些时间条目。你应该可以删除任意多的内容,一次删除一个或者一次删除几个(见图 12-17 )。您可以通过在触摸屏上滑动或用鼠标右键单击来选择多个项目。
图 12-17 。除了一个时间条目外,其他条目都已删除
注意尽管本书中没有涉及,但编辑现有时间条目的功能包含在本书随附的源代码中。
结论
这是另一个包含大量代码的章节。在添加添加、查看和删除时间条目的能力的过程中,我介绍了几种不同的方法来利用WinJS.Promise
类,以允许您的应用在等待异步操作完成时保持响应。当从 Clok dashboard 屏幕保存新的时间条目时,我介绍了如何协调两个不同的承诺并执行一些代码,一旦两个承诺都实现了,就使用join
函数。在查看和过滤时间条目的过程中,我讲述了如何创建自己的Promise
对象来异步执行一些代码,返回那个Promise
,并处理Promise
可能处于的各种可能状态。我还讲述了如何定义一个支持被取消的Promise
,以及如何使用Promise.timeout
来取消一个响应不够快的Promise
。承诺可以为处理异步代码的结果提供一个很好的语法。在下一章,我将介绍 web workers,它将允许您运行多线程异步代码。``
十三、Web 工作器
在第十二章中,我介绍了承诺作为一种处理异步操作返回值的方法。虽然承诺在如何进行异步工作方面提供了很大的灵活性,但我指出异步并不意味着多线程。JavaScript 应用在单线程环境中执行。异步并不意味着“同时做两件事”相反,它的意思是“以后再做这件事,完成后让我知道发生了什么。”也就是说,在多线程上执行的应用有几个优点,例如利用多个处理器来更快地执行任务或在后台执行一些工作,同时用户可以继续使用应用的其他部分。
那么,在开发 Windows 应用商店应用时,我们有什么选择呢?嗯,我们可以用 C#或 C++编写部分应用作为 WinRT 组件,它们支持线程。事实上,这篇关于 MSDN 的文章讨论了这个选项。我不会在本书中涉及这些细节,尽管我会在第十八章中介绍用 C#构建 WinRT 组件。不是 C#或 C++开发人员?幸运的是,HTML5 提供了 Web Workers 作为一种新的选择。
Web 工作器
worker(或者更正式地说,Web Worker)是一个在后台运行的 JavaScript 脚本。Workers 是 HTML5 的一个特性,并不是专门用来用 HTML 和 JavaScript 构建 Windows Store 应用的。如果你曾经在一个 web 开发项目中与工人一起工作过,那么你可以跳过这一章。如果你还不熟悉 workers,那么本章将提供一个基本的介绍,从清单 13-1 开始,它展示了创建一个 worker 所需的代码。
清单 13-1。 创建一个 Web 工作器的实例
var myWorker = new Worker("/path/to/worker/script.js");
工作线程在单独的线程上运行。然而,JavaScript 是一个单线程环境,所以每个 worker 都在自己的环境中执行。结果,一个工作器不能访问主 UI 线程上的任何对象,比如document
对象。它不能操纵 DOM。它不能改变页面上 JavaScript 变量的值。它完全孤立在自己的环境中。
这对线程安全来说非常好。你永远不必担心冲突,因为两个工人改变了同一个变量。您不需要处理竞争情况——两个或多个线程如果没有按照正确的顺序完成,将会导致意外的结果。你不用锁任何东西。您不必担心在其他编程环境中使多线程开发具有挑战性的许多概念。也就是说,一个不能改变主线程上任何东西的脚本有多大用处?当然,在某些情况下,当您想要“启动并忘记”时,它可能会有所帮助,但是要实际使用它们,如果有某种方法可以从工作线程影响您的主线程,那就更好了。
这就是信息的来源。虽然工作线程不能改变主线程上的任何东西,反之亦然,但消息可以从一个线程传递到另一个线程。清单 13-2 包含了一个如何配置主线程与工作线程通信的例子。
清单 13-2。 在主线程和工作线程之间传递消息
myWorker.onmessage = function(e) {
var messageFromWorker = e.data;
// do something with the message
};
// send a message to the worker thread
myWorker.postMessage(messageToWorker);
在这个进程的另一边,在 worker 线程中,有一个类似的onmessage
事件和postMessage
方法(参见清单 13-3 )。
清单 13-3。 处理和发送来自工作线程的消息
self.onmessage = function (e) {
processTheData(e.data);
};
self.postMessage(responseMessage);
尽管我们现在有两个 JavaScript 环境,每个环境都执行自己的脚本,但是每个环境仍然是单线程的。迷茫?我用一个类比来解释这是怎么回事。
想象一下,你和我都在同一个团队工作,只不过我在不同的大楼工作,我们从来没有见过面,也没有当面说过话。在这个虚构的场景中,我们唯一的交流方式是通过办公室间的 messenger 互相发送消息。当你需要我的东西时,你给信差一个便条(叫worker.postMessage
)。他或她把它放在我的桌子上,只要我能拿到它,我就拿着它(self.onmessage
)开始做你在信息中要求的工作。当我完成后,我让信使把结果送还给你(self.postMessage
)。当你看到结果(worker.onmessage
)并决定你想用它们做什么时,你就完成了这个循环。只要我们能够就如何以对方能够理解的方式写笔记达成一致,我们就可以各自做好自己的工作(各自在单线程环境中),而不会妨碍到对方。
绘制时间条目
在第十二章中,我们给 Clok 增加了一些功能,允许用户输入他们在一个项目上工作的时间。如果用户可以看到一个图表,说明他们在每个客户端上花费了多少时间,这将是很有帮助的。在本章中,我将向您展示如何使用 Web Workers 实现这一点,使用一个名为 Flotr2 ( www.humblesoftware.com/flotr2/
)的开源图形库。
注Web 超文本应用技术工作组(WHATWG),这个负责开发 HTML 规范的组织,包括 Web 工作器,已经提供了一些关于工作器的指导。它说,工人“体重相对较重”,而且“有望长寿”虽然我们的图形示例将说明开发人员如何利用工作人员,但它不是工作人员的理想用例,因为这个过程实际上非常快。在
www.whatwg.org/specs/web-apps/current-work/multipage/workers.html
在线阅读 WHATWG HTML 规范。
在我们开始之前,让我们确定时间入口图的一些要求:
- 应该创建一个条形图来表示时间条目。它应该显示在自己的页面上,从时间表页面链接。
- 应用于时间表页面的任何过滤器也应该应用于图表。
- 图表的数据点应按客户分组。也就是说,如果用户在同一个客户的多个项目上工作,在这些项目上工作的时间将被合并到图上的单个条中。
没有 Web 工作器的情况下开始
在timeEntries
文件夹中,添加一个名为graph.html
的新页面控件。顾名思义,这个页面将显示您将要添加的图表。开发这个特性的第一次迭代将集中在获得一个正确格式化的测试图,以显示在页面上。我们将从直接硬编码在页面控件的 JavaScript 文件中的图形数据开始。
Flotr2 图形库将在它动态创建的canvas
元素上生成图形。我们只需要在页面上提供一个包含这个新的canvas
元素的占位符。用清单 13-4 中高亮显示的代码修改graph.html
的 body 元素。
清单 13-4。 为图形添加容器
<body>
<div class = "graph fragment">
<header aria-label = "Header content" role = "banner">
<button class = "win-backbutton" aria-label = "Back" disabled type = "button" > </button>
<h1 class = "titlearea win-type-ellipsis">
<span class = "pagetitle" > Time Entries</span>
</h1>
</header>
<section aria-label = "Main content" role = "main">
<div id = "graphcontainer" > </div>
</section>
</div>
</body>
接下来,对graph.css
进行修改,如清单 13-5 中突出显示的。这些规则将导致 Flotr2 渲染的图形显示在页面中央。
清单 13-5。 CSS 变化
.graph section[role = main] {
/* remove the CSS properties that were added to this rule by default */
}
#graphcontainer {
width: 70vw;
height: 70vh;
margin: 8px auto;
}
Flotr2 项目相当广泛,但我将只涉及完成本章目标所需的部分。请访问该项目的网站(www.humblesoftware.com/flotr2/
)以熟悉它所提供的功能。在浏览了文档和一些例子之后,下载名为flotr2.js
的 JavaScript 文件。或者,因为 Flotr2 的创建者已经发布了许可的开源许可证,你可以在本书的源代码中找到一个副本。(见该书的 Apress 产品页面的源代码/下载标签[ www.apress.com/9781430257790
]。)一旦你有了文件的副本,将flotr2.js
放在清单 13-6 中指定的路径中,然后添加对graph.html
的 JavaScript 引用。
清单 13-6。 引用图形库
<head>
<!-- SNIPPED -->
<script type = "text/javascript" src = "/js/lib/flotr2/flotr2.js" > </script>
</head>
我之前提到过,我们将使用硬编码数据构建我们的图形功能的第一次迭代。事实上,我们的测试图基于 Flotr2 堆叠条形图示例。将清单 13-7 中的代码添加到graph.js
的ready
函数中。
清单 13-7。 生成测试数据并显示图形
var d1 = [], d2 = [], d3 = [], graph, i;
for (i = -10; i < 10; i++) {
d1.push([i, Math.random()]);
d2.push([i, Math.random()]);
d3.push([i, Math.random()]);
}
var graphdata = [
{ data: d1, label: 'Series 1' },
{ data: d2, label: 'Series 2' },
{ data: d3, label: 'Series 3' }
];
var graphoptions = {
bars: {
show: true,
stacked: true,
horizontal: false,
barWidth: 0.6,
lineWidth: 1,
shadowSize: 0
},
legend: {
position: "ne",
backgroundColor: "#fff",
labelBoxMargin: 10,
},
grid: {
color: "#000",
tickColor: "#eee",
backgroundColor: {
colors: [[0, "#ddf"], [1, "#cce"]],
start: "top",
end: "bottom"
},
verticalLines: true,
minorVerticalLines: true,
horizontalLines: true,
minorHorizontalLines: true,
},
xaxis: {
color: "#fff",
},
yaxis: {
color: "#fff",
},
HtmlText: true
};
graph = Flotr.draw(graphcontainer, graphdata, graphoptions);
我不会详细讨论所有代码,因为 Flotr2 文档涵盖了大部分内容。我确实想快速介绍一下突出显示的代码语句。代码创建了三个数据序列:d1
、d2
和d3
。它向其中的每一个添加随机数据点。但是,请注意,这不是传递给draw
方法的数据对象。相反,这些序列中的每一个都被包装成一个包含label
属性的对象的data
属性,该属性将显示在图表的图例中。Flotr2 支持在这个对象上设置额外的属性,但是我们只利用了label
属性。一个名为graphdata
的包装对象数组被传递给draw
方法,同时传递的还有我们在清单 13-4 中添加到graph.html
的graphcontainer div
的引用。graphoptions
对象为我们的图表设置了许多显示属性,这些都记录在 Flotr2 网站上。
将导航选项添加到时间表页面是加载图表页面之前需要做的最后一件事。我已经用应用栏上的一个新按钮做到了这一点。遵循清单 12-31 和图 12-11 中使用的相同模式,在项目细节屏幕的应用栏中添加一个时间表按钮,在时间表页面上创建一个新的基于 sprite 的AppBarCommand
(参见图 13-1 )。
图 13-1 。时间表应用栏上的新图表按钮
注意如果你不想创建自己的图像,你可以使用我创建的版本。您可以在本书附带的源代码中找到它。(见该书的 Apress 产品页[
www.apress.com/9781430257790
]的源代码/下载标签)。)
当然,这个按钮需要一个为其click
事件定义的处理程序。将清单 13-8 中的代码添加到list.js
中。一定要将这个函数连接到ready
函数中的click
事件。
清单 13-8。 点击处理程序,将当前过滤器传递到图形页面
graphTimeEntriesCommand_click: function (e) {
WinJS.Navigation.navigate("/pages/timeEntries/graph.html", {
filter: this.filter,
});
},
注意如果你忘记了如何将
click
事件连接到这个处理程序,参考清单 12-32 中的例子。
虽然我们不会在本节中使用它,但是我们会将用户当前的时间表过滤器传递给graph.html
。这将确保用户在时间表页面上查看的任何时间条目都包含在图表中。在本章的后面,我将向您展示如何实现这一点。同时,你应该有一个工作测试图。运行 Clok 并点击时间表应用栏上的图表按钮,看看它看起来如何(参见图 13-2 )。
图 13-2 。我们的第一张图表
多亏了一些开源开发者的辛勤工作,我们有了一个看起来非常漂亮的图表,没有很多代码。接下来,让我们看看如何在这个特性中引入一个 Web Worker。
从 Web Worker 返回图形数据
我们的最终目标是基于在时间表页面上选择的过滤器,使用一个工人来计算图表应该显示的数据点。在这一节中,我们将继续使用硬编码的数据,但是我们将把生成数据的逻辑转移到一个 worker。
在 Visual Studio 项目的js
文件夹中创建一个名为workers
的文件夹。选择专用工人文件类型,并将名为timeGraphWorker.js
的工人添加到workers
文件夹中(参见图 13-3 )。
图 13-3 。添加工人
注值得注意的是,HTML5 规范定义了专用 Web Worker 和共享 Web Worker。共享工作线程为多个脚本连接到同一个工作线程提供了一种方式。但是,Windows 应用商店应用不支持共享工作线程。在这里,我只讨论敬业的员工。如果你正在做 web 开发,目标是支持共享工作器的有限的浏览器集,你会在网上找到更多的信息。
删除 worker 中的默认代码,并用清单 13-9 中的代码替换它。
清单 13-9。 我们工人的初稿
/// <reference group = "Dedicated Worker" />
importScripts(
"//Microsoft.WinJS.1.0/js/base.js",
"/js/extensions.js",
"/js/utilities.js",
"/data/project.js",
"/data/timeEntry.js",
"/data/storage.js"
);
(function () {
"use strict";
var data = Clok.Data;
var storage = data.Storage;
self.onmessage = function (e) {
getData(e.data);
};
function getData(messageData) {
var d1 = [], d2 = [], d3 = [], i;
for (i = -10; i < 10; i++) {
d1.push([i, Math.random()]);
d2.push([i, Math.random()]);
d3.push([i, Math.random()]);
}
var graphdata = [
{ data: d1, label: 'Series 1' },
{ data: d2, label: 'Series 2' },
{ data: d3, label: 'Series 3' }
];
self.postMessage({
type: "graphdata",
data: graphdata
});
}
})();
这个脚本做的第一件事是导入许多其他脚本。因为 worker 在他们自己的环境中运行,所以包含在graph.html
页面或default.html
页面的head
元素中的脚本在 worker 中是不可用的。您可以使用importScripts
功能来引用我们需要的文件。当这个工人在onmessage
处理器中收到一条消息时,调用getData
函数。最终,messageData
参数将是来自时间表页面的filter
对象,但是我将在下一节中介绍它。在getData
中,我在清单 13-7 中引入的相同代码用于创建一个graphdata
对象,然后使用postMessage
将该对象传递回主线程。
但是这些都不会发生,直到工作线程从主线程收到一条消息。清单 13-10 包含了ready
函数的新定义,以及一个格式化日期的帮助函数。将这两个功能都添加到graph.js
中。
清单 13-10。 创建一个工作线程并与之通信
ready: function (element, options) {
var timeGraphWorker = new Worker("/js/workers/timeGraphWorker.js");
timeGraphWorker.onmessage = function getGraphData(e) {
var message = e.data;
if (message && message.type === "graphdata") {
var graphoptions = {
bars: {
show: true,
stacked: true,
horizontal: false,
barWidth: 0.6,
lineWidth: 1,
shadowSize: 0
},
legend: {
position: "ne",
backgroundColor: "#fff",
labelBoxMargin: 10,
},
grid: {
color: "#000",
tickColor: "#eee",
backgroundColor: {
colors: [[0, "#ddf"], [1, "#cce"]],
start: "top",
end: "bottom"
},
verticalLines: true,
minorVerticalLines: true,
horizontalLines: true,
minorHorizontalLines: true,
},
xaxis: {
color: "#fff",
},
yaxis: {
color: "#fff",
},
title: this.formatDate(options.filter.startDate)
+ " - " + this.formatDate(options.filter.endDate),
HtmlText: true
};
var graph = Flotr.draw(graphcontainer, message.data, graphoptions);
timeGraphWorker.terminate();
graph.destroy();
} else if (message && message.type === "noresults") {
graphcontainer.innerHTML = "No data found. Try adjusting the filters.";
}
}.bind(this);
timeGraphWorker.postMessage({
startDate: options.filter.startDate,
endDate: options.filter.endDate,
projectId: options.filter.projectId
});
},
formatDate: function (dt) {
var formatting = Windows.Globalization.DateTimeFormatting;
var formatter = new formatting.DateTimeFormatter("day month.abbreviated");
return formatter.format(dt);
},
代码中突出显示的部分与我们在清单 13-7 中添加的部分相同。然而,我没有在页面加载时直接在ready
函数中执行代码,而是将它移到了名为timeGraphWorker
的新Worker
对象的onmessage
事件处理程序中。这是在清单 13-9 中postMessage
被调用时接收从工作线程发送的消息的函数。当收到消息时,检查消息的type
属性。如果消息包含数据,主线程使用该数据来呈现图形。如果type
为noresults
,则向用户显示一条消息。因为我们的数据仍然是硬编码的,所以我们还看不到那个消息。在连接了onmessage
事件处理程序之后,对timeGraphWorker.postMessage
的调用将把来自时间表页面的filter
对象发送给工人。在这个调用发生之前,这个工人实际上并没有做任何事情。
注意在清单 13-10 的末尾,您会发现对
timeGraphWorker.terminate
的调用。有两种方法可以终止工作线程。首先,从主线程调用terminate
方法。另一种是从工作线程本身调用close
方法。它们是等价的。
如果您现在运行 Clok 并导航到图表页面,您将看到类似于图 13-4 中的图表。除了数据是随机的这一事实之外,它与图 13-2 中的数据没有任何不同,但是这个数据是在一个工作线程中计算出来的。好吧,它实际上是硬编码在一个 worker 线程中的,但你仍然创建了一些多线程 JavaScript,这令人印象深刻。
图 13-4 。我们图表的第二次迭代与第一次非常相似
将时间表过滤器 作为消息传递
到目前为止,我们已经得到了一个漂亮的图表,其中填充了在工作线程中硬编码的数据。难题的最后一部分是用来自我们的Storage
类的真实数据替换硬编码的数据。用清单 13-11 中的版本替换timeGraphWorker.js
中getData
函数的当前版本。
清单 13-11。 从存储类中获取实时录入数据
function getData(messageData) {
storage.timeEntries.getSortedFilteredTimeEntriesAsync(
messageData.startDate,
messageData.endDate,
messageData.projectId)
.then(
function complete(results) {
if (results.length < = 0) {
self.postMessage({
type: "noresults"
});
} else {
// TODO: transform the data into format Flotr2 understands
// TODO: generate friendly labels for the graph axis
// TODO: post data back to the main thread
}
}.bind(this)
);
}
现在我们有进展了。正如我在第十二章的中所讨论的,调用getSortedFilteredTimeEntriesAsync
会返回一个Promise
,当它被满足时,会将图表所需的时间输入数据传递给then
函数的complete
参数。如果没有结果,只需将消息发送回主线程。正如您在TODO
评论中看到的,当结果被发现时,有三个任务需要完成,以使图形显示出来。
- 将从
getSortedFilteredTimeEntriesAsync
接收的数据转换成 Flotr2 图形库可以处理的格式。 - 为图表轴生成友好标签。
- 将数据发送回主线程,供 Flotr2 渲染。
我将把第一个和第三个任务放在一起讨论,然后以第二个任务结束。
映射、减少并再次映射
调用getSortedFilteredTimeEntriesAsync
的结果包括创建图表所需的所有数据。实际上,我们只需要来自results
中每个timeEntry
对象的三个值。不幸的是,timeEntry
对象的格式与 Flotr2 需要的数据点不匹配。
将数据转换成正确的格式有多种方法。您可以创建许多for
循环来迭代结果,并为图形库逐步构建数据对象。我选择使用map
和reduce
函数来处理这个问题。用清单 13-12 中的代码替换第一个TODO
注释。
清单 13-12。 转换数据
var msInDay = 86400000; // to normalize dates
var graphdata = results.map(function (item) {
// First, map to an array containing only the raw data needed
return {
clientName: item.project.clientName
+ ((messageData.projectId > 0)
? ": " + item.project.name
: ""),
dateWorked: item.dateWorked.removeTimePart(),
timeWorked: Clok.Utilities.SecondsToHours(item.elapsedSeconds, false)
};
}).reduce(function (accumulated, current) {
// Second, reduce all hours worked on each day for the same client into a single value
var found = false;
for (var i = 0; i < accumulated.length; i++) {
if (accumulated[i][0] === current.clientName) {
found = true;
continue;
}
}
if (!found) {
var worked = [];
var dt = messageData.startDate;
while (dt < = messageData.endDate) {
worked[worked.length] = [dt / msInDay, 0];
dt = dt.addDays(1);
}
accumulated[accumulated.length] = [current.clientName, worked];
}
for (var i = 0; i < accumulated.length; i++) {
if (accumulated[i][0] === current.clientName) {
for (var j = 0; j < accumulated[i][1].length; j++) {
if (accumulated[i][1][j][0] === current.dateWorked.getTime() / msInDay) {
accumulated[i][1][j][1] + = current.timeWorked;
continue;
}
}
}
}
return accumulated;
}, []).map(function (item) {
// Finally, map the reduced values into the format Flotr2 requires
return { label: item[0], data: item[1] };
});
代码并不漂亮,但是它做了它需要做的事情。我在这个任务的各个步骤中强调了一些注释,以说明正在发生的事情。对map
的第一次调用将每个timeEntry
对象的层次结构简化为一个扁平的对象,只包含客户、日期和工作时间。reduce
函数的前两个块确保每个客户端在每个日期都有一个图形值,默认情况下是0
。在初始化过程之后,reduce
函数会遍历这些展平对象的数组,按照客户和日期对它们进行分组,并对每个组的工作时间进行求和。如果你熟悉 SQL,类似的任务可以用类似于清单 13-13 中的查询来完成。
清单 13-13。 这个我看清楚多了
select clientName, dateWorked, sum(timeWorked) as timeWorked
from timeEntries
group by clientName, dateWorked
不幸的是,您不能在 JavaScript 中嵌入 SQL。也就是说,本章的重点不是编写理想的 MapReduce 代码。其他人已经详细介绍了这一点,并且可以在网上找到大量的算法和技术。
graphdata
对象包含了你所有贴图和缩小的结果。你现在需要做的就是用清单 13-14 中的代码替换清单 13-11 中的第三个TODO
注释,将数据发送回主线程。
清单 13-14。 将我们紧张计算的结果发布回主线程
self.postMessage({
type: "graphdata",
data: graphdata
});
当你运行 Clok 并导航到图表时,你会看到一个漂亮、精确的图表,如图 13-5 所示。
图 13-5 。图表上的条形看起来不错,但轴标签却不怎么样
虽然条形看起来很准确,但水平轴上的标签没有任何意义。让我们解决这个问题。
为图表创建轴标签
事实证明,默认情况下,Flotr2 将日期视为非常大的数字——自 1970 年 1 月 1 日以来的毫秒数,由一个Date
对象的getTime
函数返回。这意味着我们图中的每根棒线距离第二天的棒线有 86,400,000(每天的毫秒数)个单位。这使得图中的条形很难看到,所以我将清单 13-12 中的日期除以那个数字进行了归一化。因此,横轴上的值代表自 1970 年以来的天数,而不是自 1970 年以来的毫秒数。
虽然 15,835 比 1,368,144,000,000 要好,但是对于试图计算出他或她哪一天工作了 10 小时的用户来说,这个标签并不是特别有用。幸运的是,Flotr2 允许您将标签与横轴上的每个值相关联。用清单 13-15 中的代码替换清单 13-11 中的第二个TODO
注释。
清单 13-15。 制作人性化的轴标签
var tickDays = [], otherDays = [];
var dateFormatter = function (dt) {
var formatting = Windows.Globalization.DateTimeFormatting;
var formatter = new formatting.DateTimeFormatter("day month.abbreviated");
return formatter.format(dt);
};
var dt = messageData.startDate;
while (dt < = messageData.endDate) {
if ((dt.getDay() === 0)
|| (messageData.startDate.addDays(7) > = messageData.endDate)
|| (messageData.startDate.getTime() === dt.getTime())
|| (messageData.endDate.getTime() === dt.getTime())
) {
tickDays.push([dt / msInDay, dateFormatter(dt)]);
} else {
otherDays.push([dt / msInDay, dateFormatter(dt)]);
}
dt = dt.addDays(1);
}
在这段代码中,tickDays
存储了一个友好版本的要在图表上显示的日期。如果请求的图表是七天或更短时间的数据,那么范围内的每个日期都会添加到tickDays
中。否则,tickDays
包含用户友好的日期标签,这些日期要么是星期天,要么与过滤器的startDate
或endDate
属性相匹配。任何不符合这些标准的日期都会添加到otherDays
中。除了在图表的水平轴上显示友好的标签之外,这两个数组还将决定竖线在图表上出现的位置。在此之前,我们必须将这些值返回给主线程。将清单 13-16 中突出显示的代码添加到来自清单 13-14 的postMessage
调用中。
清单 13-16。 给我们的信息增加更多的价值
self.postMessage({
type: "graphdata",
ticks: tickDays,
minorTicks: otherDays,
data: graphdata
});
为了正确显示图形标签,主线程中还需要做最后一项更改。通过将清单 13-17 中高亮显示的代码添加到xaxis
属性来修改graphoptions
对象。
清单 13-17。 配置轴标签
xaxis: {
color: "#fff",
showLabels: true,
ticks: message.ticks,
minorTicks: message.minorTicks,
},
现在 Flotr2 可以按预期渲染图形和标签了(见图 13-6 )。
图 13-6 。这些标签现在更有意义了
简单说说承诺
图表看起来很棒。图形的数据点在工作线程中计算,并在消息中传递回主线程。轴标签美观易读。你现在可以停下来,对你的工作感到高兴。事实上,这是我能做的最大限度的练习。
也就是说,如果你想找一些家庭作业,你可以多做一步。如果您喜欢通用语法,您可以将这个 Web Worker 封装在一个WinJS.Promise
中。这将允许您在一个then
或done
方法中处理来自工人的消息,而不是在一个onmessage
事件处理器中。功能没有变,只是语法变了。如果这是你感兴趣的事情,那么我推荐你阅读这个论坛的信息,作为你如何实现这个目标的例子:http://social.msdn.microsoft.com/Forums/en-US/winappswithhtml5/thread/9722d406-6de2-4705-9f00-4fdd7c2ad6b3
。
注意所列论坛帖子中描述的技术已经被 Kraig Brockschmidt 记录在他的书用 HTML、CSS 和 JavaScript 编程 Windows 8 应用(微软出版社,2012)中。
虽然不是在 Web Workers 的上下文中,但我在第十四章中使用了类似的技术,当我将数据库连接的创建包装在一个Promise
中时,当连接成功打开时就完成了。你会在清单 14-1 和清单 14-2 中看到这段代码。
结论
虽然承诺允许您以方便的方式处理异步操作,但是 Web Workers 提供了让代码在不同的线程上执行的机会。将工作线程与主线程隔离开来意味着两个线程不能引用相同的变量。虽然这一开始似乎会适得其反,但它实际上会导致一个更稳定、解耦的系统,在这个系统中,作为开发人员,您不必担心多线程开发中的常见问题,如竞争条件和锁定。配置您的主线程和辅助线程通过传递和接收消息进行通信是一项简单的任务,可以实现多线程应用开发,而没有其他语言中可能遇到的潜在脆弱性。
十四、数据源选项
在第十一章中,我介绍了数据绑定,并介绍了一些可以用来在屏幕上显示应用数据的不同技术。然而,到目前为止,Clok 中的所有数据都存储在内存中。当应用启动时加载测试数据,当应用关闭时,对该数据的任何修改都将被丢弃。虽然这使我们能够构建一个看起来不错的应用,并制定出允许用户与数据交互的细节,但这还不足以成为一个可用的应用。
然而,到本章结束时,Clok 将处于一种状态,人们实际上可以开始每天使用它。这并不是说它是一个完整的应用——在本书的其余部分,我们仍然会添加一些功能来改善用户体验。也就是说,在完成本章的练习后,我开始自己使用 Clok。
那么,怎样才能让 Clok 从一个看起来功能正常的应用变成一个实际可用的应用呢?Clok 缺少的最大特性是以持久格式保存数据的能力。在本章中,我将讨论使用 IndexedDB 处理本地数据,以及使用WinJS.xhr
函数将远程数据集成到 Clok 中。
本地数据源
本地数据的情况很明显。访问本地数据源中的数据比访问远程数据源中的相同数据更快。无论用户是否连接到互联网或内部网络,它始终可用。在用户没有连接的情况下,一个只依赖远程数据的应用是没有用的。在本节的大部分时间里,我将介绍一种被称为索引数据库 API 的技术,或简称为 IndexedDB 。
索引 b
IndexedDB 是一个数据库引擎,内置于现代 web 浏览器中,如 Internet Explorer、Firefox 和 Chrome web 浏览器。因为使用 HTML 和 JavaScript 构建的 Windows Store 应用利用了 Internet Explorer 的渲染和脚本支持,所以 IndexedDB 也可以在 Clok 等应用中使用。使用 IndexedDB,您可以将用户的数据存储在本地,就在您的应用中,使这些数据随时可用,无论是否连接。
与关系数据库管理系统(RDBMS)不同,如 Microsoft SQL Server 或 Oracle 数据库,IndexedDB 将数据存储为对象。一个对象可以有一个很深的层次结构,比如一个客户对象包含一个订单集合,每个订单都有一个产品集合。或者它可以是一个简单的对象,比如一个Project
对象或TimeEntry
对象。
注意在开始使用 IndexedDB 之前,一定要注释
storage.js
中用于用临时数据填充内存列表的代码。在本章的后面,您将把这段代码的修改版本移动到一个新文件中。
从 IndexedDB 填充内存列表
在第七章的中,我提到了一个事实,我更喜欢使用对象和对象集合,而不是构建我的用户界面来直接耦合到数据本身。这为我选择如何读写数据提供了一定的灵活性。考虑到这一点,我将说明如何使用 IndexedDB 作为数据存储来支持我们现有的内存中数据对象。
使用 IndexedDB 时,您需要做的第一件事是创建一个数据库。在storage
类定义之前,添加清单 14-1 到storage.js
中突出显示的代码。
清单 14-1。 创建索引数据库
"use strict";
var data = Clok.Data;
var _openDb = new WinJS.Promise(function (comp, err) {
var db;
var request = indexedDB.open("Clok", 1);
request.onerror = err;
request.onupgradeneeded = function (e) {
var upgradedDb = e.target.result;
upgradedDb.createObjectStore("projects", { keyPath: "id", autoIncrement: false });
upgradedDb.createObjectStore("timeEntries", { keyPath: "id", autoIncrement: false });
};
request.onsuccess = function () {
db = request.result;
// Do something with the database here
};
});
var storage = WinJS.Class.define(
// SNIPPED
因为所有 IndexedDB 操作都是异步发生的,所以我将_openDb
定义为代表数据库的Promise
。由于异步的特性,使用 IndexedDB 的一个常见模式是发出某种类型的请求,并在处理程序中检查该请求的响应。这正是清单 14-1 中发生的事情。通过调用indexedDB.open
,我请求打开名为 Clok 的数据库的版本 1。该请求的任何错误都由Promise
对象的err
函数处理。一旦成功连接到数据库,onsuccess
事件处理程序将做一些有趣的事情,稍后我将展示这一点。
然而,目前 Clok 数据库的版本 1 并不存在。当请求一个新版本的数据库时,调用onupgradeneeded
事件处理程序,在这里您可以创建集合,或者对象存储,在这里维护数据。每个对象都需要一个键,而Project
和TimeEntry
类上的id
属性是一个理想的键。因为我们已经有了逻辑来设置这些id
属性的值,所以我指定了键不会自动递增。
注意除了用于创建初始数据库,
onupgradeneeded
处理程序还用于将现有数据库升级到新版本。在这种情况下,您将在处理程序中添加代码来检查e.oldVersion
,以确定用户的数据库有多过时,以及成功升级它需要哪些步骤。你可以在www.w3.org/TR/IndexedDB/
看到这样的例子。
一旦创建并打开了数据库,就会引发success
事件,调用onsuccess
处理程序。到目前为止,那里没有什么有趣的事情发生。我只在一个名为db
的变量中设置了对数据库的引用。因为我计划在整个 Clok 中继续使用内存中的数据列表,所以当数据库打开时,应该用数据库中当前的数据填充这些列表,以便现有的屏幕继续像以前一样工作。将清单 14-2 中高亮显示的代码添加到onsuccess
处理程序中。
清单 14-2。 此代码在数据库成功打开时执行
request.onsuccess = function () {
db = request.result;
_refreshFromDb(db).done(function () {
comp(db);
}, function (errorEvent) {
err(errorEvent);
});
};
通过在_openDb
声明后添加清单 14-3 中的代码来定义_refreshFromDb
。
清单 14-3。 用数据库中的数据填充内存列表
var _refreshFromDb = function (db) {
return new WinJS.Promise(function (comp, err) {
while (storage.projects.pop()) { }
while (storage.timeEntries.pop()) { }
var transaction = db.transaction(["projects", "timeEntries"]);
transaction.objectStore("projects").openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
var project = data.Project.createFromDeserialized(cursor.value);
storage.projects.push(project);
cursor.continue();
};
};
transaction.objectStore("timeEntries").openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
var timeEntry = data.TimeEntry.createFromDeserialized(cursor.value);
storage.timeEntries.push(timeEntry);
cursor.continue();
};
};
transaction.oncomplete = comp;
transaction.onerror = err;
});
}
_refreshFromDb
函数返回一个Promise
,它首先清空内存列表。与所有 IndexedDB 操作一样,在事务中,游标对数据库中的每个对象存储打开。只要游标有值,该值就会被添加到适当的内存列表中。事务的oncomplete
处理程序被设置为Promise
的comp
处理程序,这将触发清单 14-2 中的done
函数。此时,调用_openDb Promise
的comp
处理程序来提供对数据库的引用,该引用可用于引用_openDb
的任何代码。
注意将 WinJS 对象保存到 IndexedDB 数据库时,只保存构造函数中定义的属性。当从数据库中检索对象时,任何定义为实例成员的属性,如
TimeEntry
类中的project
属性,都没有值。为了缓解这个问题,在Project
和TimeEntry
类中,我都添加了一个新的静态函数,名为createFromDeserialized
。该函数获取从游标返回的匿名对象,并基于这些值创建一个完全水合的对象。这些函数的定义可以在本书附带的源代码中找到。你可以在这本书的产品页面的源代码/下载标签中找到本章的代码示例(www.apress.com/9781430257790
)。
如果你现在运行 Clok,你会很快发现没有数据(见图 14-1 )。我们删除了测试数据,现在正在用数据库中的数据填充这些列表。但是,数据库中还没有数据。在这一章的后面,我将展示一些对我们开发人员有帮助的功能,通过提供一种方法来重置我们的测试数据和探索数据库中的数据。在此之前,让我们对 Clok 进行修改,这样当用户保存或删除数据时,我们的数据库就会更新。
图 14-1 。Clok 中没有项目
注意在 Clok 中,我已经使用 IndexedDB 来填充在前面章节中添加的数据的内存列表。这提供了不需要对用于显示和管理项目和时间输入数据的现有屏幕进行任何改变的好处。如果您愿意,您可以实现
IListDataSource
接口来创建自己的数据源,直接使用 IndexedDB。这将允许你绑定ListView
控件,例如,直接绑定到你的数据,而不必像我在 Clok 中做的那样,先把它加载到一个WinJS.Binding.List
对象中。关于如何做到这一点的示例,请参见下面的博客条目:http://stephenwalther.com/archive/2012/07/10/creating-an-indexeddbdatasource-for-winjs
。
当数据改变时更新索引数据库
通过运行应用,您可能不知道这一点,但我们已经做了所有必要的更改,将数据从 IndexedDB 数据库加载到内存中,以便在屏幕上显示。在这一节中,我将展示从数据库中保存和删除数据所需的更改。幸运的是,所有需要的更改都局限于storage.js
,这是将数据加载到内存列表并在整个应用中使用这些列表的另一个好处。将清单 14-4 中的代码添加到storage.js
中_refreshFromDb
的定义之后。
清单 14-4。 方法来处理 IndexedDB 数据库中的数据
var _getObjectStore = function (db, objectStoreName, mode) {
mode = mode || "readonly";
return new WinJS.Promise(function (comp, err) {
var transaction = db.transaction(objectStoreName, mode);
comp(transaction.objectStore(objectStoreName));
transaction.onerror = err;
});
};
var _saveObject = function (objectStore, object) {
return new WinJS.Promise(function (comp, err) {
var request = objectStore.put(object);
request.onsuccess = comp;
request.onerror = err;
});
};
var _deleteObject = function (objectStore, id) {
return new WinJS.Promise(function (comp, err) {
var request = objectStore.delete(id);
request.onsuccess = comp;
request.onerror = err;
});
};
注意所有这些函数都返回
Promise
对象,你也可以修改save
和delete
函数来返回Promise
对象。虽然我不会在本章中讨论它,但是您可以对调用这些函数的各个地方进行更新,以利用 async 提供的好处。例如,您可以在用户点击保存按钮时提供一个进度指示器,然后在完成Promise
时移除它。
定义了这些函数后,我们现在可以对save
和delete
函数进行修改。用清单 14-5 中指定的新版本替换保存和删除项目的功能。
清单 14-5。 新版本功能保存和删除项目
storage.projects.save = function (p) {
if (p && p.id) {
var existing = storage.projects.getById(p.id);
if (!existing) {
storage.projects.push(p);
}
return _openDb.then(function (db) {
return _getObjectStore(db, "projects", "readwrite");
}).then(function (store) {
return _saveObject(store, p);
});
}
return WinJS.Promise.as();
};
storage.projects.delete = function (p, permanent) {
permanent = permanent || false;
if (p && p.id) {
if (!permanent) {
var existing = storage.projects.getById(p.id);
if (existing) {
// soft delete = default
existing.status = data.ProjectStatuses.Deleted;
return storage.projects.save(existing);
}
} else {
var index = this.indexOf(p);
if (index >= 0) {
this.splice(index, 1);
return _openDb.then(function (db) {
return _getObjectStore(db, "projects", "readwrite");
}).then(function (store) {
return _deleteObject(store, p.id);
});
}
}
}
return WinJS.Promise.as();
};
对save
函数的修改非常简单。我只是添加了代码来连接到数据库,选择正确的对象存储,并将项目对象保存到该存储中。然而,我对delete
函数做了一些修改。以前,没有办法从 Clok 中永久删除一个项目。项目只是被分配了一个Deleted
状态,然后被保存。在典型的 Clok 使用过程中,情况依然如此。然而,当我们在下一节中添加重置临时数据的功能时,我们将需要一种永久删除数据的方法。
在这两个函数中,现在都返回了一个Promise
。如果操作成功,在清单 14-4 中相应的_saveObject
或_deleteObject
函数中定义的Promise
将返回给调用代码。否则,使用WinJS.Promise.as
函数返回一个空的Promise
,不向其提供任何参数。这最后一步不是必需的,但是它确保了通过调用save
或delete
返回的对象类型的一致性——它总是一个Promise
。
保存和删除时间条目功能的更新版本可以在清单 14-6 中看到。这些函数的变化与清单 14-5 中的非常相似,尽管delete
函数更简单,因为它总是永久删除时间条目。
清单 14-6。 新版本功能保存和删除时间条目
storage.timeEntries.save = function (te) {
if (te && te.id) {
var existing = storage.timeEntries.getById(te.id);
if (!existing) {
storage.timeEntries.push(te);
}
return _openDb.then(function (db) {
return _getObjectStore(db, "timeEntries", "readwrite");
}).then(function (store) {
return _saveObject(store, te);
});
}
return WinJS.Promise.as();
};
storage.timeEntries.delete = function (te) {
if (te && te.id) {
var index = this.indexOf(te);
if (index >= 0) {
this.splice(index, 1);
return _openDb.then(function (db) {
return _getObjectStore(db, "timeEntries", "readwrite");
}).then(function (store) {
return _deleteObject(store, te.id);
});
}
}
return WinJS.Promise.as();
};
注意清单 14-5 和清单 14-6 中的,你会注意到当我更新数据时,不管是保存还是删除,我都在更新内存列表和 IndexedDB 数据库。另一种方法是只更新数据库,然后从数据库中重新填充列表,类似于第一次建立数据库连接时加载列表的方式。这种方法没有错,但是我选择这个方向是为了最小化对应用的更改。每次重新加载列表都需要更改各种屏幕,以便在列表重新填充后重新加载数据。
现在让我们将一些临时数据放回到 Clok 中,以便于测试。
IndexedDB 浏览器
出于开发的目的,在 Clok 中拥有临时测试数据还是不错的。事实上,因为我们刚刚实现了持久存储所有 Clok 数据的功能,所以如果能够清除任何数据并将数据库重置为默认测试状态就好了。在这一节中,我将向您展示如何添加一个 settings 弹出按钮,它不仅允许您重置测试数据,还提供了一个小的数据库浏览器,您可以使用它来查看存储在 IndexedDB 数据库中的各种对象。
创建“设置”弹出按钮
在settings
文件夹中添加一个名为idbhelper.html
的 HTML 文件。用清单 14-7 中的代码替换idbhelper.html
的默认内容。
清单 14-7。 新设置弹出的外壳
<!DOCTYPE html>
<html>
<head>
<title>IndexedDB Helper</title>
</head>
<body>
<div id="settingsDiv" data-win-control="WinJS.UI.SettingsFlyout"
aria-label="IndexedDB Helper"
data-win-options="{settingsCommandId:'idbhelper',width:'wide'}">
<div class="win-ui-dark win-header" style="background-color: #000046;">
<button type="button" class="win-backbutton"
onclick="WinJS.UI.SettingsFlyout.show()">
</button>
<div class="win-label clok-logo">IndexedDB Helper</div>
</div>
<div class="win-content">
<div class="win-settings-section">
</div>
</div>
</div>
</body>
</html>
接下来,通过添加清单 14-8 中突出显示的代码来修改default.js
。不要像添加options
和about
设置弹出按钮那样将新的命令定义添加到e.detail.applicationcommands
中,而是单独添加,这样更容易根据用户的偏好显示或隐藏它,这一功能将在第十五章的中添加。
清单 14-8。 布线设置弹出按钮
WinJS.Application.onsettings = function (e) {
e.detail.applicationcommands = {
"options": {
title: "Clok Options",
href: "/settings/options.html"
},
"about": {
title: "About Clok",
href: "/settings/about.html"
}
};
e.detail.applicationcommands.idbhelper = {
title: "IndexedDB Helper",
href: "/settings/idbhelper.html"
};
WinJS.UI.SettingsFlyout.populateSettings(e);
};
现在你有了一个新的空的设置弹出按钮,让我们添加一些功能来帮助开发人员构建和测试 Clok。
从微软下载并配置 IDBExplorer
微软的 Internet Explorer 团队开发了一个名为 IDBExplorer 的工具。最初在内部使用,微软向开发人员开放,以探索他们的 IndexedDB 数据库,包括结构和数据。你可以从下面的博客文章中阅读和下载这个工具:http://blogs.msdn.com/b/ie/archive/2012/01/25/debugging-indexeddb-applications.aspx
。下载包含该工具的 ZIP 文件,并将名为IDBExplorer
的文件夹从该包复制到 Visual Studio 项目的settings
文件夹中。图 14-2 显示了完成后你应该有的正确的文件夹层次结构。您可以在本书附带的源代码中参考这个过程的完整版本。
图 14-2 。将 IDBExplorer 工具添加到设置文件夹中
如果我们建立的是一个网站,而不是一个 Windows 应用商店,我们早就完成了。然而,为了让这个有用的工具在我们的应用中工作,我们还需要完成几个步骤。IDBExplorer 包含一个旧版本的 jQuery。然而,jQuery 的最新版本在 Windows Store 应用中工作得更好,所以我建议从www.jquery.com
下载 jQuery 2.0 版或更高版本,并将其添加到您刚刚添加到项目的IDBExplorer
中。我选择下载缩小版,如图图 14-3 所示,但是未压缩版也可以。
图 14-3 。更新到新版本的 jQuery
注意虽然 jQuery 版与 Windows Store 应用配合良好,但在调试模式下运行 Clok(按 F5 而不是 Ctrl+F5)时,您可能偶尔会看到错误。跳过调试器中可能出现的任何错误是安全的。正常运行 Clok 时,如果没有附加调试器,这些错误不会有任何负面影响。我怀疑 jQuery 的某个未来版本会消除这个问题。
在将 IDBExplorer 工具添加到设置弹出按钮之前,您必须对其本身进行的最后一项更改是用来自清单 14-9 的代码更新IDBExplorer.html
的内容。
清单 14-9。 更新页面以更好地与 Windows 应用商店应用配合使用 s
<!DOCTYPE html>
<html FontName2">http://www.w3.org/1999/xhtml ">
<head>
<!-- IDBExplorer references -->
<script src="jquery-2.0.2.min.js"></script>
<script src="jquery.jstree.js"></script>
<script src="IDBExplorer.js"></script>
<link rel="stylesheet" type="text/css" href="IDBExplorer.css" />
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body onload="setDBName();initIDBExplorer();">
</body>
</html>
这样,IDBExplorer 就不再需要修改了,您只需通过添加一个iframe
来托管它,从而在 settings 弹出菜单中显示这个工具。将清单 14-10 中突出显示的代码添加到idbhelper.html
。
清单 14-10。 在 iframe 中托管 IDBExplorer
<div class="win-settings-section">
<iframe style="width: 550px; height: 600px"
src="/settings/IDBExplorer/IDBExplorer.html?name=Clok"></iframe>
</div>
立即运行 Clok 并打开 IndexedDB 辅助程序设置弹出按钮。您应该看到 IDBExplorer 显示了关于我们的空数据库的信息(见图 14-4 )。一旦我们将测试数据添加回 Clok,这个工具将会更有帮助。我将在下一节向您展示如何做到这一点。
图 14-4 。IDBExplorer 显示一个空数据库
添加按钮以重置和加载测试数据
在本章的前面,我让你删除了storage.js
中加载临时数据到 Clok 的代码。在这一节中,我将向您展示如何将临时数据放回到 Clok 中。首先,在 IndexedDB 辅助设置弹出按钮的顶部添加几个按钮。将清单 14-11 中突出显示的代码添加到idbhelper.html
中。
清单 14-11。 添加按钮来重置我们的测试数据
<div class="win-settings-section">
<button onclick="deleteAllData();" style="background-color:red;">Delete All Data</button>
<button onclick="addTestData();">Add Test Data</button>
<iframe style="width: 550px; height: 600px"
src="/settings/IDBExplorer/IDBExplorer.html?name=Clok"></iframe>
</div>
因为 IndexedDB Helper settings 弹出按钮仅供开发人员使用,并且应该在部署之前从项目中删除,所以我决定直接在idbhelper.html
中添加 JavaScript 代码。将来自清单 14-12 的脚本引用添加到idbhelper.html
的head
元素中。
清单 14-12。 JavaScript 引用和按钮点击处理程序
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<script src="/js/extensions.js"></script>
<script src="/js/utilities.js"></script>
<script src="/data/project.js"></script>
<script src="/data/timeEntry.js"></script>
<script src="/data/storage.js"></script>
<script>
function deleteAllData() {
// SNIPPED
}
function addTestData() {
// SNIPPED
}
</script>
重置我们的临时数据是一个两步过程。首先,我们必须删除当前数据库中的所有数据,然后我们必须将临时数据添加到数据库中。用清单 14-13 中的代码替换idbhelper.html
中的deleteAllData
函数。
清单 14-13。 删除所有数据按钮处理程序
function deleteAllData() {
var msg = new Windows.UI.Popups.MessageDialog(
"This cannot be undone. Do you wish to continue?",
"You're about to remove all data from Clok.");
msg.commands.append(new Windows.UI.Popups.UICommand(
"Yes, Delete It", function (command) {
var storage = Clok.Data.Storage;
storage.projects.forEach(function (p) {
storage.projects.delete(p, true);
});
storage.timeEntries.forEach(function (te) {
storage.timeEntries.delete(te);
});
}));
msg.commands.append(new Windows.UI.Popups.UICommand(
"No, Don't Delete It", function (command) { }));
msg.defaultCommandIndex = 0;
msg.cancelCommandIndex = 1;
msg.showAsync();
}
单击“删除所有数据”按钮时,会出现一个消息对话框,要求在继续之前进行确认。如果确认,所有项目和时间条目将从内存列表中永久删除,进而从 IndexedDB 数据库中删除数据。
添加临时数据的代码与我们在第十一章中添加的代码几乎相同,有三个显著的不同:
- 如果任何数据已经存在,则不能添加测试数据。
- 我现在调用
projects.save
和time.save
,而不是调用projects.push
和time.push
将对象添加到列表中,这将把项目添加到WinJS.Binding.List
对象和 IndexedDB 数据库中。 - 其中一个项目指定了地址细节,这在本章后面讨论远程数据时会有帮助。
用清单 14-14 中的代码替换idbhelper.html
中的addTestData
函数。
清单 14-14。 添加测试数据按钮处理程序
function addTestData() {
var projects = Clok.Data.Storage.projects;
var time = Clok.Data.Storage.timeEntries;
if (projects.length > 0 || time.length > 0) {
var msg = new Windows.UI.Popups.MessageDialog(
"You cannot add test data since Clok already contains data.",
"Cannot add test data.");
msg.showAsync();
return;
}
var createProject = function (name, projectNumber, clientName, id, status) {
// SNIPPED
}
// SNIPPED
// one needs an address for map example
var project = createProject(name1, "2012-0017", client3, 1368296808748);
project.address1 = "1 Microsoft Way";
project.city = "Redmond";
project.region = "WA";
project.postalCode = "98052";
projects.save(project);
// SNIPPED
var createTime = function (id, projectId, dateWorked, elapsedSeconds, notes) {
// SNIPPED
}
// SNIPPED
time.save(createTime(timeId++, 1368296808757, date1, 10800, "Lorem ipsum dolor sit."));
// SNIPPED
}
注你可以在本书附带的源代码中找到更详细的
addTestData
版本。您可以在该书的 press product 页面的 Source Code/Downloads 选项卡(www.apress.com/9781430257790
)上找到本章的代码示例。
运行 Clok 并打开 IndexedDB 助手设置弹出按钮。点击按钮添加测试数据,然后查看项目节点(见图 14-5 )和时间条目节点。您应该会看到所有的测试数据都被列出来了,并且您可以点击顶部窗格中的不同项目,以查看下部窗格中的详细信息。当您对项目和时间条目进行更改时,这些更改将反映在 IDBExplorer 的设置弹出菜单中。每当您需要新的测试数据时,只需返回到此设置弹出按钮来删除当前数据并添加新的测试数据。
图 14-5 。已完成的索引数据库帮助程序
注意您可能会注意到,首次启动 Clok 时,仪表板屏幕上的项目下拉列表最初并不总是包含活动项目的完整列表。但是,如果您导航到不同的页面,然后返回,列表是完整的。这是因为数据从 IndexedDB 数据库异步加载到填充该控件的
WinJS.Binding.List
中。我们可以通过使用承诺在数据加载后填充控件来解决这个问题。取而代之的是,我们将暂时保留这个问题,当我在第十七章的中讲述处理应用状态变化时,我们将解决这个问题。
SQLite〔??〕
对于本地数据存储来说,IndexedDB 是一个非常方便的选项,但它不是唯一的选项。如果你的背景和我的相似,你可能有相当多的使用关系数据库系统的经验,比如 Microsoft SQL Server。遗憾的是,您无法从 Windows 应用商店应用直接访问存储在基于服务器的数据库中的数据。您可以编写一个服务层来支持您的关系数据库,并将其作为远程数据源进行访问。
您还可以使用 SQLite 之类的工具在 Windows 应用商店应用中构建本地数据库。虽然这不是现成的直接支持,我也不会在本书中讨论,但是您可以使用第三方库来为您的应用添加对 SQLite 的支持。其中一个名为 SQLite3-WinRT 的库可以在这里找到:https://github.com/doo/SQLite3-WinRT
。
文件存储
除了 IndexedDB 和 SQLite,还可以使用文件在本地存储数据。您可以将 JavaScript 对象保存到文件中。就此而言,在文件中,你可以将文本保存为任何你想要的格式。我不会在这里讨论如何处理文件,但是我会在第十六章中讨论这个话题。
远程数据源
虽然选择在本地存储数据很重要,但这并不能降低支持远程数据源的需求。使用远程数据的场景数不胜数,例如:
- 获取到客户办公室的路线—我们接下来将研究这个场景
- 阅读和发送电子邮件
- 订阅新闻或博客订阅源
- 计算各航运公司的运费
- 从公司 CRM 中查看和编辑客户数据
- 从同一台机器上运行的另一个进程获取数据
注意需要注意的是,术语本地数据不仅仅指与应用在同一台机器上的数据。它指的是存储在应用本身中的数据。因为 Windows 应用商店应用彼此独立,所以一个应用不能直接访问另一个应用包含的数据,即使这两个应用是由同一开发人员创建的。尽管这两个应用在同一台机器上,但它们之间有一道墙,数据共享必须通过某种类型的服务进行。
通常,来自远程服务的数据通过 HTTP 公开,要么使用 REST API,要么使用 RPC API。如果您不熟悉这些术语,网上有大量关于 REST 和 RPC 的详细信息,但是它们之间的最大区别是 REST 强调查找和使用一些数据(资源),而 RPC(如 SOAP)强调执行一些远程操作,本质上是调用在不同进程中运行的函数。完全简化一下,REST 侧重于名词,RPC 侧重于动词。我将要介绍的例子从 REST API 获取数据。
WinJS.xhr
如果您开发 web 应用已经有一段时间了,那么您应该熟悉XMLHttpRequest
(XHR)、,它用于向某处的 web 服务器提交请求并评估响应。web 服务器可以位于公共互联网、私有网络上,甚至与发出请求的应用位于同一台机器上。此外,尽管 XML 是其名称的一部分,XMLHttpRequest
也可以从服务器接收 JSON 数据。因为对远程服务器的请求不会立即响应,所以使用XMLHttpRequest
发出的请求将指定一个处理函数,在响应可用时执行。清单 14-15 显示了一个非常简单的例子,使用XMLHttpRequest
请求someUrl
并对来自服务器的响应做一些事情。
清单 14-15。 用 XMLHttpRequest 从远程服务器请求东西
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
// do something with the response
} else {
// something bad happened
}
}
var asyncRequest = true;
request.open("GET", someUrl, asyncRequest);
request.send();
这个例子没有完全实现。它只检查readyState
是4
(请求已经完成)和 HTTP 状态代码是200
的情况。它认为任何其他情况都是错误的。WinJS.xhr
函数将XMLHttpRequest
的功能封装在WinJS.Promise
中。响应在Promise
的then
或done
函数中可用,而不是指定处理函数。清单 14-16 执行与清单 14-15 相同的任务,使用WinJS.xhr
和承诺代替。
清单 14-16。 用 WinJS.xhr 向远程服务器请求东西
WinJS.xhr({ url: someUrl, type: "GET" })
.done(function success(completeEvent) {
// do something with the response
}, function err(errorEvent) {
// something bad happened
});
可以说它读起来更简单一些,但是因为这个函数返回了一个Promise
,所以它非常适合 WinJS 中流行的异步编程风格。除了用GET
方法请求数据,还可以用WinJS.xhr
提交数据。如果您使用WinJS.xhr
提交数据,您可能会使用POST
方法而不是GET
,并且还必须为 options 参数指定一个data
属性。例如,您可以使用类似于清单 14-17 的代码将一个新用户保存到远程数据源中。
清单 14-17。 用 WinJS.xhr 发布数据
WinJS.xhr({ url: someUrl, type: "POST", data: { name: "Scott", dob: "Dec 1" } })
Clok 的一个新需求是为用户提供到客户所在地的驾驶方向。我将带你通过配置和使用 Bing Maps API 和WinJS.xhr
来添加这个功能。
必应地图设置
你可能熟悉微软的必应地图产品。与其他提供地图和方向的公司一样,微软也为开发者提供了一个 API,将必应地图集成到他们自己的软件中。在撰写本文时,Bing 地图可以添加到 Windows Store 应用中,对于每天使用少于 50,000 笔交易的应用(www.microsoft.com/maps
)不收取许可费。尽管我预计 Clok 会在 Windows Store 取得巨大成功,但我不认为使用率会很快接近这个数字。
尽管这项服务是免费的,但至少在最初,为了使用 Bing Maps API,需要一个密钥。创建一个帐户并登录到 Bing 地图门户(www.bingmapsportal.com
)。从这里开始,为 Windows 应用商店应用创建一个新的基本密钥(见图 14-6 )。
图 14-6 。请求阿炳地图 API 密钥
一旦你完成了表格,你的钥匙就可以用了。在图 14-7 中,你可以看到我的键列表,键本身被模糊掉了。这是一长串字母和数字。一会儿我会告诉您在哪里添加 Clok 的键。如果你把它放错了地方,你可以在任何时候从 Bing 地图门户检索到它。
图 14-7 。我当前的 Bing 地图 API 密钥列表
现在你有了钥匙,你需要一个地方来放它。在 Visual Studio 项目的data
文件夹中创建一个名为bingMapsWrapper.js
的新 JavaScript 文件。将清单 14-18 中的代码添加到bingMapsWrapper.js
中。确保在apikey
变量中添加 Bing Maps API 密钥。
清单 14-18。 定义 BingMaps 类
(function () {
"use strict";
var apikey = "PUT_YOUR_KEY_HERE";
var apiEndpoint = " http://dev.virtualearth.net/REST/v1/ ";
var xhrTimeout = 2000;
var mapsClass = WinJS.Class.define(
function constructor() { /* empty constructor */ },
{ /* static class, no instance members */ },
{
credentials: {
get: function () { return apikey; }
},
getDirections: function (start, end) {
// TODO: get the directions here
}
}
);
WinJS.Namespace.define("Clok.Data", {
BingMaps: mapsClass,
});
})();
到目前为止,这个类还很简单,只在credentials
属性中公开了您的键。getDirections
函数接受起始地址(start
)和目的地地址(end
),并将使用这些值向 Bing 地图服务请求驾驶路线。用清单 14-19 中的代码替换getDirections
的定义。
注意一定要给
default.js
中的bingMapsWrapper.js
文件添加一个脚本引用。
清单 14-19。 向必应地图服务请求路线
getDirections: function (start, end) {
var distanceUnit = "mi";
var routeRequest = apiEndpoint + "Routes?"
+ "wp.0=" + start
+ "&wp.1=" + end
+ "&du=" + distanceUnit
+ "&routePathOutput=Points&output=json"
+ "&key=" + apikey;
return WinJS.Promise.timeout(xhrTimeout, WinJS.xhr({ url: routeRequest }))
.then(function (response) {
var resp = JSON.parse(response.responseText);
if (resp
&& resp.resourceSets
&& resp.resourceSets[0]
&& resp.resourceSets[0].resources
&& resp.resourceSets[0].resources[0]
&& resp.resourceSets[0].resources[0].routeLegs
&& resp.resourceSets[0].resources[0].routeLegs[0]
&& resp.resourceSets[0].resources[0].routeLegs[0].itineraryItems
&& resp.resourceSets[0].resources[0].routeLegs[0].itineraryItems.length > 0
) {
var directions = {
copyright: resp.copyright,
distanceUnit: resp.resourceSets[0].resources[0].distanceUnit,
durationUnit: resp.resourceSets[0].resources[0].durationUnit,
travelDistance: resp.resourceSets[0].resources[0].travelDistance,
travelDuration: resp.resourceSets[0].resources[0].travelDuration,
bbox: resp.resourceSets[0].resources[0].bbox
}
var itineraryItems =
resp.resourceSets[0].resources[0].routeLegs[0].itineraryItems.map(
function (item) {
return {
compassDirection: item.compassDirection,
instructionText: item.instruction.text,
maneuverType: item.instruction.maneuverType,
travelDistance: item.travelDistance,
travelDuration: item.travelDuration,
warnings: item.warnings || []
};
});
directions.itineraryItems = new WinJS.Binding.List(itineraryItems);
return directions;
}
return null;
});
}
注意目前,Clok 将只提供以英里为单位的驾驶方向。在第十五章中,你将添加一个特性,允许用户指定他或她喜欢英里还是公里。
在构建 Bing 地图服务的 URL 之后,该值被传递给WinJS.xhr
函数,我们已经将它封装在对WinJS.Promise.timeout
的调用中。这种技术通常用于限制应用尝试连接到指定 URL 的时间。在本例中,xhrTimeout
被设置为 2000 毫秒,因此如果任何从服务获取方向的尝试花费的时间超过 2 秒,用户将被视为离线,请求将被取消。在本章的后面,我将处理取消操作产生的错误,向用户显示适当的消息。
另一方面,如果请求成功,响应(JSON 格式的文本)将被解析成一个名为resp
的 JavaScript 对象。来自 Bing 地图服务的有效响应具有非常深的层次结构。如果resp
已经定义了这个层次,那么就构建了一个directions
对象。该对象是收到的实际响应的简化版本。为了在为数据构建 UI 时简化数据绑定,我删除了层次结构中许多不必要的字段和层。getDirections
函数返回一个Promise
,而directions
对象将通过then
或done
函数用于新方向页面。我稍后将对此进行说明,但首先我们需要对项目详细信息页面进行一些更改,以允许用户请求驾驶方向。
向项目详细信息添加按钮
用户将需要一种方法来导航到我们将在下一节创建的新方向页面。您必须在项目详细信息屏幕上的应用栏中添加一个按钮,该按钮将导航到新页面。至此,您已经能够在应用栏中添加按钮并处理click
事件了,所以我只总结一下要点。向项目详细信息屏幕上的应用栏添加方向按钮。将icon
属性设置为directions
,将disabled
属性设置为true
(参见清单 14-20 )。
清单 14-20。 在项目详情屏幕上添加应用栏按钮
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'goToDirectionsCommand',
label:'Directions',
icon:'directions',
section:'selection',
tooltip:'Directions',
disabled: true}">
</button>
接下来,将清单 14-21 中的代码添加到detail.js
中。不要忘记在detail.js
的就绪函数中连接这个click
事件处理程序。
清单 14-21。 导航至方向屏幕
goToDirectionsCommand_click: function (e) {
if (this.currProject
&& this.currProject.id
&& this.currProject.isAddressSpecified()) {
WinJS.Navigation.navigate("/pages/projects/directions.html", {
project: this.currProject
});
}
},
通过添加来自清单 14-22 的高亮代码来修改detail.js
中的configureAppBar
函数。这将允许为有地址的项目启用方向按钮。如果正在查看的项目尚未保存地址,或者项目尚未保存,按钮将保持禁用状态。
清单 14-22。 启用指路按钮,如果当前项目有地址
configureAppBar: function (existingId) {
var fields = WinJS.Utilities.query("#projectDetailForm input, "
+ "#projectDetailForm textarea, "
+ "#projectDetailForm select");
fields.listen("focus", function (e) {
projectDetailAppBar.winControl.show();
}, false);
if (existingId) {
deleteProjectCommand.winControl.disabled = false;
goToTimeEntriesCommand.winControl.disabled = false;
if (this.currProject.isAddressSpecified()) {
goToDirectionsCommand.winControl.disabled = false;
}
}
},
清单 14-21 中的和清单 14-22 中的都引用了一个名为isAddressSpecified
的新函数。要定义这个函数,添加清单 14-23 中的代码,作为data\project.js
中Project
类的实例成员。
清单 14-23。 一个确定项目是否有指定地址的函数
isAddressSpecified: function () {
return (!!this.address1
|| !!this.city
|| !!this.region
|| !!this.postalCode);
}
运行 Clok 并导航到几个不同的项目,一个有客户端地址,一个没有。如果您使用了本书附带的源代码中指定的测试数据,那么一个项目将会有一个地址。如果您的测试数据中没有一个项目有客户端地址,那么添加一个地址。图 14-8 显示了没有客户地址的项目的项目详细信息屏幕,而图 14-9 显示了有客户地址的项目的相同屏幕。
图 14-8 。此项目没有客户地址
图 14-9 。这个项目有一个客户地址,用户可以请求方向
这样,用户现在可以导航到新方向页面。如果我们已经创造了它,他们就可以。现在,如果你点击方向按钮,应用只会崩溃。
显示驾驶方向
完成此功能的最后一步是向用户实际显示驾驶方向。让我们为新的方向页面定义一些简单的要求。
- 用户可以输入他或她的起始地址。
- 如果由于某种原因无法检索到方向,用户将会看到一条简单的错误消息。
- 如果从 Bing 地图服务中成功检索到方向,它们将显示在列表中。
在pages\projects
文件夹中创建一个名为directions.html
的新页面控件。将页面标题设置为 Directions,并用来自清单 14-24 的代码替换主要部分元素的内容。
清单 14-24。 指点页面
<div id="directionsTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="directionsItem">
<div class="directionsItem-instruction">
<h3 class="directionsItem-instructionText"
data-win-bind="textContent: instructionText"></h3>
</div>
<div class="directionsItem-distance">
<h2 class="directionsItem-formattedDistance"
data-win-bind="textContent: travelDistance
Clok.Data.TravelDistanceConverter"></h2>
</div>
</div>
</div>
<div id="directionsContainer">
<div id="locationsPane">
<h2>Get Directions</h2>
<div class="formField">
<label for="fromLocation">From</label><br />
<input id="fromLocation">
</div>
<div class="formField">
<label for="toLocation">To</label><br />
<span id="toLocation"></span>
</div>
<button id="getDirectionsButton">Get Directions</button>
</div>
<div id="directionsPane">
<div id="directionsSuccess" class="hidden">
<div id="totalDistance">
Total distance:
<span
data-win-bind="textContent: travelDistance
Clok.Data.TravelDistanceConverter"></span>
</div>
<div id="totalTime">
Est. travel time:
<span
data-win-bind="textContent: travelDuration
Clok.Data.TravelTimeConverter"></span>
</div>
<div
id="directionsListView"
class="itemlist win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
layout: {type: WinJS.UI.ListLayout},
itemTemplate: select('#directionsTemplate'),
selectionMode: 'none',
swipeBehavior: 'none',
tapBehavior: 'none'
}">
</div>
<div data-win-bind="textContent: copyright"></div>
</div>
<div id="directionsError" class="hidden">
Could not get directions. Please check your
addresses and internet connection.
</div>
</div>
</div>
页面的布局类似于时间表屏幕,所以我不会详细解释。一边是一个表单,另一边是一个包含方向列表的 ListView。flexbox CSS 布局用于在它们各自的侧面显示它们。
我也不会在这里涵盖完整的 CSS 文件,因为其中没有什么是你没有看过的,在本书附带的源代码中有完整版本的directions.css
。这里我要指出的一点是,我已经添加了 CSS 来突出显示方向列表中的最后一步,使用了清单 14-25 中指定的 CSS。
清单 14-25。 突出指示最后一步
#directionsPane #directionsListView .win-container:last-of-type {
background-color: limegreen;
}
我这样做是为了改善用户体验,因为它给出了一个非常明确的指示,即没有更多的步骤,这在滚动一长串看起来都一样的方向时就不太清楚了。除了last-of-type
伪元素,你还可以使用nth-of-type(odd)
或者nth-of-type(even)
在两种不同的风格之间切换。
用清单 14-26 中的代码替换directions.js
的内容。
清单 14-26。 页面定义为指路屏幕
(function () {
"use strict";
var maps = Clok.Data.BingMaps;
WinJS.UI.Pages.define("/pages/projects/directions.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
this.populateDestination(options);
getDirectionsButton.onclick = this.getDirectionsButton_click.bind(this);
},
populateDestination: function (options) {
if (options && options.project) {
var proj = options.project;
var addressParts = [
proj.address1,
proj.city,
((proj.region || "") + " " + (proj.postalCode || "")).trim()];
this.dest = addressParts.filter(function (part) {
return !!part;
}).join(", ");
toLocation.textContent = this.dest;
}
},
showDirectionResults: function (hasDirections) {
if (hasDirections) {
WinJS.Utilities.removeClass(directionsSuccess, "hidden");
WinJS.Utilities.addClass(directionsError, "hidden");
} else {
WinJS.Utilities.addClass(directionsSuccess, "hidden");
WinJS.Utilities.removeClass(directionsError, "hidden");
}
},
getDirectionsButton_click: function (e) {
if (fromLocation.value) {
maps.getDirections(fromLocation.value, this.dest)
.then(function (directions) {
if (directions
&& directions.itineraryItems
&& directions.itineraryItems.length > 0) {
WinJS.Binding.processAll(
document.getElementById("directionsContainer"), directions);
this.showDirectionResults(true);
directionsListView.winControl.itemDataSource
= directions.itineraryItems.dataSource;
directionsListView.winControl.forceLayout();
} else {
this.showDirectionResults(false);
}
}.bind(this), function (errorEvent) {
this.showDirectionResults(false);
}.bind(this));
} else {
this.showDirectionResults(false);
}
},
});
})();
当从项目细节屏幕导航到该屏幕时,当前项目作为options
参数的属性被传递到方向页面。populateDestination
函数提取目的地地址并将其转换成标准格式,该格式将在获取方向的呼叫中使用。然后,它在屏幕上显示目的地地址。showDirectionResults
功能用于在检索到方向时切换方向列表,在检索不到方向时切换错误消息。
对 Bing 地图服务的调用发生在getDirectionsButton_click
。如果用户指定了起始地址,则调用BingMaps
类中的getDirections
。如果有成功的响应,页面的数据绑定将通过调用WinJS.Binding.processAll
来连接。否则,将显示错误消息。
最后一步是添加清单 14-24 中引用的绑定转换器。将清单 14-27 中突出显示的代码添加到bingMapsWrapper.js
中。
清单 14-27。 为列表方向绑定转换器
var secondsToTravelTimeConverter = WinJS.Binding.converter(function (s) {
if (s > 3600) {
return Clok.Utilities.SecondsToHours(s, true) + " hr";
} else if (s > 60) {
return (s / 60).toFixed(0) + " min";
} else {
return "< 1 min"
}
});
var travelDistanceConverter = WinJS.Binding.converter(function (distance) {
if (distance >= 5) {
return distance.toFixed(0) + " mi";
} else if (distance >= 0.2) {
return distance.toFixed(2) + " mi";
} else {
return (distance * 5280).toFixed(0) + " ft";
}
});
WinJS.Namespace.define("Clok.Data", {
BingMaps: mapsClass,
TravelTimeConverter: secondsToTravelTimeConverter,
TravelDistanceConverter: travelDistanceConverter
});
Bing 地图服务以英里为单位返回距离,以秒为单位返回持续时间。我使用了一些简单的公式,根据英里数或秒数转换成一个更加用户友好的值。立即运行 Clok,获取从您所在位置到您的某个客户的路线。图 14-10 应该类似于你的屏幕,在右边的列表中有一个方向列表,包括你的最终目的地用绿色突出显示(或者如果你正在阅读这本书的黑白版本,用一种较浅的灰色)。
图 14-10 。从西雅图到雷德蒙的方向
注意
directions.itineraryItems
中的每个值都包含一个maneuverType
属性和一个warnings
属性。我们不会在本书中使用它们,但是雄心勃勃的开发者可以在ListView
中使用它们为用户提供额外的信息。例如,当maneuverType
为“右转”时,您可能希望显示一个指向右边的箭头,当出现“收费站”警告时,您可能会显示一个货币符号。Bing 地图服务可以返回 60 多种策略类型和 30 多种警告类型。关于机动类型和警告的更多信息可分别在http://msdn.microsoft.com/en-us/library/gg650392.aspx
和http://msdn.microsoft.com/en-us/library/hh441731.aspx
的 MSDN 上获得。
外部库
虽然用 HTML 和 JavaScript 构建的 Windows 应用商店应用中对远程数据的大多数访问将使用WinJS.xhr
来检索或提交数据,但还有其他选择。使用外部库,无论是 JavaScript 库还是其他 WinRT 库,都可以实现自己处理远程数据的方法。例如,如果您在应用中使用 jQuery,您可以使用$.get
或$.post
来处理远程 HTTP 服务。在幕后,jQuery 仍然在使用XMLHttpRequest
,但是它把它抽象出来了。同样,如果您正在构建或引用一个 C# WinRT 组件,它可能会使用HttpClient
类访问远程数据。虽然我将在第十八章的中介绍一个非常简单的 C# WinRT 组件,但我不会用这两种技术来介绍远程数据源。
Azure 移动服务
如果你正在寻找一个完整的远程数据解决方案,我会鼓励你看看 Windows Azure 移动服务。除了数据存储和检索之外,移动服务还提供了许多出色的功能,例如数据验证、单点登录用户身份验证、推送通知等等。所有这些功能在多个平台上都受支持,包括 Windows 应用商店应用、Windows Phone、iOS、Android 和 HTML 应用。移动服务是一个巨大的话题,可以用一整本书来专门讨论它。与其在这里尝试,我建议去 Windows Azure 移动服务在线开发中心获取文档和教程。
结论
外面有很多数据。有时候,你需要它在你的应用中本地可用,供离线使用,IndexedDB 是一个很好的选择。其他时候,您需要从第三方服务访问数据,或者将数据保存到您公司开发的自定义 HTTP 服务中。在这些情况下,WinJS.xhr
是一个很好的起点。然而,还存在其他选择,包括 SQLite,用于本地数据存储,以及使用外部库或 Windows Azure Mobile 服务来提供对远程数据源的访问。
Clok 现在是一个人们可以实际使用的应用。在本章之前,没有保存任何数据,每次启动应用时,任何更改都会丢失。随着本章中 IndexedDB 的引入,现在可以保存对项目和时间条目的所有更改。在接下来的几章中,我们还将继续进行一些改进,以改善用户体验,包括允许用户保存一些应用偏好,我将在第十五章中介绍。
十五、会话状态和设置
在第十四章中,我介绍了一些使用本地和远程数据源的技术。除了用户在应用中创建并与之交互的数据之外,通常还需要保存和加载其他值。例如,您可能希望保存用户正在处理的表单的内容,以便您可以在应用终止后重新填充表单。或者用户可能希望在每次运行应用时指定某些首选项。
尽管可能,但将这些类型的值存储在 IndexedDB 数据库中并不理想。实际上,虽然 IndexedDB 可以高效地存储大量信息,但有更好的方法来存储这些类型的值。如果您的应用没有其他理由使用 IndexedDB,尤其如此,因为创建和连接 IndexedDB 数据库会产生开销。
我所指的值的类型被认为是会话状态或设置。会话状态和设置是类似的概念,因为它们允许您使用简单的语法存储简单的值,通常很小。Windows 应用商店应用可以利用会话状态和两种类型的设置—本地和漫游。在本章中,我将介绍以下主题:
- 会话状态:如果应用被挂起和终止,存储维护和恢复应用状态所需的值
- 本地设置:存储在应用启动之间和重启之后必须保持的值。用户当前使用的单台计算机的本地
- 漫游设置:功能上与本地设置相同,只是它们在多台机器之间同步
注微软在 MSDN 发表了一篇题为《高效存储和检索状态》的文章它比较了您可能考虑用于存储数据、会话状态和设置的各种选项。可以在
http://msdn.microsoft.com/en-us/library/windows/apps/hh781225.aspx
找到。
会话状态
使用 Windows 8 时,您可以从开始屏幕启动任意数量的应用。您可以让一个应用以全屏模式运行,也可以让两个应用并排显示。您可以随意在打开的应用之间切换,当您返回到之前使用的应用时,您可以从您离开的地方继续。标准的东西,对吧?实际上,所有这些应用可能不会一直运行。事实上,当你从一个应用切换出来时,Windows 会挂起它。该应用将保留在内存中,直到您切换回来,此时 Windows 将恢复该应用。因为应用在内存中,所以这是一个非常无缝的体验,而且应用似乎从未停止运行。
那么,为什么要提起呢?有时,当您从某个应用切换出来后,您的电脑将没有足够的资源将该应用保存在内存中。此时,Windows 将终止该应用,释放它正在使用的资源。例如,假设您正在使用应用来完成一个很长的表单,然后切换到其他地方查找完成表单所需的信息。找到所需信息后,切换回应用并完成表单。如果在您查找信息时,Windows 终止了应用,当您切换回来时会发生什么?解决这个问题是会话状态的目的。会话状态可用于在您使用应用时捕获它的当前状态,然后在终止的应用重新启动时还原它。
幸运的是,WinJS 使得从会话状态中保存和检索项目变得很容易。我将向您展示如何在项目详细信息页面上将会话状态合并到 Clok 中。如果 Clok 在此屏幕上工作时被终止,当它恢复时,会话状态将用于使屏幕看起来就像 Clok 被终止前一样。
保存会话状态
在本节中,您将添加代码以将项目详细信息表单的当前状态保存到会话状态中。当用户更改字段中的值时,您将更新会话状态。这是一个非常简单的要求,实现也非常简单。首先,打开detail.js
,在文件顶部附近添加清单 15-1 中突出显示的类别名。
清单 15-1。 添加一些别名来方便自己
var app = WinJS.Application;
var data = Clok.Data;
var storage = Clok.Data.Storage;
WinJS.Application
类有一个名为sessionState
的对象,这是我们处理会话状态的访问点。因为 JavaScript 是一种动态语言,你可以简单地给sessionState
附加新的属性,它们就会被保存。通过添加清单 15-2 中突出显示的代码来更新ready
函数。
清单 15-2。 字段改变时更新会话状态
ready: function (element, options) {
// SNIPPED
WinJS.Utilities.query("input, textarea, select")
.listen("change", function (e) {
this.populateProjectFromForm();
app.sessionState.currProject = this.currProject;
}.bind(this));
projectStatus.addEventListener("change", function (e) {
this.populateProjectFromForm();
app.sessionState.currProject = this.currProject;
}.bind(this));
},
这段代码为任何输入字段、文本区域或下拉列表上的change
事件添加一个事件处理程序,并为projectStatus ToggleSwitch
添加一个事件处理程序。两个处理程序的代码是相同的。首先,调用您在第十一章中添加的populateProjectFromForm
函数。在此之前,该函数仅在保存currProject
变量之前使用(见清单 15-3 )。
清单 15-3。 之前使用的 populateProjectFromForm 函数
this.populateProjectFromForm();
storage.projects.save(this.currProject);
在清单 15-3 中,populateProjectFromForm
函数根据表单字段中的值更新currProject
的值。然后currProject
被保存到app.sessionState
中,顾名思义,这是 WinJS 应用中存储会话状态的地方。
到目前为止非常简单,当用户对项目细节表单进行更改时,这将非常有助于保持会话状态最新。但是,当用户键入时,不会引发 change 事件。对于文本输入控件,只有在用户更改控件的值,然后将焦点从输入字段移开后,才会引发该事件。在许多情况下,这不会有太大的不同。但是,假设在应用终止之前,您已经在描述字段中键入了几个段落。如果您一直在输入,但从未通过(例如)移动到下一个字段来触发更改事件,则您的更改不会添加到会话状态中。
为了处理这个场景,您必须处理WinJS.Application
的checkpoint
事件,该事件在应用即将被挂起时被触发。将清单 15-4 中高亮显示的代码添加到detail.js
中。
清单 15-4。处理检查点事件
ready: function (element, options) {
// SNIPPED
this.app_checkpoint_boundThis = this.checkpoint.bind(this);
app.addEventListener("checkpoint", this.app_checkpoint_boundThis);
WinJS.Utilities.query("input, textarea, select")
.listen("change", function (e) {
this.populateProjectFromForm();
app.sessionState.currProject = this.currProject;
}.bind(this));
projectStatus.addEventListener("change", function (e) {
this.populateProjectFromForm();
app.sessionState.currProject = this.currProject;
}.bind(this));
},
checkpoint: function () {
this.populateProjectFromForm();
app.sessionState.currProject = this.currProject;
},
尽管对于当前的任务来说可能会更简单,但是添加事件监听器时不寻常的语法很快就会派上用场。简而言之,我将app_checkpoint_boundThis
定义为一个函数,它是checkpoint
函数,并且this
变量的作用域与ready
函数中的相同。在checkpoint
函数中,使用了与清单 15-2 中定义的另外两个事件处理程序相同的代码。
现在,用户已经做出或正在做出的任何更改都将保存到会话状态中。如果他或她对字段进行了更改,会话状态将立即更新。如果当应用终止时,用户正在进行更改,当应用的checkpoint
事件被触发时,会话状态将被更新。
注意 Windows 8 在终止你的应用时不会引发事件。当它被挂起时,它会通过
checkpoint
事件通知应用。当对保存的值进行更改时,以及当应用被挂起时,都应该保存会话状态。
checkpoint
事件是WinJS.Application
类的一部分。这意味着应用本身,而不仅仅是这个屏幕,正在引发事件。因此,我们希望确保当这个页面不活动时,比如当用户已经导航离开时,我们不必费心处理这个事件。同样,如果用户已经明确地离开了这个页面,我们可以丢弃存储在会话状态中的值。记住:会话状态的目的是让你的应用看起来好像从未被挂起或终止过。在detail.js
中的ready
函数后添加来自清单 15-5 的代码。
清单 15-5。 当用户导航离开页面时重置内容
unload: function () {
app.sessionState.currProject = null;
app.removeEventListener("checkpoint", this.app_checkpoint_boundThis);
},
正在读取会话状态
将对象保存到会话状态非常容易。现在,我将向您展示如何在恢复一个终止的应用时使用它。当前,当该屏幕加载时,执行清单 15-6 中的代码来初始化currProject
属性。如果我们正在编辑一个现有的项目,我们将currProject
设置为该值;否则,我们将它设置为一个新的、空的Project
。
清单 15-6。curr project 的当前初始化
this.currProject = storage.projects.getById(options && options.id)
|| new Clok.Data.Project();
现在我们添加了会话状态作为初始化currProject
的另一个因素,逻辑会变得稍微复杂一些。让我们把这个逻辑移到一个新的函数中,试图让ready
函数更容易理解。用清单 15-7 中的代码替换清单 15-6 中的代码,即detail.js
的ready
函数的第一行。
清单 15-7。 替换代码
this.setCurrentProject(options);
你必须添加更新的初始化逻辑,所以通过添加清单 15-8 中的代码来定义detail.js
中的setCurrentProject
函数。
清单 15-8。 初始化 currProject 的新逻辑
setCurrentProject: function (options) {
var sessionProject = (app.sessionState.currProject)
? data.Project.createFromDeserialized(app.sessionState.currProject)
: null;
if (options && options.id && sessionProject && options.id !== sessionProject.id) {
sessionProject = null;
}
this.currProject = sessionProject
|| storage.projects.getById(options && options.id)
|| new Clok.Data.Project();
app.sessionState.currProject = this.currProject;
},
该函数做的第一件事是确定项目当前是否保存在会话状态中。如果是,但由于某种原因,它不是当前正在查看的同一项目,则会话状态中的值将被忽略。此时,currProject
被设置为来自会话状态的Project
对象,如果它存在的话。如果不是,但是我们正在编辑一个项目,currProject
被设置为那个Project
对象。否则,当我们添加一个新项目时,currProject
被设置为一个新的空的Project
对象。然后,在结束时,currProject
被保存到会话状态。
因为项目细节屏幕已经将其表单绑定到了currProject
属性,这就是我们所要做的。项目详细信息屏幕现在将在会话状态中保存其状态,并在应用终止后恢复时恢复。让我们看看如何测试这个。
测试暂停和终止
要从 Visual Studio 测试这段代码,您必须调试应用(F5),而不是不调试就运行应用(Ctrl+F5)。您也可以单击 Visual Studio 工具栏上的“调试”按钮。图 15-1 显示了我的调试按钮,调试目标设置为模拟器。这将启动 Clok 并附加 Visual Studio 调试器。
图 15-1 。在 Windows 模拟器中调试
立即调试 Clok。我更喜欢使用模拟器,但是如果你愿意,你也可以选择本地机器或者远程机器。导航至现有项目的项目详细信息屏幕,并对一个或多个字段进行更改(参见图 15-2 )。
图 15-2 。项目详细信息,在一个字段中进行了更改,在另一个字段中进行了更改
当您仍在项目详细信息屏幕上时,在保存项目之前,切换回 Visual Studio。当 Clok 连接调试器运行时,你会在工具栏上看到一个类似于图 15-3 所示的菜单。如果你只点击按钮,它将暂停应用。相反,如果您展开菜单,您可以选择挂起、恢复或挂起并关闭。
图 15-3 。模拟应用终止
由于 Windows 处理应用暂停和恢复的方式,这些场景将“正常工作”,Clok 将表现得好像它从未被中断过一样。然而,挂起和关闭选项模拟当资源太低而无法在不使用时将应用保留在内存中时,Windows 终止应用。如果您现在选择该选项,Clok 将关闭。
但是,如果您再次启动它,它应该会返回到同一个屏幕,您正在进行的更改仍然会显示在表单中。看起来应该还是像图 15-2 。
关于会话状态的快速注释
会话状态是 WinJS 的一个便利特性,它让你的应用看起来好像从未停止运行。但是,有时会丢弃会话状态。
- 我们添加了代码,以便在用户导航到另一个屏幕时丢弃它。
- 如果用户手动关闭应用,例如通过按 Alt+F4,或者从触摸屏的顶部向下滑动并将应用拖到屏幕的底部,则它会被丢弃。
- 当用户重新启动计算机时,它将被丢弃。
在本书附带的代码中(参见本书的 Apress 产品页面[ www.apress.com/9781430257790
]的源代码/下载选项卡),您可以找到一个版本的方向屏幕,它也利用了会话状态。我没有将它添加到时间输入屏幕。如果您想在屏幕上添加会话状态,这可能是一个很好的练习。这比我们在这里看到的要复杂一些,因为除了跟踪时间条目添加/编辑表单的当前状态之外,它还必须考虑在时间条目列表中选择了哪些项目(如果有的话)。
本地设置
会话状态存储值,使应用看起来好像从未停止运行,即使它可能已被挂起或终止,然后又被恢复。另一方面,本地设置有不同的目的。虽然会话状态最终会被丢弃,但在应用启动、应用关闭和计算机重新启动之间,本地设置会得到维护。本地设置是一个很好的工具,可以在这台计算机上存储与该应用相关的值,并以一种持久的方式长期存储这些值。
保存本地设置
虽然现在可以使用 Clok,但是有许多不便之处。一个突出的问题是,如果您在仪表板上启动计时器,然后关闭应用或导航到另一个屏幕,当您返回时,计时器已经停止并重置。在这一节中,我将向您展示如何使用本地设置来保持应用启动和导航之间的计时器状态。
为此,我们将保存计时器的当前状态(它的startStops
属性)、当前选择的项目以及已经输入到本地设置中的任何注释。有了这些值,当启动 Clok 或从应用的另一个屏幕导航回仪表板时,就有可能以正确的状态显示计时器。打开home.js
并将来自清单 15-9 的代码添加到文件顶部附近。
清单 15-9。 添加更多别名
var appData = Windows.Storage.ApplicationData.current;
var localSettings = appData.localSettings;
接下来,将清单 15-10 中的代码添加到PageControl
定义中的home.js
中。
清单 15-10。 功能更新本地设置
saveDashboardStateToSettings: function () {
var state = JSON.stringify({
startStops: elapsedTimeClock.winControl.startStops,
projectId: Number(project.options[project.selectedIndex].value),
timeNotes: timeNotes.value,
});
localSettings.values["dashboardState"] = state;
},
removeDashboardStateFromSettings: function () {
localSettings.values.remove("dashboardState");
},
许多类型的值可以保存到本地设置中,但不幸的是,表示计时器启动和停止时间的对象数组不能。为了解决这个问题,我使用了JSON
类中的stringify
函数将 JavaScript 对象转换成 JSON 格式的字符串。虽然没有简单地将state
变量保存到本地设置中那么方便,但这是一个简单的步骤。
在清单 15-10 中定义的函数将在home.js
中的不同地方被调用。每当用户启动或停止计时器、选择项目或输入注释时,都会调用saveDashboardStateToSettings
函数。将清单 15-11 中高亮显示的代码添加到home.js
中。
清单 15-11。 调用该功能保存本地设置
project_change: function (e) {
this.enableOrDisableButtons();
this.saveDashboardStateToSettings();
},
timeNotes_change: function (e) {
this.saveDashboardStateToSettings();
},
toggleTimer: function () {
this.timerIsRunning = !this.timerIsRunning;
this.setupTimerRelatedControls();
this.saveDashboardStateToSettings();
},
类似地,removeDashboardStateFromSettings
函数将在两个不同的时间被调用:当一个时间条目被保存或丢弃时。在save
和discard
功能的最后是一个done
功能,它目前只重置计时器。将清单 15-12 中突出显示的代码添加到这两个done
函数中,以便在不再需要该值时清理本地设置。
清单 15-12。 清除本地设置中的保存和丢弃功能
.done(function () {
self.resetTimer();
self.removeDashboardStateFromSettings();
});
正在读取本地设置
与会话状态一样,保存本地设置是一项非常简单的任务。你不会惊讶地发现,阅读它们也一样简单。在这一节中,我将向您展示如何读取之前保存的本地设置,以使 Clok 仪表板按照用户的期望工作。即使应用可能没有运行,计时器也会显示为连续运行。这也是我在《??》第十二章中重构Clock
控件的原因之一。因为我们可以根据计时器的开始和停止来计算经过的时间,所以我们可以让计时器看起来好像它一直在运行,而实际上并没有。
你要做的第一件事是添加代码来读取我们在清单 15-10 中创建的本地设置。因为我们必须将其保存为 JSON 格式的字符串,所以我们必须使用JSON.parse
函数将其转换回对象。将清单 15-13 中的代码添加到home.js
。
清单 15-13。 功能读取本地设置并初始化控件
setDashboardStateFromSettings: function () {
var state = localSettings.values["dashboardState"];
if (state) {
state = JSON.parse(state);
elapsedTimeClock.winControl.startStops = state.startStops;
project.selectedIndex = this.getIndexOfProjectId(state.projectId);
timeNotes.value = state.timeNotes;
if (elapsedTimeClock.winControl.isRunning) {
this.startTimer();
}
}
},
getIndexOfProjectId: function (projectId) {
var index = 0;
for (var i = 0; i < project.options.length; i++) {
if (!isNaN(project.options[i].value)
&& Number(project.options[i].value) === projectId) {
index = i;
break;
}
}
return index;
}
从本地设置中获取状态后,它用于为计时器、所选项目和 notes 字段设置正确的值。然后,如果定时器控件应该运行,我们通过调用startTimer
函数来启动它。startTimer
函数是新的,但其中的代码不是。我只是重构了setupTimerRelatedControls
函数,拉出了启动和停止的逻辑。更新setupTimerRelatedControls
函数,并在清单 15-14 中添加两个新函数。
清单 15-14。 重构函数
setupTimerRelatedControls: function () {
if (this.timerIsRunning) {
this.startTimer();
} else {
this.stopTimer();
}
this.enableOrDisableButtons();
},
startTimer: function () {
elapsedTimeClock.winControl.start();
timerImage.src = "/img/Clock-Running.png";
timerTitle.innerText = "Stop Clok";
this.timerIsRunning = true;
},
stopTimer: function () {
elapsedTimeClock.winControl.stop();
timerImage.src = "/img/Clock-Stopped.png";
timerTitle.innerText = "Start Clok";
this.timerIsRunning = false;
},
需要对Timer
控件做一点小小的改动。目前,让计时器开始计数的唯一方法是调用start
函数。然而,该功能仅在定时器尚未运行时有效。在我们返回计时器应该运行的仪表板屏幕的情况下,我们必须能够启动更新运行时间的间隔。将清单 15-15 中高亮显示的代码添加到timerControl.js
中的start
函数中。
清单 15-15。 添加条件允许定时器从停止的地方重新开始
start: function () {
if (!this.isRunning) {
this._intervalId = setInterval(this._updateTimer.bind(this), 250);
this.startStops[this.startStops.length] = { startTime: (new Date()).getTime() };
this.dispatchEvent("start", {});
} else if (this._intervalId <= 0) {
// timer is running, but not updating yet
this._intervalId = setInterval(this._updateTimer.bind(this), 250);
}
},
当startStops
中有一个项目有一个没有stopTime
的startTime
时,Timer
控件的isRunning
属性为true
。只要我们停留在仪表板上,就会根据isRunning
属性开始或停止更新 UI 的时间间隔。然而,如果我们启动计时器,然后重新启动 Clok,或者简单地从应用的另一个屏幕返回到仪表板屏幕,那么startStops
数组将导致isRunning
成为true
,即使间隔时间没有更新 UI。有了这个改变,调用start
将开始间隔。
现在用来自清单 15-16 的代码更新home.js
中的ready
函数。
清单 15-16。 修改就绪功能
ready: function (element, options) {
this.initializeMenuPointerAnimations();
this.bindListOfProjects();
this.setDashboardStateFromSettings();
this.setupTimerRelatedControls();
toggleTimerMenuItem.onclick = this.toggleTimerMenuItem_click.bind(this);
project.onchange = this.project_change.bind(this);
timeNotes.onchange = this.timeNotes_change.bind(this);
editProjectButton.onclick = this.editProjectButton_click.bind(this);
saveTimeButton.onclick = this.saveTimeButton_click.bind(this);
discardTimeButton.onclick = this.discardTimeButton_click.bind(this);
projectsMenuItem.onclick = this.projectsMenuItem_click.bind(this);
timesheetMenuItem.onclick = this.timesheetMenuItem_click.bind(this);
},
除了将一些语句以不同的顺序组合成类似的代码,主要的区别是我调用了我们刚刚在清单 15-13 的中添加的新的setDashboardStateFromSettings
函数。现在,在推出 Clok 并进行测试之前,还有一个问题需要解决。
您可能还记得上一章中的一个注释,描述了由于从 IndexedDB 数据库异步加载数据而导致的在仪表板上加载项目列表的延迟。如果您现在运行此代码,应用可能仍然会启动,但没有完全填充的项目列表。不用担心;数据就在那里,如果您导航到(例如)项目屏幕,然后再回到控制面板,就会显示出来。然而,我们只是添加了代码,根据保存到本地设置的状态,在这个列表中选择适当的项目。当我讨论闪屏和应用状态时,我提到我会在第十七章中讨论减轻这种情况的方法。也就是说,如果没有项目列表,很难展示这个例子的预期效果。我将向您展示如何用承诺暂时解决这个问题。将清单 15-17 中的代码作为静态成员添加到storage.js
中。
清单 15-17。 创建函数初始化 IndexedDB
initialize: function () {
return _openDb;
},
将home.js
中ready
函数的内容包装起来(参见清单 15-16 ),调用initialize
函数,返回一个Promise
对象(参见清单 15-18 )。
清单 15-18。 将 Ready 函数的内容包装在一个承诺中
ready: function (element, options) {
storage.initialize().done(function () {
// SNIPPED
}.bind(this));
},
注意这实际上是处理这个问题的一个非常有效的方法,但是我们将在第十七章中用不同的方式来解决这个问题。这修复了当仪表板是第一个加载的屏幕时的数据加载竞争情况,但是它没有解决用户以某种方式打开 Clok 到不同屏幕的问题。如果用户在不同的屏幕上恢复之前执行的已终止的应用,这可能会发生,或者它可能会通过点击通知或从 Windows 搜索界面激活 Clok 来发生,这是我将在第十九章中讨论的主题。
测试本地设置
在一个看起来不像仪表板的静态截图中,我没有太多可以演示的内容。但是,如果你现在跑 Clok,你可以通过一个小测试看到你的劳动成果。
- 发射 Clok。
- 启动计时器并选择一个项目。
- 关闭 Clok 一会儿。
- 重新推出 Clok。
当 Clok 重新启动时,仪表板将显示自您第一次启动计时器以来经过的总时间。在关闭应用之前,让计时器保持运行状态,并在关闭应用之前让计时器停止运行,以此来测试它。在这个过程中,你甚至可以重启电脑。
如果 Clok 在第十四章的结尾是一个可用的应用,那么它现在就更可用了。仍然有许多特性需要添加,但这是最明显的缺点。另一个可以改善整体用户体验的特性是允许用户为应用中的不同选项指定一些偏好。我将在下一步介绍漫游设置时讨论这个问题。
漫游设置
本地设置和漫游设置非常相似。在应用启动甚至计算机重启之间,它们都永久地存储值。因为它们都是ApplicationDataContainer
的实例,所以它们的 API 是相同的。两者都是存储用户偏好的好选择。不同之处在于,存储在本地设置中的任何内容都只能在存储它的计算机上使用。另一方面,存储在漫游设置中的任何内容都将与同一用户安装了您的软件的任何其他计算机同步。
在这一节中,我将向您展示如何使用漫游设置来存储用户对 Clok 的偏好。建议将影响用户与应用交互方式的任何此类设置存储在漫游设置中,而不是本地设置中。如果他们在另一台电脑上使用该应用,这些设置将在电脑之间同步。
起初,在计算机之间漫游设置对我来说似乎是违反直觉的,但我当时的问题是,我考虑的每个例子都是只对单台计算机有意义的设置。我正在编写一个应用,从用户的硬盘上加载图像文件,我想在设置中存储最近使用的路径。这在多台电脑上实际上没有意义,因为一台电脑上包含图像的目录可能在另一台电脑上不存在。然而,当我开始考虑其他类型的设置时,我开始明白漫游设置应该是我的首选,在适当的时候恢复到本地设置。如果一个设置影响用户与应用的交互方式,那么这个设置应该漫游。例如,在 Clok 中,我们将允许用户指定他们喜欢 12 小时制还是 24 小时制。如果他们更喜欢一台计算机上的 12 小时时钟,他们很可能更喜欢每台计算机上的 12 小时时钟。
关于漫游设置,需要记住的一点是,只有当用户使用 Microsoft 帐户登录他们的计算机时,这些设置才会漫游。如果他们没有使用 Microsoft 帐户登录计算机,漫游设置就像本地设置一样。微软于http://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx
在 MSDN 上发布了“漫游应用数据指南”。如果您想测试漫游设置的同步功能,您可以切换到 Microsoft 帐户登录,方法是转到开始屏幕,键入“用户”,将搜索上下文切换到设置,然后单击用户搜索结果。从那里,你可以配置你的电脑,这样你就可以用微软账户登录(见图 15-4 )。
图 15-4 。切换到 Microsoft 帐户
保存漫游设置
Clok 有一些特性,我们的用户可能想要为这些特性指定偏好。在这一节中,我将向您介绍如何实现 UI 来允许他们指定自己的首选项,以及如何将这些首选项保存到漫游设置中。具体来说,我们将允许用户更改以下内容:
- 当前时间是以 12 小时制还是 24 小时制显示
- 秒是否将显示为当前时间的一部分
- 发出 Bing 地图请求的连接超时
- 在方向屏幕上是以英里还是公里显示距离
- 是否启用或禁用我们在第十四章的中添加的 IndexedDB 助手设置弹出按钮
你要做的第一件事是向 Clok 选项设置弹出按钮添加控件,允许用户表明他或她的偏好。用清单 15-19 中的代码替换settings\options.html
中的win-content``div
。
清单 15-19。 构建 Clok 选项 UI
<div class="win-content">
<div class="win-settings-section">
<h3>Current Time</h3>
<div id="clockModeToggle"
data-win-control="WinJS.UI.ToggleSwitch"
data-win-options="{
title:'12-hour format or 24-hour format',
labelOn: '15:30',
labelOff: '3:30 PM'
}"></div>
<div id="clockSecondsToggle"
data-win-control="WinJS.UI.ToggleSwitch"
data-win-options="{
title:'Show or hide seconds',
labelOn: 'Show',
labelOff: 'Hide'
}"></div>
</div>
<div class="win-settings-section">
<h3>Bing Maps API</h3>
<label>Connection Speed (timeout)</label>
<label><input type="radio"
name="bingMapsTimeout"
id="bingMapsTimeout_2000"
value="2000" />Fast connection (2 sec)</label>
<label><input type="radio"
name="bingMapsTimeout"
id="bingMapsTimeout_5000"
value="5000" />Normal connection (5 sec)</label>
<label><input type="radio"
name="bingMapsTimeout"
id="bingMapsTimeout_10000"
value="10000" />Slow connection (10 sec)</label>
<div id="bingMapsDistanceUnitToggle"
data-win-control="WinJS.UI.ToggleSwitch"
data-win-options="{
title:'Metric or Imperial System',
labelOn: '6.4 km',
labelOff: '4 mi'
}"></div>
</div>
<div class="win-settings-section">
<h3>Debugging</h3>
<div id="indexedDbHelperToggle"
data-win-control="WinJS.UI.ToggleSwitch"
data-win-options="{
title:'Enable IndexedDB Helper',
labelOn: 'Enabled',
labelOff: 'Disabled'
}"></div>
</div>
</div>
我添加了几个ToggleSwitch
控件来配置当前时间。单选按钮用于指定 Bing 地图连接超时。另外两个ToggleSwitch
控件用于指定英里或公里以及启用 IndexedDB 助手。当然,简单地向用户显示这些控件并不能更新漫游设置。在settings
文件夹中新建一个名为options.js
的 JavaScript 文件,并在options.html
的head
元素中引用它(参见清单 15-20 )。
清单 15-20。 引用
<head>
<title>Options</title>
<script src="options.js"></script>
</head>
接下来,将清单 15-21 中的代码添加到新的options.js
文件中。
清单 15-21。 保存漫游设置
(function () {
"use strict";
var appData = Windows.Storage.ApplicationData.current;
var roamingSettings = appData.roamingSettings;
var page = WinJS.UI.Pages.define("/settings/options.html", {
ready: function (element, options) {
clockSecondsToggle.onchange = this.clockSecondsToggle_change;
clockModeToggle.onchange = this.clockModeToggle_change;
bingMapsTimeout_2000.onchange = this.bingMapsTimeout_change;
bingMapsTimeout_5000.onchange = this.bingMapsTimeout_change;
bingMapsTimeout_10000.onchange = this.bingMapsTimeout_change;
bingMapsDistanceUnitToggle.onchange = this.bingMapsDistanceUnitToggle_change;
indexedDbHelperToggle.onchange = this.indexedDbHelperToggle_change;
},
clockSecondsToggle_change: function (e) {
roamingSettings.values["clockSeconds"] =
clockSecondsToggle.winControl.checked;
},
clockModeToggle_change: function (e) {
roamingSettings.values["clockMode"] =
(clockModeToggle.winControl.checked)
? Clok.UI.ClockModes.CurrentTime24
: Clok.UI.ClockModes.CurrentTime12;
},
bingMapsTimeout_change: function (e) {
roamingSettings.values["bingMapsTimeout"] = Number(e.currentTarget.value);
},
bingMapsDistanceUnitToggle_change: function (e) {
roamingSettings.values["bingDistanceUnit"] =
(bingMapsDistanceUnitToggle.winControl.checked) ? "km" : "mi";
},
indexedDbHelperToggle_change: function (e) {
roamingSettings.values["enableIndexedDbHelper"] =
indexedDbHelperToggle.winControl.checked;
},
});
})();
在“设置”弹出按钮中使用用户首选项时,最佳做法是在做出任何更改后立即应用这些更改。例如,当用户改变一个ToggleSwitch
的值时,这个改变应该立即生效。因此,“设置”弹出按钮上没有“保存”或“提交”按钮。相反,我用一个函数处理了每个控件的 change 事件,该函数在用户指示更改时立即将首选项保存到漫游设置中。
正如您在这段代码中看到的,任何当前存储在本地设置中的设置都可以通过简单地将localSettings.values["someKey"]
更改为roamingSettings.values["someKey"]
而移动到漫游设置中。了解了这一点,有灵感的开发人员可以构建一个更复杂的偏好系统,允许用户指定他或她想要漫游的设置(如果有的话)。根据用户的偏好,您可以决定是使用localSettings
容器还是roamingSettings
容器。
保存漫游设置非常容易。读他们呢?
读取漫游设置
当用户没有指定某个设置的首选项时,您的应用应该使用合理的默认值。这实际上适用于任何类型的设置:本地设置、漫游设置,甚至会话状态。有几种方法可以解决这个问题。一种选择是在检查设置时使用默认值,以获得用户的偏好并发现他或她没有指定。在某些情况下,这没问题,这就是我对会话状态采取的方法。但是,使用这种方法,每次需要用户的首选项时,您都必须检查一个值并设置一个默认值。如果在应用的多个地方使用了某个特定的设置,那么在每个地方都会有重复的代码。
另一个选择是确保设置总是有一个值。这是我在 Clok 中采用的方法。将清单 15-22 中定义的函数添加到default.js
。该函数查看每个漫游设置,如果它们还没有值,则给它们分配一个默认值。
清单 15-22。 确保漫游设置有合适的默认值
var initializeRoamingSettings = function () {
roamingSettings.values["clockSeconds"] =
roamingSettings.values["clockSeconds"] || false;
roamingSettings.values["clockMode"] =
roamingSettings.values["clockMode"] || Clok.UI.ClockModes.CurrentTime12;
roamingSettings.values["bingMapsTimeout"] =
roamingSettings.values["bingMapsTimeout"] || 5000;
roamingSettings.values["bingDistanceUnit"] =
roamingSettings.values["bingDistanceUnit"] || "mi";
roamingSettings.values["enableIndexedDbHelper"] =
roamingSettings.values["enableIndexedDbHelper"] || false;
};
确保将清单 15-23 中的别名添加到default.js
的顶部,靠近其他别名。
清单 15-23。 别名用于漫游设置
var appData = Windows.Storage.ApplicationData.current;
var roamingSettings = appData.roamingSettings;
因为清单 15-22 中的代码将在应用启动后很快执行,在我们有机会尝试读取漫游设置之前,这些设置将总是有一个指定的值。这使得我们可以在需要时简单地检查设置,而不必担心如果用户从未保存某个特定设置的值该怎么办。
当我们仍然在default.js
中工作时,让我们根据用户的偏好添加代码来显示或隐藏 IndexedDB 助手设置弹出按钮。用清单 15-24 中突出显示的代码更新default.js
。这既包括对initializeRoamingSettings
的调用,也包括决定 IndexedDB 助手设置弹出按钮是否可用的逻辑。
清单 15-24。 初始化漫游设置并决定添加设置弹出按钮
initializeRoamingSettings();
// add our SettingsFlyout to the list when the Settings charm is shown
WinJS.Application.onsettings = function (e) {
e.detail.applicationcommands = {
"options": {
title: "Clok Options",
href: "/settings/options.html"
},
"about": {
title: "About Clok",
href: "/settings/about.html"
}
};
if (roamingSettings.values["enableIndexedDbHelper"]) {
e.detail.applicationcommands.idbhelper = {
title: "IndexedDB Helper",
href: "/settings/idbhelper.html"
};
}
WinJS.UI.SettingsFlyout.populateSettings(e);
};
现在,让我们重新访问“块选项设置”弹出按钮。现在,用户可以打开设置弹出按钮并保存他或她的设置。但是,加载“设定”弹出按钮时,“设定”弹出按钮上的控件不会反映每个设定的当前值。将清单 15-25 中的代码添加到options.js
。另外,一定要从options.js
中的ready
函数调用initializeSettingsControls
函数。
清单 15-25。 为 Clok 选项设置弹出按钮上的控件设置初始状态
initializeSettingsControls: function() {
clockSecondsToggle.winControl.checked =
roamingSettings.values["clockSeconds"];
clockModeToggle.winControl.checked =
roamingSettings.values["clockMode"] === Clok.UI.ClockModes.CurrentTime24;
switch (roamingSettings.values["bingMapsTimeout"]) {
case 5000:
bingMapsTimeout_5000.checked = true;
break;
case 10000:
bingMapsTimeout_10000.checked = true;
break;
default:
bingMapsTimeout_2000.checked = true;
}
bingMapsDistanceUnitToggle.winControl.checked =
roamingSettings.values["bingDistanceUnit"] === "km";
indexedDbHelperToggle.winControl.checked =
roamingSettings.values["enableIndexedDbHelper"];
},
正如我所展示的,根据设置更新 UI 或设置对象属性很简单。虽然我不会在bingMapsWrapper.js
中展示合并漫游设置所需的更新,但我会简单地提醒你在自己做这些更改时去哪里查看。应根据设置来设置xhrTimeout
变量,以及getDirections
函数中的distanceUnit
变量。此外,记得更新travelDistanceConverter
函数,以指示距离使用公制,如果这是用户指定的。如果你卡住了,你可以在本书附带的源代码中看到一个完整版本的BingMaps
类。(见该书的 Apress 产品页[ www.apress.com/9781430257790
]的源代码/下载标签)。)
数据更改事件
在上述情况下,每次需要时都会检查漫游设置。然而,如果一个设置已经被应用,然后它改变了,会发生什么呢?例如,如果用户指定他或她想要为您的应用使用一个新的主题,但是该主题在应用启动时已经被应用了,该怎么办?或者,如果用户更改了另一台机器上的漫游设置,该怎么办?当该设置与当前机器同步时,您的应用应该做什么?
WinRT 定义了一个您可以在这些情况下处理的事件。每当漫游设置同步时,datachanged
事件就会自动触发。此外,您可以在自己的代码中触发它。我将带您浏览这个场景,我们添加代码来将用户的首选项应用于当前时间的格式。在您的代码中手动触发datachanged
事件需要一行代码。用清单 15-26 中突出显示的代码更新options.js
。
清单 15-26。 表示漫游设置已经改变
clockSecondsToggle_change: function (e) {
roamingSettings.values["clockSeconds"] =
(clockSecondsToggle.winControl.checked);
appData.signalDataChanged();
},
clockModeToggle_change: function (e) {
roamingSettings.values["clockMode"] =
(clockModeToggle.winControl.checked)
? Clok.UI.ClockModes.CurrentTime24
: Clok.UI.ClockModes.CurrentTime12;
appData.signalDataChanged();
},
您还必须添加来自清单 15-27 到default.js
的代码,以处理datachanged
事件。
清单 15-27。 改变漫游设置改变时当前时间的显示
appData.addEventListener("datachanged", function (args) {
configureClock();
});
var configureClock = function () {
currentTime.winControl.showClockSeconds = roamingSettings.values["clockSeconds"];
currentTime.winControl.mode = roamingSettings.values["clockMode"];
};
除了在设置更改时将用户的首选项应用于当前时间格式之外,您还应该在 Clok 启动时应用它们。用清单 15-28 中的高亮代码修改default.js
。
清单 15-28。 配置 Clok 启动的当前时间
args.setPromise(WinJS.UI.processAll().then(function () {
configureClock();
if (nav.location) {
nav.history.current.initialPlaceholder = true;
return nav.navigate(nav.location, nav.state);
} else {
return nav.navigate(Application.navigator.home);
}
}));
在本书附带的源代码中,我也在方向屏幕上使用了类似的技术。如果用户在将他们的偏好从英里改为公里时没有看到方向屏幕,或者相反,我们没有什么可担心的。下次他们得到指示时,将使用正确的单位。但是,如果他们只是得到了以公里为单位指定距离的方向,并且他们将首选项切换到了英里,那么方向列表应该用新的单位刷新。我通过监听directions.js
中的datachanged
事件并适当地刷新指令来完成这个任务。
尺寸限制和复合设置
对于可以与漫游设置同步的内容有一些限制。每个设置的名称最长可达 255 个字符。此外,每个设置的最大大小为 8KB,复合设置除外,它的最大大小为 64KB。复合设置可用于将许多相关设置组合在一起,并将其作为一个单元进行同步。我们不会给 Clok 添加任何复合设置,但是清单 15-29 中的代码展示了如何在应用中使用它们。
清单 15-29。 复合设置
var compositeSetting = new Windows.Storage.ApplicationDataCompositeValue();
compositeSetting["first"] = "Scott";
compositeSetting["last"] = "Isaacs";
compositeSetting["dob"] = "Dec 1";
roamingSettings.values["profile"] = compositeSetting;
高优先级漫游设置
您的用户可以通过使用同一个 Microsoft 帐户登录多台机器来利用漫游功能。也就是说,虽然漫游设置会在设置它的计算机上立即生效,但它不会立即与任何其他计算机同步。您可以通过将单个设置命名为“高优先级”来指定要尽快同步的设置如上所述,它可以是一个复合设置,但大小限制为 8KB。如果您有关键设置要同步,这可能是一个有用的功能,但如果超过此限制,优先级将被移除,它将像正常的优先级设置一样同步。
结论
有句话叫魔鬼在细节中。在这一章中,我讨论了一些技术来确保你的应用中的小东西按预期工作。当应用终止后恢复时,使用会话状态来设置应用的正确状态可能会涉及大量繁琐的工作,但是如果您不这样做,用户会认为您的应用有问题。让你的用户有机会保存设置可以让你的应用运行得更流畅,也可以让用户灵活地让应用以他们认为最有帮助的方式运行。
尽管处理会话状态和设置可能很乏味,但编写或理解代码并不困难。随着越来越多的应用发布到 Windows Store 中,你会希望你的应用因其质量和对细节的关注而脱颖而出,而不是因其看似不完整而脱颖而出。我鼓励你花时间评估一下你可以实现哪些小细节来改善用户体验。
十六、文件
有了从 IndexedDB 到会话状态到本地设置等各种选项,在决定如何保存某些内容以备后用时,您有很多选择。在这一章中,我将介绍使用一种完全不同的数据类型:文件。
也许您正在构建的应用可以处理照片、电子表格、文档或任何其他类型的文件。虽然您可能会发现可以使用其他技术保存文件,例如在 IndexedDB 数据库中,但 Windows 8 应用可以在用户的文件系统中读取、写入和删除文件。计算机将文件存储在文件系统中已经有很长一段时间了,除非您有明确的要求,否则在考虑处理文件时,文件系统是最合理的起点。
窗户。存储命名空间
如果你要在 Windows Store 应用中处理文件和文件夹,你会花很多时间在Windows.Storage
命名空间 中。这个名称空间包含创建、编辑、删除、读取或列出文件或文件夹所需的所有类。一些最常见的类是StorageFile
、StorageFolder
和FileIO
类,我将在本章中一一演示。
StorageFile
和StorageFolder
类代表用户计算机上的文件和文件夹。在任何需要处理文件或文件夹的时候,您都会用到这些类。每当用户在文件选择器或文件夹选择器中进行选择时,都会返回这些类型之一。这两个类都提供了许多有用的方法,例如,复制、创建、删除和打开文件和文件夹。虽然StorageFile
类提供了在用户计算机上处理文件的方法,但是FileIO
类提供了许多处理StorageFile
对象的内容的方法。一旦你有了一个StorageFile
对象,你可以使用FileIO
类来读取文件的内容或者改变文件的内容。
注意除了操作
StorageFile
对象的FileIO
类之外,如果你有文件路径但没有StorageFile
对象,你也可以使用PathIO
类。两个类中存在相同的方法。
在 Windows 应用商店应用中处理文件和文件夹时,请记住一点。正如 WinRT 或 WinJS 库中任何潜在的高开销操作一样,StorageFile
、StorageFolder
和FileIO
类中的所有方法都是异步的。它们都返回Promise
对象来表示被请求的操作。
除了StorageFile
、StorageFolder
和FileIO
类之外,Windows.Storage
名称空间还提供了对存储应用数据的位置的访问点。该访问由第十五章中用于设置和检索本地和漫游设置的同一个ApplicationData
类提供。具体来说,您可以使用Windows.Storage.ApplicationData.current
对象的localFolder
属性来引用存储在您的应用范围内的文件。毫不奇怪,localFolder
是StorageFolder
类的一个实例。
注意 Windows Store 应用开发人员有两种方式来访问其应用的本地存储文件夹。如前所述,可以使用
Windows.Storage.ApplicationData.current.localFolder
。此外,在 WinJS 应用中,您可以使用 WinJS 的等效项WinJS.Application.local.folder
。
在这一章中,我将介绍这些类的用法,展示我是如何在 Clok 中实现两个新特性的。首先是备份存储在 Clok 数据库中的项目和时间条目的能力。第二个是文档库,用户可以在其中存储与他们的项目相关的文件。
数据备份
在任何应用中,能够备份数据都是一个有用的功能。因此,这是我添加到 Clok 中的一个特性。Clok 用户将在 Clok 选项设置弹出菜单中访问此功能。从那里,他们将能够点击保存备份按钮,这将保存他们在 Clok 中保存的项目和时间条目数据的副本。
添加这个功能并不需要太多,代码也很短。虽然这段代码很短,但它将说明如何使用前一节中提到的一些常见的类。您将会看到我已经使用了localFolder
属性来访问 Clok 的本地文件存储,还使用了来自StorageFile
、StorageFolder
和FileIO
类的方法。在我向您介绍这段代码之前,您必须先对options.html
做一个小小的改动。将清单 16-1 至options.html
中的代码添加到调试部分的代码之后。
清单 16-1。 向 Clok 选项设置弹出按钮添加控件
<div class="win-settings-section">
<h3>Backups</h3>
<p>Backup Projects and Time Entries</p>
<p>
<button id="saveBackupButton">Save Backup</button>
<span id="backupConfirmation"
class="win-type-xx-small"
style="display: none;">Backup saved</span>
</p>
<p class="win-type-xx-small">Backup location: <span id="backupPath"></span></p>
</div>
这段代码添加了一个按钮,用于启动备份过程,还添加了一个标签,用于向用户指示在哪里可以找到备份文件。您可以通过将清单 16-2 中的代码添加到options.js
的ready
函数中来配置这两个控件。
清单 16-2。 配置备份控件
saveBackupButton.onclick = this.saveBackupButton_click;
backupPath.innerText = appData.localFolder.path + "\\backups";
完成这个过程的最后一步是实现saveBackupButton_click
处理函数。将清单 16-3 中的代码添加到options.js
。
清单 16-3。 编写备份文件
saveBackupButton_click: function(e) {
var dateFormatString = "{year.full}{month.integer(2)}{day.integer(2)}"
+ "-{hour.integer(2)}{minute.integer(2)}{second.integer(2)}";
var clockIdentifiers = Windows.Globalization.ClockIdentifiers;
var formatting = Windows.Globalization.DateTimeFormatting;
var formatterTemplate = new formatting.DateTimeFormatter(dateFormatString);
var formatter = new formatting.DateTimeFormatter(formatterTemplate.patterns[0],
formatterTemplate.languages,
formatterTemplate.geographicRegion,
formatterTemplate.calendar,
clockIdentifiers.twentyFourHour);
var filename = formatter.format(new Date()) + ".json";
var openIfExists = Windows.Storage.CreationCollisionOption.openIfExists;
appData.localFolder
.createFolderAsync("backups", openIfExists)
.then(function (folder) {
return folder.createFileAsync(filename, openIfExists);
}).done(function (file) {
var storage = Clok.Data.Storage;
var backupData = {
projects: storage.projects,
timeEntries: storage.timeEntries
};
var contents = JSON.stringify(backupData);
Windows.Storage.FileIO.writeTextAsync(file, contents);
backupConfirmation.style.display = "inline";
});
},
该函数的第一部分使用DateTimeFormatter
类 根据当前日期为备份文件生成一个名称。因为DateTimeFormatter
类中的一个限制阻止了指定 12 小时或 24 小时时钟的Clock
属性在该类的实例创建后被设置,所以我首先创建了一个formatterTemplate
对象,使用所有的默认值。然后我基于formatterTemplate
创建formatter
对象,在构造函数中指定Clock
。因为构造函数是唯一可以指定使用哪个Clock
的地方,并且所有其他参数都是必需的,这允许formatter
对象对所有其他构造函数参数使用系统默认值。
这个函数的核心是函数后半部分的Promise
链。正如我上面提到的,appData.localFolder
对象是StorageFolder
的一个实例。对createFolderAsync
的调用将在应用的本地数据文件夹中创建一个backups
文件夹,如果它还不存在的话。这个文件夹被传递给then
函数,该函数通过createFileAsync
调用创建一个StorageFile
对象。这个文件被传递给done
函数,该函数通过调用JSON.stringify
将我们的应用数据序列化为一个字符串,并使用writeTextAsync
调用将其保存到文件中。最后一行只是向用户显示一条确认消息。
要了解这是如何工作的,请运行 Clok 并打开“Clok 选项设置”弹出按钮。点击保存备份按钮后(参见图 16-1 ,使用 Windows 资源管理器导航到指定的备份位置查看文件(参见图 16-2 )。
图 16-1 。在“时钟选项设置”弹出菜单中保存的备份
图 16-2 。备份文件
该文件包含 Clok 数据库中所有项目和时间条目的 JSON 表示。您可以在文本编辑器中打开该文件,如记事本(见图 16-3 ),查看其内容。虽然这不是查看数据的最方便的格式,但是您应该能够看到所有的内容。
图 16-3 。备份文件的内容
Clok 项目的文档库
我们最初的目标是为用户提供一种简单的方式来跟踪他们在项目上工作的时间。当我们在第十四章中完成这个目标时,是时候创建一些新的需求,添加一些其他有用的相关功能了。在这一节中,我将向您展示如何开始构建一个文档库,用户可以在其中存储与他们正在处理的项目相关联的文档。最终,用户将能够导出和删除文件,但是我们将从允许他们向项目添加文档开始。
创建文档库页面控件
首先要做的是在 Visual Studio 项目的pages
文件夹中创建一个名为documents
的文件夹。在documents
文件夹中,添加一个名为library.html
的新页面控件(参见图 16-4 )。
图 16-4 。解决方案浏览器为文档提供了新的页面控件
我们将在本章的剩余部分构建文档库屏幕,您要做的第一个更改是更新屏幕的标题以反映正确的页面标题,以及当前选定项目的名称。用清单 16-4 中高亮显示的代码更新library.html
。
清单 16-4。 在文档库屏幕上显示当前项目的名称
<div class="library fragment">
<header aria-label="Header content" role="banner">
<button class="win-backbutton" aria-label="Back" disabled type="button"></button>
<h1 class="titlearea win-type-ellipsis">
<span class="pagetitle">Document Library</span>
<span class="win-type-x-large" id="projectName">[Project Name]</span>
</h1>
</header>
<section aria-label="Main content" role="main">
<p>Content goes here.</p>
</section>
</div>
当然,[Project Name]
只是一个占位符。您必须添加 JavaScript 代码来设置projectName span
元素的正确值。在library.js
顶部定义了现在熟悉的storage
别名后,用清单 16-5 中突出显示的代码更新文件。
清单 16-5。 设置项目名称
ready: function (element, options) {
this.projectId = options && options.projectId;
this.setProjectName();
},
setProjectName: function () {
if (this.projectId) {
var project = storage.projects.getById(this.projectId);
projectName.innerText = project.name + " (" + project.clientName + ")";
}
},
目前,用户无法导航到文档库。用户应该能够从项目详细信息屏幕到达该屏幕。在detail.html
和detail.js
中进行所需的更改,向应用栏添加一个新按钮。按照您添加按钮以导航到时间表屏幕的方式,确保通过当前项目的id
(回头参考清单 12-32 )并且仅在查看现有项目时启用该按钮。完成后,项目细节的应用栏应该类似于图 16-5 。
图 16-5 。项目详细信息应用栏带有新建文档按钮
注意通过将
AppBarButton
的icon
属性 设置为attach
可以显示回形针图标。本章描述的工作的完整版本可以在本书附带的源代码中找到。您可以在该书的 press product 页面的 Source Code/Downloads 选项卡上找到本章的代码示例(www.apress.com/9781430257790
)。
立即运行 Clok 并导航到文档库屏幕。屏幕仍然是空的,但是项目和客户名称现在应该显示在标题中(见图 16-6 )。将这些信息添加到屏幕上是一个很小的功能,但是像这样的事情将会使用户在使用应用时保持方向感。
图 16-6 。项目名称为的文档库标题
向项目中添加文档
现在我们已经创建了文档库页面,下一个任务是允许用户向 Clok 中的项目添加文档。在 UI 中实现时,向应用栏添加一个添加命令是最自然的选择。将清单 16-6 中的代码添加到body
元素之后。
清单 16-6。 向文档库屏幕添加应用栏
<div id="libraryAppBar"
class="win-ui-dark"
data-win-control="WinJS.UI.AppBar"
data-win-options="{ sticky: true }">
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'addDocumentsCommand',
label:'Add',
icon:'add',
section:'global',
tooltip:'Add'}">
</button>
</div>
在这一节中,我将向您介绍FileOpenPicker
,它允许用户选择一个或多个要在您的应用中使用的文件。除了FileOpenPicker
,Windows.Storage.Pickers
名称空间还提供了FileSavePicker
和FolderPicker
类。我不会详细介绍FileSavePicker
类,但是它允许用户在他们计算机上的指定位置用指定的名称保存文件。顾名思义,FolderPicker
类允许用户选择一个文件夹,这将在本章后面介绍。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.pickers.aspx
)上有关于Windows.Storage.Pickers
名称空间及其所有类的更多信息。
接下来,在library.js
的ready
函数中为addDocumentsCommand
连接click
事件处理程序。另外,将清单 16-7 中突出显示的别名添加到library.js
的顶部。
清单 16-7。 向 library.js 添加别名
var storage = Clok.Data.Storage;
var appData = Windows.Storage.ApplicationData.current;
var createOption = Windows.Storage.CreationCollisionOption;
var pickerLocationId = Windows.Storage.Pickers.PickerLocationId;
本章和本书中使用的别名并不是必需的,但它们允许我们在使用它们时使用更短的语法,编写更少的代码,我相信这使它更容易阅读。我之前已经讨论过Windows.Storage.ApplicationData
,在本章中我们将使用它来访问应用的本地文件夹。枚举用于指定当你的代码试图创建的文件或文件夹已经存在时,Windows 应该如何处理。选项包括让 Windows 生成一个新文件名、覆盖现有文件、使用现有文件或直接失败。Windows.Storage.Pickers.PickerLocationId
枚举用于指定向用户显示的文件或文件夹选择器的首选位置。它包括许多公共位置,如用户的桌面文件夹或图片库。在清单 16-8 中,你可以看到这些别名是如何使用的。将下面的代码添加到library.js
中。
清单 16-8。 向库中添加文档
getProjectFolder: function() {
if (this.projectId) {
var projectId = this.projectId;
return appData.localFolder
.createFolderAsync("projectDocs", createOption.openIfExists)
.then(function (folder) {
return folder.createFolderAsync(projectId.toString(), createOption.openIfExists)
});
} else {
return WinJS.Promise.as();
}
},
canOpenPicker: function () {
var views = Windows.UI.ViewManagement;
var currentState = views.ApplicationView.value;
if (currentState === views.ApplicationViewState.snapped &&
!views.ApplicationView.tryUnsnap()) {
return false;
}
return true;
},
addDocumentsCommand_click: function (e) {
if (!this.canOpenPicker()) {
return;
}
var filePicker = new Windows.Storage.Pickers.FileOpenPicker();
filePicker.commitButtonText = "Add to Document Library";
filePicker.suggestedStartLocation = pickerLocationId.desktop;
filePicker.fileTypeFilter.replaceAll(["*"]);
filePicker.pickMultipleFilesAsync().then(function (files) {
if (files && files.size > 0) {
this.getProjectFolder().then(function (projectFolder) {
var copyPromises = files.map(function (item) {
return item.copyAsync(
projectFolder,
item.name,
createOption.replaceExisting);
});
return WinJS.Promise.join(copyPromises);
});
} else {
return WinJS.Promise.as();
}
}.bind(this));
},
我在这里定义了三个函数。第一个是getProjectFolder
,返回一个Promise
,代表用于存储当前所选项目文档的文件夹。文档将存储在一个名为与当前项目的id
相匹配的文件夹中。该文件夹将位于名为projectDocs
的本地数据文件夹中。当你的应用被抓拍时,如果你试图显示一个FileOpenPicker
,或者上面提到的任何一个选择器,就会抛出一个异常。第二个函数canOpenPicker
,检查应用的当前视图状态,是截图还是全屏。如果应用被抓拍,那么对tryUnsnap
的调用将尝试取消它的抓拍。
第三个函数是addDocumentsCommand_click
处理函数。如果对canOpenPicker
的调用成功,那么文件选择器在显示给用户之前被初始化。我已经将commitButtonText
设置为合适的值,而不是默认值“打开”我建议选取器在用户的桌面上启动,但是如果用户最近选择了另一个位置,则不强制执行该设置。最后,我指定应该显示所有文件类型。当显示选取器时,如果用户选择一个或多个文件,则使用从FileOpenPicker
返回的每个StorageFile
对象的copyAsync
函数复制所选择的文件。
现在,运行 Clok 并导航到项目的文档库。激活应用栏(用鼠标右键单击或从触摸屏底部边缘滑动),然后单击添加按钮。在图 16-7 中,我从桌面上名为Import Folder
的文件夹中选择了四个文件。当您单击“添加到文档库”按钮时,选定的文件将被添加到项目的文档库中。
图 16-7 。选择了四个文件的 FileOpenPicker
注意除了指定他们计算机上的文件夹,通过展开“文件”下的菜单,用户还可以指定实现文件选取器契约的其他应用。例如,他们可以从他们的 SkyDrive 帐户中选择文件,甚至直接从他们计算机上的相机中导入图片。
探索项目文件
此时,您可以运行 Clok 并向项目中添加任意数量的文件。虽然目前还没有办法在 Clok 中查看它们,但是你可以在 Windows 资源管理器中查看它们。在图 16-8 中,你可以看到我给一个项目添加了四个图标。
图 16-8 。将文件添加到项目中
我电脑上 Clok 的本地数据文件夹的路径是
C:\Users\sisaacs\AppData\Local\Packages\068d38c6-5cdc-44d8-a832-f96ab138e866_0bzpj67fjc6np\LocalState
该路径在您的计算机上会有所不同,但它会在%USERPROFILE%\AppData\Local\Packages
内的某个文件夹中。您很可能在Packages
文件夹中有许多文件夹,并且您的应用的数据将在其中的一个中。有几种方法可以找到你的应用的本地数据文件夹:通过反复试验,通过检查appData.localFolder.path
的值,或者通过匹配你的 Visual Studio 项目中package.appxmanifest
文件的打包选项卡上的“包名”字段(参见图 16-9 )。
图 16-9 。package.appxmanifest 中的包名是本地数据文件夹路径的一部分
作为开发人员,我们有工具来帮助我们轻松地找到这个路径并在 Windows 资源管理器中查看文件。当然,对于在 Visual Studio 中没有检查变量或package.appxmanifest
文件的优势的用户来说,这是不一样的。因此,让我们进行构建文档库的下一步:添加查看已添加文件的功能。
配置列表视图
如图图 16-7 所示的文件拾取器使用一个ListView
来选择文件。为了与默认的 Windows 行为保持一致,我们还将使用一个ListView
来向用户显示文档库中的文件。用清单 16-9 中的代码替换library.html
中的主section
。
清单 16-9。 添加用于显示文件的 ListView】
<section aria-label="Main content" role="main">
<div id="libraryTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="libraryItem" data-win-bind="item: item Clok.Library.bindLibraryItem">
<div class="libraryItem-icon-container">
<img class="libraryItem-icon" />
</div>
<div class="libraryItem-details">
<h3 class="libraryItem-filename win-type-ellipsis"></h3>
<h6 class="libraryItem-modified-container win-type-ellipsis">
<strong>Modified:</strong> <span class="libraryItem-modified"></span>
</h6>
<h6 class="libraryItem-size-container win-type-ellipsis">
<strong>Size:</strong> <span class="libraryItem-size"></span>
</h6>
</div>
</div>
</div>
<div id="libraryListView"
class="win-selectionstylefilled"
data-win-control="WinJS.UI.ListView"
data-win-options="{
itemTemplate: select('#libraryTemplate'),
selectionMode: 'multi',
swipeBehavior: 'select',
tapBehavior: 'directSelect'
}">
</div>
<div id="noDocuments" class="hidden">No documents found for this project.</div>
</section>
libraryListView
允许用户选择多个文件,每个文件根据libraryTemplate
定义的Template
显示。对于每个文件,ListView
将显示一个文件图标、文件名、文件类型和文件上次修改的日期。你可能已经注意到这个Template
与之前的ListView
例子有所不同。这次我没有单独绑定每个值,而是在顶层指定了绑定,并且指定了一个绑定初始化器Clok.Library.bindLibraryItem
。这将允许我们在 JavaScript 代码中实现更复杂的绑定,我将在下一节更详细地展示这一点。
CSS 现在应该很熟悉了,因为它非常类似于项目、方向和时间表屏幕中使用的内容。将清单 16-10 中的 CSS 代码添加到library.css
。
清单 16-10。 CSS 为文档库
.hidden {
display: none;
}
#libraryListView {
height: calc(100% - 88px);
}
#libraryListView .win-container {
background-color: #46468C;
}
#libraryListView .libraryItem {
display: -ms-grid;
-ms-grid-columns: 80px 350px;
height: 80px;
}
#libraryListView .libraryItem-icon {
-ms-grid-column: 1;
margin: 8px;
width: 64px;
height: 64px;
text-align: center;
}
#libraryListView .libraryItem-details {
-ms-grid-column: 2;
margin: 5px;
}
#libraryListView .libraryItem-filename {
font-size: 1.25em;
}
在我们之前所有的ListView
例子中,我们已经将ListView
绑定到了一个WinJS.Binding.List
对象。我们可以使用StorageFile
和StorageFolder
类上的函数来构建我们自己的List
,但是有一种更好的方式在ListView
中显示文件系统信息。
配置存储数据源
一个ListView
可以绑定到任何实现IListDataSource
接口的类。虽然我们可以用关于文件和文件夹的信息填充一个WinJS.Binding.List
对象,但是StorageDataSource
已经存在了。因为它实现了IListDataSource
,我们可以直接绑定到它。除了比其他选择更简单之外,使用StorageDataSource
还提供了文件和文件夹的“实时视图”的额外好处。因此,如果您添加了一个新文件,或者删除了一个现有的文件,数据源将立即反映这一点并更新ListView
。
在这一节中,我将演示如何创建一个简单的StorageDataSource
对象来反映项目文档库的内容。我将把这些数据绑定到上一节中创建的ListView
,然后在下一节中,我将向您展示如何允许您的用户直接从 Clok 打开文件。
创建一个绑定到我们的ListView
的对象只需要几行代码。将清单 16-11 中的代码添加到library.js
中。
清单 16-11。 创建存储数据源对象
bindProjectLibraryFiles: function () {
if (this.projectId) {
var resizeThumbnail = thumbnailOptions.resizeThumbnail;
var singleItem = thumbnailMode.singleItem;
this.getProjectFolder().then(function (folder) {
var fileQuery = folder.createFileQuery();
var dataSourceOptions = {
mode: singleItem,
requestedThumbnailSize: 64,
thumbnailOptions: resizeThumbnail
};
var dataSource = new WinJS.UI.StorageDataSource(fileQuery, dataSourceOptions);
dataSource.getCount().then(function (count) {
if (count >= 1) {
libraryListView.winControl.itemDataSource = dataSource;
WinJS.Utilities.addClass(noDocuments, "hidden");
WinJS.Utilities.removeClass(libraryListView, "hidden");
} else {
WinJS.Utilities.removeClass(noDocuments, "hidden");
WinJS.Utilities.addClass(libraryListView, "hidden");
}
});
});
}
},
StorageDataSource
构造函数接受一个查询对象和一个选项对象。查询参数可以是用户计算机上常见 Windows 库列表中的字符串(“音乐”、“图片”、“视频”或“文档”),也可以是实现IStorageQueryResultBase
的对象。例如,如果你专门处理用户的图片或音乐库,那么只需将其中一个字符串作为第一个参数传递给StorageDataSource
构造函数就非常简单了。如果您正在使用用户计算机上的另一个位置,则必须创建一个查询对象。
在清单 16-11 中,因为我已经引用了一个StorageFolder
对象——当前项目的文档库——我可以调用createFileQuery
函数来获得一个有效的查询对象。StorageDataSource
构造函数的第二个参数定义了一些附加选项,主要与查询结果中包含的缩略图信息相关。关于StorageDataSource
类和这两个构造函数参数的更多信息可以在 MSDN 网站(http://msdn.microsoft.com/en-us/library/windows/apps/br212651.aspx
)上找到。
注意
createFileQuery
函数构建了一个查询,允许您处理StorageFolder
对象顶层的所有文件。除了createFileQuery
之外,StorageFolder
类还定义了其他五个返回不同类型的文件或文件夹查询的函数,以及两个可以用来同时查询文件和文件夹的函数。有关这些其他StorageFolder
功能的更多信息,请访问 MSDN 网站(http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.storagefolder.aspx
)。
创建数据源后,我调用了getCount
函数,检查当前项目的文档库中是否有文件。如果有,那么我将ListView
的itemDataSource
属性设置到这个数据源,并使结果可见。如果不存在文件,我会向用户显示一条消息。
我在上一节中指出了我如何只将数据绑定到用于格式化文件信息的WinJS.Binding.Template
的顶层。我没有单独绑定每个元素,而是选择使用名为bindLibraryItem
的绑定初始化函数来处理一些更复杂的绑定需求。将清单 16-12 中突出显示的代码添加到library.js
中的页面定义之后。
清单 16-12。 绑定初始化函数
(function () {
"use strict";
// SNIPPED
WinJS.UI.Pages.define("/pages/documents/library.html", {
// SNIPPED
});
function bindLibraryItem(source, sourceProperty, destination, destinationProperty) {
var filenameElement = destination.querySelector(".libraryItem-filename");
var modifiedElement = destination.querySelector(".libraryItem-modified");
var sizeElement = destination.querySelector(".libraryItem-size");
var iconElement = destination.querySelector(".libraryItem-icon");
filenameElement.innerText = source.name;
modifiedElement.innerText = source.basicProperties
&& source.basicProperties.dateModified
&& formatDateTime(source.basicProperties.dateModified);
var size = source.basicProperties &&**source.basicProperties.size;**
**if (size > (Math.pow(1024, 3))) {**
**sizeElement.innerText = (size / Math.pow(1024, 3)).toFixed(1) + " GB";**
**}**
**else if (size > (Math.pow(1024, 2))) {**
**sizeElement.innerText = (size / Math.pow(1024, 2)).toFixed(1) + " MB";**
**}**
**else if (size > 1024) {**
**sizeElement.innerText = (size / 1024).toFixed(1) + " KB";**
**}**
**else {**
**sizeElement.innerText = size + " B";**
**}**
**var url;**
**if (source.thumbnail &&** **isImageType(source.fileType)) {**
**url = URL.createObjectURL(source.thumbnail, { oneTimeOnly: true });**
**} else {**
**url = getIcon(source.fileType);**
**}**
**iconElement.src = url;**
**iconElement.title = source.displayType;**
**}**
**WinJS.Utilities.markSupportedForProcessing(bindLibraryItem);**
**WinJS.Namespace.define("Clok.Library", {**
**bindLibraryItem: bindLibraryItem,**
**});**
`})();`
当 WinJS 为
ListView中的每一项调用我们的绑定初始化器时,它将数据源对象——文件——作为参数
source传递给这个函数,并将 HTML 元素作为参数
destination。可以为每个数据绑定值指定不同的初始化器,如果是这样的话,我们也可以利用
sourceProperty和
destinationProperty 参数。然而,在这种情况下,我忽略了这些。相反,我使用查询选择器来查找应该向用户显示数据绑定信息的元素,并根据我的自定义逻辑设置这些元素的
innerText`属性。例如,根据文件的大小,文件大小可以显示为千兆字节、兆字节、千字节或简单的字节。
文件图标略有不同。StorageDataSource
提供了将在 Windows 资源管理器中使用的图标,我可以选择在ListView
中显示这些图标。相反,只有当库中的文件是图像时,我才显示提供的图标。这将导致图像的缩略图版本显示在列表视图中。在所有其他情况下,我更喜欢显示与 Clok 的现代风格相匹配的自定义文件类型图标,并且我使用getIcon
函数来确定该图标应该是什么,稍后我会介绍这个函数。
注意我用于文件类型图标的图片可以在本书附带的源代码中找到。你可以在本书的 press 产品页面(
www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
在定义了绑定初始化器之后,我通过调用markSupportedForProcessing
并将其公开为Clok.Library.bindLibraryItem
,使得它可以从library.html
开始使用。此外,还需要一些其他函数来使这个绑定初始化器工作。在library.js
中的bindLibraryItem
定义后添加清单 16-13 中的代码。
清单 16-13。 函数支持绑定初始化器
function formatDateTime(dt) {
var formatting = Windows.Globalization.DateTimeFormatting;
var dateFormatter = new formatting.DateTimeFormatter("shortdate");
var timeFormatter = new formatting.DateTimeFormatter("shorttime");
return dateFormatter.format(dt) + " " + timeFormatter.format(dt);
}
function isImageType(fileType) {
fileType = (fileType || "").toLocaleUpperCase();
return fileType === ".PNG"
|| fileType === ".GIF"
|| fileType === ".JPG"
|| fileType === ".JPEG"
|| fileType === ".BMP";
}
function getIcon(fileType) {
fileType = (fileType || "").replace(".", "");
var knownTypes = ["WAV", "XLS", "XLSX", "ZIP",
"AI", "BMP", "DOC", "DOCX", "EPS", "GIF",
"ICO", "JPEG", "JPG", "MP3", "PDF", "PNG",
"PPT", "PPTX", "PSD", "TIFF", "VSD", "VSDX"];
if (knownTypes.indexOf(fileType.toLocaleUpperCase()) >= 0) {
return "/img/fileTypes/" + fileType + ".png";
}
return "/img/fileTypes/default.png";
}
雄心勃勃的开发人员可能会用一个动态检查img/fileTypes
文件夹内容的定义来替换getIcon
的定义,以确定哪些图标可用。因为这些图标包含在用户下载 Clok 时安装的包中,所以在本地数据文件夹中找不到这些图标。相反,你应该在Windows.ApplicationModel.Package.current.installedLocation
中寻找这些文件,它也是一个StorageFolder
对象。
要显示当前项目文档库中的文件,还需要两个简单的步骤。您必须在library.js
的顶部添加两个别名(参见清单 16-14 中的,并在ready
函数中添加对this.bindProjectLibraryFiles
的调用。
清单 16-14。 再加两个别名
var storage = Clok.Data.Storage;
var appData = Windows.Storage.ApplicationData.current;
var createOption = Windows.Storage.CreationCollisionOption;
var pickerLocationId = Windows.Storage.Pickers.PickerLocationId;
var thumbnailOptions = Windows.Storage.FileProperties.ThumbnailOptions;
var thumbnailMode = Windows.Storage.FileProperties.ThumbnailMode;
立即运行 Clok 并导航到文档库。您将看到您之前添加的文件,如果您现在添加更多文件,StorageDataSource
将自动用新文件更新ListView
(参见图 16-10 )。
图 16-10 。文档库中有各种图片和文档
正在启动文件
现在文件显示在文档库中,一个有用的特性是允许用户直接从 Clok 打开文档。在这一节中,我将演示如何添加一个新的应用栏按钮。将清单 16-15 中的代码添加到library.html
中的AppBar
定义中。
清单 16-15。 向应用栏添加打开按钮
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'openDocumentCommand',
label:'Open',
icon:'openfile',
section:'selection',
tooltip:'Open',
disabled: true}">
</button>
为了防止一次意外启动多个文件,我添加了一次只能打开一个文件的要求。为了加强这一点,我处理了ListView
的onselectionchanged
事件,并且只有在ListView
中选择了一个项目时,打开按钮才被激活。将清单 16-16 中的代码添加到library.js
中以实现这一点。
清单 16-16。 根据列表视图中选择的项目数量,启用或禁用打开按钮
libraryListView_selectionChanged: function (e) {
// Get the number of currently selected items
var selectionCount = libraryListView.winControl.selection.count();
if (selectionCount <= 0) {
openDocumentCommand.winControl.disabled = true;
libraryAppBar.winControl.hide();
} else if (selectionCount > 1) {
openDocumentCommand.winControl.disabled = true;
libraryAppBar.winControl.show();
} else { // if (selectionCount === 1) {
openDocumentCommand.winControl.disabled = false;
libraryAppBar.winControl.show();
}
},
当用户点击这个按钮时,调用Windows.System.Launcher.launchFileAsync
将在默认的文件查看器中打开文件。将清单 16-17 中的click
事件处理程序添加到library.js
中。
清单 16-17。 打开按钮的 Click 事件处理程序
openDocumentCommand_click: function (e) {
libraryListView.winControl.selection.getItems()
.then(function (selectedItems) {
if (selectedItems && selectedItems[0] && selectedItems[0].data) {
return Windows.System.Launcher.launchFileAsync(selectedItems[0].data);
}
})
.then(null, function error(result) {
new Windows.UI.Popups
.MessageDialog("Could not open file.", "An error occurred. ")
.showAsync();
});
},
代码将只打开第一个被选择的文件,但是因为你在清单 16-16 中添加了代码,无论如何不应该有多于一个的项目被选择。如果文件可以打开,则启动该文件类型的默认应用;否则,将显示一条错误消息。一个重载版本的launchFileAsync
允许你在启动时指定额外的选项。例如,您还可以添加一个带有。。。应用栏按钮,并指定应该让用户选择为所选文件启动的应用。这对于图像尤其有用,因为默认应用可能是不允许用户编辑图像的应用。通过允许用户指定他们希望启动的应用,他们可以选择编辑文件。因为代码几乎与我刚刚介绍的相同,所以我不会在本书中演示,但是我已经在本书附带的源代码中实现了这个功能。(见该书的 Apress 产品页的源代码/下载标签[ www.apress.com/9781430257790
]。)
允许用户在默认应用中启动文档的最后一步是在ready
函数中连接事件处理程序。将清单 16-18 中突出显示的代码添加到library.js
。
清单 16-18。 连接起新的事件处理器
ready: function (element, options) {
this.projectId = options && options.projectId;
this.setProjectName();
this.bindProjectLibraryFiles();
libraryListView.winControl.onselectionchanged =
this.libraryListView_selectionChanged.bind(this);
openDocumentCommand.winControl.onclick = this.openDocumentCommand_click.bind(this);
addDocumentsCommand.winControl.onclick = this.addDocumentsCommand_click.bind(this);
},
立即运行 Clok 并导航到文档库。当您选择一个文件时,您将能够使用该文件类型的默认应用打开它(参见图 16-11 )。
图 16-11 。单击打开按钮将使用 Microsoft ExcelT3 启动所选电子表格
管理项目文件
我们的文档库正在整合,已经成为 Clok 的一个有用的补充。在我结束这一章之前,我想再增加两个特性。为了说明从 Clok 的本地数据文件夹中复制文件,我将实现一个导出特性,为了说明如何从StorageFolder
中删除文件,我将添加一个删除特性。
注意那些认为自己是超级用户的用户会很高兴地发现,他们在 Windows 资源管理器中使用的键盘快捷键也适用于
ListView
控件中的项目。如果一个ListView
当前处于焦点上,您可以使用 Ctrl+A 选择一个ListView
中的所有项目,使用 ESC 键取消选择所有项目,使用箭头键浏览ListView
中的项目。此外,您可以按住 Shift 键或 Ctrl 键单击ListView
中的项目来选择多个项目。
使用文件夹选择器导出文件
允许用户向 Clok 添加任意多的文档,但不提供任何获取文档的方法,这有多大用处?在本节中,我将向您展示如何向文档库添加导出功能。首先,让我们在应用栏中添加一个新按钮。将清单 16-19 中的代码添加到library.html
中。
清单 16-19。 向应用栏添加导出按钮
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'exportDocumentsCommand',
label:'Export',
icon:'savelocal',
section:'selection',
tooltip:'Export',
disabled: true}">
</button>
毫不奇怪,从文档库中导出文件类似于向其中添加文件。StorageFile
对象的copyAsync
方法执行实际的文件复制操作。最大的区别在于,不是用FileOpenPicker
挑选文件添加到文档库中,也不是用getProjectFolder
函数确定StorageFolder
,而是从ListView
对象selection
属性中获取对一组StorageFile
对象的引用,并允许用户用FolderPicker
指定一个StorageFolder
。一旦你有了一个StorageFolder
和StorageFile
对象的集合,基本过程是一样的。将清单 16-20 中的代码添加到library.js
中。
清单 16-20。 导出按钮的点击处理程序
exportDocumentsCommand_click: function (e) {
if (!this.canOpenPicker()) {
return;
}
var folderPicker = new Windows.Storage.Pickers.FolderPicker;
folderPicker.suggestedStartLocation = pickerLocationId.desktop;
folderPicker.fileTypeFilter.replaceAll(["*"]);
folderPicker.pickSingleFolderAsync().then(function (folder) {
if (folder) {
return libraryListView.winControl.selection.getItems()
.then(function (selectedItems) {
var copyPromises = selectedItems.map(function (item) {
return item.data.copyAsync(
folder,
item.data.name,
createOption.generateUniqueName);
});
return WinJS.Promise.join(copyPromises);
});
} else {
return WinJS.Promise.as();
}
}).then(function error(result) {
new Windows.UI.Popups
.MessageDialog("All files successfully exported.", "File export is complete.")
.showAsync();
}, function error(result) {
new Windows.UI.Popups
.MessageDialog("Could not export all selected files.", "An error occurred. ")
.showAsync();
});
},
注意记住在
ready
函数中绑定这个click
事件处理程序,并修改libraryListView_selectionChanged
,当列表中的一个或多个文档被选中时,激活导出按钮。
虽然过程与添加文档相同,但我在这里做了一处更改。如果存在命名冲突,copyAsync
函数将为复制的文件生成一个新名称,而不是替换已经存在的文件。您可以自己实现的一个有用的功能是允许用户指定他或她是否想要替换现有文件,保留文件的两个副本,或者取消导出操作。
当您添加代码以在ListView
中选择一个或多个项目时启用导出按钮,并在library.js
的ready
函数中绑定此click
事件处理程序后,文档导出功能就完成了。但是,在测试之前,让我们继续添加删除功能。
从本地应用文件夹中删除文件
“呜呜!我不是故意加那个文件的。”"这个模型已经过时了。""我的硬盘快满了。"
用户希望从文档库中删除文件的原因有很多,这些只是其中的几个。幸运的是,删除文件就像导出文件一样简单。首先,让我们在应用栏中添加一个新按钮。将清单 16-21 中的代码添加到library.html
中。
清单 16-21。 向应用栏添加删除按钮
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'deleteDocumentsCommand',
label:'Delete',
icon:'delete',
section:'selection',
tooltip:'Delete',
disabled: true}">
</button>
在删除一个文件之前,这是一个永久的操作,您将提示用户确定他或她想要点击删除按钮。将清单 16-22 中的代码添加到library.js
中。
清单 16-22。 删除按钮的点击处理程序
deleteDocumentsCommand_click: function (e) {
var msg = new Windows.UI.Popups.MessageDialog(
"This cannot be undone. Do you wish to continue?",
"You're about to permanently delete files.");
var buttonText = (libraryListView.winControl.selection.count() <= 1)
? "Yes, Delete It"
: "Yes, Delete Them";
msg.commands.append(new Windows.UI.Popups.UICommand(buttonText, function (command) {
libraryListView.winControl.selection.getItems()
.then(function (selectedItems) {
var deletePromises = selectedItems.map(function (item) {
return item.data.deleteAsync();
});
return WinJS.Promise.join(deletePromises);
})
.then(null, function error(result) {
new Windows.UI.Popups
.MessageDialog("Could not delete selected files.", "An error occurred.")
.showAsync();
});
}));
msg.commands.append(new Windows.UI.Popups.UICommand(
"No, Don't Delete Anything",
function (command) { }
));
msg.defaultCommandIndex = 0;
msg.cancelCommandIndex = 1;
msg.showAsync();
},
如果用户确认他或她确实希望继续,只需为选择中的每个StorageFile
调用deleteAsync
即可。在ListView
中选择一个或多个项目时,不要忘记启用删除按钮,并且一定要在library.js
的ready
功能中绑定这个click
事件处理程序。
现在是时候看看你的工作成果了。现在,您已经有了一个功能完整的文档库。立即运行 Clok 并导航至文档库(参见图 16-12 )。浏览所有的函数,看看它们是如何工作的。添加一些文件,打开它们,导出它们,然后删除它们。你应该会发现这种体验非常自然,任何使用过 Windows 8 一段时间的人都应该很熟悉。
图 16-12 。已建成的文档库
结论
处理文件是不可避免的。虽然StorageFile
和StorageFolder
函数的异步特性改变了您与文件和文件夹交互的方式,但是如果您已经用其他编程语言进行过任何涉及用户计算机文件系统的开发,这些差异就很容易理解了。在Windows.Storage
名称空间中有许多高级功能。如果您发现这一介绍性章节不能满足您的所有需求,请务必在线阅读 MSDN 文档(http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.aspx
)。
当我介绍共享契约时,我们将在第十九章中再次访问文档库,当我讨论如何使用用户的相机时,我们将在第二十二章中再次访问。`
十七、处理状态变化
可以说,任何应用最重要的要求是它在任何情况下都能按照用户的期望工作。显然,正确地执行核心任务对您的应用来说很重要,但这不是我在这里讨论的内容。
你添加到你的应用中使其看起来“正常工作”的细节或小改动会让用户一次又一次地回来。类似于我在第十章中描述的微妙的动画,这种对细节的关注对大多数用户来说可能从来都不明显,但是如果你忽略它,他们肯定会注意到。在这一章中,我将讨论两个方面,你可以在这两个方面做些小工作,使你的应用看起来更加完美。
首先要考虑应用的激活状态。你的应用是如何开始的?用户以前是如何与你的应用交互的?由于资源限制,用户是否认为你的应用仍然在后台运行,即使 Windows 已经终止了它?第二个方面是考虑应用的视图状态。用户是横向还是纵向浏览你的应用?当他们看着你的应用旁边的另一个应用时,他们有没有在一个狭窄的窗格中抓取你的应用?当他们看着你旁边的应用时,他们是否在一个狭窄的窗格中抓拍了另一个应用?
在构建应用时考虑这些类型的问题将允许您做出有助于满足用户期望的决策。幸运的是,所需的代码通常很容易实现。因此,只需相对较小的努力,您就可以解决任何应用最重要的需求。
应用激活状态
有几种方法可以让用户激活你的应用。最简单也是最常见的方式是通过点击开始屏幕上的一个磁贴来启动它。但是,您的应用也可以通过以下方式之一启动:
- 当用户使用 Windows Search charm 在您的应用中查找内容时
- 当用户使用 Windows Share charm 与您的应用共享某些内容时
- 当用户在
FileOpenPicker
或FileSavePicker
中指定您的应用时,从另一个应用打开或保存文件
这些只是你的应用被激活的许多方式中的一部分。有十几种方法可以激活 Windows 应用商店应用。枚举定义了所有不同类型的应用激活方式。正如我提到的,从开始屏幕上的磁贴启动你的应用是最常见的一种激活方式,也是我在本章中讨论的唯一一种。在第十九章,我将演示如何在你的应用中搜索,以及如何与你的应用共享文档。
注MSDN(
http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.activation.activationkind
)上有Windows.ApplicationModel.Activation.ActivationKind
枚举的文档。
当你在第九章的中第一次从导航应用模板创建 Clok Visual Studio 项目时,default.js
被添加到你的项目中,包含了许多样板代码。这段代码包括处理应用的activated
事件。很容易忽略添加的内容,但现在让我们仔细看看。如果你打开default.js
,你会看到激活的事件处理程序(见清单 17-1 )。
清单 17-1。 应用的激活事件处理程序
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
// SNIPPED
app.addEventListener("activated", function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
// SNIPPED
}
});
该处理程序的第一行检查应用是如何激活的。在这个生成的代码中,事件处理程序只在应用启动时做一些事情,很可能是从开始屏幕启动。
扩展闪屏
每个 Windows Store 应用都有一个闪屏。当你在 Visual Studio 中使用第四章中讨论的项目模板创建一个新项目时,Visual Studio 会在灰色背景上添加一个占位符图像。这是默认的 Clok 闪屏,可以在图 17-1 中看到。在第二十三章中,我将向你展示如何用定制的闪屏来打造你的应用。
图 17-1 。默认锁定闪屏
在应用的第一个屏幕可见之前,Windows 会自动显示该初始屏幕一小段时间。但是,有时您可能需要在用户能够与您的应用交互之前执行一些任务。例如,您可能需要将数据加载到内存中,从 web 服务请求数据,或者打开一个大文档。在这些情况下,如果这些任务没有在您的第一个屏幕可见之前完成,您的应用可能会出现故障。这正是我们在第十四章中遇到的问题。
当 Clok 启动时,数据从我们的 IndexedDB 数据库加载到WinJS.Binding.List
中,用于填充 Clok 仪表板上的项目下拉列表。因为这些数据是异步加载的,所以仪表板屏幕通常在必要的数据出现在List
之前就加载了。在第十五章中,我将home.js
改为先初始化List
,然后将ready
函数的内容包装在Promise
的done
函数中(见清单 17-2 )。
清单 17-2。home . js 中的当前就绪功能
ready: function (element, options) {
storage.initialize().done(function () {
// SNIPPED
}.bind(this));
},
在这一节中,我将向您展示如何在 Clok 的激活过程中使用扩展闪屏来加载这些数据。一个扩展的闪屏并不像名字最初暗示的那样。这不是对 Windows 默认显示的闪屏的更改。它也不一定是一个单独的屏幕,尽管理论上你可以在一个单独的屏幕上实现它。实现扩展闪屏最常见的方式是在default.html
的div
元素中定义它。div
被配置成和默认的闪屏一模一样,可以选择显示额外的控件,比如一个进度条来告诉用户应用还在加载。
这是我们将在 Clok 中使用的技术。首先从home.js
中删除清单 17-2 中高亮显示的代码行。然后将清单 17-3 中的代码从添加到default.html
,紧跟在开始的body
标签之后。
清单 17-3。 标记为扩展闪屏
<div id="extendedSplash" class="hidden">
<img id="splashImage" src="/img/splashscreen.png" />
<progress id="splashProgress" style="color: white;"></progress>
</div>
参考图像splashscreen.png
与图 17-1 中默认启动画面上使用的图像相同。这个文件可以在package.appxmanifest
中配置,我会在第二十三章中介绍。在图像下方,我添加了一个不确定的进度条,当扩展闪屏可见时,它将继续显示动画。为了使扩展闪屏与默认闪屏的外观相匹配,将清单 17-4 中的 CSS 添加到default.css
中。
清单 17-4。 CSS 为扩展闪屏
.hidden {
display: none;
}
#extendedSplash {
background-color: #3399aa;
height: 100%;
width: 100%;
position: absolute;
top: 0px;
left: 0px;
text-align: center;
}
#extendedSplash #splashImage {
position: absolute;
}
我们将利用绝对定位来确保splashscreen.png
显示在扩展闪屏中与默认闪屏相同的位置。我一会儿会展示这个。此外,我还设置了扩展闪屏的背景色,以匹配 Clok 的背景色。如果你没有好的理由,微软的建议是让扩展闪屏的背景色与默认闪屏的背景色匹配,默认闪屏在package.appxmanifest
中指定。目前,Clok 的默认闪屏是灰色背景,但我会在第二十三章中告诉你如何改变。因此,虽然背景颜色目前与默认启动屏幕的颜色不匹配,但最终会在 Clok 准备好用于 Windows 商店之前匹配。
注微软的闪屏指南可在 MSDN (
http://msdn.microsoft.com/en-us/library/windows/apps/hh465338.aspx
)上获得。
我将向您展示如何创建一个管理扩展闪屏的类。这个新类将负责在需要时显示和隐藏闪屏,并确保长时间运行的初始化代码在进程中的正确位置运行。为了做好准备,在default.js
中需要一点重构。为了帮助确保我们的代码在本节结束时仍然可读和可理解,从清单 17-1 中截取的代码必须被移到它自己的函数中。在default.js
中,修改activated
事件处理程序并创建一个新函数,如清单 17-5 所示。
清单 17-5。 重构激活事件处理程序
app.addEventListener("activated", function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
launchActivation(args);
}
});
var launchActivation = function (args) {
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.
}
if (app.sessionState.history) {
nav.history = app.sessionState.history;
}
initializeRoamingSettings();
// add our SettingsFlyout to the list when the Settings charm is shown
WinJS.Application.onsettings = function (e) {
e.detail.applicationcommands = {
"options": {
title: "Clok Options",
href: "/settings/options.html"
},
"about": {
title: "About Clok",
href: "/settings/about.html"
}
};
if (roamingSettings.values["enableIndexedDbHelper"]) {
e.detail.applicationcommands.idbhelper = {
title: "IndexedDB Helper",
href: "/settings/idbhelper.html"
};
}
WinJS.UI.SettingsFlyout.populateSettings(e);
};
args.setPromise(WinJS.UI.processAll().then(function () {
configureClock();
if (nav.location) {
nav.history.current.initialPlaceholder = true;
return nav.navigate(nav.location, nav.state);
} else {
return nav.navigate(Application.navigator.home);
}
}));
}
到目前为止,这在功能上等同于您刚才替换的代码。现在,我们将更改activated
事件处理程序来实例化一个新类Clok.SplashScreen.Extender
,它将管理扩展的闪屏。用清单 17-6 中突出显示的代码更新default.js
。
清单 17-6。 修改激活事件处理程序
app.addEventListener("activated", function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
var extender = new Clok.SplashScreen.Extender(
extendedSplash,
args.detail.splashScreen,
function (e) {
args.setPromise(Clok.Data.Storage.initialize());
simulateDelay(2000);
launchActivation(args);
});
}
});
这个新类的构造函数有三个参数。第一个是对div
的引用,代表default.html
中扩展的闪屏。第二个是对默认闪屏的引用,可以从activated
事件的参数中获得。第三个参数是初始化应用的函数。在此功能完成之前,将显示扩展的闪屏。
注意清单 17-6 中的代码引用了一个名为
simulateDelay
的函数。实际上,Clok 的初始化非常快。此功能用于模拟更长的初始化过程,应在扩展闪屏经过全面测试后删除。本书附带的源代码中提供了simulatedDelay
函数的定义。你可以在该书的 press 产品页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790
)。
当然,Clok.SplashScreen.Extender
类还不存在。在 Visual Studio 的js
文件夹中添加一个名为extendedSplash.js
的 JavaScript 文件。将清单 17-7 中的代码从添加到extendedSplash.js
。
清单 17-7。 定义闪屏扩展器类
(function () {
"use strict";
var util = WinJS.Utilities;
var extenderClass = WinJS.Class.define(
function constructor(extendedSplash, defaultSplash, loadingFunctionAsync) {
this._extendedSplash = extendedSplash;
this._defaultSplash = defaultSplash;
this._loadingFunctionAsync = loadingFunctionAsync;
this._defaultSplash.ondismissed = this._splash_dismissed.bind(this);
this._show();
},
{
_splash_dismissed: function (e) {
WinJS.Promise.as(this._loadingFunctionAsync(e))
.done(function () {
this._hide();
}.bind(this));
},
_show: function () {
this._updatePosition();
util.removeClass(this._extendedSplash, "hidden");
},
_hide: function () {
if (this._isVisible()) {
util.addClass(this._extendedSplash, "hidden");
}
},
_isVisible: function () {
return !util.hasClass(this._extendedSplash, "hidden");
},
_updatePosition: function () {
var imgLoc = this._defaultSplash.imageLocation;
var splashImage = this._extendedSplash.querySelector("#splashImage");
splashImage.style.top = imgLoc.y + "px";
splashImage.style.left = imgLoc.x + "px";
splashImage.style.height = imgLoc.height + "px";
splashImage.style.width = imgLoc.width + "px";
var splashProgress = this._extendedSplash.querySelector("#splashProgress");
splashProgress.style.marginTop = (imgLoc.y + imgLoc.height) + "px";
},
},
{ /* no static members */ }
);
WinJS.Namespace.define("Clok.SplashScreen", {
Extender: extenderClass,
});
})();
当默认启动画面关闭时,执行清单 17-6 中指定的初始化功能_loadingFunctionAsync
,当该功能完成时,扩展启动画面被隐藏。用WinJS.Promise.as
包装对_loadingFunctionAsync
的调用允许Extender
类将初始化函数视为已经返回了一个Promise
,即使它没有返回。_updatePosition
功能用于根据图像在默认闪屏上的位置,确定扩展闪屏中的图像和进度条在屏幕上的位置。
完成扩展闪屏的最后一步是添加对extendedSplash.js
到default.html
的引用。此外,其他脚本引用的顺序可能需要修改,以确保在尝试使用它们之前定义了类和数据。用来自清单 17-8 的代码更新default.html
的head
元素。
清单 17-8。【default.html】剧本参考文献
<head>
<meta charset="utf-8" />
<title>Clok</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<!-- Clok references -->
<link href="/css/default.css" rel="stylesheet" />
<link href="/css/themeroller.css" rel="stylesheet" />
<!-- Clok generic scripts -->
<script src="/js/extensions.js"></script>
<script src="/js/utilities.js"></script>
<script src="/js/navigator.js"></script>
<!-- Clok data and extended splash screen -->
<script src="/data/project.js"></script>
<script src="/data/timeEntry.js"></script>
<script src="/data/storage.js"></script>
<script src="/data/bingMapsWrapper.js"></script>
<script src="/js/extendedSplash.js"></script>
<!-- Clok controls -->
<script src="/controls/js/timerControl.js"></script>
<script src="/controls/js/clockControl.js"></script>
<script src="/js/default.js"></script>
</head>
现在,当您启动 Clok 时,在初始闪屏关闭后,会显示带有进度条的扩展闪屏(参见图 17-2 )。
图 17-2 。带有进度条的扩展闪屏
一旦Clok.Data.Storage
类被初始化,先前的激活码,现在在launchActivation
函数中,被执行,然后闪屏被隐藏。
先前执行状态
当用户启动你的应用时,让他们得到他们期望的体验是很重要的。最有可能的是,他们的期望是基于他们最后一次看到你的申请。在第十五章中,我介绍了会话状态,并展示了如何将应用的当前状态保存到会话状态,这样当用户返回到你的应用时,它可以被恢复。恢复终止的会话将大大有助于提供预期的体验,尤其是因为用户可能没有意识到会话在一开始就被终止了。
当您的应用被激活时,用户可能会有其他期望。如果他们通过在触摸屏上向下滑动或按下键盘上的 Alt+F4 来手动关闭应用,他们可能希望在下次启动应用时看到主屏幕。如果激活是点击第二个磁贴(我将在第二十一章中讨论这个话题)或 Windows Search charm 的结果,他们可能不希望看到主屏幕。此外,最明智的行为可能会有所不同,这取决于他们是否已经在使用该应用。
launchActivation
函数中的第一段代码演示了如何确定用户最后一次与应用交互的方式。我已经将该块复制到清单 17-9 的中。
清单 17-9。 检查上一次执行状态
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.
}
您可以使用previousExecutionState
的值来决定如何处理应用的激活。虽然我们还没有在 Clok 中实现任何基于previousExecutionState
的定制逻辑,但是这段代码展示了如果它的previousExecutionState
是terminated
,我们可以如何不同地初始化 Clok。在Windows.ApplicationModel.Activation.ApplicationExecutionState
枚举中定义了五个选项。除了terminated
,其他可能的previousExecutionState
值如下:
notRunning
:应用尚未运行,因为用户登录到了 Windows,或者从未运行过(新安装)。- 应用最后一次运行时,用户通过向下滑动或按 Alt+F4 关闭了它。
running
:当用户通过例如从 Windows Search charm 中进行搜索或其他类型的激活(例如点击次级磁贴或通知)来激活应用时,该应用当前正在运行。suspended
:当用户通过例如从 Windows Search charm 进行搜索或其他类型的激活(例如点击辅助磁贴或通知)来激活应用时,该应用当前被挂起。
MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.activation.applicationexecutionstate.aspx
)上提供了Windows.ApplicationModel.Activation.ApplicationExecutionState
枚举的完整定义。查看清单 17-9 中的代码,你会注意到,默认情况下,从 Visual Studio 项目模板创建的项目对所有这些都一视同仁,除了terminated
。这是一个合理的缺省值,但是在开发应用时要记住这个属性。当您的应用按预期运行时,您将改善用户体验。特别是,我将在第十九章中讨论一些情况,对于不同的previousExecutionState
值,您的应用应该有不同的行为。
处理应用暂停
当用户离开您的应用时,Windows 会挂起它。当他或她切换回来时,Windows 将恢复您的应用,对用户来说它将是无缝的。但是,有时如果您的应用被挂起,并且当前使用的应用需要额外的资源,Windows 将终止您的应用。当用户切换回你终止的应用时,你有责任利用会话状态让它看起来无缝,正如在第十五章中所讨论的。
正如我在《??》第十五章中提到的,当 Windows 挂起你的应用时会引发WinJS.Application.oncheckpoint
事件。但是,当应用终止时,不会引发任何事件。理想情况下,当用户在应用中进行更改时,您应该保持会话状态最新。然而,oncheckpoint
事件在应用被挂起时被调用,允许您保存对会话状态的任何未保存的更改。
需要明确的是,当保存会话状态并在应用挂起时处理oncheckpoint
事件时,目的不是为了恢复一个挂起的应用。Windows 会自动为您处理这些问题。因为您的应用在终止时没有得到通知,保存会话状态并处理oncheckpoint
事件使您能够恢复一个终止的应用。
当 Visual Studio 第一次创建 Clok 项目时,它在default.js
中包含了一个用于oncheckpoint
事件的处理程序,该处理程序可用于在挂起时将应用级设置存储到会话状态。例如,用户的导航历史被保存到default.js
中的会话状态。此外,如第十五章中的所示,您可以在页面控件中处理oncheckpoint
事件,并在那里解决页面级会话状态问题。更多关于oncheckpoint
事件的信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br229839.aspx
)上找到。
应用视图状态
应用的另一种状态是视图的状态。您的应用可以处于几种不同的视图状态之一:横向(图 17-3 )、纵向(图 17-4 )、对齐或填充(图 17-5 )。
图 17-3 。天气在景观中的应用
图 17-4 。16 纵向视图中的天气应用
图 17-5 。快照视图中的天气应用和填充视图中的地图应用
查看图 17-3 、图 17-4 和图 17-5 中的天气应用,您会看到应用的布局随着视图的变化而变化。在横向视图中,天气预报从左到右显示。在纵向视图中,从上到下显示。在捕捉视图中,它也从上到下显示,但细节较少。虽然这些更改是必须遵循的规则,但考虑用户可能与应用交互的不同方式并相应地更改它也很重要。
到目前为止,在 Clok 中,我们只考虑了用户在横向视图中运行 Clok 的选项。如果在 Windows 模拟器中运行 Clok,可以在纵向视图中模拟运行 Clok(图 17-6 )。
图 17-6 。纵向视图中的 Clok 仪表板屏幕
这看起来不太好,是吗?不幸的是,它在快照视图中看起来更糟(见图 17-7 )。
图 17-7 。在快照视图中锁定仪表板屏幕
在纵向视图和快照视图中都无法访问核心功能。正如你可能想象的那样,Clok 中的其他屏幕在除了风景以外的视图中看起来同样糟糕。
用 CSS 媒体查询更新布局
更新 Clok Dashboard 屏幕以在其他视图状态下工作可以完全在 CSS 中完成,使用 CSS 媒体查询。我在第九章中简要介绍了 CSS 媒体查询,当时我描述了如何根据用户屏幕的宽度以不同的尺寸在背景中显示 Clok 徽标。简而言之,媒体查询允许您指定仅在满足特定条件时才生效的 CSS 规则。在第九章中,你添加了仅在屏幕宽度小于等于 1400 像素时应用的 CSS 规则(参见清单 17-10 )。
清单 17-10。 CSS 媒体查询示例来自第九章
@media screen and (max-width: 1400px) {
#contenthost {
background-size: 40%;
}
}
除了使用屏幕宽度作为条件,我们还可以使用当前视图状态。清单 17-11 中的 CSS 规则仅在 Clok 以纵向视图运行时适用。将此 CSS 添加到home.css
的末尾。
清单 17-11。
@media screen and (-ms-view-state: fullscreen-portrait) {
.homepage section[role=main] {
margin-left: 100px;
display: block;
}
.homepage section[role=main] #rightPane {
display: none;
}
.homepage #mainMenu {
height: 424px;
-ms-flex-direction: column;
}
.homepage #elapsedTime #elapsedTimeClock {
font-size: 6em;
font-weight: 200;
}
}
因为 Clok Dashboard 屏幕的大部分样式在纵向视图中仍然有效,所以您只需要指定正在变化的规则。例如,当 Visual Studio 创建一个新的页面控件时,它会在 CSS 文件中包含媒体查询,以更改主section
的边距。在此媒体查询之前,我们可能对主section
的 CSS 所做的任何其他更改仍然适用,即使屏幕处于纵向视图。因此,在清单 17-11 中,我不需要指定所有的 CSS 来配置mainMenu
元素来使用 flexbox 布局。相反,我只需要改变 flexbox 的高度和方向。
在 Windows 模拟器中运行 Clok now,并将其旋转至纵向视图(参见图 17-8 )。
图 17-8 。在纵向视图中修改了 Clok 仪表板屏幕
通过对 flexbox 定义的一个简单更改,相机、项目和时间表菜单选项现在显示在开始/停止时钟菜单选项旁边,而不是在它下面。
在您的 CSS 媒体查询中,-ms-view-state
的其他选项包括fullscreen-portrait
、snapped
、filled
、fullscreen-landscape
。此外,可以使用许多其他媒体查询,如纵横比。媒体可能提出的问题可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh453556.aspx
)上找到。
在本书附带的源代码中,我对快照和填充视图中的 Clok Dashboard 屏幕进行了类似的更改,还对项目、项目细节和文档库屏幕进行了更改。我不会在本书中讨论这些变化,因为使用 CSS 媒体查询的技术是相同的,CSS 覆盖本身也非常简单。
用 JavaScript 更新布局
一些适应不同视图状态的更改可以很容易地完全通过 CSS 更改来完成。然而,其他更改也需要更改 JavaScript 代码。回到第七章的,当我第一次介绍ListView
控件时,我展示了一个将layout
属性设置为GridLayout
和ListLayout
的例子。在GridLayout
中,如果没有指定layout
属性,这是默认设置,条目以网格格式从上到下,然后从左到右填充ListView
。使用ListLayout
时,ListView
中的项目在一列中从上到下显示。天气应用使用ListLayout
在纵向视图(参见图 17-4 )和抓图视图(参见图 17-5 )中显示天气预报,在本节中,我将向您展示如何更新 Clok 的项目屏幕以在抓图视图中使用ListLayout
,尽管它将继续在纵向视图中使用GridLayout
。
为了实现这一改变,在list.html
中不需要更新。在list.css
的媒体查询中需要一些 CSS 规则,但是我不会在这里讨论这些。它们类似于前一节中所做的 CSS 更改,包含在本书附带的源代码中。为项目屏幕添加肖像和快照支持所需的大部分工作将在list.js
完成。将清单 17-12 中高亮显示的代码添加到list.js
。
清单 17-12。 修改 ListView 绑定,基于视图状态
filter_value_changed: function (e) {
this.filteredProjects = storage.projects.getGroupedProjectsByStatus(this.filter.value);
listView.winControl.itemDataSource = this.filteredProjects.dataSource;
listView.winControl.groupDataSource = this.filteredProjects.groups.dataSource;
zoomedOutListView.winControl.itemDataSource = this.filteredProjects.groups.dataSource;
this.configureListViewLayout();
},
configureListViewLayout: function () {
var viewState = Windows.UI.ViewManagement.ApplicationView.value;
if (viewState === Windows.UI.ViewManagement.ApplicationViewState.snapped) {
listView.winControl.layout = new WinJS.UI.ListLayout();
semanticZoom.winControl.enableButton = false;
} else {
listView.winControl.layout = new WinJS.UI.GridLayout();
zoomedOutListView.winControl.layout = new WinJS.UI.GridLayout();
semanticZoom.winControl.enableButton = true;
}
},
在为ListView
控件设置数据源后,调用一个新函数configureListViewLayout
来确定ListView
应该如何显示。当前视图状态在Windows.UI.ViewManagement.ApplicationView.value
中可用。如果 Clok 是snapped
,那么SemanticZoom
控件被禁用,并且ListView
对象的layout
属性被设置为ListLayout
。否则,ListView
控件将继续使用GridLayout
,并且SemanticZoom
被启用。
您需要采取的最后一步是在视图状态改变时处理这种情况。如果你添加一个名为updateLayout
的函数到你的页面控件定义中,当屏幕尺寸改变时,该方法将被调用。将清单 17-13 中的代码从添加到list.js
清单 17-13。 处理视图状态变化
updateLayout: function (element, viewState, lastViewState) {
this.configureListViewLayout();
},
现在,当屏幕大小发生变化时,比如当 Clok 被抓取或取消抓取时,将调用configureListViewLayout
函数来确定ListView
是否应该用ListLayout
或GridLayout
来渲染。
现在运行 Clok 并导航到项目屏幕。ListView
现在将在一个单列列表中显示所有项目(参见图 17-9 )。
图 17-9 。项目屏幕现在捕捉时使用列表布局
在本书附带的源代码中,您会发现对文档库屏幕的类似更改。因为用于更新该屏幕的技术与我刚才介绍的技术相同,所以我不会在本书中再次介绍。
不支持捕捉视图时
有时候,由于某种原因,应用中的某些屏幕,或者整个应用,可能无法在快照视图中实现。一个常见的例子是必须在风景视图中玩的游戏。甚至商店应用在快照视图中也被禁用(见图 17-10 )。
图 17-10 。商店应用在快照视图中被禁用
在 Clok 的情况下,有几个屏幕在 snapped 视图中不受支持:方向屏幕、时间表屏幕和时间表图表屏幕。类似于图 17-10 中所示的商店应用,当你不打算为一个特定的屏幕启用快照视图时,你应该显示一些东西来告诉用户这个屏幕不可用。如果不可用,你不应该保留图 17-7 中的默认行为。此外,您不应该将用户重定向到另一个屏幕。当他们取消你的应用的快照时,他们希望仍然在他们快照前的屏幕上。
我们可以为快照视图中不支持的每个屏幕单独实现“不可用屏幕”功能。这将为在每种情况下显示不同的内容提供一些灵活性。然而,在 Clok 中,我们将构建这一功能,以便可以轻松地重用它。第一步是定义用户在捕捉视图中不支持的屏幕时应该看到的内容。将清单 17-14 中的代码添加到default.html
中,在本章前面添加的扩展闪屏之后。
清单 17-14。 【屏幕不可用】消息
<div id="snappedNotSupported" class="hidden">
<img id="notSupportedImage" src="/img/logo.png" />
<div>This screen is not available while Clok is snapped.</div>
</div>
类似于图 17-10 中的商店应用,Clok 将显示其徽标,我们的信息位于徽标下方的中心。将清单 17-15 中的代码添加到default.css
中,以设置消息的样式。
清单 17-15。 消息样式
#snappedNotSupported {
background-color: #3399aa;
height: 100%;
width: 100%;
padding-top: 200px;
text-align: center;
}
消息现在已经定义好了,但是默认情况下我用 CSS 隐藏了它。我们需要一种方法,在需要的时候轻松显示这个消息。将清单 17-16 中的功能添加到utilities.js
中。
清单 17-16。 功能在截图时显示“屏幕不可用”信息
DisableInSnappedView: function () {
var viewState = Windows.UI.ViewManagement.ApplicationView.value;
var appViewState = Windows.UI.ViewManagement.ApplicationViewState;
var snappedNotSupported = document.getElementById("snappedNotSupported");
if (snappedNotSupported) {
if (viewState === appViewState.snapped) {
WinJS.Utilities.removeClass(snappedNotSupported, "hidden");
} else {
WinJS.Utilities.addClass(snappedNotSupported, "hidden");
}
}
},
这个新函数,DisableInSnappedView
,将决定 Clok 的当前视图状态。如果调用 Clok 时它处于快照视图中,将显示“不可用屏幕”消息。此时,在不支持快照视图的屏幕上调用DisableInSnappedView
就很简单了。将清单 17-17 中突出显示的代码添加到directions.js
中。
清单 17-17。 在不支持快照视图的屏幕上调用新功能
ready: function (element, options) {
// SNIPPED
Clok.Utilities.DisableInSnappedView();
},
updateLayout: function (element, viewState, lastViewState) {
Clok.Utilities.DisableInSnappedView();
},
该屏幕调用了两次DisableInSnappedView
函数。当屏幕第一次加载时,它在ready
函数中被调用,并且每当在updateLayout
函数中视图状态改变时,它被再次调用。在这两个地方调用它将确保显示消息,无论用户在已经处于 snapped 视图时试图导航到该屏幕,还是他或她首先导航到该屏幕,然后试图 snap Clok。
因为在快照视图中不支持时间表屏幕和时间表图表屏幕,所以清单 17-17 中的代码也应该添加到这些屏幕的 JavaScript 文件中。一旦你完成了这些,运行 Clok 并导航到时间表屏幕并抓取应用(见图 17-11 )。
图 17-11 。在快照视图中,时间表屏幕不可用
注意当我在第二十三章中介绍准备您的应用在 Windows 商店中共享的步骤时,当前显示的
logo.png
文件将被替换。
Windows 的未来版本
在撰写本文时,Windows 下一版本的预览版已经发布。在 Windows 8 中,可以同时查看两个 Windows 应用商店应用,其中一个在预定义宽度的快照视图中。从 Windows 8.1 开始,可以同时查看两个以上的应用,每个视图的宽度可以由用户设置。因此,命名视图状态(如快照)将被弃用,并可能在 Windows 的未来版本中被删除。
对于 Windows 8.1,微软建议更改你的 CSS 媒体查询(见清单 17-18 )和 JavaScript(见清单 17-19 )来检查特定的宽度,并根据这些值更新界面。
清单 17-18 。CSS 媒体查询更改示例
/* replace media queries that check for named view states like this ... */
@media screen and (-ms-view-state: snapped) {
/* SNIPPED */
}
/* ... with media queries that check for specific widths like this */
@media (max-width: 500px) {
/* SNIPPED */
}
清单 17-19 。示例 JavaScript 更改
// replace JavaScript that checks for named view states like this ...
var viewState = Windows.UI.ViewManagement.ApplicationView.value;
if (viewState === Windows.UI.ViewManagement.ApplicationViewState.snapped) {
// SNIPPED
}
// ... with JavaScript that checks for specific widths like this
var windowWidth = document.documentElement.offsetWidth;
if (windowWidth <= 500) {
// SNIPPED
}
MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/dn263112.aspx
)上描述了下一个版本的 Windows 将会做出的这些和其他一些改变。
结论
本章涵盖的主题——了解应用的激活状态和视图状态——可以总结为“做用户期望的事情”知道用户最后一次是如何与你的应用交互的将允许你适当地初始化应用。注意应用的视图状态,以及应用运行时视图状态可能如何变化,这将允许您设置屏幕格式以适应适当的空间。这些小细节是你的用户可能会忽略的,但是如果你没有做,他们会注意到的。
十八、外部库
正如你现在所知道的(我希望),这本书的目的是介绍如何用 HTML 和 JavaScript 构建 Windows Store 应用。这本书的每一章都集中在教你一个概念,允许你使用现有的 HTML 和 JavaScript 知识来构建原生的 Windows 应用。然而,这一章将有一个稍微不同的焦点:使用外部库。
外部库在可重用的包中提供功能,无论它们是 JavaScript 库还是 WinRT 组件。如果您的背景是 web 开发,您可能会首先想到无处不在的 jQuery 库。如果您的背景是与。NET 框架,您可能会想到诸如 log4net 之类的东西,这是一个用于执行日志记录操作的流行库。
当我第一次写这本书的大纲时,我本来打算给这一章起名叫第三方库,但是我很快意识到这并不能提供一个完整的画面。虽然将他人编写的应用库合并到您的应用中是很常见的,但是构建您自己的可重用组件并将其添加到您的应用中也是很常见的。
在这一章中,我将介绍一些你在评估第三方 JavaScript 库时必须考虑的事情。我还将提供一个简单的例子,用 C#创建一个 WinRT 组件供 Clok 使用,同时允许您在其他 Windows 应用商店应用甚至 Windows Phone 应用中重用该功能。
JavaScript 库
JavaScript 库并不缺乏。如果你是一名 web 开发人员,你可以找到一个 JavaScript 库来完成你想要的几乎任何任务,从通用库,如 jQuery,到专用库,如我在第十三章中讨论的 Flotr2 图形库。除了这些第三方库之外,您还可以构建自己的自定义 JavaScript 库,并在多个 Windows 应用商店应用以及您的网站上使用它。
许多 JavaScript 库可以在 Windows 应用商店应用中使用。如果你有一个喜欢的 JavaScript 库,你会很高兴知道它可能会工作。也就是说,有一些注意事项要记住。
安全问题
你可能认为让一个外部库访问用户的计算机是危险的,你是对的。当然,在编写软件时,无论是构建网站、桌面应用还是 Windows 应用商店应用,这都应该是一个问题。在决定将第三方库合并到您的应用中时应该小心,在 Windows 应用商店应用中可能比在网站上更小心,因为用户可能已经授予您的 Windows 应用商店应用对文件系统或相机的访问权限。
为了限制风险,应用中的页面在两种上下文之一中运行:本地上下文或 web 上下文。在 web 上下文中运行的页面,例如在iframe
中托管的网页,对用户计算机的访问是有限的,并且不能访问 WinRT。应用中包含的页面,比如我们添加到 Clok 中的每个页面,都在本地上下文中运行。这些页面对用户的计算机有更大的访问权限。因此,在本地上下文中运行的任何脚本对它可以添加到页面的内容都有一些限制。
如果本地上下文中的一个脚本将 HTML 添加到页面中,该 HTML 将由window.toStaticHTML
函数处理,以确定是否允许动态添加。如果它包含可能是恶意的代码,比如脚本或iframe
,就会抛出异常。当使用某些属性或函数添加内容时,会发生这种情况。例如,试图设置innerHTML
或outerHTML
属性只有在内容被toStaticHTML
成功处理后才会成功。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465388.aspx
)上提供了不允许的 HTML 元素和属性的列表。
如果您要添加到应用中的 JavaScript 库使用了这些受限的属性或函数,您应该确保进行彻底的测试,以确保您的应用按预期工作。有关哪些属性和功能受到限制的更多信息,请访问 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx
)。
注意请记住,您可能包含的 JavaScript 库本身并不一定是恶意的。这些限制是为了防止这些库动态地将恶意代码添加到通过
XmlHttpRequest
从互联网甚至从用户输入获得的页面中。
绕过安全限制
在一些合法的情况下,您可以拥有一些您信任并且知道是安全的内容,但是通过由toStaticHTML
函数强制实现的安全性,这是不允许的。如果您正在编写自己的 JavaScript 库,或者可以修改第三方库,有一些方法可以绕过这些限制,尽管应该谨慎使用。这些限制是有原因的——保护你的用户。只有当您可以确定没有潜在的危险影响时,才应该使用这些方法,因为本节中描述的方法不受上一节中描述的过滤的影响。
在上一节中,我提到了innerHTML
和outerHTML
属性。虽然设置这些属性需要经过toStaticHTML
的过滤,但是WinJS.Utilities.setInnerHTMLUnsafe
和WinJS.Utilities.setOuterHTMLUnsafe
功能则不需要。如果您确信内容是安全的,您可以使用这些方法来设置这些属性。关于setInnerHTMLUnsafe
功能和setOuterHTMLUnsafe
功能的更多信息可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br211696.aspx
和http://msdn.microsoft.com/en-us/library/windows/apps/br211698.aspx
)上获得。
此外,如果你必须调用一个通常被认为不安全的函数,比如动态添加对 DOM 的 JavaScript 引用,你可以把它包装在对MSApp.execUnsafeLocalFunction
的调用中(参见清单 18-1 )。
清单 18-1。 执行不安全的功能
MSApp.execUnsafeLocalFunction(function() {
// something typically considered unsafe
});
与用于网站的 HTML 和 JavaScript 相比,用于 Windows 应用商店的 HTML 和 JavaScript 的其他差异和相似之处可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx
)上找到。
同样,应该非常谨慎地使用这些方法,并且只有当您控制了将对页面进行的更改时才使用这些方法。
WinRT 组件
除了 JavaScript 库,您还可以将 Windows 运行时组件或 WinRT 组件整合到您的应用中。WinRT 组件 是一个可以从 Windows 应用商店应用中使用的 DLL。该组件可以用 C#、VB 或 C++编写,可以在用这些语言编写的 Windows 应用商店应用中使用,也可以在用 HTML 和 JavaScript 编写的 Windows 应用商店应用中使用。
许多第三方 WinRT 组件是可用的。例如,如果您想使用 SQLite 数据库而不是 IndexedDB,您可以在www.sqlite.org/download.html
下载一个组件。想要在您的应用中添加电话呼叫支持吗?Twilio 有 WinRT 组件的源代码,可以在www.github.com/twilio/twilio-csharp
获得。如果你的应用将利用 Windows Azure 移动服务,你可以从www.windowsazure.com/en-us/develop/mobile
获得你需要的信息。
当然,许多其他 WinRT 组件也是可用的,但是在本章的剩余部分,我将介绍如何构建一个非常简单的 WinRT 组件,然后将它集成到 Clok 中。
建的原因 WinRT 组件
有几个原因可能导致您决定构建 WinRT 组件。可能您需要的一些功能在 JavaScript 语言本身中不可用,并且不是由 WinJS 或 WinRT 库提供的。构建 WinRT 组件的另一个常见原因是代码重用。您可以构建一个组件,并在多个 Windows 应用商店应用甚至 Windows Phone 应用中重用它。此外,您可能已经有了为另一个平台编写的大量代码,例如用 C++编写的 iOS 应用代码。
对于这一章,我创建了一个新的需求来添加一些 JavaScript 中没有的功能。在第十一章中,当定义Project
类时,我将id
属性的默认值设置为基于当前时间的值。对于 Clok 来说,这可能没问题,但是,正如我当时提到的,不能保证它是唯一的。我本可以在第十四章中引入 IndexedDB 时改变这种行为,因为 IndexedDB 允许你指定一个自动递增的标识符,但是我不想让Project
和TimeEntry
类依赖于数据源的选择。此外,虽然本书没有涉及,但我可能会在某个时候决定 Clok 需要跨多个设备共享其数据。虽然在同一毫秒内产生两个物体,从而产生相同的id
的可能性非常小,但这并非不可能。除了将所有存储和id
生成转移到某个中央服务器,要求 Clok 始终连接到互联网,使用全球唯一标识符 GUID,因为id
是最佳选择。不幸的是,JavaScript 没有创建 GUIDs 的方法。幸运的是,C#做到了,而且非常容易。
注意正如我提到的,WinRT 组件可以在 C#、VB 或 C++中创建。在这个例子中,我将使用 C#,但是如果您对其他语言更熟悉的话,也可以随意使用其中的一种。
创建 C# WinRT 组件
如果你做过的话。NET 开发,那么您可能熟悉如何在 Visual Studio 中创建新的类库项目。您会发现创建 WinRT 组件的过程几乎是相同的。以下步骤将引导您完成这一过程。
-
Right-click the Clok solution in Visual Studio’s Solution Explorer. Select Add
New Project (see Figure 18-1).
图 18-1 。向 Clok Visual Studio 解决方案添加新项目
-
In the left pane of the New Project dialog (see Figure 18-2), select Visual C#
Windows Store.
图 18-2 。Visual Studio 的 C# Windows 应用商店项目模板
-
选择 Windows 运行时组件项目模板。
-
给这个项目起个名字:ClokUtilities。
-
单击“确定”创建您的项目。
Visual Studio 为你创建项目的时候,还会在项目中添加一个名为Class1.cs
的 C#类文件(见图 18-3 )。在解决方案资源管理器中右键单击Class1.cs
文件,并将其重命名为Guid.cs
。
图 18-3 。自动创建的类文件(左),重命名为 Guid.cs(右))
这个类文件Guid.cs
,是本书中我们将要修改的 ClokUtilities 项目中唯一的文件。但是,您可以使用这个项目添加任何其他您想用 C#构建的功能。组件不限于单个类。然而,我们现在的要求很简单。我将向您展示一些执行两个简单任务的 C#代码。首先,它提供了一种创建 GUID 并在 JavaScript 中使用它的方法。其次,它为我们提供了一种检查字符串以确定它是否是有效 GUID 的方法。用清单 18-2 中的代码替换Guid.cs
的内容。
清单 18-2。Guid 类
public sealed class Guid
{
public static string NewGuid()
{
return System.Guid.NewGuid().ToString();
}
public static bool IsGuid(string guidToTest)
{
if (string.IsNullOrEmpty(guidToTest))
{
return false;
}
System.Guid guid;
return System.Guid.TryParse(guidToTest, out guid);
}
}
现在,从 Visual Studio 的“生成”菜单中,选择“生成解决方案”选项。就这样。您刚刚创建了一个 WinRT 组件,尽管它非常简单。在进入下一节之前,我想指出一些关于这个类和 WinRT 组件的事情。首先,注意到Guid
类是公共的和密封的。这是在 JavaScript 中使用 WinRT 类所必需的。
第二,虽然好用。在组件内部,公开的类型 必须是 WinRT 类型。这包括任何公共函数的返回类型、公共函数的任何参数的类型以及任何公共属性的类型。由于这个要求,我不能从NewGuid
函数返回类型为System.Guid
的值。相反,我必须首先将它转换成一个字符串,然后返回那个值。MSDN ( http://msdn.microsoft.com/en-us/library/br205768(v=vs.85).aspx
)上有 WinRT 类型列表。
第三,当使用 JavaScript 中的 WinRT 类时,属性和函数的名称是大小写字母——也就是说,名称中的第一个字符是小写字母,后面的每个单词都以大写字母开头。另一方面,名称空间和类是 Pascal 大小写——每个单词,包括名称的第一个字符,都以大写字符开头。因此,当你在本章后面使用 JavaScript 中的Guid
类时,你将调用ClokUtilities.Guid.newGuid
,尽管我们在 C#中将其定义为ClokUtilities.Guid.NewGuid
。
因为这不是一本关于使用 C#创建 WinRT 组件的书,所以我就不赘述了。然而,你可以在http://msdn.microsoft.com/en-us/library/windows/apps/br230301.aspx
和http://msdn.microsoft.com/en-us/library/windows/apps/hh779077.aspx
找到大量关于在 MSDN 上用 C#创建 WinRT 组件的附加信息。
更新时钟
在这一节中,我将带您完成利用您刚刚创建的Guid
类所需的更改。我将介绍如何更新Project
和TimeEntry
类的id
属性以使用newGuid
函数,以及使用isGuid
函数测试有效的 GUIDs。
引用 Clok 中的 ClokUtilities 项目
在使用新的 WinRT 组件之前,必须首先从 Clok 引用 ClokUtilities 项目。
在解决方案资源管理器中,展开 Clok 项目。然后右击引用并选择添加引用。 . .从上下文菜单中(参见图 18-4 )。
图 18-4 。打开参考管理器
在打开的参考管理器窗口中,从左窗格中选择解决方案项目,并勾选 ClokUtilities 旁边的框(参见图 18-5 )。然后单击“确定”按钮添加引用并关闭“引用管理器”窗口。
图 18-5 。在引用管理器中添加引用
添加了对组件的引用后,现在可以从 JavaScript 代码中访问上一节中创建的Guid
类。要完成从使用数字到 GUIDs 的转换,需要对现有文件做一些小的改动。
更改数据类别
所需的大部分更改是对data
文件夹中的类进行的:Project
、TimeEntry,
和Storage
类。让我们从Project
类开始。在data
文件夹中,打开project.js
并用清单 18-3 中突出显示的代码更新constructor
函数。
清单 18-3。 变更项目施工方
function constructor() {
this.id = ClokUtilities.Guid.newGuid();
this.name = "";
// SNIPPED
},
这会将id
属性的默认值从基于当前时间的数字更改为从 ClokUtilities WinRT 组件生成的 GUID 的字符串表示形式。在project.js
中的createFromDeserialized
功能也需要类似的改变。用清单 18-4 中突出显示的代码更新createFromDeserialized
函数。
清单 18-4。 改变项目工厂方法
createFromDeserialized: function (value) {
var project = new Clok.Data.Project();
project.id = (ClokUtilities.Guid.isGuid(value.id) && value.id) || project.id;
project.name = value.name;
// SNIPPED
return project;
},
这种变化利用了isGuid
函数来确保id
的值始终是一个 GUID。
对TimeEntry
类的更改是类似的。清单 18-5 突出显示了timeEntry.js
中的constructor
函数需要做的两处修改,而清单 18-6 中突出显示的修改,看起来与清单 18-4 几乎相同,应该在projectId
属性定义中进行。
清单 18-5。 更改 TimeEntry 构造函数
function constructor() {
this.id = ClokUtilities.Guid.newGuid();
this._projectId = "";
this._dateWorked = (new Date()).removeTimePart();
this.elapsedSeconds = 0;
this.notes = "";
},
清单 18-6。 改变 TimeEntry 类中的 projectId 属性
projectId: {
get: function () {
return this._projectId;
},
set: function (value) {
this._projectId = (ClokUtilities.Guid.isGuid(value) && value) || this._projectId;
}
},
在Storage
类中唯一需要改变的是getSortedFilteredTimeEntriesAsync
函数,它用于确定在时间表页面上显示哪些时间条目。用清单 18-7 中突出显示的代码行更新storage.js
。
清单 18-7。 存储类中唯一的变化
storage.timeEntries.getSortedFilteredTimeEntriesAsync = function (begin, end, projectId) {
return new WinJS.Promise(function (complete, error) {
setTimeout(function () {
try {
var filtered = this
.createFiltered(function (te) {
if (begin) {
if (te.dateWorked < begin) return false;
}
if (end) {
if (te.dateWorked >= end.addDays(1)) return false;
}
if (projectId && ClokUtilities.Guid.isGuid(projectId)) {
if (te.projectId !== projectId) return false;
}
if (!te.project || te.project.status !== data.ProjectStatuses.Active)
return false;
return true;
});
var sorted = filtered.createSorted(storage.compareTimeEntries);
complete(sorted);
} catch (e) {
error(e);
}
}.bind(this), 10);
}.bind(this));
};
}
更改 Clok 仪表板屏幕
您必须对 Clok Dashboard 屏幕进行四项更改以支持 GUIDs。其中三个变化是完全一样的,所以我只展示一次。用清单 18-8 中突出显示的代码更新home.js
中的editProjectButton_click
函数。
清单 18-8。 清除转换为数字
editProjectButton_click: function (e) {
var id = project.options[project.selectedIndex].value;
nav.navigate("/pages/projects/detail.html", { id: id });
},
因为在 JavaScript 中我们将 GUIDs 视为字符串,所以这一更改是为了移除id
属性到数字的转换。除了editProjectButton_click
之外,在save
功能和saveDashboardStateToSettings
功能中也需要相同的变化,同样在home.js
中也是如此。实际上,在getIndexOfProjectId
函数中也需要同样的改变,除了验证一个值是一个 GUID,而不是验证它是一个数字。用清单 18-9 中突出显示的代码更新home.js
中的getIndexOfProjectId
函数。
清单 18-9。 在下拉列表中查找指定项目
getIndexOfProjectId: function (projectId) {
var index = 0;
for (var i = 0; i < project.options.length; i++) {
if (ClokUtilities.Guid.isGuid(project.options[i].value)
&& project.options[i].value === projectId) {
index = i;
break;
}
}
return index;
}
更改索引数据库辅助设置弹出按钮
除了上面描述的更改,您还必须对idbhelper.html
中的addTestData
函数进行更改。在您可以使用“IndexedDB 辅助程序设置”弹出按钮上的“添加测试数据”按钮之前,这是必需的。必要的更改只是用硬编码的 GUIDs 替换硬编码的数字,所以我在这里不举例说明这些更改。您可以在该书的产品页面(www.apress.com/9781430257790
)的“源代码/下载”选项卡上找到本章的源代码示例中的更新文件。
调试 WinRT 组件
当调试使用您创建的 WinRT 组件的应用时,Visual Studio 不允许您同时调试 C#代码和 JavaScript 代码。不幸的是,你必须做出选择。幸运的是,在两者之间切换只需要点击几下。首先,在解决方案资源管理器中右击 Clok 项目并选择属性菜单项(参见图 18-6 )。
图 18-6 。在解决方案资源管理器中选择属性菜单项
这将打开 Clok 属性页窗口。在左窗格中选择调试,然后将调试器类型更改为您希望调试的类型(参见图 18-7 )。选择“仅脚本”,用于调试 HTML/JavaScript 项目,如 Clok,或选择“仅托管”,用于调试用 C#编写的 WinRT 组件,如 ClokUtilities。
图 18-7 。更改调试器类型
无论调试哪种类型的代码,都不能直接启动 WinRT 组件。因此,您必须确保 Clok 项目是在您开始调试时启动的项目。在开始调试之前,可以通过在解决方案资源管理器中右击 Clok 项目并选择“设为启动项目”来实现这一点。
升级期间保留用户数据
如果您现在运行 Clok,您将不会在 Clok 仪表板屏幕的下拉列表中看到任何项目。您也不会在时间表页面上看到任何以前的时间条目。这种变化风险很低,因为 Clok 还没有发布。然而,如果用户已经在使用 Clok,并且存储了项目和时间输入数据,那么如果这些数据全部丢失,他们会非常失望。
当然,如果您有用户,您应该关心在这样的更改期间保留用户的数据。当将来进行类似的更改时,您应该确保包含将应用中的所有现有数据修改为新格式的代码。有许多方法可以实现这一点,但是最合适的地方是在 IndexedDB 初始化代码中。在storage.js
中,不是调用indexedDB.open("Clok", 1)
,而是调用indexedDB.open("Clok", 2)
,以表明您想要打开一个到数据库版本 2 的连接。因为第一次尝试打开数据库时,版本 2 并不存在,所以你必须更新onupgradeneeded
函数来将数据从版本 1 迁移到版本 2(参见清单 18-10 )。
清单 18-10。 将数据迁移到新格式的不完整例子
request.onupgradeneeded = function (e) {
var upgradedDb = e.target.result;
if (e.oldVersion < 1) {
// Version 1: the initial version of the database
upgradedDb.createObjectStore("projects", { keyPath: "id", autoIncrement: false });
upgradedDb.createObjectStore("timeEntries", { keyPath: "id", autoIncrement: false });
}
if (e.oldVersion < 2) {
// Version 2: data updated to use GUIDs for id values
// TODO - modify all projects and time entries
}
};
但是我们还没有任何用户,所以现在,只需运行 Clok 并使用 IndexedDB 助手来删除所有当前数据并添加新的测试数据。一旦你这样做了,你应该不会注意到与第十七章结尾的版本有什么不同。所有的变化都在幕后。
结论
外部库是重用功能的好方法,不管是别人创建的还是你自己创建的。大量的 JavaScript 库几乎可用于任何目的,或者您可以重用为您的网站创建的 JavaScript 库。如果您需要 JavaScript 中没有的功能,或者如果您有最初为另一个平台编写的现有代码,WinRT 组件是补充您的项目的好方法。虽然您在本章中创建的 WinRT 组件非常简单,但是可以应用相同的技术来构建更复杂的功能,这些功能可以在所有 Windows 应用商店应用以及 Windows Phone 应用中使用。
十九、搜索和共享契约
借助 Windows 8,用户可以以全新的不同方式与您的应用进行交互。当然,他们可以启动您的应用,并利用您构建的用户界面。除此之外,您的应用可以实现某些功能,这些功能将允许与操作系统本身更紧密地集成。通过利用契约,您可以确保您的应用以一致的方式支持 Windows 的通用功能。
当你签署一份协议,做一些工作来换取报酬,这份文件描述了你和你的客户双方的责任和期望。同样,Windows 8 合约描述了您的应用与另一个应用之间的交互,或者您的应用与 Windows 本身之间的交互。Windows 8 包括许多您可以在应用中实现的契约。例如,通过实现文件保存选取器协定(请参阅本章后面的“文件选取器”一节),当用户从另一个应用中保存文件时,他们可以选择将文件保存在您的应用中,或者您可以实现联系人选取器扩展以向用户提供存储在您的应用中的联系人详细信息。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh464906.aspx
)上列出了 Windows 应用商店应用可以实现的契约。
您已经实现了设置契约,允许 Clok 向设置窗格添加一些设置弹出按钮。在这一章中,我将带你实现搜索契约,为 Clok 添加对从 Windows Search charm 搜索项目的支持,以及实现共享契约,为在文档库之间共享文档添加支持。
搜索
对于 Windows 8,用户希望使用一个通用的搜索界面。不管他们在搜索什么,他们都会使用 Windows 搜索的魅力。Windows 8 中始终提供 Windows charms,包括搜索功能。有几种方法可以打开它们。
- 键盘上的 Windows 徽标键+C
- 从触摸屏的右边缘滑动
- 将鼠标移动到屏幕的右上角
当符咒可见时,通过选择图 19-1 中所示的搜索符咒,系统范围的搜索界面可用。
图 19-1 。Windows 搜索的魅力
注用户也可以通过在键盘上键入 Windows Logo 键+Q 来直接激活 Windows 搜索界面。此外,您可以编写代码从应用内部激活该接口。
在 Windows 搜索界面打开的情况下,用户可以输入搜索词。默认情况下,搜索范围是当前活动的应用,但用户可以选择支持搜索的任何其他应用,以及切换到搜索文件或设置。在图 19-2 中,你可以看到我最近在 Channel 9 应用中搜索“WinJS”。只需点击一下鼠标,我就可以选择在 Clok 中搜索。
图 19-2 。Windows 8 中的通用搜索界面
在本节的剩余部分,我将向您展示如何向 Clok 添加支持,以允许用户从 Windows 搜索界面搜索项目。方便的是,当您在应用中添加搜索契约时,许多繁琐的工作都会自动完成。
添加搜索契约
为了实现搜索契约并让 Clok 出现在用户可以搜索的应用列表中,必须完成几个步骤。
- 搜索契约必须在包清单(
package.appxmanifest
)中声明。 - 当应用激活的
kind
为search
时,一个activated
事件处理器必须处理这种情况。 - 必须创建搜索结果屏幕。
- 必须实现搜索应用数据的功能,以便它可以显示在搜索结果屏幕上。
这些步骤可以按任何顺序进行,都可以手动完成。对于某些应用来说,这可能是在应用中添加搜索的最简单或最好的方式。但是,前三个步骤都可以通过向项目中添加一个新项来完成。在这一节中,我将带您了解如何将搜索契约项添加到 Clok,以便它出现在用户可以搜索的应用列表中。在接下来的部分中,我将介绍一些定制默认搜索契约实现的步骤,以便更好地适应 Clok。
搜索契约是从您用于添加页面控件的同一“添加新项目”对话框中添加的。事实上,正如您稍后将看到的,就像新的页面控件一样,当添加搜索契约时,会创建三个文件——一个 HTML 文件、一个 CSS 文件和一个 JavaScript 文件。你需要为这些新文件准备一个家。在pages
文件夹中创建一个名为searchResults
的文件夹。右键单击新的searchResults
文件夹,选择添加新项目的选项。在打开的添加新项目对话框中,选择搜索契约项目类型并创建一个名为searchResults.html
的新项目(参见图 19-3 )。
图 19-3 。添加搜索契约
尽管这个过程添加了将用于显示搜索结果的 HTML、CSS 和 JavaScript 文件,但是 HTML 文件中实际上并没有引用 JavaScript 文件。这与创建页面控件时不同。相反,您必须手动添加对searchResults.js
到default.html
的引用。将清单 19-1 中突出显示的代码添加到default.html
中。
清单 19-1。 引用搜索 default.html 契约脚本
<script src="/pages/searchResults/searchResults.js"></script>
<script src="/js/default.js"></script>
我们将在接下来的部分对searchResults.js
和default.js
进行一些修改。在此之前,查看项目清单中为您所做的更改是很重要的。打开package.appxmanifest
并切换到申报选项卡(参见图 19-4 )。
图 19-4 。package.appxmanifest 文件会自动更新
当您将搜索契约项添加到项目中时,Visual Studio 声明 Clok 通过将正确的元素添加到package.appxmanifest
中来支持搜索。这个文件实际上是一个 XML 文件,Visual Studio 提供了一个很好的界面来更新这个文件。清单 19-2 展示了 Visual Studio 添加到清单中的 XML,允许 Clok 支持搜索。
清单 19-2。 自动对 package.appxmanifest 进行修改
<Extensions>
<Extension Category="windows.search" />
</Extensions>
虽然在这一点上没有实际的搜索工作,但是清单的这一变化使得 Clok 出现在用户可以搜索的应用列表中(见图 19-5 )。
图 19-5 。现在可以搜索 Clok 了
注意 Windows 会自动显示一个与您的应用打包在一起的图标文件。在第二十三章中,我将展示如何更新图标以匹配应用的风格。
向 Clok 数据模型添加搜索功能
一旦我们完成了这个搜索实现,用户将能够从 Windows 搜索界面按项目名称、客户名称或项目编号搜索项目。我们可以通过一些更智能的搜索功能使这种搜索变得非常复杂,但是对于本书来说,我们只需添加一个函数,该函数将返回其中一个可搜索字段包含用户输入的文本的任何项目。
目前,storage.js
有一个基于项目状态过滤项目的功能。清单 19-3 定义了一个搜索函数,它将返回所有未被删除的项目,并且项目名称、客户名称或项目编号包含用户的搜索文本。将清单 19-3 中的searchProjects
函数添加到storage.js
中,就在getGroupedProjectsByStatus
函数定义之前。
清单 19-3。 向 storage.js 添加搜索功能
storage.projects.searchProjects = function (queryText) {
var filtered = this
.createFiltered(function (p) {
if (p.status == data.ProjectStatuses.Deleted) return false;
if (!queryText) return false;
if ((p.name.toUpperCase().indexOf(queryText.toUpperCase()) >= 0)
|| (p.clientName.toUpperCase().indexOf(queryText.toUpperCase()) >= 0)
|| (p.projectNumber.toUpperCase().indexOf(queryText.toUpperCase()) >= 0)) {
return true;
}
return false;
});
return filtered.createSorted(storage.compareProjects);
};
除了这个函数,您还需要添加一个比较和排序项目的函数。您已经定义了一个函数来比较和排序项目组,所以将清单 19-4 中的函数添加到storage.js
中,就在compareProjectGroups
函数定义之前。该功能将按照客户和项目名称对项目进行排序。
清单 19-4。 为项目排序功能
compareProjects: function (left, right) {
// first sort by client name...
if (left.clientName !== right.clientName) {
return (left.clientName > right.clientName) ? 1 : -1;
}
// then sort by project name...
if (left.name !== right.name) {
return (left.name > right.name) ? 1 : -1;
}
return 0;
},
对生成的搜索结果屏幕的更改
当您向 Clok 添加搜索协定时,Visual Studio 为搜索结果屏幕添加了 HTML 和 CSS 文件。它们非常接近在 Clok 中显示项目搜索结果所需的内容。唯一需要改变的是用于格式化每个结果的WinJS.Binding.Template
。因为默认的Template
没有指定属于Project
对象的字段,所以用来自清单 19-5 的代码更新searchResults.html
。
清单 19-5。 修改模板
<div class="itemtemplate" data-win-control="WinJS.Binding.Template">
<div data-class="listViewItem" data-win-bind="className: status">
<h4 data-win-bind="innerHTML: name searchResults.markText"></h4>
<h6>
<span data-win-bind="innerHTML: projectNumber searchResults.markText"></span>
(<span data-win-bind="innerText: status"></span>)
</h6>
<h6 data-win-bind="innerHTML: clientName searchResults.markText"></h6>
</div>
</div>
除了对Template
做一些小改动,还需要做一些相应的 CSS 改动。这个 CSS 非常类似于您在创建列出所有项目的项目屏幕时添加的内容。用清单 19-6 中突出显示的代码更新searchResults.css
。
清单 19-6。 更新了 CSS 规则
/* SNIPPED */
.searchResults section[role=main] .resultslist .win-container {
margin-bottom: 10px;
margin-left: 23px;
margin-right: 23px;
}
.searchResults section[role=main] .resultslist [data-class=listViewItem] {
min-width: 250px;
height: 75px;
padding: 5px;
overflow: hidden;
}
.searchResults section[role=main] .resultslist [data-class=listViewItem].active {
background-color: #000046;
}
.searchResults section[role=main] .resultslist [data-class=listViewItem].inactive {
background-color: #464646;
color: #cccccc;
}
/* Define a style for both selected filters and text matching the query. */
.searchResults section[role=main] .resultslist [data-class=listViewItem] mark {
background: transparent;
color: limegreen;
}
@media screen and (-ms-view-state: snapped) {
/* SNIPPED */
对生成的 JavaScript 文件的更改
当您在本章前面的“添加新项”对话框中添加搜索协定时,Visual Studio 为您添加了大量代码。但是,仍有一些代码需要您自己添加。例如,searchResults.js
有占位符,让您定义哪些过滤器可供用户利用,当用户单击一个项目时处理事件,当然,还可以确定屏幕上应该显示什么结果。
在这一节中,我将介绍这些以及您必须对searchResults.js
进行的一些其他更改。让我们首先定义用户过滤搜索结果的选项。在第十一章的中,我们在项目屏幕中添加了过滤器,用于查看所有项目、仅活动项目或仅非活动项目。我们将在搜索结果屏幕中添加相同的选项。Visual Studio 在searchResults.js
中生成的代码包含一个名为_generateFilters
的函数。屏幕已经配置为使用该函数来显示我们添加的任何过滤器,并实现过滤行为。用清单 19-7 中突出显示的代码更新searchResults.js
中的_generateFilters
,添加一个活动过滤器和一个非活动过滤器。
清单 19-7。 为活动和非活动项目添加过滤器
_generateFilters: function () {
this._filters = [];
this._filters.push({
results: null,
text: "All",
predicate: function (item) { return true; }
});
var statuses = Clok.Data.ProjectStatuses;
this._filters.push({
results: null,
text: "Active",
predicate: function (item) { return item.status === statuses.Active; }
});
this._filters.push({
results: null,
text: "Inactive",
predicate: function (item) { return item.status === statuses.Inactive; }
});
},
因为我们在清单 19-3 的中定义了searchProjects
函数,所以找到匹配用户查询的项目就像调用该函数一样简单。用清单 19-8 中突出显示的代码更新searchResults.js
中的_searchData
函数。
清单 19-8。 检索搜索结果
_searchData: function (queryText) {
var storage = Clok.Data.Storage;
return storage.projects.searchProjects(queryText);
}
一旦用户看到该函数返回的结果列表,他或她可能会点击其中一个来查看该项目的项目细节屏幕。清单 19-9 包含了_itemInvoked
函数的新定义,供您在searchResults.js
中更新。
清单 19-9。 导航至项目详情屏幕
_itemInvoked: function (args) {
args.detail.itemPromise.done(function itemInvoked(item) {
WinJS.Navigation.navigate("/pages/projects/detail.html", { id: item.data.id });
});
},
Visual Studio 添加的searchResults.js
文件中提供的一个有用特性是WinJS.Binding.converter
函数_markText
。该功能将在结果中突出显示用户搜索词的任何出现。默认情况下,_markText
使用区分大小写的匹配来确定要突出显示的内容。例如,如果我搜索“win”,结果中将返回 Northwind Traders 的项目和任何名为 Windows Store App 的项目。然而,只有“Northwind”中的“win”会被默认的_markText
功能高亮显示,因为 Windows Store 应用以大写的 W、开头,而我搜索的“win”是小写的 w 。用清单 19-10 中的替换searchResults.js
中该函数的定义,这样所有匹配都将被突出显示,无论大小写。
清单 19-10。 高亮匹配术语
_markText: function (text) {
return text.replace(new RegExp(this._lastSearch, "i"), function (match, capture) {
return "<mark>" + match + "</mark>";
});
},
这个文件中需要的最后一个修改是更改页面标题。用清单 19-11 中高亮显示的代码行更新searchResults.js
中的_initializeLayout
函数。
清单 19-11。 改变页面标题
_initializeLayout: function (listView, viewState) {
/// <param name="listView" value="WinJS.UI.ListView.prototype" />
if (viewState === appViewState.snapped) {
listView.layout = new ui.ListLayout();
document.querySelector(".titlearea .pagetitle").textContent
= '"' + this._lastSearch + '"';
document.querySelector(".titlearea .pagesubtitle").textContent = "";
} else {
listView.layout = new ui.GridLayout();
document.querySelector(".titlearea .pagetitle").textContent = "Clok";
document.querySelector(".titlearea .pagesubtitle").textContent
= "Results for "" + this._lastSearch + '"';
}
},
对应用激活的更改
根据您的应用的需要,您可能只需做很少的修改就可以使用生成的代码,就像我在上一节中展示的那样。然而,在 Clok 的情况下,稍微偏离生成的代码会提供更好的体验。在第十七章中,我们定义了当用户点击开始屏幕上的磁贴启动 Clok 时会发生什么。当他或她从 Windows 搜索界面激活 Clok 时,也需要大部分相同的激活逻辑。
不管用户如何激活 Clok,我们仍然需要从 IndexedDB 中合并我们的数据模型类,初始化漫游设置,并将设置弹出按钮添加到设置窗格中。在searchResults.js
中生成的代码包括一个用于WinJS.Application.onactivated
事件的事件处理程序。因为激活过程几乎是一样的,我们将把它合并到已经存在于default.js
中的事件处理程序中。首先,让我们去掉searchResults.js
中多余的事件处理程序。从searchResults.js
中的清单 19-12 中找到代码并删除它。
清单 19-12。 从 searchResults.js 中移除激活的处理程序
WinJS.Application.addEventListener("activated", function (args) {
// SNIPPED
});
用清单 19-13 中的高亮代码修改default.js
中onactivated
事件的处理程序。这将允许我们现有的所有逻辑执行,无论用户从开始屏幕启动 Clok 还是通过执行搜索。
清单 19-13。 通过搜索检查激活
app.addEventListener("activated", function (args) {
if ((args.detail.kind === activation.ActivationKind.launch)
|| (args.detail.kind === activation.ActivationKind.search)) {
var extender = new Clok.SplashScreen.Extender(
extendedSplash,
args.detail.splashScreen,
function (e) {
args.setPromise(Clok.Data.Storage.initialize());
simulateDelay(500);
launchActivation(args);
});
}
});
虽然我们在整本书中添加的所有激活码都适用于任何一种激活类型,但在launch
激活和search
激活之间有一个区别。Clok 应该导航到不同的屏幕,这取决于它是如何被激活的。在激活launch
的情况下,用户应该看到他或她之前所在的屏幕,或者 Clok 仪表板屏幕。然而,在search
激活的情况下,有一些可能性。将清单 19-14 中突出显示的代码添加到default.js
中。
清单 19-14。 激活后导航到正确的屏幕
args.setPromise(WinJS.UI.processAll().then(function () {
configureClock();
if (args.detail.kind === activation.ActivationKind.search) {
var searchPageURI = "/pages/searchResults/searchResults.html";
var execState = activation.ApplicationExecutionState;
if (args.detail.queryText === "") {
if ((args.detail.previousExecutionState === execState.closedByUser)
|| (args.detail.previousExecutionState === execState.notRunning)) {
return nav.navigate(Application.navigator.home);
} else if ((args.detail.previousExecutionState === execState.suspended)
|| (args.detail.previousExecutionState === execState.terminated)) {
return nav.navigate(nav.location, nav.state);
}
else {
return nav.navigate(searchPageURI, { queryText: args.detail.queryText });
}
} else {
if (!nav.location) {
nav.history.current = {
location: Application.navigator.home,
initialState: {}
};
}
return nav.navigate(searchPageURI, { queryText: args.detail.queryText });
}
} else if (nav.location) {
nav.history.current.initialPlaceholder = true;
return nav.navigate(nav.location, nav.state);
} else {
return nav.navigate(Application.navigator.home);
}
}));
有两种方法可以从 Windows 搜索界面激活 Clok 。
- 用户可以输入搜索词,然后在应用列表中单击 Clok。
- 用户可以单击应用列表中的 Clok,然后输入搜索词。
如果用户输入搜索词,然后选择 Clok,清单 19-14 中的代码会将他们的搜索词——属性——传递给搜索结果屏幕,显示所有结果。然而,如果用户激活了 Windows 搜索界面,并在输入任何搜索词之前点击 Clok,以将他们的搜索“预先限定”到 Clok,queryText
的值将为空。在这种情况下,用户可能对他们在搜索时将看到的内容有不同的期望。因此,我们遵循类似于用户启动 Clok 时的模式。如果应用被暂停或终止,让用户返回到他们认为仍然活动的屏幕。如果应用之前没有运行,显示 Clok 仪表板屏幕。如果它正在运行,请导航到搜索结果屏幕,即使尚未显示任何结果。您可以决定在应用中以不同的方式处理这些情况。重要的是考虑用户在各种情况下期望发生什么。
现在运行 Clok 并在 Clok 中执行搜索。图 19-6 显示了我搜索“周期”的结果,所有匹配的项目都在结果中突出显示。
图 19-6 。搜索结果
您可能已经意识到这个屏幕与列出所有项目的项目屏幕是多么相似。你可以把这两者合并成一个屏幕。这将消除一些多余的代码,如 CSS 和标记,以显示结果和过滤结果的功能。目前最大的区别是,默认情况下,项目屏幕显示 Clok 中的所有项目,而搜索结果屏幕不会显示任何结果,直到执行搜索。然而,有一个特别的原因需要考虑将它们作为单独的屏幕。使用专用的搜索结果屏幕,您可以修改逻辑,使其也包括对时间条目的搜索,甚至是对合并结果列表中的文档的搜索。如果将项目屏幕和搜索结果屏幕结合起来,这种行为就没有意义。同样,做你认为对你的用户最好的事情。要留心。
完成本节后,您会注意到将“添加新项”对话框中的搜索协定项添加到 Visual Studio 项目中实际上并不是一个必需的步骤。当您完成这一步时,Visual Studio 没有添加任何特别的东西。您可以手动完成每个步骤。事实上,如果我们需要对生成的代码进行更多的修改,或者如果我们想要将项目屏幕与搜索结果屏幕结合起来,那么手动完成这些工作可能会更简单。重申一下我在本章前面说过的,实现搜索契约包括几个步骤。
- 搜索契约必须在包清单(
package.appxmanifest
)中声明。 - 当应用激活的
kind
为search
时,一个activated
事件处理器必须处理这种情况。 - 必须创建搜索结果屏幕。
- 必须实现搜索应用数据的功能,以便它可以显示在搜索结果屏幕上。
Visual Studio 可以为您生成部分或大部分代码,您也可以手动生成。
调试搜索激活
在测试应用的可选激活时,我应该指出一个有用的调试技巧。大部分时间我启动 Clok 都没有附加 Visual Studio 调试器。您可以通过键入 Ctrl+F5 来实现这一点,或者您可以转到 Debug Start 而不进行调试。当我必须通过代码来解决问题或者在 Clok 运行时查看不同变量的值时,我用 Visual Studio 调试器启动 Clok。你可以通过键入 F5,进入调试
开始调试(见图 19-7 ,或者点击工具栏中的按钮来完成。
图 19-7 。开始调试或不调试就开始
默认情况下,调试时,Visual Studio 将启动 Clok。大多数时候,这是我们所希望的。然而,在调试其他类型的激活的情况下,您实际上并不希望 Clok 最初运行,这样您就可以调试添加到onactivated
事件处理程序中的各种代码分支。幸运的是,有一个简单的方法可以改变这一点。
在解决方案资源管理器中右击 Clok 项目,并从上下文菜单中选择属性。确保选择 Clok 项目,而不是顶层的同名 Clok 解决方案。在“Clok 属性页”窗口的左窗格中,选择“调试”。然后将启动应用的值更改为否(参见图 19-8 )。
图 19-8 。更改调试选项
如果您现在调试(按 F5),Visual Studio 将切换到其调试模式,并等待 Clok 被激活,然后再执行其他操作。如果您现在使用 Windows 搜索界面来搜索项目,您将能够逐步完成激活处理程序。只需记住在调试与应用激活无关的其他 Clok 特性之前,将该属性设置回 Yes。
分享
Windows 8 的另一个新功能是在应用之间共享数据的能力。当一个应用支持共享时,用户可以使用图 19-9 中所示的共享符打开 Windows 共享界面,使数据对另一个应用可用。
图 19-9 。窗户也有魅力
注用户也可以直接激活分享界面,绕过 Share charm,在键盘上键入 Windows Logo 键+H。此外,我将在本章后面展示如何以编程方式激活共享界面。
开箱即用,您可以为您的应用添加对共享文本、HTML、URIs、图像和文件的支持。此外,您还可以创建自定义数据类型并共享它们。在这一节中,我将介绍将 Clok 配置为共享目标和共享源的步骤。
份额目标
在实现共享目标协定后,您的应用将向 Windows 表明自己是共享目标。共享目标是能够从另一个应用(共享源)接收共享数据的应用。当实现共享目标契约时,您指定您的应用能够接收什么类型的数据,并且当用户共享该类型的数据时,您的应用将在他或她可以选择的目标应用列表中。在这一节中,我将向您展示如何添加对与 Clok 共享文档的支持,以及如何将它们添加到项目的文档库中。
在我们开始在 Clok 中实现共享目标契约之前,你需要在你的电脑上安装一个可以共享文件的应用。您可能已经有一个或多个应用可以做到这一点,但是与其尝试确定哪些应用能够共享文档,我建议您查看名为“共享内容源应用示例”的示例项目您可以单独下载这个示例项目(http://code.msdn.microsoft.com/windowsapps/Sharing-Content-Source-App-d9bffd84
),但是我建议从 MSDN 下载整个示例应用包(http://msdn.microsoft.com/en-US/windows/apps/br229516
)。当您在 Visual Studio 中构建这个应用时,它将在您的开始屏幕上显示为 Share Source JS。
添加份额目标契约
与搜索协定类似,实现共享目标协定最简单的方法是在 Visual Studio 中将特定类型的项添加到项目中。在 Visual Studio 项目的pages
文件夹中创建一个名为shareTarget
的新文件夹。然后在shareTarget
文件夹中添加一个名为shareTarget.html
的新份额目标合约项(见图 19-10 )。
图 19-10 。添加股份目标合约
当您添加这种类型的项时,Visual Studio 会对package.appxmanifest
进行修改。它添加了所需的共享目标声明,因此 Clok 将显示在 Windows 共享界面的目标列表中。默认情况下,它指定您的项目可以接收共享文本和 URIs(参见图 19-11 )。
图 19-11 。Visual Studio 添加的共享目标声明
我们不会在 Clok 中支持这些格式,所以把它们都删除。因为我们希望用户能够将文档共享到文档库中,所以我们必须指定支持哪些文件类型。如果我们只想接受 Microsoft Word 文档,我们可以添加一个新的受支持的文件类型并指定”。docx”作为文件类型。然而,在 Clok 的情况下,用户可能希望共享 Microsoft Word 文档、电子表格、一些模型或任何数量的其他类型的文件。为此,只需勾选“支持任何文件类型”复选框(见图 19-12 )。
图 19-12 。更新 package.appxmanifest 文件
在 Clok 中,共享文件被简单地保存到文档库中,根本不用查看文件的内容。如果您的应用将使用共享文件的内容,您应该将文件类型限制为您的应用可以理解的类型。此外,正如任何时候应用接受来自用户的数据一样,在对文件做任何其他事情之前,您应该仔细分析内容以确保它们是适当的格式。很容易将文件的名称和扩展名从badThing.exe
更改为niceThing.xml
。
更改生成的共享目标屏幕
除了对package.appxmanifest
的更改,当您添加共享目标契约时,Visual Studio 创建了一个页面,当用户选择 Clok 作为共享目标时,该页面将显示给用户。Windows 会将shareTarget.html
加载到一个类似设置弹出的滑动窗口中。必须对该页面进行定制,以便以对您的应用有意义的方式处理共享数据。
在向 Clok 中的项目文档库添加文档的情况下,我们将显示一些关于共享文件的细节,并向用户提供一个项目列表以供选择。与设置弹出按钮不同,此页面不是作为default.html
的一部分加载的,因此您必须包含完成共享操作所需的任何脚本引用。用清单 19-15 中的代码更新shareTarget.html
。
清单 19-15。 更新 searchTarget.html 内容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="ms-design-extensionType" content="ShareTarget" />
<title>Share Target Contract</title>
<link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<link href="/css/default.css" rel="stylesheet" />
<link href="/css/themeroller.css" rel="stylesheet" />
<script src="/js/extensions.js"></script>
<script src="/js/utilities.js"></script>
<script src="/js/navigator.js"></script>
<script src="/data/project.js"></script>
<script src="/data/timeEntry.js"></script>
<script src="/data/storage.js"></script>
<link href="shareTarget.css" rel="stylesheet" />
<script src="shareTarget.js"></script>
</head>
<body>
<!-- The content that will be loaded and displayed. -->
<section aria-label="Main content" role="main">
<header>
<div>
<img class="shared-thumbnail" src="#" alt="share metadata image" />
</div>
<div class="shared-metadata">
<h2 class="shared-title win-type-ellipsis"></h2>
<h4 class="shared-description"></h4>
<ul id="fileNames"></ul>
</div>
</header>
<div id="projectContainer">
<label for="project">Select a project to add these documents to:</label><br />
<select id="project">
<option value="">Choose a project</option>
</select>
</div>
<div class="sharecontrols">
<div class="progressindicators">
<progress></progress>
<span>Sharing...</span>
</div>
<input class="submitbutton" type="button" value="Share" />
</div>
</section>
</body>
</html>
一些 CSS 的变化也是必要的。首先,因为在清单 19-15 中,你用projectContainer div
替换了一个commentbox
元素,在shareTarget.css
中,用清单 19-17 中的选择器更新清单 19-16 中的 CSS 选择器。
清单 19-16。 找到这个 CSS 规则
section[role=main] .commentbox {
-ms-grid-column-align: stretch;
-ms-grid-column: 2;
-ms-grid-row: 2;
height: 25px;
margin-top: 0px;
width: calc(100% - 4px);
}
清单 19-17。 改变选择器
section[role=main] #projectContainer {
-ms-grid-column-align: stretch;
-ms-grid-column: 2;
-ms-grid-row: 2;
height: 25px;
margin-top: 0px;
width: calc(100% - 4px);
}
在清单 19-15 中,您添加了一个无序列表,其中包含用户与 Clok 共享的文件名列表。将清单 19-18 中的 CSS 规则添加到shareTarget.css
中。
清单 19-18。 CSS 文件列表规则
section[role=main] header .shared-metadata #fileNames {
overflow-y: scroll;
height: 80px;
}
接收共享文件
当您将共享目标契约添加到 Clok 时,Visual Studio 生成的 JavaScript 非常简单。这是一个很好的起点,但还不够。在这一节中,我将带您了解shareTarget.js
中所需的更改。将清单 19-19 中高亮显示的别名添加到shareTarget.js
的顶部。
清单 19-19。 添加一些别名
var app = WinJS.Application;
var appData = Windows.Storage.ApplicationData.current;
var storage = Clok.Data.Storage;
var createOption = Windows.Storage.CreationCollisionOption;
var standardDataFormats = Windows.ApplicationModel.DataTransfer.StandardDataFormats;
var share;
当用户在 Windows Share 界面的目标列表中选择 Clok 时,Clok 会被shareTarget ActivationKind
激活。因为我们正在构建的共享屏幕不是作为default.html
的一部分托管的,所以您必须处理searchTarget.js
中的onactivated
事件。Visual Studio 生成的代码为此定义了一个处理程序,它包含大量代码。在许多情况下,将代码直接包含在onactivated
事件处理程序中是完全可以接受的。但是,建议尽快完成此事件处理程序。我们将遵循本例中 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh758302.aspx
)上建议的指导,将大部分逻辑移到一个新函数中,并引发一个自定义事件来执行该函数。在shareTarget.js
中,用清单 19-20 中的代码替换onactivated
事件处理程序。
清单 19-20。 从 Windows 搜索界面处理激活
app.onactivated = function (args) {
if (args.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.shareTarget) {
WinJS.Application.addEventListener("shareactivated", shareActivated, false);
WinJS.Application.queueEvent({ type: "shareactivated", detail: args.detail });
}
};
var shareActivated = function (args) {
var thumbnail;
document.querySelector(".submitbutton").disabled = true;
document.querySelector(".submitbutton").onclick = onShareSubmit;
bindListOfProjects();
project.onchange = project_change;
share = args.detail.shareOperation;
document.querySelector(".shared-title").textContent = share.data.properties.title;
document.querySelector(".shared-description").textContent
= share.data.properties.description;
thumbnail = share.data.properties.thumbnail;
if (thumbnail) {
// If the share data includes a thumbnail, display it.
args.setPromise(thumbnail.openReadAsync().done(function displayThumbnail(stream) {
document.querySelector(".shared-thumbnail").src
= window.URL.createObjectURL(stream);
}));
} else {
// If no thumbnail is present, expand the description and
// title elements to fill the unused space.
document
.querySelector("section[role=main] header")
.style
.setProperty("-ms-grid-columns", "0px 0px 1fr");
document
.querySelector(".shared-thumbnail")
.style
.visibility = "hidden";
}
if (share.data.contains(standardDataFormats.storageItems)) {
share.data.getStorageItemsAsync().done(function (files) {
if (files && files.length > 0) {
var names = files.map(function (file) {
return "<li>" + file.name + "</li>";
}).join("");
fileNames.innerHTML = names;
}
});
}
};
在这段代码中,共享屏幕的配置类似于您在ready
函数中初始化之前页面的方式。全局变量share
被设置为代表 Windows 将包含在激活中的shareOperation
。这个变量是您将如何访问与您的应用共享的数据(在本例中是文件)。如果share
变量指定了缩略图,则显示该缩略图;否则,允许标题和描述扩展到共享屏幕的整个宽度。最后,这个共享操作中包含的所有文件都被列出,以确保用户了解哪些文件将被复制到他或她选择的项目的文档库。
与 Clok 仪表板屏幕类似,共享屏幕将包括一个活动项目列表。用户将使用此列表来选择哪个项目的文档库将接收他或她共享的文件。添加从清单 19-21 到shareTarget.js
的函数,以填充项目列表,并仅在项目被选中时启用共享按钮。
清单 19-21。 绑定项目列表
var bindListOfProjects = function () {
storage.initialize().then(function () {
project.options.length = 1; // remove all except first project
var activeProjects = storage.projects.filter(function (p) {
return p.status === Clok.Data.ProjectStatuses.Active;
});
activeProjects.forEach(function (item) {
var option = document.createElement("option");
option.text = item.name + " (" + item.projectNumber + ")";
option.title = item.clientName;
option.value = item.id;
project.appendChild(option);
});
});
};
function project_change() {
document.querySelector(".submitbutton").disabled
= (project.options[project.selectedIndex].value === "");
}
为了让您走上正确的道路,生成的代码包括一个 Share 按钮的click
事件的处理函数。它会显示进度指示器,这样用户就知道发生了什么,尽管在 Clok 的情况下,共享操作通常会很快完成,所以用户可能永远也看不到它们。用清单 19-22 中突出显示的代码更新shareTarget.js
中的onShareSubmit
函数。
清单 19-22。 修改按钮点击处理程序
function onShareSubmit() {
document.querySelector(".progressindicators").style.visibility = "visible";
document.querySelector(" #project ").disabled = true;
document.querySelector(".submitbutton").disabled = true;
share.reportStarted();
addDocuments();
share.reportCompleted();
}
除了显示进度指示器之外,生成的代码还被修改为禁用项目列表。它还禁用了“共享”按钮,以防止意外的重复提交。它最终调用reportCompleted
函数,这将关闭 Clok 并让用户返回到他或她之前使用的应用。除了调用addDocuments
(您稍后将定义的函数)将文件复制到文档库,我还调用了reportStarted
。我在这里没有使用它,但是如果您的应用在接收共享数据时遇到任何错误,您也可以考虑调用reportError
函数。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.datatransfer.sharetarget.shareoperation.aspx
)上记录了ShareOperation
类的这些和其他功能。
完成使 Clok 成为文件共享目标的任务的最后一步是实现addDocuments
函数。该函数将确定哪个storageFolder
对应于所选项目,并从share
变量中复制所有的storageItem
对象,有效地将它们添加到该项目的文档库中。添加从列表 19-23 到shareTarget.js
的功能。
清单 19-23。 向库中添加文档
var addDocuments = function () {
var projectId = project.options[project.selectedIndex].value;
getProjectFolder(projectId).then(function (projFolder) {
if (share.data.contains(standardDataFormats.storageItems)) {
share.data.getStorageItemsAsync().done(function (files) {
var copyPromises = files.map(function (item) {
return item.copyAsync(projFolder, item.name, createOption.replaceExisting);
});
WinJS.Promise.join(copyPromises);
});
}
});
};
var getProjectFolder = function (projectId) {
return appData.localFolder
.createFolderAsync("projectDocs", createOption.openIfExists)
.then(function (folder) {
return folder.createFolderAsync(projectId.toString(), createOption.openIfExists)
});
};
看到它的实际应用
至此,您已经成功地将 Clok 配置为文件共享目标。通常,我不会用一整节的时间来尝试一个新特性,但是在这种情况下,已经有足够多的内容要介绍了。在调试该功能之前,确保启动调试器时不会启动 Clok(回头参考图 19-8 )。
注意如果你不打算用附加的调试器测试这个特性,那么你可以跳过这一步,但是你必须构建并运行 Clok 一次,让它注册为一个共享目标。
在本章的前面,我提到了共享源代码 JS 示例项目。现在构建并运行该项目。这个项目是构建一个可以作为共享操作源的应用的例子,这个主题我将在下一节讨论。因此,它包含了许多测试 Clok 时不需要的特性。通过尝试不同的场景,比如与另一个应用共享文本,来熟悉这个应用。为了测试 Clok,你应该特别关注标题为“共享文件”的场景(见图 19-13 )。
图 19-13 。选择目标应用
在图 19-13 中,你可以看到我使用了选择文件按钮来指定两个应该共享的文件:bicycle-data.xlsx
和motorcycle-data.xlsx
。使用“选择文件”按钮从您自己的计算机中选择一些文件与 Clok 共享。选择文件后,通过以下方法之一激活 Windows 共享界面:
- 单击 Share Source JS 应用中的 Share 按钮,以编程方式打开 Windows 共享界面。
- 点击之前在图 19-9 中显示的共享图标,打开 Windows 共享界面。
- 使用键盘快捷键 Windows Logo 键+H 直接打开 Windows 共享界面,绕过 Windows charms。
当 Windows 共享界面处于活动状态时,您应该会看到 Clok 被列为可选的共享目标。你不会看到 Clok 作为其他场景的选项,比如那些共享文本或 HTML 内容的场景。一旦选择了 Clok,应用就会被激活。因为searchTarget.html
被指定为搜索目标契约的起始页,所以您在过去几页中构建的页面将会打开,并且将会执行shareTarget.js
中的激活码。你应该看到你选择的文件列表,以及可以接收这些文件到他们的文档库的活动项目列表(见图 19-14 )。
图 19-14 。与克洛克共享文件
从列表中选择一个项目,然后单击“共享”按钮。共享屏幕将关闭,您将返回到共享源 JS 应用。如果您现在启动 Clok 并导航到所选项目的文档库,您的文件将被列出,就好像您是从 Clok 中直接添加的一样。
当使用 Share Source JS 应用的“Share files”场景时,文件被添加到我在上一节中提到的全局share
变量中。这种情况平等对待所有文件,不区分文档、电子表格、图像或任何其他文件类型。另一方面,Share Source JS 应用的“共享图像”场景专门共享图像。因为共享数据是一个图像,所以该场景通过共享操作指定了共享图像的缩略图。尽管 Clok 不支持图像的共享目标契约,但共享源 JS 示例的“共享图像”场景以位图数据格式共享图像,并将其作为文件共享。在清单 19-20 中,我们指定如果缩略图存在,它将被显示。测试该场景,查看缩略图可用时如何显示(参见图 19-15 )。
图 19-15 。用缩略图共享图像
共享源
在上一节中,您添加了对 Clok 的支持,以便从其他应用接收共享文件。在本节中,您将实现该操作的相反方面,通过向文档库添加对从 Clok 到其他应用共享文件的支持,使 Clok 成为一个共享源。
在上一节中,您使用了 Share Source JS 应用来与 Clok 共享文件。该应用的开发人员在每个屏幕上添加了一个按钮来启动共享过程。这是一个可选的步骤,因为用户总是可以使用 Windows charms 来实现这一点,但是这是一个很好的步骤,有两个原因。首先,点击一个已经可见的按钮通常比使用其他方法打开 Windows 共享界面更简单。第二,也是更重要的一点,在我看来,在屏幕上看到一个共享按钮让用户清楚地知道当前屏幕上的项目是可以共享的。我不止一次使用 Windows Store 应用,我认为它可以让我分享一些东西。然而,当我点击 Windows Share charm 时,出现一条消息告诉我不能(见图 19-16 )。
图 19-16 。目前没有可共享的内容
向文档库添加共享按钮
因此,在进入共享文件的细节之前,让我们在 Document Library 屏幕上添加一个用户界面,在应用栏中添加一个共享按钮。因为我们将应用栏配置为每当用户在文档库中选择文件时自动出现,所以共享文档的能力将是显而易见的,并且只需点击一下就可以启动共享过程。将清单 19-24 中的代码添加到library.html
中。
清单 19-24。 添加新 App 栏命令
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'shareDocumentsCommand',
label:'Share',
icon:'url(/img/Share-small-sprites.png)',
section:'selection',
tooltip:'Share',
disabled: true}">
</button>
注意一如既往,本书附带的源代码包括一个完整的项目,包含本章使用的所有源代码,包括用于共享
AppBarCommand
图标的Share-small-sprites.png
图像。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790
)。
当用户点击这个按钮时,showShareUI
功能会导致 Windows 共享界面打开。将清单 19-25 中的代码添加到library.js
中。
清单 19-25。 以编程方式打开 Windows 共享界面
shareDocumentsCommand_click: function (e) {
dataTransferManager.showShareUI();
},
在继续之前,请确保在ready
函数中配置该事件处理程序并更新libraryListView_selectionChanged
,以便在选择文件时启用共享应用栏按钮,类似于删除应用栏按钮。另外,将清单 19-26 中显示的dataTransferManager
别名添加到library.js
的顶部。
清单 19-26。 添加别名
var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager;
使用 DataTransferManager 类
对清单 19-25 中的调用将导致 Windows 共享界面打开。当它打开时,接下来发生的事情是 Windows 将从 Clok 请求应该共享的数据。这是通过引发一个DataTransferManager
对象的datarequested
事件来实现的。你要在library.js
里处理这个事件。您还必须确保在用户离开该页面时停止处理该事件。因为您的应用可以从不同的屏幕共享不同类型的数据,所以您必须确保仅当应该共享的数据在范围内时才处理该事件。通过添加清单 19-27 到library.js
中突出显示的代码来添加和删除datarequested
事件的事件处理程序。
清单 19-27。 添加和删除事件处理程序
ready: function (element, options) {
// SNIPPED
var transferMgr = dataTransferManager.getForCurrentView();
this.transferMgr_dataRequested_boundThis = this.transferMgr_dataRequested.bind(this);
transferMgr.addEventListener("datarequested", this.transferMgr_dataRequested_boundThis);
},
unload: function () {
var transferMgr = dataTransferManager.getForCurrentView();
transferMgr.removeEventListener("datarequested", this.transferMgr_dataRequested_boundThis);
},
注意你可能想知道为什么我创建了名为
transferMgr_dataRequested_boundThis
的函数。一般来说,我更喜欢使用bind
函数来表示函数中的this
关键字引用声明该函数的this
的相同值。为了确保在unload
函数中删除相同的事件处理程序,我将transferMgr_dataRequested_boundThis
定义为一个已经绑定了this
关键字的函数。另一种流行的技术是定义一个全局变量,通常命名为that
或$this
,并在整个代码中使用它来代替this
关键字。您可能在许多其他地方看到过这种技术,如果您开发 JavaScript 已经有一段时间了,您可能对它很熟悉。如果是这样,您应该继续以这种方式编写代码。就像用 HTML 和 JavaScript 构建 Windows 应用商店应用时遇到的任何其他事情一样,除了提供 WinJS 和 WinRT 库之外,您编写的代码只是“普通的 HTML 和 JavaScript”如果您有自己喜欢的开发实践,您仍然可以使用它们。
当 Windows 从 Clok 请求共享数据时,最终调用的transferMgr_dataRequested
函数,是这个特性的主力。将清单 19-28 中的代码添加到library.js
中。
清单 19-28。 向 Windows 提供共享数据
transferMgr_dataRequested: function (e) {
var request = e.request;
var selectionCount = libraryListView.winControl.selection.count();
if (selectionCount <= 0) {
request.failWithDisplayText("Please select one or more documents and try again.");
return;
}
libraryListView.winControl.selection.getItems()
.then(function (selectedItems) {
var project = storage.projects.getById(this.projectId);
if (selectionCount === 1 && isImageType(selectedItems[0].data.fileType)) {
// handle single image
request.data.properties.title = "Image shared from Clok project";
request.data.properties.description
= "From " + project.name + " (" + project.clientName + ")";
var streamRef = Windows.Storage.Streams.RandomAccessStreamReference;
var stream = streamRef.createFromFile(selectedItems[0].data);
request.data.properties.thumbnail = stream;
request.data.setBitmap(stream);
} else {
// handle non-images or multiple files
request.data.properties.title = "File(s) shared from Clok project";
request.data.properties.description
= selectionCount.toString() + " file(s) from "
+ project.name + " (" + project.clientName + ")";
}
// share as files whether single image, non-images or multiple files
var files = selectedItems.map(function (item) {
return item.data;
});
request.data.setStorageItems(files);
}.bind(this));
},
如果用户没有选择任何要共享的文件,可以使用failWithDisplayText
功能向他或她显示消息或说明(参见图 19-17 )。
图 19-17 。目前(仍然)没有可共享的内容
Clok 中的文档库可以包含任何类型的文件,包括图像文件,比如 JPG 或 PNG 文件。Windows 中的共享功能支持共享任何类型的文件,但也支持将图像作为数据流共享。如果您想要共享的图像不在磁盘上,这很有用,例如,可能是因为您从数据库中检索到了它。一些应用可能支持接收共享图像流,而其他应用可能支持接收文件;Clok 只支持以文件形式接收图像。
知道有些应用可能不支持这种或那种格式,当从文档库屏幕共享一个单一的图像时,清单 19-28 中的代码以两种格式共享图像。这将增加用户可以指定为共享目标的应用的数量,从而改善他们的体验。如果用户共享多个文件,或者不是图像的文件,那么我们只将数据作为文件共享。此外,如果只共享一个图像,我们指定该图像应该用作共享操作的缩略图。
看到它的实际应用
让 Clok 成为一个共享资源要简单得多。剩下唯一要做的事就是尝试一下。运行 Clok 并导航到包含一些文件的文档库。选择一个或多个文件,并通过以下方法之一共享它们(参见图 19-18 ):
- 点按应用栏中的“共享”按钮。
- 点击图 19-9 所示的分享符。
- 使用键盘快捷键 Windows 徽标键+H,绕过 Windows charms。
图 19-18 。共享文档库中的文件
注意如果你在用 Visual Studio 调试,别忘了再次更改项目属性,这样当你开始调试时 Clok 会自动启动。
选择目标应用。在我的测试中,我选择了邮件应用,正如你在图 19-19 中看到的,它有一个更大的共享屏幕,允许用户将文件作为附件发送给客户端。
图 19-19 。将文件共享到邮件应用
其他类似于分享的概念
除了将您的应用配置为共享目标或共享源之外,还有其他方法允许用户将数据放入您的应用以及从您的应用中获取数据。支持这种类型的共享的两种常见方法(这里我指的是“共享”的一般定义,而不是 Windows 8 特定的定义)是文件选取器和复制粘贴。
文件拾取器
文件打开选择器契约和文件保存选择器契约分别类似于共享源和共享目标。当用户使用文件选择器打开或保存文件时,这些契约允许选择您的应用。例如,当向文档库添加文件时,您可以选择已声明文件打开选择器契约的其他应用,并直接从这些应用导入文件,如 SkyDrive 或 Photos 应用(参见图 19-20 ),即使它们当前不存在于您计算机的文件系统中。
图 19-20 。文件打开选择器
同样,如果您将文件保存选择器契约添加到 Clok,您可以将邮件应用中的电子邮件附件直接保存到文档库中。我不会在本书中实现任何文件选择器,但是你可以考虑把它们作为作业添加到 Clok 中。更多关于文件拾取器契约的信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465174.aspx
)上找到。
复制并粘贴
复制粘贴已经成为我们生活的一部分很久了。我记得大约 30 年前,我在自己的第一台苹果电脑上使用了复制粘贴技术。Windows 8 以最令人期待的方式支持复制和粘贴,开箱即用:用户将能够自动从文本输入控件中复制和粘贴。此外,您可以以编程方式操作剪贴板。
一个有用的例子是添加支持,将时间表屏幕中的时间条目复制到制表符分隔的格式中,这样可以很容易地粘贴到 Microsoft Excel 中。事实上,虽然我不会在本书中涉及它,但是该特性已经在本书附带的源代码中实现了。(您可以在本书[ www.apress.com/9781430257790
]的产品页面的源代码/下载选项卡上找到本章的代码示例。)Clok 中支持复制粘贴的另一种情况是将它添加到文档库中,以允许用户将文档从一个项目的文档库复制到另一个项目的文档库中。有关在应用中复制和粘贴数据的更多信息,请访问 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh758298.aspx
)。
结论
Windows 8 做了大量工作来提高应用中完成常见任务的一致性。通过在应用之间搜索和共享数据的通用界面,用户将很快熟悉 Windows 应用商店应用支持这些功能的方式。尽管如果搜索是您的应用的一个重要特性,偏离通用的 Windows 搜索界面是完全可以接受的,甚至是推荐的,但是在大多数情况下,遵循已建立的约定更好——对用户更好,因为它是一致的,对您作为开发人员也更好,因为许多管道代码已经为您编写好了。
除了搜索和共享契约,Windows 8 还提供了许多契约和扩展,帮助您构建用户自动熟悉的应用。我鼓励你在 MSDN 上回顾它们。
二十、打印
我写软件已经很多年了。每当讨论添加打印功能的话题时,我都会有两种内部反应。如果我在做一个 Web 开发项目,我会想,“好吧,没什么大不了的。易于打印的网页通常很简单。”如果我正在开发一个 WinForms 或 WPF 桌面应用,我会想,“唉。”给桌面应用添加打印支持并不是什么难事,但通常比其他具有类似价值的功能更具挑战性。
幸运的是,如果您正在使用 HTML 和 JavaScript 构建 Windows Store 应用,那么向已安装的应用添加打印支持也很简单。正如您将在本章中看到的,打印可以使用您当前用于打印网页的相同技术来完成。但是,Windows 应用商店应用还提供了额外的打印功能,只需多做一点工作,就可以获得一些其他方式无法获得的额外优势。
这件事的美妙之处在于你可以选择。如果额外的好处(我将在接下来的几页中讨论)不值得在您的应用中额外增加 50 行左右的代码,那么您不必这样做。当然,我即将给你那 50 行代码,所以也许这是值得的。
网页开发-风格打印
如果你的背景是 web 开发,那么我将在本章中讨论的从 Windows 应用商店打印的第一个技术应该是熟悉的。在这一节中,我将演示使用 CSS 和window.print
在 Clok 中打印项目细节屏幕,这是一个内置于 HTML 浏览器(如 Internet Explorer)中的功能,它与 Windows Store 应用共享相同的 HTML 呈现引擎。我将在本节中介绍以下内容:
- 将打印按钮添加到项目详细信息屏幕的应用栏
- 使用媒体查询来指定仅在打印时应用的 CSS 规则
- 实现将内容发送到打印机的代码
向项目详细信息屏幕添加打印按钮
我们要做的第一步是向项目详细信息屏幕的应用栏添加一个打印按钮。将清单 20-1 中的代码添加到pages\projects\detail.html
中AppBar
定义的末尾。
清单 20-1。 向应用栏添加打印按钮
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'printCommand',
label:'Print',
icon:'url(/img/Print-small-sprites.png)',
section:'global',
tooltip:'Print',
disabled: true}">
</button>
注本书附带的源代码包括一个完整的项目,其中包含本章使用的所有源代码和图像文件。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(
www.apress.com/9781430257790
)。
与方向和时间表应用栏按钮类似,当应用处于快照视图时,这个打印按钮也应该隐藏。将清单 20-2 中突出显示的代码添加到媒体查询中的detail.css
中,该查询指定了 Clok 何时处于快照视图中的规则。
清单 20-2。 在抓拍视图中隐藏打印按钮
#projectDetailAppBar #goToDirectionsCommand,
#projectDetailAppBar #goToTimeEntriesCommand,
#projectDetailAppBar #printCommand {
display: none;
}
此外,只有当用户查看现有项目的详细信息时,才应启用此按钮。将清单 20-3 中的代码添加到detail.js
中的configureAppBar
函数中。
清单 20-3。 启用打印按钮
printCommand.winControl.disabled = false;
CSS 媒体查询用于打印
下一步是指定打印时将应用的替代 CSS 规则。与项目详细信息屏幕的当前布局相比(见图 20-1 ),该屏幕的打印版本不需要后退按钮或如此宽的左边缘边距。此外,应用栏和当前时间应该隐藏。最后,Description 字段应该扩展以显示更长的文本,为了更好地衡量,我们将在打印时将其移动到页面的末尾。
图 20-1 。项目详细信息屏幕
该表单使用 CSS 网格布局在屏幕上定位各种元素。在第十一章中,添加了项目细节屏幕,我们在detail.html
中用内联样式指定了-ms-grid-row
和-ms-grid-column
CSS 属性。因为您将在打印时使用 CSS 来重新定位描述字段,所以您必须对定义它的 HTML 做一点小小的更改。用清单 20-4 中突出显示的代码更新detail.html
。
清单 20-4。 更新描述表单字段
<div class="formField" id="descriptionLabelAndField"
style="-ms-grid-column: 1; -ms-grid-column-span: 3;" >
<label for="projectDescription">Description</label><br />
<textarea id="projectDescription" data-win-bind="value: description"></textarea>
</div>
CSS 属性-ms-grid-row
已经被删除,现在必须在新规则中将其添加到detail.css
中。将清单 20-5 中突出显示的代码添加到detail.css
中。
清单 20-5。 在屏幕上查看时,将描述字段定位在网格的第二行
#descriptionLabelAndField {
-ms-grid-row: 2;
}
#projectDescription {
height: 60px;
width: calc(90vw - 120px);
}
现在,您必须定义打印屏幕时将应用的 CSS 规则。如果您有许多规则,您可以将它们添加到第二个 CSS 文件中,并从detail.html
中引用它们。在这种情况下,不需要太多规则,所以将来自清单 20-6 的 CSS 添加到detail.css
的末尾。
清单 20-6。 CSS 媒体查询打印
@media print {
.fragment header[role=banner] {
-ms-grid-columns: 0px 1fr;
}
.detail section[role=main] {
margin-left: 0px;
margin-right: 0px;
}
.detail header .win-backbutton {
display: none;
}
#projectDetailForm .formField.required input,
#projectDetailForm .formField.required textarea,
#projectDetailForm .formField.required select {
border: inherit;
background-color: inherit;
}
#descriptionLabelAndField {
-ms-grid-row: 7;
}
#projectDescription {
height: 350px;
width: 90vw;
}
#currentTime,
#projectDetailAppBar {
display: none;
}
}
因为这些规则包含在打印介质查询(@media print
)中,所以它们将仅在打印该屏幕时应用。这些规则实现了我在本节开始时描述的各种需求,比如删除 Back 按钮,将 Description 字段放在 CSS 网格布局的第七行,从而将它移动到页面的末尾。
发送到打印机
最后一步是将页面发送到打印机。将清单 20-7 中的代码添加到detail.js
中。还要确保在detail.js
的ready
函数中连接这个click
事件处理程序。
清单 20-7。 打印屏幕
printCommand_click: function (e) {
window.print();
},
立即运行 Clok 并导航到现有项目的项目详细信息屏幕。如果点击打印按钮,打印窗格将会打开,允许您从安装在您计算机上的打印机列表中选择一台打印机(参见图 20-2 )。
图 20-2 。打印机选择
选择打印机后——本例中为 Microsoft XPS Document Writer 显示打印预览窗格(参见图 20-3 )。此视图允许您查看将要打印内容的缩略图,以及所选打印机支持的一些打印选项。在图 20-3 中,唯一可见的选项是将方向从纵向改为横向。
图 20-3 。打印预览
注意微软 XPS Document Writer 是一款虚拟打印机,可以将任何可以打印的内容转换成 XPS 文件。如果我选择了物理打印机,我会看到其他选项,如更改打印份数或将彩色模式从彩色更改为黑白的选项。可以为打印预览窗格配置附加选项。有关这些其他选项的更多信息,请访问 MSDN (
http://msdn.microsoft.com/en-us/library/windows/apps/hh761453.aspx
)。
在打印预览窗格中,单击打印按钮会将文档提交到选定的打印机。在图 20-2 和图 20-3 的例子中,我的电脑上会创建一个 XPS 文件。
WinRT 打印
在上一节中,我描述了用于打印网页的典型方法:定义打印友好的 CSS 规则并启动打印过程。当构建 web 应用时,你可以通过调用window.print
来触发打印,类似于你在清单 20-7 中看到的。此外,网络浏览器本身提供打印功能来执行相同的任务。默认情况下,Windows Store 应用没有打印功能,但是 Windows 允许应用使用 Devices charm 进行打印(参见图 20-4 )。
图 20-4 。Windows 设备魅力
与 Windows Share charm 一样,默认情况下不会向其他设备发送任何内容(参见图 20-5 )。然而,通过添加相对少量的代码,您可以使您的应用发送到其他设备,如打印机。
图 20-5 。此应用目前无法发送到其他设备
创建打印机类
这种与 Windows 的集成是在应用中使用 WinRT 打印类的好处之一。您可以在应用中包含一个打印按钮来打印某些内容,但是在应用中添加必要的挂钩来支持 Windows 打印界面是一个简单的步骤,可以使您的应用看起来更加完美。
在应用中声明打印契约比添加搜索契约或共享目标契约更简单。你不必对package.appxmanifest
做任何修改。在最简单的层面上,需要三个步骤。
- 获取一个
Windows.Graphics.Printing.PrintManager
类的实例并处理它的printtaskrequested
事件。 - 创建一个
Windows.Graphics.Printing.PrintTask
类的实例。 - 指定要打印的源文档。
虽然这三个步骤是打印的唯一要求,但在打印时,您还可以采取其他一些步骤来使您的应用更加健壮和完善。例如,应用中支持打印的每个屏幕都必须处理PrintManager
对象的printtaskrequested
事件。但是,在任何给定时间,只能有一个此事件的处理程序处于活动状态。因此,如果您有多个必须支持打印的屏幕,则必须注销与当前可见屏幕不关联的此事件的处理程序。
此外,PrintTask
对象有一个completed
事件,您可以选择处理它来通知您的应用打印过程是成功还是失败。使用 WinRT 打印类的另一个主要优点是,从 Windows 向应用反馈打印作业的状态。使用window.print
时,您的应用不会收到任何关于打印作业状态的反馈。在许多情况下,这不是必需的,但是如果成功的打印对于应用的用户来说是至关重要的一步,那么使用 WinRT 打印类获得的附加信息将会很有帮助。
在这一节中,我将讨论我添加到 Clok 中的一个类,该类封装了这些细节,并使向应用中任意数量的屏幕添加打印支持变得更加容易。在 Visual Studio 项目的js
文件夹中创建一个名为printing.js
的新 JavaScript 文件。请务必在default.html
中添加对该文件的引用。添加从清单 20-8 到printing.js
的代码。
清单 20-8。 添加打印实用程序类
(function () {
"use strict";
var printingClass = WinJS.Class.define(
function ctor() {
this.printManager = Windows.Graphics.Printing.PrintManager.getForCurrentView();
this.printManager_printtaskrequested_boundThis
= this.printManager_printtaskrequested.bind(this);
this._document = null;
this._title = "Clok";
this._completed = null;
},
{
register: function (title, completed) {
this._title = title || this._title;
this._completed = completed || this._completed;
this.printManager.addEventListener("printtaskrequested",
this.printManager_printtaskrequested_boundThis);
},
unregister: function () {
this.printManager.removeEventListener("printtaskrequested",
this.printManager_printtaskrequested_boundThis);
},
setDocument: function (doc) {
this._document = doc;
},
print: function () {
Windows.Graphics.Printing.PrintManager.showPrintUIAsync();
},
printManager_printtaskrequested: function (e) {
if (this._document) {
var printTask = e.request.createPrintTask(this._title, function (args) {
args.setSource(MSApp.getHtmlPrintDocumentSource(this._document));
printTask.oncompleted = this._completed;
}.bind(this));
}
},
}
);
WinJS.Namespace.define("Clok", {
Printer: printingClass,
});
})();
在这段代码定义的Clok.Printer
类的构造函数中,获得了当前视图的PrintManager
。在register
函数中,它的printtaskrequested
事件被处理,在unregister
函数中,该处理程序被移除。print
功能只需打开 Windows 打印界面,点击设备图标即可打开。printManager_printtaskrequested
处理函数创建所需的PrintTask
对象,表明要打印的源是在setDocument
函数中指定的值。如果在register
函数中指定了completed
事件处理程序,当PrintTask
对象的completed
事件被引发时,它将被调用。
发送到打印机
上一节中创建的Printer
类封装了支持应用中最常见的打印场景所需的所有逻辑。在这一节中,我将向您展示如何在支持打印的屏幕上使用Printer
类。我们将更新本章前面添加到项目细节屏幕的打印逻辑,以使用新的Printer
类。
注意本章前面添加到项目细节屏幕的打印友好的 CSS 仍然需要用于这个部分。使用 WinRT 打印类不会影响文档的打印呈现方式。它只影响如何将呈现的文档发送到打印机。
在本章的前面,我指定了只有在查看现有项目时才应该启用打印。使用window.print
技术时,用户将永远无法使用设备的魅力启动打印过程。在这种情况下,只需禁用新项目的打印按钮就足够了。在更新 Clok 以使用 WinRT 打印类之后,用户将能够从 Devices charm 进行打印,因此禁用 Clok 中的打印按钮是不够的。相反,您必须指定应该打印的文档。将清单 20-9 中的函数添加到detail.js
中。
清单 20-9。 指定要打印的文件
configurePrintDocument: function (existingId) {
if (existingId) {
this.printer.setDocument(document);
} else {
this.printer.setDocument(null);
}
},
在这种情况下,当查看一个现有的项目时,当前的document
对象——如果您是 web 开发人员,您所熟悉的同一个document
对象——将被发送到打印机。如果用户没有查看现有项目,则指定null
,禁用当前屏幕的打印。在这一章的后面,我将讨论你必须打印替代内容的一些选项,也就是说,你如何打印不仅样式不同于当前屏幕而且实际上完全不同的内容。
configurePrintDocument
定义引用了this.printer
,您还没有定义它。将清单 20-10 中高亮显示的代码添加到detail.js
中的ready
函数和unload
函数中。
清单 20-10。 注册和注销打印机类的实例
ready: function (element, options) {
this.printer = new Clok.Printer();
this.printer.register("Project Detail", function (e) {
if (e.completion === Windows.Graphics.Printing.PrintTaskCompletion.failed) {
// printing failed
}
});
this.setCurrentProject(options);
this.configureAppBar(options && options.id);
this.configurePrintDocument(options && options.id);
var form = document.getElementById("projectDetailForm");
WinJS.Binding.processAll(form, this.currProject);
// SNIPPED
},
unload: function () {
app.sessionState.currProject = null;
app.removeEventListener("checkpoint", this.app_checkpoint_boundThis);
this.printer.unregister();
},
在ready
中,this.printer
被定义,它的register
函数被调用。在这个例子中,我已经为completed
事件指定了一个处理程序,但是我把实现留给了您,作为一个练习。目前,我只添加了一个打印任务失败的条件。此外,您可以测试e.completion
何时为submitted
、canceled
或abandoned
。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.graphics.printing.printtaskcompletion
)上有关于Windows.Graphics.Printing.PrintTaskCompletion
枚举的文档。
除了为completed
事件指定一个处理程序,我还为底层的PrintTask
对象指定了一个标题。在许多情况下,用户永远不会看到这个标题,但是有两种情况下,一个友好的、相关的标题会对用户有所帮助。
-
They will see this title when they view the printer’s queued jobs (see Figure 20-6).
图 20-6 。打印机队列为古腾堡,我家的打印机
-
当使用 XPS Document Writer 或其他创建文件而不是将文档发送到物理打印机的虚拟打印机驱动程序时,它将是默认文件名。
还有一个步骤来改变项目细节屏幕,以使用 WinRT 打印类来代替window.print
。在我们的新Printer
类中,对window.print
的调用必须替换为对print
函数的调用。用清单 20-11 中的代码更新detail.js
中的printCommand_click
处理函数的定义。
清单 20-11。 用新的打印机类打印
printCommand_click: function (e) {
this.printer.print();
},
对于最简单的打印任务,window.print
可能就足够了。然而,正如您在本节中看到的,与 Windows 集成用于打印需要少量的开发投资。在许多应用中,在清单 20-8 中定义的Clok.Printer
类只需稍加修改或不加修改就可以使用,所以我鼓励你在必须打印的应用中加入这个功能。
打印替代内容
前两节演示了打印当前屏幕的打印机友好版本。然而,有时你必须打印不同但相关的内容。有时,由 WinJS 控件生成的 HTML 不适合打印,即使它包含您希望打印的内容。在这一节中,我将展示两种可以用来解决打印替代内容问题的技术:
- 在
head
元素中指定打印的替代文档 - 从
iframe
打印文件
为了演示这些技术,我们将让用户能够打印驾驶路线和发票。在这两种情况下,我们都将使用在上一节中创建的Clok.Printer
类。
打印驾驶路线
在规划本章内容时,我想到的第一个可能的打印示例是从方向屏幕打印驾驶方向。事实证明,从ListView
开始打印并不总是能得到想要的结果。我没有尝试为方向屏幕上的ListView
控件生成的复杂 HTML 制定易于打印的 CSS 规则,而是决定直接从 Bing 地图网站打印方向。
使用 HTML 页面的head
中的link
元素,您可以指定打印时应该使用的替代内容。清单 20-12 展示了一个在link
元素中使用媒体查询来指定打印替代内容的例子。
清单 20-12。 使用链接元素
<link id="alternateContent" rel="alternate" media="print" href="printer.html" />
Bing 地图打印友好页面的链接是动态的。显示从密尔沃基到华盛顿州雷德蒙方向的页面非常适合打印
http://www.bing.com/maps/print.aspx?cp=45.3601835,-105.018913&pt=pf&rtp=pos.43.041809_-87.906837_Milwaukee%2C%20WI∼pos.47.678558_-122.130989_Redmond%2C%20WA
因为它是动态的,所以您不能将link
元素添加到directions.html
中,如清单 20-12 所示。相反,您将构建这个link
元素,并在directions.js
中将它动态添加到页面中。在本节的其余部分,我将带您了解 Clok 中需要进行的更改,以允许用户从 Bing 地图网站打印行驶方向。
向方向屏幕添加打印按钮
我们要做的第一步是向方向屏幕添加一个带有打印按钮的应用栏。将清单 20-13 中的代码添加到pages\projects\directions.html
中。
清单 20-13。 向路线屏幕添加应用栏
<div id="directionsAppBar"
class="win-ui-dark"
data-win-control="WinJS.UI.AppBar"
data-win-options="{ sticky: true }">
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'printCommand',
label:'Print',
icon:'url(/img/Print-small-sprites.png)',
section:'global',
tooltip:'Print',
disabled: true}">
</button>
</div>
虽然方向屏幕不会打印用户正在查看的文档,但是您仍然可以使用您在本章前面添加的Clok.Printer
类来打印 Bing 地图中的行驶方向。在下一节中,我们将对Printer
类做一些小的修改,以便更容易地支持为打印指定替代内容。同时,用清单 20-14 中突出显示的代码更新directions.js
。
清单 20-14。 注册、注销、处理 Printer 类实例的点击事件
ready: function (element, options) {
this.printer = new Clok.Printer();
this.printer.register("Directions");
printCommand.onclick = this.printCommand_click.bind(this);
// SNIPPED
},
unload: function () {
// SNIPPED
this.printer.unregister();
},
printCommand_click: function (e) {
this.printer.print();
},
注意诚然,Clok 的方向屏幕不像 Bing 地图网站那样全面,没有地图或改变目的地的手段。为了给用户提供这些选项,你会发现,在本书附带的源代码中,我在应用栏中添加了一个额外的按钮,用于启动用户当前搜索的 Bing 地图网站。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(
www.apress.com/9781430257790
)。
更改打印机类别
打印替代内容的任务可以在不对本章前面添加的Printer
类做任何修改的情况下完成。必须添加到页面的head
元素中的link
元素可以很容易地在directions.js
中创建。事实上,我最初就是这样开发这个功能的。然而,本着使Printer
类更容易重用的精神,在这一节中,我将带您经历一些小的变化,以将该逻辑添加到Printer
类中。用清单 20-15 中突出显示的代码更新printing.js
。
清单 20-15。 支持打印机类内的替换内容
unregister: function () {
this.printManager.removeEventListener("printtaskrequested",
this.printManager_printtaskrequested_boundThis);
this._removeAlternateContent();
},
setAlternateContent: function (href) {
this._removeAlternateContent();
var alternateContent = document.createElement("link");
alternateContent.setAttribute("id", "alternateContent");
alternateContent.setAttribute("rel", "alternate");
alternateContent.setAttribute("href", href);
alternateContent.setAttribute("media", "print");
document.getElementsByTagName("head")[0].appendChild(alternateContent);
this.setDocument(document);
},
_removeAlternateContent: function () {
var alternateContent = document.getElementById("alternateContent");
if (alternateContent) {
document.getElementsByTagName("head")[0].removeChild(alternateContent);
}
},
在setAlternateContent
函数中创建了link
元素。在创建并添加到document
对象后,setDocument
被当前的document
对象调用,该对象包含必要的link
元素作为参数。在定义link
元素之前,当Printer
类被取消注册时,调用_removeAlternateContent
函数,该函数用于删除任何先前指定的替代内容。这种安全措施确保了永远不会指定一个以上的这样的元素。
更改 BingMaps 类
易于打印的 Bing 地图页面的 URI 包括经度和纬度坐标,如下所示:
http://www.bing.com/maps/print.aspx?cp=45.3601835,-105.018913&pt=pf&rtp=pos.43.041809_-87.906837_Milwaukee%2C%20WI∼pos.47.678558_-122.130989_Redmond%2C%20WA
需要起点和终点的坐标,以及它们之间中心点的坐标。目前,Clok.Data.BingMaps
类不包含任何坐标,但是因为 Bing Maps API 包含了所需的坐标,所以添加这些值会很容易。用清单 20-16 中突出显示的代码更新data
文件夹中bingMapsWrapper.js
的getDirections
函数。
清单 20-16。 包括起点和终点的坐标
var directions = {
copyright: resp.copyright,
distanceUnit: resp.resourceSets[0].resources[0].distanceUnit,
durationUnit: resp.resourceSets[0].resources[0].durationUnit,
travelDistance: resp.resourceSets[0].resources[0].travelDistance,
travelDuration: resp.resourceSets[0].resources[0].travelDuration,
bbox: resp.resourceSets[0].resources[0].bbox ,
startCoords: resp.resourceSets[0].resources[0].routeLegs[0].actualStart.coordinates,
endCoords: resp.resourceSets[0].resources[0].routeLegs[0].actualEnd.coordinates
}
注意如果你正在使用本书附带的源代码,在运行这些示例之前,你必须在
bingMapsWrapper.js
中添加你的 Bing 地图 API 密钥。
设置替代内容
使用户能够打印驾驶路线指引的最后一步是指定应该打印的替代内容。因为替代内容是由 URI 指定的,所以当用户请求方向时,必须构建该 URI。用清单 20-17 中突出显示的代码更新directions.js
中的getDirectionsButton_click
处理函数。
清单 20-17。 将 URI 构造为打印友好页面,并将其设置为要打印的替代内容
getDirectionsButton_click: function (e) {
printCommand.winControl.disabled = true;
this.printer.setDocument(null);
if (fromLocation && fromLocation.value && this.dest) {
maps.getDirections(fromLocation.value, this.dest)
.then(function (directions) {
if (directions
&& directions.itineraryItems
&& directions.itineraryItems.length > 0) {
WinJS.Binding.processAll(
document.getElementById("directionsContainer"), directions);
this.showDirectionResults(true);
directionsListView.winControl.itemDataSource
= directions.itineraryItems.dataSource;
directionsListView.winControl.forceLayout();
var printPage = " [`www.bing.com/maps/print.aspx?cp`](http://www.bing.com/maps/print.aspx?cp) ="
+ ((directions.startCoords[0] + directions.endCoords[0]) / 2) + ","
+ ((directions.startCoords[1] + directions.endCoords[1]) / 2)
+ " & pt=pf & rtp=pos." + directions.startCoords[0] + "_"
+ directions.startCoords[1] + "_" + fromLocation.value
+ "∼pos." + directions.endCoords[0] + "_" + directions.endCoords[1]
+ "_" + this.dest
this.printer.setAlternateContent(printPage);
printCommand.winControl.disabled = false;
} else {
this.showDirectionResults(false);
}
}.bind(this), function (errorEvent) {
this.showDirectionResults(false);
}.bind(this));
} else {
this.showDirectionResults(false);
}
},
该功能的第一个变化是当用户发起新的驾驶路线指引请求时禁用打印功能。调用setDocument
函数是通过null
调用的,因为该参数将禁用当前屏幕的打印,即使用户试图使用设备的魅力进行打印。如果用户请求的驾驶方向可用,则创建 URI 并传递给setAlternateContent
函数。这将启用从设备的魅力打印,但不会自动启用打印应用栏按钮,所以我们明确启用它。
立即运行 Clok,导航到指定了客户地址的项目的方向屏幕,然后输入起始位置并单击获取方向。方向载入后,使用打印按钮或 Devices charm 打印行驶方向(参见图 20-7 )。
图 20-7 。便于打印的驾驶路线预览
注如果您使用 Microsoft XPS Document Writer 测试您的打印功能,它会在您的 Documents 文件夹中保存一份 XPS 文档。
打印项目时间表的发票
在上一节中,您使用了一个link
元素来指定打印的替代内容。该内容的 URI 可以引用应用中的一个文件或 Internet 上的一个页面,但无论是哪种情况,打印的内容都是通过请求指定 URI 的内容来检索的。在这一节中,我将讨论在一个iframe
元素中托管内容,并指定iframe
的内容应该用作打印的替代内容,我将向您展示如何使用这种技术从时间表屏幕构建一个可打印的发票。
添加发票选项设置弹出按钮
到目前为止,在 Clok 中,用户还没有办法指定他们自己公司的名称或他们的计费率——这是创建发票所需的两件事。在本节中,您将添加一个新的发票选项设置弹出按钮,以允许用户指定这两个值,并提供一个段落来描述他们的账单条款。这些值可以添加到您在第十五章中创建的现有 Clok 选项设置弹出按钮的新部分,但是,在我看来,它们与该设置弹出按钮上可用的其他设置无关,所以我建议添加一个新的。因为这些步骤与构建“Clok 选项设置”弹出按钮的步骤几乎相同,所以我不会在这里讨论细节,但我会突出显示您需要采取的步骤。
-
在 Visual Studio 项目的
settings
文件夹中创建一个名为invoiceOptions.html
的新设置弹出按钮和一个名为invoiceOptions.js
的对应 JavaScript 文件。 -
允许用户为三个新的漫游设置提供值:
- a.
invoiceCompanyName
- b.
invoiceDefaultRate
- c.
invoicePaymentOptions
- a.
-
Specify default values for these roaming settings in the
intializeRoamingSettings
function indefault.js
(see Listing 20-18). These values will be used if the user does not specify values of his or her own.清单 20-18。 为新的漫游设置提供默认值
roamingSettings.values["invoiceCompanyName"] = roamingSettings.values["invoiceCompanyName"] || "Your Company Name"; roamingSettings.values["invoiceDefaultRate"] = roamingSettings.values["invoiceDefaultRate"] || 50.00; roamingSettings.values["invoicePaymentOptions"] = roamingSettings.values["invoicePaymentOptions"] || "Payment is due within 30 days.";
-
在设置了
applicationcommands
变量的default.js
中引用这个新的设置弹出按钮,使其包含在设置窗格中。
该过程与您在第十五章中添加锁定选项设置弹出按钮时遵循的过程相同。本书随附的源代码中提供了发票选项设置弹出按钮的完整版本,以及本章中的所有其他源代码。
注意这些变化将允许用户指定一个单一的计费率用于所有项目。您可能希望添加的一个有用功能是允许用户另外指定每个项目的计费率。如果指定了项目费率,则该费率将用于发票计算;否则,将使用默认汇率。
更新时间表屏幕
在 HTML ( pages\timeSheets\list.html
)和 JavaScript 代码(pages\timeSheets\list.js)
)中,需要对时间表屏幕进行一些更改。按照本章前面几节中使用的相同模式,在时间表屏幕的应用栏中添加一个打印发票按钮。不要忘记注册一个Clok.Printer
的实例,并通过调用Printer
实例的print
函数来处理打印发票按钮的click
事件。
在本节中,您将使用一个iframe
元素的内容作为打印的替代内容。现在将那个iframe
元素添加到list.html
(参见清单 20-19)。
清单 20-19。将包含发票的 iframe 元素
<iframe height="0" width="0" id="invoiceFrame" src="/templates/invoice.html"></iframe>
iframe
可以放在屏幕的任何地方,因为它没有高度和宽度。但是,我建议将它放在timeEntriesContainerdiv
之前,并将height
和width
属性临时设置为正数,这样您就可以在测试期间看到iframe
的内容。src
属性中引用的invoice.html
文件尚不存在。我将在下一节介绍该文件的细节,但同时,在 Visual Studio 项目的根目录下创建一个名为templates
的新文件夹,然后在该文件夹中添加一个名为invoice.html
的占位符 HTML 文件(参见图 20-8 )。
图 20-8 。向 Visual Studio 项目添加发票模板
仅当使用应用栏中的过滤器按钮选择单个项目时,才能打印发票。当用户更新过滤器时,您必须添加代码来确定项目是否被选中。用清单 20-20 中突出显示的代码更新list.js
中的filter_changed
函数。
清单 20-20。 过滤器更换时重新生成发票
filter_changed: function (e) {
this.updateResultsArea(searchInProgress);
this.printer.setDocument(null);
printInvoiceCommand.winControl.disabled = true;
storage.timeEntries.getSortedFilteredTimeEntriesAsync(
this.filter.startDate,
this.filter.endDate,
this.filter.projectId)
.then(
function complete(results) {
if (results.length <= 0) {
timeEntryAppBar.winControl.show();
this.updateResultsArea(noMatchesFound);
} else {
if (ClokUtilities.Guid.isGuid(this.filter.projectId)) {
this.printer.setDocument(invoiceFrame.document);
printInvoiceCommand.winControl.disabled = false;
}
this.updateInvoiceIframe(results);
this.updateResultsArea(timeEntriesListView);
}
this.showAddForm();
this.filteredResults = results;
timeEntriesListView.winControl.itemDataSource = results.dataSource;
}.bind(this),
function error(results) {
this.updateResultsArea(searchError);
}.bind(this)
);
},
当一个项目被选中,并且this.filter.projectId
包含一个 GUID 时,调用setDocument
函数,将iframe
的document
对象作为参数。接下来您将添加的updateInvoiceIframe
函数用于将时间表数据从时间表屏幕传递到iframe
中的发票。将清单 20-21 中的代码从添加到list.js
。
清单 20-21。 发送时间单数据到发票
updateInvoiceIframe: function (results) {
var invoiceLines = results.map(function (item) {
return {
elapsedSeconds: item.elapsedSeconds,
dateWorked: item.dateWorked,
notes: item.notes
};
});
var invoiceProject = results.getAt(0).project;
var invoiceData = {
project: invoiceProject,
lines: invoiceLines
}
invoiceFrame.postMessage(invoiceData, "ms-appx://" + document.location.host);
},
与iframe
的交流是使用postMessage
完成的,与在第十三章中完成的与 Web 工作器的交流方式非常相似。创建一个invoiceData
对象,它包含发票上要包含的每个时间条目的详细信息,以及对正在开票的项目的引用。出于安全原因,对postMessage
的调用包括当前域作为第二个参数。在invoice.html
,我们将验证发布的消息来自同一个域。
生成发票
在本节中,我将展示invoice.html
的内容,当用户从时间表屏幕打印时,将打印该文件。iframe
可以包含您希望应用包含的任何内容。对于 Clok,我将invoice.html
构建为一个单独的文件,在文件中包含必要的 CSS 和 JavaScript,而不是像我们在应用的其余部分所做的那样引用外部特定于页面的 CSS 和 JavaScript 文件。在上一节中,您创建了一个占位符invoice.html
文件。用清单 20-22 中的代码更新文件。如果您不想键入所有代码,您可以在本书附带的源代码中找到该文件的完整版本。
清单 20-22。invoice.html 发票模板的内容
<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css">
body {
font-family: sans-serif;
}
h4.sectionHead {
margin-bottom: 0px;
}
.invoiceLines {
border-collapse: collapse;
border-spacing: 0px;
}
.invoiceLines th,
.invoiceLines td {
border: 1px solid black;
margin: 0px;
padding: 2px;
}
.invoiceLines .totals {
font-weight: bold;
}
.invoiceLines #totalDesc {
background: black;
}
</style>
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<script src="/data/timeEntry.js"></script>
<script src="/data/project.js"></script>
<script>
var appData = Windows.Storage.ApplicationData.current;
var roamingSettings = appData.roamingSettings;
var formatDate = function (dt) {
var formatting = Windows.Globalization.DateTimeFormatting;
var formatter = new formatting.DateTimeFormatter("shortdate");
return formatter.format(dt);
}
window.onmessage = function (message) {
if (message.origin !== "ms-appx://" + document.location.host) {
return;
}
WinJS.UI.processAll().then(function () {
var compName = roamingSettings.values["invoiceCompanyName"];
var rate = Number(roamingSettings.values["invoiceDefaultRate"]);
var pmtOptions = roamingSettings.values["invoicePaymentOptions"];
if (pmtOptions.indexOf("<br") < 0) {
pmtOptions = pmtOptions.replace(/\r\n/g, "<br />").replace(/\n/g, "<br />");
}
var invoiceLines = document.getElementById("invoiceLines");
var template = document.getElementById("invoiceLineTemplate").winControl;
var sumHour = 0;
var sumCost = 0;
invoiceLines.innerText = "";
message.data.lines.forEach(function (item) {
var hrs = item.elapsedSeconds / 3600;
item.dateWorked = formatDate(item.dateWorked);
item.hours = hrs.toFixed(2);
item.lineCost = (rate * hrs).toFixed(2);
sumHour += hrs;
sumCost += rate * hrs;
template.render(item, invoiceLines);
});
invoiceDate.innerText = formatDate(new Date());
projectName.innerText = message.data.project.name;
projectNumber.innerText = message.data.project.projectNumber;
companyName.innerText = compName;
clientName.innerHTML = message.data.project.clientName;
contactName.innerHTML = message.data.project.contactName;
address1.innerHTML = message.data.project.address1;
address2.innerHTML = message.data.project.address2;
city.innerHTML = message.data.project.city;
region.innerHTML = message.data.project.region;
postalCode.innerHTML = message.data.project.postalCode;
totalHours.innerText = sumHour.toFixed(2);
totalCost.innerText = sumCost.toFixed(2);
paymentOptions.innerHTML = pmtOptions;
});
}
</script>
</head>
<body>
<h1>Invoice</h1>
<h2 id="companyName"></h2>
<h4 class="sectionHead">To:</h4>
<div>
<div id="clientName"></div>
<div id="contactName"></div>
<div id="address1"></div>
<div id="address2"></div>
<div>
<span id="city"></span>,
<span id="region"></span>
<span id="postalCode"></span>
</div>
</div>
<h4 class="sectionHead">For:</h4>
<div>
<div>
Invoice Date: <span id="invoiceDate"></span>
<br />
Project: <span id="projectName"></span>
<br />
Ref #: <span id="projectNumber"></span>
<br />
</div>
</div>
<h4 class="sectionHead">Invoice Details</h4>
<table style="display: none;">
<tbody data-win-control="WinJS.Binding.Template" id="invoiceLineTemplate">
<tr>
<td data-win-bind="textContent: dateWorked"></td>
<td data-win-bind="textContent: notes"></td>
<td data-win-bind="textContent: hours"></td>
<td data-win-bind="textContent: lineCost"></td>
</tr>
</tbody>
</table>
<table class="invoiceLines">
<thead>
<tr>
<th>Date</th>
<th>Note</th>
<th>Hours</th>
<th></th>
</tr>
</thead>
<tbody id="invoiceLines"></tbody>
<tfoot>
<tr class="totals">
<td id="totalDesc" colspan="2"></td>
<td id="totalHours"></td>
<td id="totalCost"></td>
</tr>
</tfoot>
</table>
<p id="paymentOptions"></p>
</body>
</html>
invoice.html
大部分都是标准的、无趣的 HTML、CSS、JavaScript。然而,我在清单 20-22 中强调了几件事,我想指出来。
- 因为
invoice.html
加载了ms-appx
协议,它可以完全访问 WinRT 和 WinJS 库,以及我们添加到 Clok 中的任何类。你可以在上面看到,我已经添加了对 WinJS JavaScript 文件以及 Clok 的Project
和TimeEntry
类定义的脚本引用。 - 作为引用 WinJS 库的结果,我能够利用
WinJS.Binding.Template
类创建一个名为invoiceLineTemplate
的模板,该模板定义了发票中的行项目将如何显示。 - 在
onmessage
处理函数中,通过清单 20-21 中的postMessage
发送的invoiceData
可作为message.data
使用。基于该对象的属性填充单个占位符,发票的行项目由invoiceLineTemplate
模板呈现。 - 您在本章前面创建的三个漫游设置的值用于显示用户的公司名称和帐单条款,以及计算应付金额。
立即运行 Clok 并导航至时间表屏幕。在过滤了单个项目的列表后,您可以使用应用栏中的打印发票按钮或使用 Devices charm 来打印发票(参见图 20-9 )。
图 20-9 。从时间表屏幕打印的发票样本
高级打印主题
在本章中,我只讲述了从 Windows 应用商店打印的基础知识。当然,打印遵循 80/20 规则,即你需要完成的 80%的工作是用 20%的可能特性完成的。甚至可能是 90/10 法则。然而,Windows.Graphics.Printing
名称空间中还有许多其他的类,您可能会发现它们对一些更特殊的打印需求很有用。例如,有一些类可以简化任务,比如交流打印任务的进度,或者允许用户在打印预览窗格中指定更多或更少的选项,比如打印质量或纸张大小。
MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.graphics.printing.aspx
)上有关于Windows.Graphics.Printing
名称空间的更多信息。
结论
从 Windows 应用商店应用打印的基础与从网站打印非常相似。在这一章中,我介绍了一些定义打印内容的不同方法,以及一些启动打印过程的不同方法。可以通过对屏幕内容应用仅打印 CSS 规则、链接到应该打印而不是当前屏幕的替代内容的 URI,或者打印iframe
元素的内容来指定内容。您可以使用原生的window.print
函数来启动打印过程,但是只需少量的工作,您就可以利用 WinRT 打印类,为您的用户提供更加集成的体验。只需稍加修改或不加修改,本章创建的Clok.Printer
类应该能够处理您可能面临的大量打印任务。
二十一、通知和磁贴
在我看来,Windows 8 最好的一个特点是,只要看一眼屏幕,就能获得如此多的信息。
随着 Live Tiles 的引入,应用可以以小块的形式向用户提供最新的信息。在许多情况下,我只需要摘要信息(“你的下一次会议是明天早上在约翰的办公室”或“现在气温 78 度,阳光明媚”),而其他时候,它会提示我启动应用以获得更多细节(“约翰刚刚通过电子邮件向我发送了我们会议的议程”或“我安装的四个应用有可用的更新”)。此外,toast 通知是一个小矩形,在屏幕的右上角显示一条简短的消息,无论您是在开始屏幕上还是使用其他应用,它都是一种在发生事情时从应用获取更新的好方法。
在这一章中,我将介绍几种不同的方法,让你的用户可以方便地访问你的应用中的信息。我将介绍用于显示小更新的 toast 通知和 Live Tiles,我还将介绍用于让用户快速访问应用中常用屏幕的辅助 Tiles。
吐司通知
Toast 通知,有时简称为toast或通知,是从应用向用户提供简短、及时信息的一种很好的方式。这些通知在屏幕的上角显示为一个小矩形,无论用户在计算机上做什么,无论是使用您的应用、使用不同的 Windows 应用商店应用 、在开始屏幕上还是在桌面上,这些通知都会出现(参见图 21-1 )。用户可以单击通知来激活您的应用;他们可以驳回通知;或者他们可以简单地忽略它,它就会消失。
图 21-1 。Toast 通知,同时使用商店应用,提醒我写这一章
有几种类型的通知可用:
- Local :当用户使用您的应用时创建并显示的通知
- Scheduled :在用户使用你的应用时创建的通知,但是直到将来某个特定的时间才会显示,那时用户可能在使用你的应用,也可能不在使用
- Push :从远程服务器(如 Windows Azure Mobile Services)创建并发送的通知,显示用户是否正在使用你的应用
本地通知
在这一节中,我将向您展示如何向 Clok 添加一个简单的 toast 通知。当用户在定时器已经运行的情况下恢复时钟时,将显示通知,告知用户定时器已经运行了多长时间。
对应用清单的更改
在您的应用可以显示任何 toast 通知之前,您必须在应用清单中做一个小的配置更改。这是一个很容易被忽视的简单步骤。如果忽略此步骤,则不会出现错误;创建通知的代码只是被悄悄地忽略了。在我意识到我的错误之前,我曾经浪费了大约十分钟试图找出为什么通知没有被显示。
幸运的是,这是一个简单的改变。打开package.appxmanifest
并向下滚动到应用 UI 选项卡的可视资产部分。从左侧列表中选择所有图像资产,并将 Toast capable 设置为 Yes(见图 21-2 )。
图 21-2 。启用 toast 通知
此外,Windows 模拟器的一个限制是它不显示 toast 通知。在测试该功能之前,确保您的调试目标设置为本地机器或远程机器(见图 21-3 )。
图 21-3 。更改调试目标
Toast 通知模板
当决定向用户显示通知时,您必须仔细考虑必须包含哪些信息。根据你的需要,你可以从 WinRT 库中的八个模板中选择一个(见图 21-4 )。
图 21-4 。WinRT 中可用的 toast 通知模板
所有八个图标都包括在package.appxmanifest
中为您的应用指定的小图标。其中四个模板允许您指定要包含的文本—根据您选择的模板,在一至三段之间。例如,如果您需要显示一个简短的句子,您可以选择toastText01
模板,而toastText02
模板将允许您指定一个标题,以及另一段将换行的文本。此外,其他四个模板提供了相同的选择,但也允许您指定通知文本附带的图像。各种模板记录在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761494.aspx
)上。
注意虽然每段文本没有特定的字符限制,但如果某个字符串对于其分配的空间来说太长,它会被截断,并添加省略号。记住这一点,确保你展示的每一条信息都清晰简洁。
创建通知
让我们通过向 Clok 添加通知来看看这是如何工作的。要显示这八个模板中的一个,您必须创建 XML 来定义要使用的模板和要在通知中显示的值。您将更新 Clok 仪表板以显示通知 Clok 已经启动,计时器已经运行。将清单 21-1 中定义的函数添加到home.js
。
清单 21-1。 创建通知
getStillRunningToastContent: function () {
var seconds = elapsedTimeClock.winControl.timerValue;
if (elapsedTimeClock.winControl.isRunning && seconds > 0) {
var hours = Math.floor(Clok.Utilities.SecondsToHours(seconds, false));
var template = notifications.ToastTemplateType.toastImageAndText02;
var toastContent = notificationManager.getTemplateContent(template);
// image
var imageNodes = toastContent.getElementsByTagName("image");
imageNodes[0].setAttribute("src", "ms-appx:///img/Clock-Running.png");
// text
var textNodes = toastContent.getElementsByTagName("text");
textNodes[0].appendChild(toastContent.createTextNode("Clok is running"));
textNodes[1].appendChild(toastContent.createTextNode(
"Clok has been running for more than " + hours + " hours."));
return toastContent;
}
},
showLocalToast: function () {
var toastContent = this.getStillRunningToastContent();
if (toastContent) {
var toast = new notifications.ToastNotification(toastContent);
notificationManager.createToastNotifier().show(toast);
}
},
getStillRunningToastContent
功能首先确定定时器是否正在运行以及已经过了多长时间。如果用户刚刚启动了 Clok,并且计时器仍在运行,则创建一个通知。在本例中,我选择了toastImageAndText02
模板,它允许我在通知中指定一个图像、一个标题和一段稍长的文本。
在清单 21-1 中,我使用了getTemplateContent
函数来检索一个Windows.Data.Xml.Dom.XmlDocument
对象,该对象表示创建通知所需的 XML。然后我操纵那个XmlDocument
对象的节点来指定应该显示的图像和文本。除了操作一个XmlDocument
对象,您还可以构建一个包含必要 XML 的字符串,并从中创建一个通知。创建动态切片的过程遵循相同的步骤,我将在本章后面的“创建动态切片”一节中演示字符串操作技术。
``getStillRunningToastContent函数返回通知,
showLocalToast函数将通知传递给
ToastNotifier对象的
show函数,后者将显示通知。现在将清单 21-2 中突出显示的代码添加到
home.js中的
setDashboardStateFromSettings`函数中。
清单 21-2。 显示通知
setDashboardStateFromSettings: function () {
var state = localSettings.values["dashboardState"];
if (state) {
state = JSON.parse(state);
elapsedTimeClock.winControl.startStops = state.startStops;
project.selectedIndex = this.getIndexOfProjectId(state.projectId);
timeNotes.value = state.timeNotes;
if (elapsedTimeClock.winControl.isRunning) {
this.startTimer();
this.showLocalToast();
}
}
},
此外,将清单 21-3 中突出显示的别名添加到home.js
。
清单 21-3。 给通知类添加别名
var appData = Windows.Storage.ApplicationData.current;
var localSettings = appData.localSettings;
var notifications = Windows.UI.Notifications;
var notificationManager = notifications.ToastNotificationManager;
var nav = WinJS.Navigation;
var storage = Clok.Data.Storage;
现在运行 Clok 并启动计时器,然后关闭 Clok。片刻之后,再次启动 Clok。此时,Clok 会显示一个通知,提醒你定时器正在运行(见图 21-5 )。
图 21-5 。向 Clok 用户显示通知
虽然我不会在这里介绍它,但这本书附带的源代码包括一个简单的功能,可以改善用户体验,允许用户在 Clok 选项设置弹出按钮中指定他们是否希望在 Clok 启动时看到这个提醒。您可以在本书的产品详细信息页面(www.apress.com/9781430257790
)的源代码/下载选项卡上找到本章的代码示例。
预定通知
在上一节中,您添加了一些功能,让用户在启动 Clok 时清楚地知道计时器是否已经在运行。作为一名用户,我可能会发现这很有用,例如,当我启动 Clok 来获取到我的客户所在位置的驾驶路线时,我会想起我昨天启动了计时器。如果我已经完成了这项工作,这个提醒可能会促使我快速停止计时器,并在我记忆犹新的时候更正时间条目。
然而,当我不使用 Clok 时,情况会怎样呢?如果我正在写电子邮件或玩游戏呢?在这一节中,我将向您展示如何安排在计时器运行八小时后出现通知。
安排通知
在上一节中,您创建了一个ToastNotification
对象,并使用show
函数将它立即显示给用户。安排通知非常类似。在本节中,您将创建一个ScheduledToastNotification
对象,并使用addToSchedule
函数在未来的某个时间将它显示给用户。将清单 21-4 中的代码添加到home.js
中。
清单 21-4。 调度未来通知
scheduleToast: function () {
var reminderThreshold = 8; // hours
var toastContent = this.getStillRunningToastContent();
if (toastContent) {
var seconds = elapsedTimeClock.winControl.timerValue;
var notifyTime = (new Date()).addSeconds(-seconds).addHours(reminderThreshold);
if (notifyTime.getTime() > (new Date()).getTime()) {
var snoozeTime = 30 * 60 * 1000; // 30 min
var snoozeCount = 5;
var toast = new notifications.ScheduledToastNotification(
toastContent,
notifyTime,
snoozeTime,
snoozeCount);
toast.id = "IsRunningToast";
notificationManager.createToastNotifier().addToSchedule(toast);
}
}
},
注意在本书附带的源代码中,我给
Date
原型添加了几个新函数:addSeconds
、addMinutes
和addHours
。
这段代码使用相同的getStillRunningToastContent
函数来定义通知。在做了一些日期和时间计算以确定计时器何时到达八小时后,从getStillRunningToastContent
函数返回的toastContent
对象被用来创建一个ScheduledToastNotification
对象。此外,我还添加了可选代码,允许用户将通知“暂停”30 分钟,最多五次。可以通过忽略通知、在触摸屏上将其扫走或点击鼠标悬停在通知上时出现的×按钮来暂停通知。
如果用户点击通知,打盹将被取消;将推出 Clok 并且将显示 Clok 仪表板屏幕。如果您为定义通知的XmlDocument
中的toast
节点的launch
属性指定一个值,那么您可以检查args.detail.arguments
属性以在应用的激活过程中检索该值,并导航到不同的屏幕,而不是 Clok 仪表板屏幕。例如,如果您正在创建日历应用,单击会议提醒通知应该会在应用中打开该会议。关于launch
属性的更多信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br230846.aspx
)上找到。在本章的后面,当我介绍二级瓷砖时,我将涉及一个不同但相似的主题。
在调用addToSchedule
之前,我给toast
对象的id
属性赋值。该id
属性最长可达 16 个字符,可用于引用尚未显示的预定通知。此外,如果您使用相同的id
属性值创建另一个计划通知,新通知将替换先前定义的通知。通过为每个通知提供不同的id
属性值,可以安排多个不同的通知。例如,如果您正在构建一个日历应用,您可以通过为每个通知指定不同的id
值,为用户日历中的每个会议安排提醒通知。因为 Clok 中只有一个计时器,所以我将id
属性硬编码为IsRunningToast
。在下一节中,您将使用它来取消预定的通知。在取消通知之前,必须首先对其进行计划。用清单 21-5 中突出显示的代码更新home.js
中的setupTimerRelatedControls
函数,以调用新的scheduleToast
函数。
清单 21-5。 定时器启动时的预定通知
setupTimerRelatedControls: function () {
if (this.timerIsRunning) {
this.startTimer();
this.scheduleToast();
} else {
this.stopTimer();
}
this.enableOrDisableButtons();
},
现在,当计时器启动时,如果计时器已经运行了八个小时,将会出现一个通知。如果用户启动 Clok 时计时器已经在运行,他们看到的通知将与他们让计时器运行八小时时看到的通知相同。该行为允许您在创建预定通知时重用现有的getStillRunningToastContent
函数。然而,如果你现在运行 Clok 并启动计时器,当通知最终在预定时间出现时,就不太对了(见图 21-6 )。
图 21-6 。计时器运行八小时后出现的通知
注意为了更快的测试,我建议暂时将
notifyTime
变量改为(new Date()).addSeconds(20)
。当您完成这一部分的测试时,请确保将该值改回清单 21-4 中指定的值,以防止无休止的预定通知循环。
发生了什么事?安排通知在将来显示时,您必须在安排时创建通知的内容。如果我们保持getStillRunningToastContent
功能不变,即使计时器运行 8 小时后通知会正确显示,消息也会错误地显示运行时间减少。显示的确切值将取决于用户是否停止和恢复计时器。例如,如果他或她在中午停止计时器吃午饭,然后在午饭后恢复计时器,则消息可能指示计时器已经运行了四个多小时。要解决这个问题,需要做一些改变,以确定最终显示的消息应该是什么。我们必须添加将变量seconds
指定为getStillRunningToastContent
函数的参数的能力。用清单 21-6 中突出显示的代码更新home.js
中的getStillRunningToastContent
函数。
清单 21-6。 指定秒为参数
getStillRunningToastContent: function ( seconds ) {
seconds = seconds || elapsedTimeClock.winControl.timerValue;
// SNIPPED
},
如果没有为seconds
参数提供值,它将遵循您之前在清单 21-1 中添加的逻辑,使用定时器的当前值。因此,您不必对showLocalToast
函数做任何修改。然而,必须更新scheduleToast
函数以将该值传递给getStillRunningToastContent
函数。用清单 21-7 中突出显示的代码更新home.js
中的scheduleToast
函数。
清单 21-7。 更新到 scheduleToast 功能
scheduleToast: function () {
var reminderThreshold = 8; // hours
var toastContent = this.getStillRunningToastContent(reminderThreshold * 60 * 60);
// SNIPPED
},
现在,当显示预定通知时,它显示正确的信息(见图 21-7 )。
图 21-7 。计时器运行八小时后出现的正确通知
注意您可能希望添加一个很好的特性来改善用户体验,那就是在 Clok Options settings 弹出菜单中包含一个设置,允许用户为
reminderThreshold
变量提供一个值。
取消预定通知
您在上一节中所做的更改将安排在计时器运行八小时后出现通知。正确嗯,从技术上来说,它会安排在运行八小时时显示一个通知。我表述的方式略有不同,但这是一个重要的区别。如果用户将计时器停在 7 小时,现在会发生什么?如果他或她保存了时间条目会发生什么?如果他或她丢弃它会发生什么?
如果这一部分的标题还没有给出答案,就目前的情况来看,当计时器到达八小时时,通知仍然会显示。幸运的是,解决方法很简单。每当用户计时器停止时,任何预定的通知都应被取消。将清单 21-8 中高亮显示的代码添加到home.js
中的setupTimerRelatedControls
函数中。
清单 21-8。 定时器停止时取消预定通知
setupTimerRelatedControls: function () {
if (this.timerIsRunning) {
this.startTimer();
this.scheduleToast();
} else {
this.stopTimer();
this.unscheduleToast();
}
this.enableOrDisableButtons();
},
接下来,将清单 21-9 中定义的unscheduleToast
函数添加到home.js
中。
清单 21-9。 从日程中删除通知
unscheduleToast: function () {
var notifier = notificationManager.createToastNotifier();
var scheduled = notifier.getScheduledToastNotifications();
for (var i = 0, len = scheduled.length; i < len; i++) {
if (scheduled[i].id === "IsRunningToast") {
notifier.removeFromSchedule(scheduled[i]);
}
}
},
如您所料,getScheduledToastNotifications
函数获得了当前为您的应用安排的所有通知的列表。遍历它们,我已经识别出了与我们在清单 21-4 中设置的id
值相同的那个,并把它传递给removeFromSchedule
函数来取消它。现在,任何时候计时器停止,任何未来的通知都将被取消。
添加声音
在某些情况下,通知可能非常重要,足以抓住用户的注意力。假设您正在构建一个闹钟应用。如果用户睡着了,仅仅显示一个通知不足以引起他或她的注意。或者通知可能是针对用户已经指示的非常重要和及时的事情。例如,如果用户正在打电话而不是在使用计算机,您可以做些什么来提高用户看到通知的可能性?
在这种情况下,您可以考虑在通知中添加声音。将清单 21-10 中高亮显示的代码添加到home.js
中的getStillRunningToastContent
函数中。
清单 21-10。 包括带通知的音频
getStillRunningToastContent: function (seconds) {
seconds = seconds || elapsedTimeClock.winControl.timerValue;
if (elapsedTimeClock.winControl.isRunning && seconds > 0) {
// SNIPPED
// audio
var toastNode = toastContent.selectSingleNode("/toast");
toastNode.setAttribute("duration", "long");
var audio = toastContent.createElement("audio");
audio.setAttribute("src", "ms-winsoundevent:Notification.Looping.Call");
audio.setAttribute("loop", "true");
toastNode.appendChild(audio);
return toastContent;
}
},
这些更改会导致通知显示更长时间,并在显示时重复播放特定的声音(循环播放)。这是一个简单的改变,将对我们在 Clok 中创建的所有与定时器相关的通知生效。但是,要记住这个特性有一些限制。
您只能引用短列表中的声音。有五种非循环声音适用于简单的通知。事实上,根据你在电脑上的声音控制面板(见图 21-8 )中的设置,你可能已经听到了每个通知的默认声音。我一会儿会回到这一点。
图 21-8 。声音控制面板的声音选项卡
此外,还有 20 种更长的声音适合循环播放。这些声音适用于在聊天应用中接收来电的情况,类似于您的电话会响铃几次,让您有机会接听来电。这些循环声音,其中一个是我在清单 21-10 中使用的,不管声音控制面板中的设置如何,都将被播放,即使选择了无声音声音方案。
前五个非循环声音映射到图 21-8 中的程序事件列表中的特定项目。例如,Notification.Default
声音对应于声音控制面板中的通知程序事件。如果用户将他或她的声音方案更改为没有为通知事件指定声音的方案,如果您将Notification.Default
指定为要播放的声音,他或她将听不到任何声音。这最初给我造成了一段时间的困惑,因为我通常选择无声的声音方案。因此,当我指定一个循环声音时,我会听到声音,但当我指定一个非循环声音时,我听不到声音。
这又引出了另一点。我刚才提到,你可能一直听到每个通知的声音。默认情况下,除非另有说明,否则所有通知在显示时都会播放Notification.Default
声音。如果您在计算机上选择了一种声音方案,并且该方案为通知程序事件指定了一种声音,则本章中显示的每个通知都会播放该声音。如果需要,您可以显示没有声音的通知,而不管用户选择的声音方案。为此,不是设置 toast 的 XML 定义的audio
节点的src
属性,而是将其silent
属性设置为true
。有关可指定的各种声音的更多信息可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx
)上找到。
但是,在为通知设置声音时,以及在一般情况下使用通知时,都要小心。如果用户觉得你的应用太吵,他或她可能会对你的应用感到失望。如果您觉得应该在通知中包含声音,尤其是较长的循环声音,那么允许用户指定他或她想要听到的声音(如果有的话)可能是个好主意。
推送通知
到目前为止,我已经介绍了本地通知和预定通知。在这两种情况下,通知都是在应用运行时创建的。另一方面,推送通知是从另一个服务器创建和发送的。例如,如果您要扩展 Clok 以支持云中具有集中存储系统的多个用户,那么当另一个用户向项目的文档库添加文档时,您可以向一个用户发送推送通知。
正如我在第十四章中提到的,微软的 Windows Azure 移动服务提供了推送通知功能,此外还有许多其他功能,所有这些都在多个平台上得到支持。除了我在第十四章 ( www.windowsazure.com/en-us/develop/mobile
)中提到的 Windows Azure Mobile Services 开发中心之外,还有一个将来自 Azure Mobile Services 的推送通知集成到 Windows Store 应用中的示例应用(http://code.msdn.microsoft.com/windowsapps/Tile-Toast-and-Badge-Push-90ee6ff1
)。与我在整本书中提到的许多其他示例应用不同,这个示例项目在 Windows SDK 示例应用包中不可用,必须单独下载。
瓷砖
安装应用时,每个 Windows 应用商店应用都有一个添加到开始屏幕的磁贴。这是构建应用的一个要求。事实上,当你从第四章中讨论的任何 Visual Studio 项目模板中创建一个项目时,Visual Studio 会自动向你的项目添加一些默认图像,包括一个名为logo.png
的文件,它显示在你的应用的磁贴上(参见图 21-9 )。
图 21-9 。每个 Visual Studio 项目都包含默认磁贴徽标
在第二十三章中,我将向你展示如何更新你的应用的磁贴图像和颜色,以补充我们已经在应用中构建的内容。然而,在本章中,我将介绍为您的应用添加宽切片、实时切片和辅助切片。
宽瓷砖
顾名思义,宽瓷砖比标准方形瓷砖宽。在 Windows 8 中,标准磁贴尺寸为 150×150,宽磁贴尺寸为 310×150(参见图 21-10 )。
图 21-10 。天气应用的宽瓦
添加宽瓷砖是一个简单的过程。第一步是创建一个图像。对于 Clok,我复制了一份logo.png
文件,将其命名为widelogo.png
,并将其添加到 Clok Visual Studio 项目的images
文件夹中。然后我用一个图像编辑程序将图像文件的宽度增加到 310 像素。第二步,也是最后一步,是在package.appxmanifest
中引用该文件(见图 21-11 )。
图 21-11 。在应用清单中设置宽平铺
如果您在应用清单中指定了一个宽磁贴,当您的应用安装后,开始屏幕上显示的默认磁贴将是宽磁贴(参见图 21-12 )。用户将能够通过在开始屏幕上右键单击标准图块和宽图块,并从出现的应用栏中选择他或她想要的选项来在标准图块和宽图块之间切换。
图 21-12 。宽砖
实时瓷砖
任何使用 Windows 8 超过五分钟的人都熟悉实时磁贴,即使他们不熟悉这个术语。更新开始屏幕上的磁贴以显示相关信息的技术是操作系统的一个流行特性。在这一节中,我将向您展示如何在开始屏幕上为 Clok 创建一个动态磁贴。与本章前面添加的通知类似,如果 Clok 计时器正在运行,实时磁贴会向用户一目了然地显示出来。
平铺模板
在向您展示如何创建动态切片之前,我想快速讨论一下可用于创建动态切片的模板。正如我在本章前面介绍的 toast 通知一样,有许多预定义的模板可以用来创建动态磁贴。虽然有八个模板可用于通知,但有四十六个模板可用于实时平铺,十个用于标准方形平铺,三十六个用于宽平铺。这太多了,本书无法详细介绍。一般来说,它们可以分为以下几种类型的实时切片:
-
Text-only templates: Similar to notifications, these have several formats, each supporting a different number of text elements, such as the example in Figure 21-13. These are available for both standard and wide Live Tiles.
图 21-13 。纯文本宽模板
-
Image-only templates: As the name suggests, no text elements are specified as part of the template (see Figure 21-14). However, if you have a particular tile layout that cannot be achieved using any of the other templates, one option is to use an image-only template and specify an application-generated image as the content. These are available for both standard and wide Live Tiles.
图 21-14 。仅宽图像模板
-
Text-and-image templates: These combine images and text into a single Live Tile, as shown in Figure 21-15, and are available only for wide Live Tiles.
图 21-15 。宽文本和图像模板
-
Peek 图块模板 :这些是实时图块,可以在纯图像视图和类似于其他图块模板的视图之间来回“翻转”。图 21-16 显示了 peek 瓷砖的变化过程。
图 21-16 。变化时宽 peek 模板的进展
需要记住的一点是,只有当用户开始屏幕上的方块设置为标准方块大小时,才会使用方块活动方块模板。同样,只有当用户开始屏幕上的图块设置为宽尺寸时,才会使用宽实时图块模板。因此,为应用支持的每个切片大小指定模板是一个很好的做法。我将在下一节向您展示如何做到这一点。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761491.aspx
)上提供了这些类别中所有可用模板的完整列表。
创建实时图块
创建实时互动程序与创建通知非常相似。动态切片模板用于创建指定各种属性的 XML 文档。从上一节提到的模板列表中,我选择了一个标准的正方形模板(TileSquarePeekImageAndText04
)和一个宽模板(TileWidePeekImage06
)。在我向您展示如何实现这些之前,请将清单 21-11 中突出显示的别名添加到home.js
。
清单 21-11。 添加另一个别名
var appData = Windows.Storage.ApplicationData.current;
var localSettings = appData.localSettings;
var notifications = Windows.UI.Notifications;
var notificationManager = notifications.ToastNotificationManager;
var tileUpdateManager = notifications.TileUpdateManager;
var nav = WinJS.Navigation;
var storage = Clok.Data.Storage;
接下来,将清单 21-12 中定义的两个函数添加到home.js
中。
清单 21-12。 功能启用和禁用实时平铺
enableLiveTile: function () {
var tileContentString = "<tile>"
+ "<visual>"
+ "<binding template=\"TileSquarePeekImageAndText04\" branding=\"logo\">"
+ "<image id=\"1\" src=\"ms-appx:///img/Clock-Running.png\"/>"
+ "<text id=\"1\">Clok is running</text>"
+ "</binding> "
+ "<binding template=\"TileWidePeekImage06\" branding=\"none\">"
+ "<image id=\"1\" src=\"ms-appx:///img/widelogo.png\"/>"
+ "<image id=\"2\" src=\"ms-appx:///img/Clock-Running.png\"/>"
+ "<text id=\"1\">Clok is running</text>"
+ "</binding>"
+ "</visual>"
+ "</tile>";
var tileContentXml = new Windows.Data.Xml.Dom.XmlDocument();
tileContentXml.loadXml(tileContentString);
var tile = new notifications.TileNotification(tileContentXml);
tileUpdateManager.createTileUpdaterForApplication().update(tile);
},
disableLiveTile: function () {
tileUpdateManager.createTileUpdaterForApplication().clear();
},
注意在本章的前面,我展示了如何通过操作
XmlDocument
对象来创建通知。虽然这也是 Live Tiles 的一个选项,但是在本例中,我构建了一个包含必要 XML 的字符串。无论是创建通知还是动态磁贴,这两种技术都是允许的。
您会注意到,在enableLiveTile
函数中,我在同一个 XML 中指定了标准的方形实时图块和宽实时图块,每个图块都在同一个visual
元素中包含的不同的binding
元素中。当在enableLiveTile
函数的末尾调用update
函数时,同时指定这两个函数将更新图块的两个版本。如果用户已经将任一尺寸的磁贴固定到他或她的开始屏幕,则静态徽标图像将被适当的动态磁贴替换。唯一剩下的步骤是在计时器运行时启用实时图块,在计时器不运行时禁用它。用清单 21-13 中突出显示的代码更新home.js
中的setupTimerRelatedControls
函数。
清单 21-13。 定时器启动时启用实时磁贴,定时器停止时禁用
setupTimerRelatedControls: function () {
if (this.timerIsRunning) {
this.startTimer();
this.scheduleToast();
this.enableLiveTile();
} else {
this.stopTimer();
this.unscheduleToast();
this.disableLiveTile();
}
this.enableOrDisableButtons();
},
现在运行 Clok 并启动计时器。保持计时器运行,并切换到开始屏幕。根据您锁定的瓷砖尺寸,您会看到瓷砖被标准方形活动瓷砖(参见图 21-17 )或宽活动瓷砖(参见图 21-18 )所取代。
图 21-17 。克洛克的标准尺寸活瓷砖“偷看”
图 21-18 。克洛克的宽尺寸活瓷砖“偷看”
注意Windows 模拟器中不显示实时磁贴。为了测试该功能,在测试该功能之前,您必须将调试目标设置为本地机器或远程机器(参见图 21-3)。
次级瓦片
每个 Windows 应用商店应用都自动有一个可以固定在开始屏幕上的磁贴。单击时,它将启动应用并显示应用的默认屏幕。在上一节中,我向您展示了如何将您的应用的默认磁贴更改为动态磁贴,这可以为用户提供一目了然的有用信息。像默认磁贴一样,它也将显示应用的默认屏幕。
这一部分的主题,二级磁贴,也是类似的,它们都可以显示在用户的开始屏幕上。但是,它们是不同的,因为它们会导致应用在启动时加载不同的页面。您可以使用该功能在启动时自动加载应用的顶层部分,例如 Clok 中的时间表屏幕。您还可以使用它来显示更详细的屏幕,例如特定项目的项目详细信息屏幕。在这一节中,我们将在 Clok 中实现后一个功能,允许用户在开始屏幕上添加或删除第二个磁贴——通常称为固定和取消固定——这将允许他们只需单击一下就可以查看 Clok 中特定项目的详细信息。
向项目详细信息屏幕添加按钮
向开始屏幕添加或固定辅助磁贴需要用户的许可。您不能以编程方式将辅助磁贴添加到用户的开始屏幕,因为这是用户单击按钮的直接结果。相反,Windows 提供了一个请求用户许可的界面(见图 21-19 ),当你想要添加磁贴时,你必须显示这个界面,显示在弹出控件中。
图 21-19 。请求用户允许将磁贴固定到开始屏幕
在下一节中,我将向您展示如何显示弹出控件来请求用户添加图块的权限。在本节中,您将向项目详细信息屏幕上的应用栏添加一个新按钮。将来自清单 21-14 的代码添加到pages\projects
文件夹中detail.html
文件的打印按钮之前。
清单 21-14。 添加图钉按钮
<button
data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{
id:'pinUnpinCommand',
label:'Pin to Start',
icon:'pin',
section:'global',
tooltip:'Pin to Start',
disabled: true}">
</button>
用户将点击该按钮以将项目固定到他或她的开始屏幕上。如果项目已经固定,此按钮可用于从开始屏幕移除或取消固定。在接下来的几节中,我将向您展示如何重新使用这个按钮来处理锁定和取消锁定任务。在此之前,还有几个剩余的任务需要完成,到本书的这一点应该已经很熟悉了。在detail.js
的ready
函数中,为这个按钮添加一个click
事件处理程序到一个名为pinUnpinCommand_click
的函数中,这个函数将在下一节中添加。另外,不要忘记更新configureAppBar
功能,以便在查看现有项目的详细信息时启用该按钮。
固定辅助单幅图块
在这一节中,我将介绍将辅助磁贴添加到用户开始屏幕所需的几个步骤。首先将清单 21-15 中的的高亮别名添加到detail.js
。
清单 21-15。 添加别名
var app = WinJS.Application;
var startScreen = Windows.UI.StartScreen;
var secondaryTile = startScreen.SecondaryTile;
var data = Clok.Data;
var storage = Clok.Data.Storage;
当创建第二个图块时,构造函数要求您提供一个tileId
参数。该值是最多 64 个字符的字符串,可以包括字母、数字、句点或下划线字符。您将使用该字符串在整个应用中标识图块。在这一章的后面,我将向你展示如何使用这个值智能地处理来自二级磁贴的应用激活。您还将使用该值来允许用户取消固定磁贴。将清单 21-16 中突出显示的代码添加到detail.js
中的ready
函数中。
清单 21-16。 为图块设置一个 id
ready: function (element, options) {
// SNIPPED
this.setCurrentProject(options);
this.secondaryTileId = "Tile.Project." + this.currProject.id.replace(/-/g, ".");
// SNIPPED
},
使用该值的另一种情况是当使用 WinRT 库来确定用户的开始屏幕上是否已经存在该磁贴时。将清单 21-17 中的 app bar 按钮click
事件处理程序添加到detail.js
中。
清单 21-17。 处理点击事件
pinUnpinCommand_click: function (e) {
if (!secondaryTile.exists(this.secondaryTileId)) {
this.pinToStart();
}
},
如果图块不存在,则调用pinToStart
函数请求用户允许添加图块。将清单 21-18 中的pinToStart
函数添加到detail.js
中。
清单 21-18。 请求允许将磁贴添加到开始屏幕
pinToStart: function () {
// build the tile that will be added to the Start screen
var uriLogo = new Windows.Foundation.Uri("ms-appx:///img/Projects.png");
var displayName = this.currProject.name + " (" + this.currProject.clientName + ")";
var tile = new secondaryTile(
this.secondaryTileId,
displayName,
displayName,
this.currProject.id,
startScreen.TileOptions.showNameOnLogo,
uriLogo);
tile.foregroundText = startScreen.ForegroundText.light;
// determine where to display the request to the user
var buttonRect = pinUnpinCommand.getBoundingClientRect();
var buttonCoordinates = {
x: buttonRect.left,
y: buttonRect.top,
width: buttonRect.width,
height: buttonRect.height
};
var placement = Windows.UI.Popups.Placement.above;
// make the request and update the app bar
tile.requestCreateForSelectionAsync(buttonCoordinates, placement)
.done(function (isCreated) {
// TODO
}.bind(this));
},
pinToStart
函数中的前几行创建了用户将被要求添加到他或她的开始屏幕上的磁贴。我指定了显示在 Clok Dashboard 屏幕上的相同项目图标作为此标题上的图标。用户可以更改的名称将有一个由项目和客户名称组成的缺省值,指定showNameOnLogo
将使这个名称显示在图标下面。除了为tileId
参数提供secondaryTileId
,我还提供了当前项目的id
属性作为arguments
参数。在下一节中,我将向您展示如何在 Clok 的激活过程中使用arguments
参数。
pinToStart
函数的下几行用于确定请求用户许可的弹出按钮将显示在哪里。建议的做法是将弹出按钮的位置基于打开弹出按钮的应用栏按钮的位置。这可以防止不必要的鼠标移动,并使这个过程对用户来说更加自然。pinToStart
函数中的最后一段代码请求用户的许可并添加图块。这是异步发生的,我们将向done
函数添加更多的代码,以便在过程完成时更新应用栏。
运行 Clok 并导航到现有项目的项目详细信息屏幕。单击“锁定以启动应用栏”按钮,然后在打开的弹出按钮中单击“锁定以启动”按钮(参见前面的图 21-19 )。几秒钟后,一个新的磁贴会出现在你的开始屏幕上(参见图 21-20 )。新的磁贴会添加到开始屏幕的末尾,因此您可能需要滚动才能看到磁贴。
图 21-20 。用户添加到他或她的开始屏幕上的第二块磁贴
您并不局限于单个副牌。您的用户可以为他们经常使用的项目添加图块。此外,除了制作静态的次级磁贴,正如我们在本节中所做的,您还可以为您的次级磁贴创建动态磁贴。为辅助切片创建实时切片的过程与为应用本身创建实时切片的过程几乎相同。不像在清单 21-12 中那样调用createTileUpdaterForApplication
函数,而是调用createTileUpdaterForSecondaryTile
函数,将secondaryTileId
作为参数传递,类似于清单 21-19 中的代码。
清单 21-19。 为辅助图块创建活动图块
var secondaryTile = new notifications.TileNotification(secondaryTileXml);
tileUpdateManager.createTileUpdaterForApplication(secondaryTileId).update(secondaryTile);
从辅助图块激活时钟
此时,您可以在开始屏幕上添加辅助磁贴,但是如果您单击此磁贴,您会发现自己回到了 Clok 仪表板上,而不是查看特定项目的项目详细信息屏幕。向default.js
中的launchActivation
添加几行将纠正这一问题。将清单 21-20 中突出显示的代码添加到default.js
中。
清单 21-20。 更新了 launchActivation 功能
if (args.detail.kind === activation.ActivationKind.search) {
// SNIPPED
} else if ((args.detail.tileId.indexOf("Tile.Project.") >= 0)
&& (ClokUtilities.Guid.isGuid(args.detail.arguments))) {
nav.navigate("/pages/projects/detail.html", { id: args.detail.arguments });
} else if (nav.location) {
nav.history.current.initialPlaceholder = true;
return nav.navigate(nav.location, nav.state);
} else {
return nav.navigate(Application.navigator.home);
}
在args.detail.tileId
属性中可以找到被点击的辅助磁贴的tileId
属性。尽管在 Clok 中,我们只添加了一种类型的辅助磁贴,我还是决定使用tileId
属性来决定如何处理辅助磁贴的激活。如果tileId
属性的值是正确的格式,并且如果arguments
属性的值是 GUID,那么应用导航到所选项目的项目细节屏幕。
注意当您的应用启动时,您也可以通过点击一个 toast 通知来使用
arguments
属性。在这种情况下,arguments
属性的值是在通知的 XML 定义的launch
属性中设置的。
取消固定辅助单幅图块
假设一个用户将一个项目的辅助块钉在他或她的开始屏幕上。当项目完成时,他或她可能想要从开始屏幕上移除该图块。当然,这可以直接在开始屏幕上完成,方法是右键单击磁贴并从出现的应用栏中选择 Unpin from Start。在这一节中,我将向您展示允许用户从 Clok 中移除磁贴的步骤。
完成此任务的第一步是更新应用栏中的 Pin to Start 按钮。将清单 21-21 中定义的函数添加到detail.js
。
清单 21-21。 改变应用栏按钮
updatePinUnpinCommand: function () {
if (secondaryTile.exists(this.secondaryTileId)) {
pinUnpinCommand.winControl.icon = "unpin";
pinUnpinCommand.winControl.label = "Unpin from Start";
pinUnpinCommand.winControl.tooltip = "Unpin from Start";
} else {
pinUnpinCommand.winControl.icon = "pin";
pinUnpinCommand.winControl.label = "Pin to Start";
pinUnpinCommand.winControl.tooltip = "Pin to Start";
}
},
该函数被调用时,将更新应用栏中的pinUnpinCommand
按钮。如果图块已经存在,按钮将被更改以指示单击时将取消固定图块。如果图块不存在,按钮将被更新,指示单击它会将图块添加到开始屏幕。现在,我们必须从几个不同的位置调用这个函数。首先,用来自清单 21-22 的高亮代码更新detail.js
中的ready
函数。
清单 21-22。 调用函数更新 App 栏按钮
ready: function (element, options) {
// SNIPPED
this.setCurrentProject(options);
this.secondaryTileId = "Tile.Project." + this.currProject.id.replace(/-/g, ".");
this.updatePinUnpinCommand();
// SNIPPED
},
在清单 21-18 中的函数中,还必须从添加到pinToStart
函数中的done
函数中调用updatePinUnpinCommand
函数。将清单 21-23 中突出显示的代码添加到detail.js
中的pinToStart
函数中。
清单 21-23。 添加磁贴后调用函数更新应用栏按钮
// make the request and update the app bar
tile.requestCreateForSelectionAsync(buttonCoordinates, placement)
.done(function (isCreated) {
this.updatePinUnpinCommand();
}.bind(this));
现在,当一个项目有一个二级磁贴固定在开始屏幕上时,应用栏图标会变成从开始处取消固定。现在单击该按钮实际上不会做任何事情,因为click
事件处理程序只在图块不存在时做一些事情。通过将清单 21-24 中突出显示的代码添加到pinUnpinCommand_click
处理函数中来改变这一点。
清单 21-24。 如果图块已经存在,则执行不同的操作
pinUnpinCommand_click: function (e) {
if (!secondaryTile.exists(this.secondaryTileId)) {
this.pinToStart();
} else {
this.unpinFromStart();
}
},
与请求向开始屏幕添加新互动程序的权限一样,您还必须请求从开始屏幕移除现有辅助互动程序的权限。unpinFromStart
函数为你处理这个问题(参见清单 21-25 )。该功能与pinToStart
功能有相似之处。它确定在哪里显示请求,然后异步请求用户删除选定的图块。如果成功,调用updatePinUnpinCommand
函数将从开始应用栏按钮切换回开始按钮。
清单 21-25。 请求用户允许从开始屏幕中移除次级磁贴
unpinFromStart: function () {
var buttonRect = pinUnpinCommand.getBoundingClientRect();
var buttonCoordinates = {
x: buttonRect.left,
y: buttonRect.top,
width: buttonRect.width,
height: buttonRect.height
};
var placement = Windows.UI.Popups.Placement.above;
var tile = new secondaryTile(this.secondaryTileId);
tile.requestDeleteForSelectionAsync(buttonCoordinates, placement)
.done(function (success) {
this.updatePinUnpinCommand();
}.bind(this));
},
现在运行 Clok 并导航到一个项目的项目详细信息屏幕,该项目在开始屏幕上有一个辅助图块。应用栏中的按钮将更改为“从开始处取消固定”。点击此按钮将在弹出控件中显示删除图块的请求(参见图 21-21 )。单击弹出按钮中的“从开始位置取消固定”按钮将从开始屏幕中移除磁贴,并将应用栏按钮切换回默认状态。
图 21-21 。请求用户允许从开始屏幕中删除一个互动程序
Windows 的未来版本
在撰写本文时,Windows 下一版本的预览版已经发布。Windows 8.1 将推出两种新的磁贴尺寸,大(310×310)和小(70×70)。此外,将添加新的动态切片模板以支持新的大切片尺寸(小切片将不支持动态切片)。因此,模板的名称将会改变。本章中使用的名称,例如TileSquarePeekImageAndText04
,对于 Windows 8 应用仍然是必需的,并且在 Windows 8.1 应用中也将受到支持,但在 Windows 的未来版本中可能会被删除。
作为即将到来的变化的一个例子,在 Windows 8.1 发布后的任何新开发中,当前命名为TileSquarePeekImageAndText04
的磁贴模板应该被称为TileSquare150x150PeekImageAndText04
。这些以及其他即将到来的变化在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/bg182890.aspx
)上都有描述。
结论
随着用户对 Windows 8 越来越熟悉,他们会希望能够快速、轻松地访问信息。通过本章中介绍的简单步骤,您可以在通知和动态磁贴中提供即时信息,并使用辅助磁贴快速访问应用中常用的信息。并不是每个应用都是这些技术的完美候选,不必要的添加这些特性会让你的应用看起来嘈杂或者繁忙。但是,经过仔细考虑,您可以构建一个不仅能满足用户基本需求,还能改善用户体验的应用。`
二十二、摄像机和位置
在撰写本文时,几乎所有的平板电脑和笔记本电脑都内置了网络摄像头。我查看了今天在一个流行的零售网站上列出的前 15 台笔记本电脑,每一台都包括一个网络摄像头。我自己的笔记本电脑有一个内置在盖子里,我的 Surface 平板电脑有两个——一个前置摄像头和一个后置摄像头。对于计算机没有内置网络摄像头的用户来说,USB 网络摄像头是一项相当便宜的投资,一些基本型号的价格不到 20 美元。
在以前的 Windows 版本中,将相机集成到应用中并不总是一个简单的解决方案。然而,在 Windows 8 中,微软创造了一种简单、直接的方式来为您的应用添加照片功能。
除了集成摄像头之外,通过利用来自 Windows 位置提供商的数据,您可以让您的应用具有位置感知能力。WinRT 使用简单的 API 公开位置数据,允许您的应用请求计算机的当前位置或处理事件以在计算机移动时接收更新。
在这一章中,我将向你展示如何将相机和位置数据集成到 Clok 中。Clok 用户将能够部署相机,将照片添加到项目的文档库。他们还将能够获得从当前位置到客户办公室的行车路线。
照相机
你参加过有人在白板上写字的会议吗?也许他们在绘制应用的用户界面,或者绘制流程图,或者只是做笔记。无论如何,当会议结束后,在另一群人进入房间参加他们自己的会议之前,可能会发生一些事情。
- 有人匆忙地试图将所有的笔记记录到纸上或他或她的电脑上的一个文档中。
- 有人在白板上用大字写下“保存”,打算在将来的某个时候重新查看笔记或图表。
- 有人掏出手机,给白板拍了张照片。
我个人用手机拍了很多白板的照片。在本节中,您将向 Clok 添加一个功能,该功能将允许用户使用平板电脑上的相机来捕捉这样的照片,并将它们添加到项目的文档库中。
注在本章中,我将使用术语网络摄像头、摄像头、照片和视频。在这些例子中,我指的是连接到用户电脑的网络摄像头捕捉到的图像或视频。您将在本章中添加到 Clok 的功能将只对拥有内置网络摄像头或 USB 网络摄像头的用户可用。此外,您的计算机必须有网络摄像头,以便测试您将在此部分添加的功能。在这本书里,我不会讨论使用数码相机,比如傻瓜相机或者 DSLR 相机。
虽然已经可以使用相机应用拍摄照片,然后使用 Share charm 将其添加到文档库中,但您在本节中所做的更改将允许您从 Clok 应用中捕捉照片,而不必单独启动相机应用。在应用中使用网络摄像头有两种方法。你可以使用CameraCaptureUI
类或者MediaCapture
类。
CameraCaptureUI 类
CameraCaptureUI
类允许你用相对较少的代码行,快速地为你的应用添加相机功能。摄像头捕捉功能由内置的 Windows 界面处理,在概念上与文件打开选择器非常相似。在这一节中,我将解释如何向您的用户展示这个界面,以及如何将他们拍摄的照片放入文档库中。
应用清单更改
访问连接到用户计算机的 webam 是一个潜在的安全问题。因此,您必须在项目的应用清单中指明它可能会使用用户的网络摄像头。当一个潜在用户在 Windows Store 中阅读你的应用时,这个事实就被公之于众了(见图 22-1 )。
图 22-1 。Windows 应用商店中的相机应用列表
对清单的更改是一个简单的复选框。打开package.appxmanifest
并切换到功能选项卡。在功能列表中,勾选网络摄像头项目(参见图 22-2 )。
图 22-2 。指定网络摄像头功能
然而,即使在应用清单中有这个声明,您的应用也不能完全开放对摄像机的访问。你的应用第一次试图访问摄像机时——你将在接下来的章节中添加代码来完成——用户被提示确认他或她将允许它(见图 22-3 )。
图 22-3 。提示用户获得许可
此外,用户可以随时更改此设置,方法是打开权限设置弹出按钮并切换网络摄像头设置的值(参见图 22-4 )。
图 22-4 。用户可以随时撤销该权限
当您使用CameraCaptureUI
类时,窗口将显示的界面将提示用户更改权限,如果他或她以前阻止了对摄像机的访问(见图 22-5 )。
图 22-5 。CameraCaptureUI 界面指示用户启用权限来使用相机
更新 Clok 仪表板屏幕上的摄像头按钮
指定了摄像头功能,下一步是给用户一个访问摄像头屏幕的方法,我们将在下一节中构建。Clok 仪表盘屏幕上的相机菜单选项已经存在,但目前尚未实现。从home.html
中移除清单 22-1 中高亮显示的notImplemented
CSS 类。
清单 22-1。 移除高亮显示的 CSS 类
<div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem notImplemented ">
您还必须在home.js
中为该菜单选项定义一个click
事件处理程序。将清单 22-2 中的代码添加到home.js
中,并在ready
函数中绑定这个click
事件处理程序。
清单 22-2。 点击事件处理程序获取相机菜单选项
cameraMenuItem_click: function (e) {
nav.navigate("/pages/documents/cameraCapture.html");
},
在本书附带的源代码中,我还为用户添加了右击相机菜单选项并将其固定在开始屏幕上的功能(见图 22-6 )。因为我刚刚在第二十一章的中谈到了这一点,所以这里就不赘述了。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790
)。
图 22-6 。将 Clok 相机固定在开始屏幕上
添加相机页面控件
虽然CameraCaptureUI
类显示的用户界面是由 Windows 自己提供的,我们无法控制它提供的布局和功能,但仍然需要一个页面来允许用户预览捕获的图像并选择要添加到的文档库。在pages\documents
文件夹中,创建一个名为cameraCapture.html
的新页面控件。更新cameraCapture.html
中的标题,将其改为Camera
(见清单 22-3 )。
清单 22-3。 改变屏幕标题
<span class="pagetitle">Camera</span>
当你完成建立这个屏幕时,它会在屏幕的左边显示一个捕获的照片的预览,在右边,一个下拉列表选择照片将被添加到哪个项目的文档库。用清单 22-4 中的代码更新cameraCapture.html
中的主section
。如果用户的计算机没有摄像头,则会显示一条消息。
清单 22-4。 摄像头屏幕的布局
<section aria-label="Main content" role="main">
<div id="cameraContainer">
<div id="cameraPane">
<img id="capturedImage" src="/img/camera-placeholder.png" />
</div>
<div id="controlsPane">
<select id="projects">
<option value="">Choose a project</option>
</select>
<button id="goToDocumentsButton" disabled="disabled"></button>
<br />
<button id="saveCameraCaptureButton">Save</button>
<button id="discardCameraCaptureButton">Discard</button>
</div>
</div>
<div id="noCamera" class="hidden">No camera is available on this computer.</div>
</section>
和往常一样,参考图片可以作为本书附带的源代码的一部分获得。用清单 22-5 中的 CSS 更新cameraCapture.css
。
清单 22-5。 造型相机屏幕
.cameraCapture section[role=main] {
margin-left: 120px;
margin-right: 120px;
}
.hidden {
display: none;
}
.cameraCapture #cameraPane {
float: left;
width: 720px;
height: 540px;
}
.cameraCapture #cameraPane #capturedImage {
max-width: 720px;
max-height: 540px;
}
.cameraCapture #controlsPane {
float: left;
margin-left: 10px;
}
.cameraCapture #controlsPane #goToDocumentsButton {
border: 0px;
min-width: inherit;
font-size: 1.5em;
}
@media screen and (-ms-view-state: fullscreen-portrait) {
.cameraCapture section[role=main] {
margin-left: 20px;
margin-right: 20px;
}
}
在接下来的几节中,您将利用cameraCapture.js
中的一些别名。与其一次添加一个,不如现在全部添加。将清单 22-6 中的代码添加到cameraCapture.js
中。
清单 22-6。 添加一些别名
var appData = Windows.Storage.ApplicationData.current;
var createOption = Windows.Storage.CreationCollisionOption;
var capture = Windows.Media.Capture;
var devices = Windows.Devices.Enumeration;
var nav = WinJS.Navigation;
var storage = Clok.Data.Storage;
当应用处于快照状态时,Clok 的相机屏幕将不会启用。在第十七章中,你增加了一个名为DisableInSnappedView
的函数,应该是从ready
和updateLayout
函数中调用的。将清单 22-7 中的代码从添加到到cameraCapture.js
。
清单 22-7。 禁用抓拍视图中的相机屏幕
ready: function (element, options) {
Clok.Utilities.DisableInSnappedView();
},
updateLayout: function (element, viewState, lastViewState) {
Clok.Utilities.DisableInSnappedView();
},
确定摄像机是否存在
尽管笔记本电脑和平板电脑内置摄像头越来越普遍,但很可能有些用户的电脑没有摄像头。在本节中,您将确定摄像机是否可用,并根据摄像机的存在初始化 Clok 的摄像机屏幕。
Windows.Devices.Enumeration
名称空间定义了许多类,您可以利用这些类为您的应用添加对各种类型设备的支持。这包括外部存储设备、音频输入和输出设备以及视频输入设备。视频设备是可以捕捉照片或视频的工具,如网络摄像头。关于这个名称空间及其包含的所有类的更多信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.enumeration.aspx
)上找到。在这一节中,我们将利用Windows.Devices.Enumeration.DeviceInformation
类来确定计算机是否有摄像头。将清单 22-8 中突出显示的代码添加到cameraCapture.js
中的ready
函数中。
清单 22-8。 确定摄像头是否存在并初始化屏幕
ready: function (element, options) {
this.file = null;
var deviceInfo = devices.DeviceInformation;
return deviceInfo.findAllAsync(devices.DeviceClass.videoCapture)
.then(function (found) {
return found && found.length && found.length > 0;
}, function error() {
return false;
}).then(function (exists) {
this.showCameraControls(exists);
if (exists) {
this.bindProjects();
capturedImage.addEventListener("click", this.capturedImage_click.bind(this));
saveCameraCaptureButton.addEventListener("click",
this.saveCameraCaptureButton_click.bind(this));
discardCameraCaptureButton.addEventListener("click",
this.discardCameraCaptureButton_click.bind(this));
goToDocumentsButton.onclick = this.goToDocumentsButton_click.bind(this);
projects.addEventListener("change", this.projects_change.bind(this));
this.resetScreen();
// TODO: automatically initiate capture
}
}.bind(this));
Clok.Utilities.DisableInSnappedView();
},
这里,我使用了findAllAsync
函数来枚举用户计算机上的所有视频捕获设备(网络摄像头)。如果有任何错误,或者没有发现错误,我稍后将展示的showCameraControls
函数将向用户显示一条消息。然而,如果发现网络摄像头,showCameraControls
功能将显示在清单 22-4 中定义的用户界面,一个活动项目的下拉列表将被填充,各种事件处理程序将被连接到它们各自的控件。这些步骤都是您通常会直接包含在ready
函数中的典型步骤。因为这个屏幕只有在摄像机存在时才可用,所以我简单地将它们嵌套在一个条件中。在ready
函数顶部的file
变量将被用来保存对一个StorageFile
对象的引用,该对象最终将从CameraCaptureUI
接口返回。我稍后将回到这一点。
注意在 Clok 中,我们已经为用户提供了向文档库中添加文档(包括图像)的其他技术。在其他应用中,如果网络摄像头不可用,您可能需要考虑提供处理图像的替代方法,如文件打开选择器。
清单 22-8 中的代码引用了一些函数和事件处理程序。通过添加清单 22-9 中到cameraCapture.js
的代码来定义这些。
清单 22-9。 摄像机屏幕的功能和事件处理程序
showCameraControls: function (show) {
if (show) {
WinJS.Utilities.removeClass(cameraContainer, "hidden");
WinJS.Utilities.addClass(noCamera, "hidden");
} else {
WinJS.Utilities.addClass(cameraContainer, "hidden");
WinJS.Utilities.removeClass(noCamera, "hidden");
}
},
bindProjects: function () {
projects.options.length = 1; // remove all except first project
var activeProjects = storage.projects.filter(function (p) {
return p.status === Clok.Data.ProjectStatuses.Active;
});
activeProjects.forEach(function (item) {
var option = document.createElement("option");
option.text = item.name + " (" + item.projectNumber + ")";
option.title = item.clientName;
option.value = item.id;
projects.appendChild(option);
});
},
projects_change: function (e) {
if (!this.file) {
saveCameraCaptureButton.disabled = true;
} else {
saveCameraCaptureButton.disabled = !projects.options[projects.selectedIndex].value;
}
var id = projects.options[projects.selectedIndex].value;
goToDocumentsButton.disabled = !ClokUtilities.Guid.isGuid(id);
},
goToDocumentsButton_click: function (e) {
var id = projects.options[projects.selectedIndex].value;
if (ClokUtilities.Guid.isGuid(id)) {
nav.navigate("/pages/documents/library.html", { projectId: id });
}
},
discardCameraCaptureButton_click: function (e) {
this.resetScreen();
},
resetScreen: function () {
capturedImage.src = "/img/camera-placeholder.png";
this.file = null;
saveCameraCaptureButton.disabled = true;
discardCameraCaptureButton.disabled = true;
},
清单 22-9 中的大部分代码类似于你已经添加到 Clok 中的代码。我特别强调了projects_change
事件处理程序中的一个模块,这个模块可能不会立即显示出来。仅当从下拉列表中选择了一个项目并且图像已经被CameraCaptureUI
接口捕获时,该块才启用保存按钮。
此外,我突出显示了resetScreen
函数。顾名思义,该功能会将相机屏幕置于准备捕捉图像的状态。除了在第一次加载屏幕和用户丢弃照片时调用该函数之外,在下一节中,您将看到在用户捕获并保存照片后如何调用该函数。
使用 CameraCaptureUI 界面捕捉照片
要完成这一功能,还需要几个步骤。您仍然需要向用户显示CameraCaptureUI
界面,检索捕获的图像并显示它,以便用户可以预览它,并实现将图像保存到项目文档库中的功能。通过添加清单 22-10 到cameraCapture.js
中的代码,可以完成这三个剩余步骤中的前两个。
清单 22-10。 显示 CameraCaptureUI 界面
capturedImage_click: function (e) {
this.showCameraCaptureUI();
},
showCameraCaptureUI: function () {
var dialog = new capture.CameraCaptureUI();
dialog.photoSettings.maxResolution =
capture.CameraCaptureUIMaxPhotoResolution.highestAvailable;
dialog.captureFileAsync(capture.CameraCaptureUIMode.photo)
.done(function complete(file) {
if (file) {
var photoBlobUrl = URL.createObjectURL(file, { oneTimeOnly: true });
capturedImage.src = photoBlobUrl;
saveCameraCaptureButton.disabled =
!projects.options[projects.selectedIndex].value;
discardCameraCaptureButton.disabled = false;
this.file = file;
} else {
this.resetScreen();
}
}.bind(this), function error(err) {
this.resetScreen();
}.bind(this));
},
当用户点击预览图像时,将显示CameraCaptureUI
界面。与FileOpenPicker
类似,CameraCaptureUI
界面是一个由 Windows 创建和管理的对话框。使用 promises,所选择的StorageFile
,这次包含照相机刚刚捕获的照片,可作为done
函数的complete
参数的一个参数。在清单 22-10 中,我已经从该文件创建了一个对象 URL,并将预览图像的源设置为该 URL。如果已经从下拉列表中选择了一个项目,保存按钮被激活,并且文件的引用被存储在清单 22-8 中定义的file
变量中。如果有任何错误,或者没有拍摄照片,则调用resetScreen
函数将屏幕上的每个控件重置为其原始状态。
用户可以很容易地单击预览图像占位符来启动照片捕捉过程。然而,如果他们只是点击了 Clok 仪表板上的相机菜单选项,或者他们可能已经钉在开始屏幕上的次级磁贴,我们可以立即向他们显示CameraCaptureUI
界面,而不需要他们再次点击。清单 22-8 包含一个 TODO 注释。用清单 22-11 中突出显示的代码替换cameraCapture.js
的ready
函数中的注释。
清单 22-11。 自动显示 CameraCaptureUI 界面
if (exists) {
// SNIPPED
// only if navigated, not if back arrow
if (!nav.canGoForward) {
this.showCameraCaptureUI();
}
}
注意当我最初编写这个功能时,我没有在条件中包含对
showCameraCapture
的调用。然而,在点击清单 22-4 中添加的goToDocumentsButton
,然后点击每个页面控件上包含的后退箭头之后,在那种情况下加载CameraCaptureUI
界面对我来说似乎很尴尬。通过检查WinJS.Navigation.canGoForward
属性的值,您可以确定是否加载了摄像头屏幕,这是单击返回箭头的结果。
这个难题的最后一部分是实现将照片保存到所选项目的文档库中的功能。将清单 22-12 中的代码添加到cameraCapture.js
中。
清单 22-12。 保存照片到文档库
getProjectFolder: function (projectId) {
return appData.localFolder
.createFolderAsync("projectDocs", createOption.openIfExists)
.then(function (folder) {
return folder.createFolderAsync(projectId.toString(), createOption.openIfExists)
});
},
saveCameraCaptureButton_click: function (e) {
var dateFormatString = "{year.full}{month.integer(2)}{day.integer(2)}"
+ "-{hour.integer(2)}{minute.integer(2)}{second.integer(2)}";
var clockIdentifiers = Windows.Globalization.ClockIdentifiers;
var formatting = Windows.Globalization.DateTimeFormatting;
var formatterTemplate = new formatting.DateTimeFormatter(dateFormatString);
var formatter = new formatting.DateTimeFormatter(formatterTemplate.patterns[0],
formatterTemplate.languages,
formatterTemplate.geographicRegion,
formatterTemplate.calendar,
clockIdentifiers.twentyFourHour);
var filename = formatter.format(new Date()) + ".png";
var projectId = projects.options[projects.selectedIndex].value;
this.getProjectFolder(projectId)
.then(function (projectFolder) {
return this.file.copyAsync(projectFolder,
filename,
createOption.generateUniqueName);
}.bind(this)).then(function (file) {
this.resetScreen();
}.bind(this));
},
从您在第十六章的中创建文档库的工作中,以及从您在第十九章的中添加的使 Clok 成为共享目标的代码中,您应该熟悉getProjectFolder
函数。在根据当前日期和时间为filename
变量生成一个值后,由file
变量引用的照片,使用第十六章中介绍的copyAsync
函数被复制到项目的文档库中。然后调用resetScreen
功能,允许用户快速拍摄另一张照片。
现在运行 Clok 并点击 Clok 仪表盘屏幕上的相机菜单选项。如果你有一台相机并且已经授权 Clok 使用它,你会看到CameraCaptureUI
界面(见图 22-7 )。
图 22-7 。即将捕获正在进行的一些工作的照片
拍照后,您将看到该照片的预览,并能够选择将其保存到哪个项目的文档库(参见图 22-8 )。
图 22-8 。预览捕获的照片
最后,为了验证一切都按预期工作,导航到该文档库。您可以选择使用相机屏幕上的回形针图标作为快捷方式。您应该可以在文档库中看到您的照片(参见图 22-9 )。
图 22-9 。捕获的照片,保存到文档库中
在本例的大部分时间里,我假设用户是另一个软件开发人员,他/她正在用自己的平板电脑运行 Clok,参加一个关于他/她正在进行的项目的会议。实际上,这个功能可以很容易地被平面设计师用来拍摄可能启发他或她的项目设计的照片,或者甚至被景观承包商用来拍摄景观项目前后的照片。用户甚至可以是捕捉他或她当前正在进行的工作的照片的作者。
媒体捕获类概述
CameraCaptureUI
类提供了一个简单的方法来将一个附加的摄像机集成到你的应用中。然而,这个界面是由 Windows 创建和管理的,所以您对它没有任何控制权。如果你对图 22-7 (前面)中显示的全屏捕捉体验不满意,那么你就不走运了。
不完全是。如果您需要对照片捕捉体验有更多的控制,那么您应该使用MediaCapture
类,而不是使用CameraCaptureUI
类。通过这个类,您可以将实时视频预览和图像捕捉功能嵌入到您自己的用户界面中。
我不会在本书中实现这个类,但是我会在这里简单讨论一下。在 Windows SDK 示例包(http://msdn.microsoft.com/en-US/windows/apps/br229516
)包含的示例中,有一个叫做使用捕获设备进行媒体捕获的示例。这个项目展示了几个将相机集成到应用用户界面中的例子。看一看BasicCapture.html
。在该文件中,您将看到一个如清单 22-13 中的所示的video
元素。
清单 22-13。 来自 MediaCapture 示例项目的视频元素
<video width="320" height="240" id="previewVideo1" style="border: 1px solid black"> </video>
这是一个标准的 HTML5 video
元素。在关联的 JavaScript 文件BasicCapture.js
中,创建了一个名为mediaCaptureMgr
的MediaCapture
对象,并在该对象上设置了许多属性。一旦它被配置,来自清单 22-13 的video
元素的src
属性被设置,引用mediaCaptureMgr
对象(见清单 22-14 )。
清单 22-14。 设置视频源
video.src = URL.createObjectURL(mediaCaptureMgr, { oneTimeOnly: true });
为了从这个mediaCaptureMgr
对象中捕获图像,调用了capturePhotoToStorageFileAsync
函数。类似于清单 22-10 中的captureFileAsync
函数,代表捕获图像的StorageFile
对象作为done
函数的complete
参数的一个参数。
我在本节中排除了许多细节,但是引用的示例相当简单。如果将相机捕捉功能集成到您自己的应用的 UI 中的能力很重要,那么我鼓励您仔细看看示例项目。此外,关于MediaCapture
类的更多信息可以在 MSDN ( http://msdn.microsoft.com/library/windows/apps/Windows.Media.Capture.MediaCapture.aspx
)上找到。
位置
在 Windows 8 中,用户电脑的位置由 Windows 位置提供商提供。位置提供者可以使用多种不同的方法来确定计算机的位置,该位置由纬度和经度测量值以及这些测量值的准确性指示符来指示。Windows 8 中的提供商使用以下技术尝试确定位置,从最不准确到最准确:
- IP 地址数据
- WiFi 三角剖分〔??〕〔??〕
- 全球定位系统
提供商将报告可用的最精确的测量值。例如,如果 GPS 设备存在于计算机中,那么将返回这些结果,而不是 IP 地址查找的结果。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh464919.aspx
)上有更多关于 Windows 位置提供商的信息。
在这一节中,我将展示如何为用户提供一种从他们当前位置获取驾驶方向的简单方法。
应用清单更改
与相机一样,访问用户的位置是一个潜在的安全问题。您必须在项目的应用清单中指示您打算访问用户的位置。与相机一样,对清单的更改也是一个简单的复选框。打开package.appxmanifest
并切换到功能选项卡。在功能列表中,检查位置项(参见图 22-10 )。
图 22-10 。指定定位能力
更新方向屏幕
实现这一功能只需很少的改动。第一步是在方向屏幕上添加一个按钮,用于确定用户的当前位置。将清单 22-15 中突出显示的代码添加到directions.html
中。
清单 22-15。 添加按钮获取当前位置
<div class="formField">
<label for="fromLocation">From</label><br />
<input id="fromLocation">
<button id="getLocationButton"> & #xe1d2;</button>
</div>
然后将清单 22-16 中的新 CSS 规则添加到directions.css
中。
清单 22-16。 造型按钮
#locationsPane #getLocationButton {
border: 0px;
min-width: inherit;
}
虽然这个按钮还没有做任何事情,但是你可以运行 Clok 并导航到方向屏幕来看看它看起来怎么样(见图 22-11 )。
图 22-11 。获取当前位置的按钮
在下一节中,我将向您展示当用户单击这个按钮时,如何获取他或她的当前位置。
确定当前位置
如果不能确定用户的位置,让用户点击按钮是没有意义的。在本节中,您将添加一些代码,以便仅当用户的位置可用时才启用按钮,还将添加一些代码,以便在单击按钮时用他或她的位置填充 From 字段。将清单 22-17 中高亮显示的代码添加到directions.js
的ready
函数中。
清单 22-17。 变为就绪功能
printCommand.onclick = this.printCommand_click.bind(this);
this.currentCoords = null;
getLocationButton.disabled = true;
this.checkForGeoposition();
getLocationButton.onclick = this.getLocationButton_click.bind(this);
fromLocation.value = app.sessionState.directionsFromLocation || "";
checkForGeoposition
函数是这个特性的主力。将清单 22-18 中的功能添加到directions.js
中。
清单 22-18。 请求用户的当前位置
checkForGeoposition: function () {
var locator = new Windows.Devices.Geolocation.Geolocator();
var positionStatus = Windows.Devices.Geolocation.PositionStatus;
if (locator != null) {
locator.getGeopositionAsync()
.then(function (position) {
this.currentCoords = position.coordinate;
getLocationButton.disabled = (locator.locationStatus !== positionStatus.ready);
}.bind(this));
}
},
这个函数获取名为locator
的Windows.Devices.Geolocation.Geolocator
类的一个实例。如果locator
不存在,上一节添加的新按钮永远不会启用。然而,如果getGeopositionAsync
函数返回一个有效位置,按钮被激活,坐标被存储在currentCoords
变量中。
幸运的是,Bing Maps API 完全能够使用坐标,而不是地址,作为起点或目的地。因此,所需的最后一步是在单击按钮时用用户的纬度和经度填充 From 字段。将清单 22-19 中的代码添加到directions.js
中。
清单 22-19。 填充从字段
getLocationButton_click: function (e) {
fromLocation.value = this.currentCoords.latitude.toString()
+ ", " + this.currentCoords.longitude.toString();
},
现在,当您运行 Clok 并单击按钮时,您的纬度和经度将显示在 From 字段中。获取方向按钮将把这些坐标传递给 Bing Maps API,并检索从您当前位置到客户办公室的行驶方向(参见图 22-12 )。
图 22-12 。获取从当前位置到客户办公室的路线
读者的家庭作业——作业#1
如您所见,只需很少的代码,您就可以将来自 Windows 位置提供程序的数据集成到您的应用中。不过,需要记住的一点是位置数据的准确性。正如我上面提到的,位置提供者可以从许多来源检索位置数据。您将总是收到可用的最准确的数据,但是如果 IP 地址数据是所有可用的数据,则可能只精确到几英里以内。根据您的情况,您可能希望您的用户可以接受这种可能的不准确性,但是在驾驶方向的情况下,让起始位置偏离多达 10 英里并不是一个好的体验。
当使用getGeopositionAsync
功能检索坐标时,包含了这些坐标的精度指标,以米为单位。在本书附带的源代码中,我添加了一个条件,只有当this.currentCoords.accuracy
的值在 200 米以内时,才启用getLocationButton
。
有关使用Geolocator
类和其他位置相关类的更多信息,请访问 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.aspx
)。
读者的家庭作业——作业#2
在本章中,我向您展示了如何使用Geolocator
类的checkForGeoposition
函数来请求用户的当前位置。Geolocator
类还有一个名为positionchanged
的事件,当检测到用户位置发生变化时会引发该事件。虽然我还没有在 Clok 中构建任何利用该事件的特性,但是存在一些可能性,您可以选择添加到 Clok 中。例如:
- 您可以更新方向屏幕,以便根据用户驾车前往目的地时的当前位置,突出显示路线中的当前步骤。
- 您可以添加一个 toast 通知,当用户到达他或她的一个客户端的位置时显示该通知。单击此通知可能会启动预先选择了该项目的 Clok 仪表板屏幕。
关于positionchanged
事件的更多信息可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.geolocator.positionchanged.aspx
)上获得。
结论
借助 Windows 8,微软可以轻松地将您的应用与可能连接到用户计算机的各种硬件相集成。相机和位置数据在许多现代计算机中都很容易获得,只需少量代码,您就可以在应用中使用它们。在本章中,我只触及了这个功能的表面。在许多情况下,这些简单的实现足以改善用户对应用的体验。但是,请记住,如果需要,还可以使用更高级的功能。
二十三、在 Windows 应用商店中共享应用
大多数完成一件作品的工匠不想把它藏在一个只有他们能看到的地方——他们想与其他人分享,他们能欣赏它或从中获得一些价值。对于软件应用和艺术品来说都是如此。在 Windows 8 之前,如果你问“我如何与其他人分享我的应用?”答案总是集中在创建安装包的后勤工作或者 XCOPY 部署的好处上。然而,答案很可能是没有任何迹象表明如何确保人们知道该应用的可用性,或者如何将该应用货币化,如果这是目标的话。在本章中,您将了解 Windows 应用商店,这是分发为 Windows 8 构建的应用的主要方法。您将了解如何为您的应用赋予独特的品牌、打包应用以供分发,以及导航提交流程。本章不包括建立开发者账户或使用 Windows StoreAPI 支持的应用内支付功能。
打造您的应用品牌
在本书中,你可能已经注意到示例应用 Clok 显示了相同的、不太明显的闪屏,如图 23-1 所示。这是因为闪屏图像是所有 Windows 应用所需的几个组件之一,并且 Microsoft 为每个新项目的每个必需组件都提供了一个默认值,以确保项目可以立即构建和运行。
图 23-1 。默认应用闪屏
将这些通用元素替换为通过颜色、风格或图像来增加应用身份的替代元素,可以为最终用户创造更好的体验,并且不会给他或她留下应用开发人员不愿意改变默认设置的第一印象。在本节中,您将了解到package.appxmanifest
文件的应用 UI 和打包标签中的品牌选项(参见图 23-2 )。您可以在 Visual Studio 项目的根目录下的解决方案资源管理器中找到该文件。
图 23-2 。package.appxmanifest 文件的应用 UI 选项卡
显示名称
显示名称在图 23-2 中可见,用于识别您的应用。它是一个最多 256 个字符的字符串,显示在 Windows Store 搜索结果和应用列表中,以及 Windows 8 和应用磁贴中的搜索列表中(如果尚未定义短名称)。因为这个值是向可能正在搜索应用的人公开显示的,所以应该注意使它具有独特性、描述性和吸引力。
默认语言
默认语言以两位数的语言代码后跟一个连字符和一个两个字母的区域性代码的形式指定应用的主要语言。面向英国英语用户的应用的默认语言是 en-UK,而美国英语用户的默认语言是美国英语。Windows 应用商店要求从有限的选项集中选择主要语言,因此在 MSDN 搜索构建应用包并查看可用的语言选择可能是个好主意。
支持的旋转
默认情况下,Windows 8 中的应用应该处理用户旋转显示屏的操作,但这适合它们,并且仍然提供有效的界面。有些应用不适合可能的旋转,开发人员可以从以下旋转列表中选择(如图图 23-3 所示),以表明他们的应用支持什么:
- 风景
- 肖像
- 横向翻转
- 纵向翻转
图 23-3 。旋转选项
平铺
平铺设置在应用 UI 选项卡的可视资源部分进行配置,它们控制 Windows 如何显示为应用显示的各种平铺。这些设置主要围绕各种大小的徽标,但也包括侧重于应用的名称显示和颜色的设置。图 23-4 显示了用于编辑主图块标志设置的视觉资产部分。
图 23-4 。主要互动程序徽标的视觉资产部分
徽标图像显示在 Windows 开始屏幕中您的应用的方块上。此图像是必需的,因为即使您打算默认在开始屏幕上显示宽平铺,用户也可以选择显示方形平铺。与闪屏图像一样,徽标必须是 PNG 或 JPG 格式。它还有特定的尺寸要求,必须是 150×150 像素。除了方形瓷砖徽标外,以下位置的窗户还需要 30×30 像素的徽标:
- 在开始屏幕的搜索结果中
- 在缩小的开始屏幕中的完整应用列表中
- 在可搜索应用列表中
当应用选择提供宽图块时,将使用 310×150 像素的徽标。如果图像需要根据不同的屏幕分辨率进行缩放,可以选择为所有徽标提供额外的尺寸。
除了指定用于应用图块的徽标之外,您还可以控制应用名称的显示。这些设置允许多达 13 个字符的可选“短名称”,这可以提供缩短的应用名称版本,以适合磁贴。如果提供了此设置,应用的磁贴和通知将使用此值而不是应用的全名来显示。此外,您可以控制在什么情况下应用的名称(或简称,如果已配置)应显示在应用图块上。选项包括以下内容:
- 所有徽标
- 仅标准(方形)徽标
- 仅宽标志
当显示应用名称时,您还可以选择文本是暗还是亮。图 23-5 显示了 Clok 示例应用的主要磁贴标志,图 23-6 展示了开始屏幕上的 Clok 磁贴,其名称配置为使用浅色文本显示。正如您所看到的,在决定名称显示时,您必须小心谨慎,以确保您的应用的品牌得到维护,并且您不会以抑制可读性的徽标和文本组合而告终。
图 23-5 。克罗克快来
图 23-6 。方块瓷砖
因为 Clok 名称实际上是 logo 的一部分,所以不需要在磁贴上显示应用名称;但是,新闻应用受益于将名称包含在徽标下。一个好的经验法则是让用户清楚当他们选择它时你的磁贴将启动什么应用,而不是不必要的冗余。接受这个建议,我将把应用的名字从磁贴上去掉,因为这个名字是 logo 的一部分。
闪屏
用户在启动应用时首先看到的是应用执行任何必要的初始化时显示的闪屏。应用必须指定闪屏图像,还可以指定显示图像的背景颜色。与平铺图像一样,这是在应用 UI 选项卡的“可视资源”部分完成的。大小和格式要求非常具体,因为图像必须是 620×300 像素,并且必须是 PNG 或 JPG 格式。图 23-7 显示了一个更适合 Clok 的闪屏。
图 23-7 。Clok 闪屏
注本书附带的源代码包括一个完整的项目,其中包含本章使用的所有源代码和图像文件。你可以在本书的 press 产品页面(
http://www.apress.com/9781430257790
)的 Source Code/Downloads 选项卡上找到本章的代码示例。
通知
正如您在第二十一章中所了解到的,诸如动态磁贴和 toast 之类的通知机制为您的应用提供了一个在用户使用其他应用时与他们联系的机会。当标记您的应用时,您可以指示该应用是否将使用 toast 通知。此外,您可以将应用配置为在锁定屏幕激活时显示通知。这些通知可以显示为 24×24 单色徽章图像,也可以显示为宽标题 toast 通知。如果应用配置为在锁屏通知中显示磁贴和文本,则必须在磁贴品牌选项中指定宽磁贴徽标。
发布者显示名称
当您创建开发人员帐户以在 Windows 应用商店中发布应用时,您需要填写的字段之一是发布者显示名称。您在商店的顾客将会看到这个名字。此外,您的项目必须配置为使用相同的值。您可以在package.appxmanifest
文件的打包选项卡上为您的项目设置发布者显示名称(参见图 23-8 )。
图 23-8 。设置发布者显示名称
除了显示在 Windows 应用商店中之外,此值还显示在应用的“设置”面板中。在更新发布者显示名称字段后,比较图 23-9 (我们在第九章中创建的设置窗格)和图 23-10 中的版本。
图 23-9 。原始设置窗格,来自第九章
图 23-10 。更新的设置面板
设置弹出按钮
你可以选择添加到你的应用中的另一个好方法是在每个SettingsFlyout
控件的标题上添加一个小图标。您可以向每个弹出按钮添加相同的图标,但是,我们将向每个弹出按钮添加不同的上下文图标。将清单 23-1 中的高亮代码添加到我们在第九章中创建的about.html SettingsFlyout
控件中。
清单 23-1。 给设置弹出按钮添加标题图标
<div class="win-ui-dark win-header" style="background-color: #000046;">
<button type="button" class="win-backbutton"
onclick="WinJS.UI.SettingsFlyout.show()">
</button>
<div class="win-label clok-logo">About Clok</div>
<img src="/img/about-icon.png" style="position: absolute; right: 40px;" />
</div>
运行应用并激活设置面板中的关于 Clok 选项。你会看到标题的右边显示了一个图标(见图 23-11 )。
图 23-11 。更新的设置弹出按钮
为了完成这一步,在options.html
中添加一个类似的行,将图像文件名从about-icon.png
改为options-icon.png
。
导航提交流程
在我描述如何创建您将要提交的包之前,讨论提交应用的过程似乎有点不太合适,但是提交过程通常是从在您开始编写应用代码之前保留应用名称开始的。当完成的应用包准备好提交时,它将恢复。在这一节中,我将讨论提交过程中的不同步骤。
提交申请的第一步是通过点击仪表板菜单项在 Windows 开发中心(http://dev.windows.com
)创建一个 Windows 开发者账户(参见图 23-12 )。如果您已经有一个开发人员帐户,您将能够登录以查看仪表板。如果您还没有开发人员帐户,您将能够完成创建一个帐户的步骤,然后继续操作仪表板。
图 23-12 。Windows 开发中心
一旦你建立了个人资料,提交申请的过程就可以开始了。图 23-13 显示了当前正在进行的我的应用的仪表板,以及提交应用的链接。顾名思义,您应该单击该链接开始。
图 23-13 。我当前正在运行的应用
从现在开始,您只需按照提交向导的步骤进行操作。图 23-14 显示了一个正在进行的 Clok 提交。
图 23-14 。Clok 提交正在进行中
在每一步之后,我将在下面简要介绍,图 23-14 中的清单将会更新,以指示还剩下哪些步骤。请注意,尽管必须完成所有步骤才能向 Windows 应用商店提交应用,但并不要求按顺序完成所有步骤。举个例子,我在服务步骤之前完成了年龄评定步骤。
应用名称
注册的应用名称部分是在编写应用之前开始提交流程的两个最重要的原因之一。在此步骤中,您可以指定将在 Windows 应用商店中标识您的应用的名称。完成该步骤可确保您选择的名称尚未被使用或保留,并为您提供一年的名称保留期,在此期间您可以完成申请并提交,而不必担心是否有人使用了您选择的名称。
销售详情
在销售细节步骤中,您可以选择是否以及如何从您的应用中赚钱。这可以包括让你的应用免费可用,提供有时间或功能限制的试用版,或者通过广告或应用内购买来赚钱。如果您选择销售您的应用,您必须为该应用选择一个定价等级,通常从 1.49 美元到 4.99 美元不等。对于第一笔 25,000 美元的销售额,微软保留 30%的商店费用,然后商店费用减少到 20%,留给开发者 80%的净收入。
服务
在服务功能步骤中,您定义应用是否应接收来自服务器的推送通知,并生成服务器将用于向您的应用发送通知的身份信息。除了保留名称的好处之外,获得在这个步骤中生成的值以包含在您的应用代码中是在编写应用代码之前开始提交过程的第二大原因。
年龄分级
此步骤用于指定应用用户的预期或适当年龄。Windows 应用商店不接受包含成人内容的应用。不允许被指定或未被评级但有价值的申请,PEGI 16 或 ESRB 成熟评级。您可以上传第三方分级证书或通过分配 Windows 应用商店年龄分级来进行自我分级。
密码术
在这一步中,您需要指出您的应用是否使用了加密技术。这一点很重要,因为许多国家对加密软件或使用加密组件的软件有进出口限制,您在此处的选择将影响您的应用可用的地理区域。
根据您的应用的加密需求,如果您将加密用于除密码加密之外的其他用途,您可能需要提供出口商品分类编号(ECCN)。如果您没有,您将被引导到工业和安全局的网站,以获得有关如何找到一个的更多信息。
包装
在此步骤中,您将上载在 Visual Studio 中创建的完整应用包。提交页面允许您使用浏览按钮导航到计算机上的包,或者将包从 Windows 资源管理器窗口拖放到浏览器页面。
描述
在此步骤中,您将为每种支持的语言输入应用的描述。这种描述是必须的,并且将在你的应用的营销中使用,因此它以一种鼓励潜在用户选择你的应用而不是其他可能做相同或相似工作的应用的方式描述你的应用是很重要的。在此步骤中,您还需要提交至少一个屏幕截图,用于推广您的应用、版权信息和支持联系信息。除了这些必需的元素之外,您还可以选择包括以下内容:
- 描述性要点
- 关键词
- 附加许可条款
- 其他截图和宣传图片
- 应用网站和隐私政策的链接
测试人员注意事项
提交到 Windows 商店的每个应用都将经过一系列自动测试,以确保符合微软的指导原则,并且还将由微软测试人员手动审查质量和内容。如果这些测试人员需要信息,比如登录凭证,以便成功地运行应用,您可以在这里为他们提供注释。此处的字符数是有限的,因此如果您的笔记需要的空间超过分配的空间,您可以提供一个指向其他说明的 URL。重要的是要意识到测试人员可能会有很多应用要测试,所以你让他们的工作越容易,他们测试你的应用的心情就越好。
最终提交
为测试人员输入注释后,您有机会检查并最终确定您的提交。从现在开始,这个过程就不在你的掌控之中了。应用将通过自动测试运行,并在决定是否接受您的应用进入商店之前进行人工审查。开发人员门户中的仪表板可用于查看您正在进行的任何提交的当前状态。
注本章讨论了提交过程,但并未深入探讨评估您提交作品的要求。要查看当前要求,请在 MSDN 搜索主题 Windows 8 应用认证要求。本主题涵盖了 Windows 8 应用的技术和内容相关要求。
打包您的应用
如前一节所述,打包应用以提交到 Windows 应用商店的第一步是在应用商店中保留应用名称。创建应用名称后,您可以使用 Visual Studio 商店菜单中的“将应用与商店项目相关联”(如图 23-15 所示)将当前项目与您预订的 Windows 商店元数据相关联。该元数据包括包显示名称、包名称、发布者 ID 和发布者显示名称。
图 23-15 。将应用与 Windows 应用商店关联
除了将项目与 Windows Store 中保留的应用名称相关联之外,Store 菜单还提供了一个捕获屏幕截图选项,该选项在我在第九章中讨论的模拟器中启动您的应用,并包括其自己的屏幕捕获工具。使用图 23-16 中所示的摄像机图标调用该命令。必须捕获至少一个屏幕截图才能提交,这些图像将用于 Windows 应用商店以及 Microsoft 用于推广您的应用的任何其他材料。
图 23-16 。相机图标
完成应用并准备提交到 Windows 应用商店后,创建应用包菜单项将启动一个简短的向导,该向导将为您创建必要的包文件。选择上传应用包将打开开发者门户的 web 浏览器窗口,并允许您在包步骤继续应用提交流程,此时您将上传您已创建的包。
结论
在本章中,您学习了如何宣传您的应用的独特品牌,以及如何保留名称和提交应用以在 Windows 应用商店上提供。如果你有一个应用要与世界分享,你应该开一个开发者账户,保留你的名字,开始用你的技能创造下一个伟大的应用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)