HTML5 和 JavaScript 的 Windows8 开发高级教程(四)
十一、使用ToggleSwitch、Rating和Tooltip控件
在这一章,我开始详细描述 WinJS UI 控件,使用我在第十章中创建的框架应用。这些 UI 控件是创建与更广泛的 Windows 视觉主题一致的应用的重要构件,值得详细研究。
WinJS UI 控件的机制类似于您可能使用过的其他 JavaScript UI 工具包,如 jQuery UI。将控件应用于元素,并应用其他元素、样式和事件处理程序来创建丰富的视觉效果。
您可以轻松地在 Windows 应用中使用 jQuery UI 或类似的库,但最终会得到奇怪的结果,并且会错过一些控件与其他 WinJS 功能(如数据绑定)的紧密集成。在本章和接下来的章节中,我将依次描述每个控件。我告诉你如何应用和配置控件,何时应该使用控件,以及如何观察用户与控件的交互。
在这一章中,我从三个相对简单的控件开始:ToggleSwitch、Rating和Tooltip控件。这些是最基本的控件,虽然它们很有用,但是它们并不有趣,或者与您可能遇到的其他 JavaScript UI 工具包没有什么不同。表 11-1 对本章进行了总结。
使用切换开关控制
我将从WinJS.UI.ToggleSwitch控件开始。这是一个很好的开始控件,因为它很简单,而且因为我在第十章中创建的框架应用中使用了这个控件来演示本章和后续章节中其他控件的功能。
顾名思义,ToggleSwitch控件让用户在on和off状态之间切换。鼠标用户可以点击ToggleSwitch的空白部分来改变状态,触摸用户可以向左或向右滑动开关。ToggleSwitch控件在从一种状态转换到另一种状态时会执行一个简短的动画。你可以在图 11-1 中看到ToggleSwitch是如何出现的。稍后,我将向您展示我为创建这个布局而添加到示例框架中的内容。
***图 11-1。*示例应用中的 ToggleSwitch 控件
何时使用拨动开关控制
当你需要向用户展示一个二元决策时,你应该使用ToggleSwitch控件。改变值的滑动动作非常友好,比普通的 HTML 复选框或带有Yes和No选项的select元素更容易使用。我经常使用这个控件让用户配置应用设置(我在第二十章中描述过)。
提示如果你在一列中显示几个
ToggleSwitch控件,确保所有的true / on值都在同一侧——如果一些开关需要放在右边来启用某个功能,而其他开关放在左边,只会让用户感到困惑。
演示 ToggleSwitch 控件
为了演示ToggleSwitch控件,我在第十章的UIControls项目的pages文件夹中添加了一个新的 HTML 文件。您可以在清单 11-1 的中看到这个名为ToggleSwitch.html的文件的内容。
清单 11-1 。/pages/ToggleSwitch.html 文件的内容
`
您可以从清单中看到,我使用了键toggleSwitch来定位controls.js文件中的定义对象。您可以在清单 11-2 中看到那些定义对象。
清单 11-2 。ToggleSwitch 控件的定义对象
`(function () {
WinJS.Namespace.define("App.Controls", {
toggleSwitch: [ { type: "toggle", id: "checked", title: "Value", value: true }, { type: "toggle", id: "disabled", title: "Disabled", value: false }], });
})();`
最后,我通过对清单 11-3 中的文件进行添加,将命令添加到了导航栏中。
清单 11-3 。将 ToggleSwitch.html 文件添加到导航栏
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, ** { name: "ToggleSwitch", icon: "\u0031" },** ]; ...
应用和配置 ToggleSwitch 控件
通过将data-win-control属性设置为WinJS.UI.ToggleSwitch,将ToggleSwitch控件应用于div元素。正如我在第十章中所解释的,当底层 HTML 元素被WinJS.UI.processAll方法处理时,它被赋予一个winControl属性(要么是因为你已经显式调用了该方法,要么是因为内容已经使用WinJS.UI.Pages.render方法加载,该方法为你调用了processAll)。
属性返回由属性指定的控件对象,这允许你在代码中调用方法和设置控件定义的属性。为了方便起见,您可以使用data-win-options属性在 HTML 中以声明方式设置属性值,指定一个包含名称/值对的 JSON 片段。
对于左边的ToggleSwitch,我已经为title属性设置了一个值,该值用于显示在控件上方的文本,如清单 11-4 所示。
清单 11-4 。使用 data-win-options 属性设置标题属性
`...
表 11-2 描述了由ToggleSwitch定义的配置属性,所有这些属性都可以使用data-win-options属性或在您的 JavaScript 代码中设置。
在演示ToggleSwitch控件的代码中,我创建了改变checked和disabled属性的配置控件。通过查看templates.js文件中的createtoggle方法,你可以看到我是如何使用 JavaScript 来设置其他属性的,我在第十章的最后给你展示了这个文件。我使用来自定义对象的值来设置labelOn、labelOff和title属性。
样式化 ToggleSwitch 控件
WinJS UI 控件可以使用一组 CSS 类来设置样式。这是使用 WinJS 控件的好处之一,也是我还没有感觉到需要在我的任何 Windows 应用项目中使用 jQuery UI 的原因之一(这说明了一些问题,因为我喜欢 jQuery 和 jQuery UI)。我已经在表 11-3 中列出并描述了ToggleSwitch控件支持的一组类。
因为我使用了ToggleSwitch控件来帮助演示许多其他 WinJS UI 控件,所以我在/css/default.css文件中添加了一些 CSS 样式,这些样式使用了表 11-3 中的类。你可以在清单 11-5 中看到这些风格。
小心不要忘乎所以的设计 UI 控件。你想让你的应用和一般的 Windows 外观保持一致。为此,您应该只设置控件的样式,以便它们易于阅读,并且与布局中使用的配色方案相对应。
清单 11-5 。将样式应用于 ToggleSwitch 控件
`... .controlPanel .win-toggleswitch { width: 90%; margin: 15px; padding-left: 20px; }
.win-toggleswitch .win-title { color: white; font-size: 20px; } ...`
每当你覆盖一个ToggleSwitch控件的样式时,你必须使用win-toggleswitch类,甚至当你覆盖一个子样式时,比如win-title。如果你不这样做,那么由 Visual Studio 项目中的默认 CSS 定义的样式(在ui-light.css和ui-dark.css文件中,我在第二章中描述过)将具有更大的特异性,而你的自定义值将不会有任何影响。
对于default.css文件中的样式,我将一个border和一些margin和padding应用于我用作配置控件的ToggleSwitch控件,并更改了所有ToggleSwitch控件标题的文本大小和颜色。
提示很难弄清楚定制风格到底有什么效果。通过在调试器中运行您的应用,切换到 Visual Studio
DOM Explorer窗口,单击Select Element按钮并单击布局中的控件,您可以看到 WinJS UI 控件的结构。您将能够看到围绕应用了data-win-control属性的元素创建的 HTML 元素,并看到底层组件的样式。
处理 ToggleSwitch 控件事件
当用户更改开关的位置时,ToggleSwitch控件发出change事件(当以编程方式更改属性值时,不会发出该事件)。我将依次列出每个控件的事件,以便在您需要和翻阅本章时更容易找到这些信息。表 11-4 描述了change事件,尽管只有一个而且是非常基本的事件。
我在ToggleSwitch.html文件中添加了一个处理程序,它通过更新右侧面板中一个配置控件的checked值来响应change事件,使得这些控件之间的关系是双向的。您可以在清单 11-6 的中看到事件处理程序。
清单 11-6 。添加 ToggleSwitch 更改事件的处理程序
`
data-win-options="{title:'This is a ToggleSwitch:'}">
我使用then方法确保配置控件已经由Templates.createControls方法创建,并使用addEventListener方法设置处理程序。注意,我直接在应用了ToggleSwitch控件的div元素上设置了事件监听器,但是我通过读取winControl.checked值来获取控件的状态。这是所有 WinJS UI 控件的模式——事件是从底层 HTML 元素发出的,但是控件的状态是通过winControl属性访问的。
要测试事件处理程序,请启动示例应用,单击导航栏上的ToggleSwitch按钮,并切换左侧布局面板中的ToggleSwitch控件。您将看到右侧面板中的Value配置控件的状态同步变化。这种关系反过来也一样,但那是因为我在Templates.createtoggle方法中为change事件设置了一个监听器,我在第十章中描述过。
提示当
ToggleSwitch被禁用时,用户不能移动开关位置,但如果通过编程修改winControl.checked属性,开关将正确反映变化。
使用评级控制
WinJS.UI.Rating控件允许用户通过提供星级来表达意见。在图 11-2 中,你可以看到Rating控件是如何出现在左侧面板中的。用户可以通过点击或触摸星星来指定评级,并通过在星星阵列上上下拖动鼠标或手指来更改评级。稍后,我将向您展示我在示例项目中添加的内容,以创建这个布局。
***图 11-2。*一个 WinJS。UI .评级控制
何时使用评级控制
用星星的数量来表达观点或评价的想法是如此根深蒂固,以至于你不应该将这个控件用于任何其他目的。如果您想从用户那里获得一个数字,那么使用一个常规的input元素,将type属性设置为number。
如果你确实想要用户的意见,那么Rating控件是理想的。尝试使用常见的星级数(3、5 和 10 是常用的),在整个应用中坚持这个数字,并确保你利用这个机会表达积极和消极的观点。每当使用Rating控制时,确保每个星级具有相同的含义。
演示评级控制
为了演示Rating控件,我在UIControls项目的pages文件夹中添加了一个新的 HTML 文件。您可以在清单 11-7 中看到这个名为Rating.html的文件的内容。
清单 11-7。/pages/rating . html 文件的内容
`
.win-rating .win-star.win-user.win-full { color: yellow; } .win-rating .win-star.win-average.win-full { color: #000; } .win-rating .win-star.win-average.win-full, .win-rating .win-star.win-tentative.win-full { color: white; }
这个文件包含了一些特定于Rating控件的 CSS 样式(我会简单描述一下)。清单 11-8 显示了我添加到controls.js文件中的定义对象,以在图 11-2 所示的右侧面板中创建配置元素。
***清单 11-8。*评级控件的定义对象
... **rating**: [ { type: "toggle", id: "enableClear", title: "Enable Clear", value: true }, { type: "toggle", id: "disabled", title: "Disabled", value: false }, { type: "input", id: "userRating", title: "User Rating", value: 0 }, { type: "input", id: "maxRating", title: "Max Rating", value: 6 }, { type: "input", id: "averageRating", title: "Ave. Rating", value: 2.6 }], ...
为了让用户能够导航到Rating.html页面,我对templates.js文件进行了添加,如清单 11-9 所示。
清单 11-9 。将 Rating.html 文件添加到导航栏
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, ** { name: "Rating", icon: "\u0032" },** ]; ...
应用和配置评级控制
通过将data-win-control属性设置为WinJS.UI.Rating,将Rating控件应用于div元素。我用配置控件管理的属性在表 11-5 中描述,并可通过winControl属性访问。
我为Rating控件定义的配置控件允许您尝试更改除了tooltipStrings之外的所有属性,我在Rating.html页面的script元素中设置了tooltipStrings(我将在下面解释)。
管理评级
在averageRating、maxRating和userRating属性之间有一个特定的交互,你需要在其中工作以从Rating控件获得正确的效果。
maxRating属性指定了Rating显示的星的数量,并有效地设置了可以使用该控件选择的评级的上限。显示的星星数是一个整数。
属性可以用来显示其他地方的评级。平均值的常见用途是显示其他用户的评级或当前用户在以前提供的评级。您可以将averageRating值指定为实数(如4.2),部分星星将会显示出来。userRating属性表示用户选择的值。这是另一个整数值。
只有当userRating属性为零时,才会显示averageRating值。一旦用户提供了一个等级(或者在外部设置了userRating属性),就会隐藏averageRating值并显示userRating值。我在Rating控件的例子中添加了控件,让你指定这些属性的值,你可以在图 11-3 中看到从averageRating到userRating值的转换。
***图 11-3。*用用户评级值替换平均评级
如果enableClear属性是true,那么用户可以拖动或滑动到Rating控件的左边,清除userRating值。发生这种情况时,再次显示averageRating值。如果enableClear属性为false,则控制不能被清除。请谨慎使用此设置,因为它会诱使用户做出不可逆的评级。如果您确实将enableClear设置为false,那么您应该提供一个附近且明显的控件,将userRating属性设置回零,并让用户重新开始。一般来说,你应该让用户放弃他们提供的任何值——当这不可能的时候,用户会感到困惑和烦恼。
设置工具提示
如果用户在一颗星星上悬停或滑动,Rating控件将显示一个工具提示。默认情况下,这些是数字,反映评级代表的星级数。如果您将tooltipStrings设置为一个字符串数组,那么数组值将被用作工具提示内容。我在pages/Rating.html文件的脚本元素中显式设置了tooltipStrings属性。为了方便起见,下面是我使用的值:
... rating.winControl.tooltipStrings = ["Terrible", "Pretty Bad", "Not So Good", "Reasonable", "Good", "Excellent"]; ...
你可以在图 11-4 中看到一个工具提示值是如何呈现给用户的。
***图 11-4。*显示工具提示的评级控件
提示如果你设置了一个比
maxRating属性的值多一项的数组,那么当用户向左拖动或滑动以清除评级时(假设enableClear属性为true),将显示最后一个字符串值。
设置评级控件的样式
Rating控件支持多种样式,您可以覆盖这些样式来定制控件的外观,每种样式都在表 11-6 中进行了描述。
这些类必须组合成非常特殊的排列来设计一个Rating控件。清单 11-10 显示了来自Rating.html文件中style元素的 CSS,我用它来改变示例中Rating控件的默认样式。
清单 11-10 。Rating.htmlfile 文件中的 CSS
... .win-rating .win-star.win-user.win-full { color: yellow; } .win-rating .win-star.win-average.win-full { color: #000; } .win-rating .win-star.win-average.win-full, .win-rating .win-star.win-tentative.win-full { color: white; } ...
应用表格中的样式时必须遵循的顺序是:
.win-rating **.win-star.win-[rating value].win-[star state]**
我用粗体标记的类应用于同一个元素,这意味着不用空格来分隔它们。如果您想要为userRating值设置显示的完整星号的样式,您可以覆盖这个序列:
.win-rating .win-star.win-**user**.win-**full**
您可以覆盖多组星形的样式,但需要小心。最安全的方法是列出多个完整的类组合,如下所示:
... .win-rating .win-star.win-average.win-full, .win-rating .win-star.win-user.win-full, .win-rating .win-star.win-tentative.win-full { color: white; } ...
您可以省略一些类来用选择器撒一个更大的网,但是您仍然必须确保选择器比微软定义的那些更具体,后者使用完整的类序列。最直接的方法是使用已经应用了Rating控件的元素的id,就像这样:
... #rating .win-star.win-full { color: green; } ...
处理评级控制事件
Rating控件支持三个事件,我已经在表 11-7 中描述过。
清单 11-11 展示了我如何在/pages/Ratings.html文件中添加脚本元素来处理change事件。我使用这个事件来更新布局右侧面板中的配置控件,这会影响到userRating属性。
清单 11-11 。处理评级变更事件
`...
...`
我对由Template.createControls方法返回的Promise对象使用了then方法,以确保在创建配置元素并将其添加到应用布局之前,我不会创建事件处理程序。
使用工具提示控件
当鼠标或手指停留在一个元素上或当键盘获得焦点时,Tooltip控件弹出一个包含有用信息的窗口。这是一个简单的控件,但是在如何配置和应用一个Tooltip方面仍然有很大的灵活性。你可以在图 11-5 的中看到一个工具提示的例子。HTML 中的其他控件用于配置Tooltip的不同方面,我将在下面的章节中解释。
***图 11-5。*一个工具提示控件
何时使用工具提示控件
当你想为用户提供指导或一些补充信息时,可以使用Tooltip控件,也许是为了帮助他们在选择应用中的特定选项时做出明智的选择,或者只是关于他们正在查看的内容的一些附加信息。
提示
Tooltip是一个瞬态控件,会自动消失(我在下面解释发生这种情况的情况)。您不能对任何类型的交互式内容使用瞬变控件,如button或Rating控件。为此,您需要使用Flyout控件,我在第七章的中介绍了它,并在第十二章的中再次详细描述了它。
演示工具提示控件
您可以看到我添加到controls.js文件中的定义对象,以演示清单 11-12 中的控件。将使用tooltip键访问这些对象。
清单 11-12 。工具提示控件的定义对象
... tooltip: [ { type: "toggle", id: "infotip", title: "Infotip", value: false, labelOn: "Yes", labelOff: "No" }, { type: "select", id: "placement", title: "Placement", values: ["top", "bottom", "left", "right"], labels: ["Top", "Bottom", "Left", "Right"]}, { type: "buttons", labels: ["Open", "Close"] }], ...
我在添加到UIControls项目的pages文件夹中的新 HTML 文件中使用了定义对象。您可以在清单 11-13 的中看到这个名为Tooltip.html的文件的内容。
清单 11-13 。/pages/Tooltip.html 文件的内容
`
#tooltipContent img { float: left; } #blockTooltip button { font-size: 18pt; margin: 10px;}$("#rightPanel button").listen("click", function (e) { inlineTooltip.winControle.target.innerText.toLowerCase(); }); } });
<div id="blockTooltip" data-win-control="WinJS.UI.Tooltip" data-win-options="{contentElement: tooltipContent, infotip: true}"> Press Me
Apples have been grown for thousands of years in Asia
and Europe, and were brought to North America by European
colonists.
该文件遵循与前面示例相同的基本模式。该文件比前面的例子要长,因为它包含了一些简单的内容,我使用本节中演示的Tooltip控件来显示这些内容。
另一个区别是,我在应用布局的右侧面板中为我创建的button元素添加了一个click事件的处理程序。这个处理程序调用由与被点击的button内容相对应的winControl属性定义的方法,因此点击Open按钮调用winControl.open方法,点击Close按钮调用winControl.close方法。我将很快解释这两种方法。
演示该控件的最后一步是将Tooltip.html文件添加到导航栏,我通过添加到清单 11-14 中所示的templates.js文件来完成。
清单 11-14 。将 Tooltip.html 文件添加到导航栏
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, ** { name: "Tooltip", icon: "\u0033" },** ]; ...
注意
Tooltip.html文件中的标记包含一个img元素,其src属性引用了images文件夹中一个名为apple.png的文件。你可以获得我作为本书源代码下载的一部分使用的图像,或者,如果你喜欢,只需重命名你必须交给apple.png的图像,并将其复制到 Visual Studio 项目的images文件夹中。
应用和配置工具提示控件
基本前提是将与Tooltip相关的 HTML 元素包装在一个已经应用了WinJS.UI.Tooltip控件的div元素中,如清单 11-15 所示。
清单 11-15 。将工具提示控件应用于块元素
... **<div id="blockTooltip" data-win-control="WinJS.UI.Tooltip"** ** data-win-options="{contentElement: tooltipContent, infotip: true}">** <button>Press Me</button> **</div>** ...
在这个取自/pages/Tooltip.html文件的片段中,我将一个button元素包装在一个已经应用了Tooltip的div元素中。当用户将鼠标指针悬停或者将手指放在button元素上时,将显示由contentElement属性指定的元素的内容。在示例中,我已经将contentElement属性设置为tooltipContent,它引用了清单 11-16 中的元素。
清单 11-16 。用于工具提示控件的内容
`...
这个元素的内容是由img和span元素组成的标准 HTML 标记。请注意,我已经将内容元素放在了一个div元素中,该元素的 CSS display属性被设置为none——这防止用户看到内容,直到它被Tooltip控件显示出来。
提示这是我使用了在上一节中添加到项目中 img/apple.png`文件的地方。你可以在图 11-6 中看到这个图像。
当鼠标指针悬停在图 11-6 中的Press Me按钮上时,可以看到结果。
***图 11-6。*设置工具提示控件的内容
contentElement是可以在Tooltip控件上设置的属性之一,可以通过data-win-options属性设置,也可以通过winControl属性以编程方式设置。你可以在表 11-8 中看到完整的属性集,我将在接下来的章节中演示它们。
创建内联工具提示&使用 innerHTML 属性
您还可以将Tooltip控件应用到span元素,如果您使用的是内嵌显示的内容,并且div元素会扰乱布局,这将非常有用。我在Tooltip.html文件中定义的另一个工具提示已经以这种方式应用,如清单 11-17 所示。
清单 11-17 。将工具提示控件应用于 span 元素
`...
Tooltip将应用的元素仍然围绕着它所涉及的内容,在这个例子中是单词 apple。当鼠标指针悬停在文本块中的单词 Apples 上时,Tooltip将显示给用户。
作为使用contentElement属性的替代方法,我已经使用innerHTML属性设置了Tooltip控件的内容。这个属性让你将Tooltip的内容设置为一个字符串值,这是我在图 11-5 中展示的工具提示。
设置显示持续时间
用户可以通过多种方式触发Tooltip。如果用户触摸或按住元素上的鼠标按钮,那么将显示Tooltip弹出窗口,直到手指从显示屏上移开或释放鼠标按钮。
如果用户通过键盘操作(比如在元素间跳转)或者将鼠标悬停在目标控件上来触发Tooltip,那么弹出窗口会显示 5 秒钟,之后会自动关闭。
通过将infotip属性设置为true,可以将这个时间延长到 15 秒。该值旨在用于包含大量信息且用户不太可能在较短时间内解析的Tooltip。
该示例右侧面板中的ToggleSwitch为左侧面板中的内嵌Tooltip设置了infotip属性。通过使用这个开关,您可以看到不同时间跨度的效果。
提示如果你需要让你所有的
Tooltip显示 15 秒,你可能要重新考虑你向用户呈现内容的方式。对于引导用户浏览你的应用的小信息片段来说,是完美的。考虑使用一个Flyout作为通用弹出窗口,正如我在第十二章中所描述的。
设置工具提示位置
placement属性控制Tooltip弹出窗口相对于目标元素出现的位置。默认值是top,意味着弹出窗口显示在元素的稍上方,但是您也可以指定left、right和bottom。示例中右侧面板中的select元素设置左侧面板中内联Tooltip的placement属性。你可以在图 11-7 中看到交替放置的效果。
***图 11-7。*左侧、底部和右侧工具提示位置
你不必担心调整placement值来确保Tooltip弹出窗口适合屏幕。如果由placement属性指定的值意味着弹出窗口不合适,WinJS 将自动调整弹出窗口的位置。表 11-9 显示了当弹出窗口不能显示指定值时,对于placement属性的每个可能值将尝试的一系列回退布局。
提示对于为左撇子用户配置的设备,回退序列中左右位置的顺序将会颠倒。
弹出窗口的确切位置是相对于鼠标或触摸事件的位置,并受到一个偏移量的影响,该偏移量取决于触发Tooltip的事件类型。键盘事件的偏移量是 15 像素(例如,当用户切换到目标元素时),鼠标事件的偏移量是 20 像素,触摸事件的偏移量是 45 像素。较大的偏移量旨在允许用户阅读Tooltip内容,而不会被光标、手写笔或手指遮挡。
以编程方式管理工具提示控件
通过使用open和close方法,你可以直接控制Tooltip何时显示和隐藏——尽管如果你使用这些方法,你应该暂停一会儿,考虑你使用Tooltip的方式是否与其他 Windows 应用一致。(如果你想要一个通用的弹出窗口,你应该使用一个Flyout,正如我在第十二章中描述的那样)。
为了快速参考,我在表 11-10 中描述了这些方法。这看起来可能是多余的,但是 WinJS UI 控件不使用一致的方法命名方案,当你希望能够快速发现Tooltip控件是否定义了show或open和close或hide时,就会出现这种情况。
open方法采用可选参数,用于模拟不同种类的触发事件,支持的值有touch、mouseover、mousedown和keyboard。如果不提供参数,则使用default值。每个值都有略微不同的效果,包括用于定位弹出窗口的偏移量(如前一节所述)、弹出窗口显示前的延迟以及弹出窗口关闭前的显示时间。
您需要小心,因为touch、mousedown和default参数不会自动关闭弹出窗口。在touch和mousedown模式下,这是因为弹出窗口会一直显示,直到手指从屏幕上移开或鼠标按钮松开。如果您对open菜单使用这些参数,那么您必须使用close方法显式关闭Tooltip弹出菜单。示例右侧面板中的Open和Close按钮调用了open和close方法(调用open方法时没有参数,因此直到使用Close按钮才会关闭弹出窗口)。
提示我发现不带参数调用
open方法在开发过程中非常有用,因为它让我看到了Tooltip的内容是如何显示的,而无需执行触发操作。
设置工具提示控件的样式
Tooltip控件支持一个 CSS 类:win-tooltip。您可以使用该类作为设计显示内容样式的起点。没有其他类,因为在Tooltip中没有固定的元素结构。清单 11-18 显示了我在/css/default.css文件中为Tooltip控件定义的样式。
清单 11-18 。样式化工具提示控件
... .win-tooltip { background-color: #8ED09C; color: white; border: medium solid white; font-size: 16pt; } ...
处理工具提示事件
Tooltip控件支持我在表 11-11 中描述的四个事件。为了完整起见,我列出了这些事件,但是我还没有发现这些事件在实际项目中的用途,因为Tooltips旨在呈现简单、自包含的内容。
就像Tooltip控件的其他一些特性一样,如果你需要这些事件,我建议你停下来考虑一下你的应用设计。这可能是因为你有非常特殊的需求,但更有可能的是你要强迫Tooltip控件做一些打破 Windows 应用交互规则的事情。考虑一下我在第十二章的中描述的Flyout控制是否是一个更合适的选择。
总结
在这一章中,我已经向你介绍了三个最简单的 WinJS UI 控件——尽管,正如你所看到的,要在你的应用中获得正确的效果,还有很多细节需要考虑。在下一章,我将把注意力转向时间和日期选择器以及Flyout和Menu控件。
十二、使用TimePicker和DatePicker和Flyout
在这一章中,我继续探索 WinJS UI 控件,重点是TimePicker、DatePicker、Flyout和Menu控件。TimePicker和DatePicker控件允许用户指定时间和日期,顾名思义。这些是带有一些设计问题的基本控件,这些设计问题使它们比它们本应具有的功能更难使用(也更没用)。你已经在第七章的中看到了Flyout控件,在那里我将它与 AppBar 一起使用。作为一个通用控件,?? 有着更广泛的存在,我解释了在这种情况下使用它所需要知道的一切。表 12-1 提供了本章的总结。
使用时间选择器控件
WinJS.UI.TimePicker控件允许用户选择时间。HTML5 在input元素中增加了支持时间和日期输入的功能,但是在 Internet Explorer 10 中不支持,你必须使用TimePicker和DatePicker控件来代替(我将在本章后面描述DatePicker)。你可以在图 12-1 的中看到TimePicker控件是如何显示给用户的。
***图 12-1。*时间选择器控制和支持配置设置
何时使用时间选择器控件
当您需要用户指定一天中的时间时,您应该使用TimePicker控件。TImePicker控件尊重用户机器的语言环境设置,因此使用本地化的时间首选项捕获时间——尽管,正如您将看到的,这并不完全成功。
控件的质量没有我希望的那么好。众所周知,本地化的时间和日期首选项很难得到正确,必须做出一些让步,但即使如此,在TimePicker中反映出的不幸的设计选择使它的用处大大降低。我对这个控件和它的同伴DatePicker的总体印象是,这是一个仓促的工作,很少考虑控件将如何使用。
演示时间选择器控件
按照上一章的模式,我在第十章的中开始的 Visual Studio 项目的pages文件夹中添加了一个名为TimePicker.html的文件。您可以在清单 12-1 中看到该文件的内容。
清单 12-1 。TimePicker.html 文件的内容
`
` `Templates.createControls(rightPanel, picker, "timePicker", proxyObject) .then(function () {
proxyObject.bind("showPeriod", function (val) { $('.win-timepicker-period').setStyle("display", val ? "block" : "none"); });
proxyObject.bind("clock", function (val) { picker.winControl.clock = val ? "24HourClock" : "12HourClock"; });
["hour", "minute"].forEach(function (item) { proxyObject.bind(item + "Length", function (val) { picker.winControl[item + "Pattern"] = "{" + item + ".integer(" + val + ")}"; }); }); }); } });
Enter a time:
<div id="picker" data-win-control="WinJS.UI.TimePicker" data-win-options="{minuteIncrement: 10}">`
这是我需要代理对象特性的第一个控件,我在第十章的中将其添加到示例框架中。正如您将看到的,当我解释由TimePicker控件定义的属性如何工作时,我不容易将我为布局中的右侧面板生成的配置控件中的值映射到我可以与TimePicker属性一起使用的值。
您可以看到我如何在添加到/js/controls.js文件的定义对象中使用代理对象来演示清单 12-2 中的TimePicker控件。
清单 12-2 。将定义对象添加到 TimePicker 控件的 controls.js 文件中
... **timePicker**: [ { type: "toggle", id: "showPeriod", title: "Show Period", value: true, useProxy: true, labelOn: "Yes", labelOff: "No" }, { type: "toggle", id: "clock", title: "24 Hour Clock", value: false, useProxy: true, labelOn: "Yes", labelOff: "No" }, { type: "input", id: "hourLength", title: "Hour Length", value: 2, useProxy: true }, { type: "input", id: "minuteLength", title: "Min Length", value: 2, useProxy: true }, { type: "span", id: "current", value: "Pick a Time", title: "Value" }], ...
最后,为了让用户能够通过导航条导航到TimePicker.html文件,我添加了templates.js文件,如清单 12-3 所示。
清单 12-3 。增加了通过导航栏导航到 TimePicker.html 文件的支持
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, ** { name: "TimePicker", icon: "\u0034" },** ]; ...
应用和配置时间选择器控件
通过将data-win-control属性设置为WinJS.UI.TimePicker,将TimePicker控件应用于div元素。TimePicker控件支持许多配置属性,我已经在表 12-2 中总结了这些属性,并且我已经为这些属性创建了配置控件。我将在接下来的小节中详细解释这些属性。
设置时钟类型
您可以使用clock属性选择将要显示的时间类型。如果该属性设置为12HourClock,TimePicker显示三个选择元素,允许用户选择小时、分钟和周期(AM或PM)。如果clock属性设置为24HourClock,那么只显示两个select元素,但是小时菜单中有 24 个项目,允许指定 24 小时时间。您可以使用标记为24 Hour Clock的控件更改示例中的clock属性,12 小时和 24 小时时钟显示如图图 12-2 所示。
***图 12-2。*设置时间选择器控制的时钟类型
设置分钟增量
minuteIncrement属性指定可以为分钟值选择的最小间隔。例如,将minuteIncrement属性设置为10将允许用户将分钟设置为0、10、20、30、40和50分钟。值15将允许0、15、30和45分钟。问题是在控件初始化后更改属性没有任何效果——您必须决定您想要的时间间隔,并使用data-win-options属性指定它有一个配置选项。
在这个例子中,我使用data-win-options属性将minuteIncrement属性设置为10,如下所示:
`...
你可以在图 12-3 的中看到这种效果,我点击了TimePicker控件的分钟部分来显示可用的增量。
***图 12-3。*在时间选择器控件上限制分钟增量
指定显示模式
通过hourPattern、minutePattern和periodPattern属性,您可以指定时间的各个组成部分的显示方式。实际上,他们只是让你指定使用多少字符,即使这样,你也必须努力工作。
名称空间包含对象,?? 控件用它来格式化和解析时间值。DateTimeFormatter支持一个全面的基于模板的系统来处理时间和日期。这里有一个例子:
{hour.integer}:{minute.integer(2)}:{second.integer(2)} {period.abbreviated}
该模板包含小时、分钟和秒的组件。分和秒用两个字符显示,周期用缩写形式显示(例如AM或PM)。
TimePicker控件中的模式属性作用于该模板的片段。您不能更改元素出现的顺序,但可以更改每个元素使用的字符数。因此,举例来说,如果您想确保小时总是使用 12 小时制显示,那么您可以将minutePattern属性设置为{hour.integer(2)}。括号字符({和})是值的必需部分,这意味着在使用data-win-options属性设置这些属性的值时必须小心——很容易混淆括号和引号字符的顺序。
考虑到您可以对这些属性进行的唯一更改是设置字符数,我认为让TimePicker控件负责将整数值转换成模板片段会更明智。实际上,您需要对日期/时间格式的底层工作有足够的了解,但这样做不会有任何好处。但是,正如我之前所说的,TimePicker控件并没有经过特别的考虑或实现。
我在示例的右面板中包含了两个input元素,让您可以更改小时和分钟元素使用的字符数。我通过代理对象更新了hourPattern和minutePattern属性,如清单 12-4 所示。
清单 12-4 。在整数值和模板片段之间转换
... ["hour", "minute"].forEach(function (item) { proxyObject.bind(item + "Length", function (val) { picker.winControl[item + "Pattern"] = "{" + item + ".integer(" + val + ")}"; }); }); ...
你可以在图 12-4 中的Hour Length和Min Length input元素中看到输入3的效果。
***图 12-4。*指定用于显示小时和分钟时间成分的字符数
以编程方式管理时间选择器
TimePicker控件没有定义任何方法。我包含这一部分只是为了保持与其他控件的一致性,这样您就不会认为它被错误地忽略了。
设置时间选择器控件的样式
TimePicker控件支持许多 CSS 类,这些类可以用来设计整个控件或其中一个元素的样式。我已经在表 12-3 中描述了类的集合。
在这个例子中,我使用Show Period配置控件通过win-timepicker-period类改变周期组件的可见性,如清单 12-5 所示。
清单 12-5 。使用 CSS 类定位 TimePicker 控件的组件
... proxyObject.bind("showPeriod", function (val) { ** $('.win-timepicker-period').setStyle("display", val ? "block" : "none");** }); ...
当clock属性被设置为12HourClock时,改变标记为Show Period的ToggeSwitch的状态将改变周期select元素的可见性。
响应 TimePicker 事件
当用户改变控件显示的时间时,TimePicker发出change事件。你可以看到我对TimePicker.html文件中的script元素所做的添加,以处理清单 12-6 中的change事件,在那里我显示了用户选择的时间,用TimePicker作为应用布局右侧面板中span元素的内容。
清单 12-6 。从 TimePicker 控件处理变更事件
`...
...`
TimePicker.current属性返回一个标准的 JavaScript Date对象,它允许我调用toLocaleTimeString方法来获得一个值,我可以安全地在布局中显示该值,如图图 12-5 所示,在这里我使用了选取器来选择晚上 9:50。
***图 12-5。*响应时间选择器控件的变更事件
使用日期选择器控件
DatePicker控件是对TimePicker的补充,允许用户选择日期。DatePicker与TimePicker控件有很多相似之处——可悲的是,包括一些不太有用的特征,比如模板片段。
DatePicker控件在结构和外观上与TimePicker控件非常相似,并为用户提供了三个select元素,用户可以用它们来选择日期。在图 12-6 中,你可以看到DatePicker是如何显示的,以及我生成的用来演示不同日期相关特性的配置控件。
***图 12-6。*日期选择器控件
何时使用 DatePicker 控件
DatePicker控件适合在您希望用户选择日期时使用。DatePicker控件使用设备区域设置来执行它的大部分日期格式化,并且像TimePicker一样,不提供覆盖区域设置的机制(尽管有一个使用不同种类日历的选项)。这意味着您应该只在一个应用中使用DatePicker,该应用已经在将要部署它的每个地区进行了彻底的测试。
演示日期选择器控件
我在第十章中开始的 Visual Studio 项目的pages文件夹中添加了一个名为DatePicker.html的文件。您可以在清单 12-7 中看到该文件的内容。
清单 12-7 。DatePicker.html 文件的内容
`
Templates.createControls(rightPanel, picker, "datePicker", proxyObject) .then(function () { proxyObject.bind("dateLength", function (val) { picker.winControl.datePattern = "{day.integer(" + val + ")}"; }); }); } });
Select a date:
**
这是另一个我需要代理对象特性的控件,我在第十章的中添加到示例框架中。DatePicker控件使用与TimePicker控件相同的模板模式系统,这意味着我需要代理对象来改变显示的日期格式(我将很快演示)。
您可以看到我如何在添加到/js/controls.js文件的定义对象中使用代理对象来演示清单 12-8 中的DatePicker控件。
清单 12-8 。将定义对象添加到 DatePicker 控件的 control.js 文件中
... **datePicker**: [ { type: "input", id: "dateLength", title: "Date Length", value: 2, useProxy: true }, { type: "select", id: "monthPattern", title: "Month Style", values: ["{month.full}", "{month.abbreviated}"], labels: ["Full", "Abbreviated"]}, { type: "select", id: "yearPattern", title: "Year Style", values: ["{year.full}", "{year.abbreviated}"], labels: ["Full", "Abbreviated"]}, { type: "select", id: "calendar", title: "Calendar", values: ["GregorianCalendar", "HebrewCalendar", "ThaiCalendar"], labels: ["Gregorian", "Hebrew", "Thai"]}, { type: "span", id: "current", value: "Pick a Date", title: "Value"}], ...
最后,为了让用户能够通过导航条导航到TimePicker.html文件,我添加了templates.js文件,如清单 12-9 所示。
清单 12-9 。增加了通过导航栏导航到 TimePicker.html 文件的支持
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, ]; ...
应用和配置日期选择器控件
通过将data-win-control属性设置为WinJS.UI.DatePicker,将DatePicker控件应用于div元素。默认情况下有三个下拉菜单(但是可以使用 CSS 类隐藏——请参见设计 DatePicker 控件一节的详细信息),允许用户选择日期的日、月和年组成部分。DatePicker控件支持表 12-4 中列出和描述的配置属性。
使用不同的日历
属性允许你指定一个日历给 ?? 使用。默认值来自设备区域设置。在本例中,我在右侧面板中添加了一个select元素,允许您选择GregorianCalendar、HebrewCalendar和ThaiCalendar值,其效果可以在图 12-7 的中看到。
***图 12-7。*使用不同的日历显示日期
我添加了对这些日历类型的支持,以展示可用的横截面。该属性支持的全套值为:GregorianCalendar、HijriCalendar、HebrewCalendar、JapaneseCalendar、KoreanCalendar、ThaiCalendar、TaiwanCalendar、UmAlQuraCalendar和JulianCalendar。
指定显示模式
DatePicker控件使用模板片段的方式类似于TimePicker控件,通过datePattern、monthPattern和yearPattern属性公开。
datePattern的格式是{day.integer( n )},其中n是用于显示日期的字符数。注意,虽然属性的名称是***date***Pattern,但是片段是 **day** .integer(即date对day)。对于月份和年份组件,您可以选择完整值和缩写值。表 12-5 显示了这两个属性的一组支持值。
我添加了三个配置控件来管理日期的显示方式。通过Date Length input元素,您可以更改用于显示日部分的字符数,通过Month Style和Year Style select元素,您可以看到完整和简化显示的样子。在图 12-8 中,您可以看到缩写的月份和年份设置是如何显示的。
***图 12-8。*显示月份和年份的缩写值
以编程方式管理日期选择器
DatePicker控件没有定义任何方法。我包含这一部分只是为了保持与其他控件的一致性,这样您就不会认为它被错误地忽略了。
设置 DatePicker 控件的样式
DatePicker控件支持许多 CSS 类,这些类可以用来设计整个控件或其中一个元素的样式。我已经在表 12-6 中描述了类的集合。
我没有在例子中使用任何样式,但是我发现它们对于隐藏控件中的单个组件很有用,这样用户可以指定一个不太精确的日期。因此,举例来说,如果我想让用户只选择月份和年份,我会在DatePicker.html文件中添加一个style元素,如清单 12-10 中的所示。
清单 12-10 。使用 CSS 类隐藏 DatePicker 控件的组件
`...
** .win-datepicker-date {** ** display: none;** ** }**...`
隐藏其中一个组件会导致控件被调整大小,如图 12-9 所示。
***图 12-9。*显示 DatePicker 控件中组件的子集
响应 DatePicker 事件
当用户选择一个新的日期时,DatePicker发出change事件。你可以看到我在DatePicker.html文件中添加了script元素来处理清单 12-11 中的change事件,这里我使用用户用DatePicker选择的日期来更新应用布局右侧面板中span元素的内容。
清单 12-11 。从 DatePicker 控件处理变更事件
`...
...`
DatePicker.current属性返回一个标准的 JavaScript Date对象,这允许我调用toLocaleDateString方法来获得一个我可以显示的值。
重访弹出控件
我在第七章的中向你介绍了WinJS.UI.Flyout控件,当时我向你展示了如何使用Flyout来响应AppBar命令。Flyout控件是一个通用的弹出菜单,可以在任何情况下使用,这就是为什么我要返回到这个控件,这样我就可以向你展示如何在应用栏之外使用它。我在这个部分的例子中使用了两个Flyout控件,其中一个你可以在图 12-10 中看到。右侧面板中的控件允许您配置可见的Flyout控件。
***图 12-10。*使用弹出控件
何时使用弹出控件
Flyout是一个通用控件,可以在任何想要在主布局之外呈现内容的情况下使用。Flyout有一些特殊的用途,比如使用AppBar(参见第七章)或者使用Menu控件(在本章稍后描述),但是除此之外,你可以在你的应用环境中做任何有意义的事情。对于简单的纯信息内容,考虑使用WinJS.UI.Tooltip控件,我在第十一章中描述了它,它需要更少的编程控制。
提示请务必阅读本章后面的使用弹出按钮进行用户交互一节,了解如何使用弹出按钮显示需要用户交互的元素,如
button元素和Rating控件。在这些情况下,Flyout有一些特点需要特别注意。
演示弹出控件
为了演示Flyout控件,我在 Visual Studio 项目的pages文件夹中添加了一个名为Flyout.html的新文件。你可以在清单 12-12 中看到这个文件的内容。这个文件比我添加到项目中的其他一些文件稍大,因为它包含了Flyout控件和我将在其中显示的内容。
清单 12-12 。Flyout.html 文件的内容
`
#rateButton, #flyoutInteractive button { font-size: 16pt; margin-top: 10px} ").then(function () { $("button").listen("click", function (e) { if (e.target.id == "rateButton") { flyoutInteractive.winControl.show(e.target); } else if (e.target.id == "flyoutInteractiveButton") { flyoutInteractive.winControl.hide(); } else if (e.target.innerText == "Show") { flyout.winControl.show(targetImg); } else if (e.target.innerText == "Hide") { flyout.winControl.hide(); } }); }); } });**
Apples
Apples grow on small, deciduous trees. Apples have been grown for thousands of years in Asia and Europe, and were brought to North America by European colonists.**
How much do you like apples?
为了创建您可以在图 12-10 的右侧面板中看到的配置控件,我将定义对象添加到了您可以在清单 12-13 中看到的controls.js文件中。
清单 12-13 。弹出控件的定义对象
... **flyout**: [ { type: "select", id: "placement", title: "Placement", values: ["top", "bottom", "left", "right"], labels: ["Top", "Bottom", "Left", "Right"]}, { type: "select", id: "alignment", title: "Alignment", values: ["left", "center", "right"], labels: ["Left", "Center", "Right"]}, { type: "buttons", labels: ["Show", "Hide"] }], ...
最后,为了让用户可以通过导航栏导航到Flyout.html文件,我添加了templates.js文件,您可以在清单 12-14 中看到。
清单 12-14 。将 Flyout.html 文件添加到导航栏
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, ** { name: "Flyout", icon: "\u0036" },** ]; ...
应用和配置弹出控件
WinJS.UI.Flyout控件应用于div元素。Flyout没有固定的内部结构,您可以创建任何元素和控件的组合来满足您的需求,包括用户可以与之交互的内容——稍后我将向您详细介绍这项技术。
显示弹出控件
Flyout定义了show和hide方法,可以用来控制控件的可见性。表 12-7 总结了这些方法以供快速参考。
show方法有一个强制参数,用它指定一个锚元素,它是Flyout所在的附近。show方法支持两个可选参数,允许您覆盖placement和alignment属性的值——我将在下一节解释这些属性的用途和值。
Flyout控件支持我在第十二章中提到的 light-dissolve 技术,当用户点击或触摸Flyout弹出窗口之外的显示屏时,它会自动消失。您可以通过调用hide方法显式地解除Flyout,但是这只在您响应Flyout中的交互控件时有用,我将很快演示这一点。
示例中有两个Flyout控件,您可以通过单击右侧面板中的Show和Hide按钮来查看如何应用show和hide方法。点击Show按钮会出现一个显示一些静态文本的Flyout(这是图 12-10 中的Flyout)。当点击Show按钮时,我使用标记中的img元素作为锚元素,这就是为什么Flyout位于图中图像的正上方。您可以通过点击Hide按钮或点击Flyout之外的应用布局中的任意位置来关闭Flyout。
提示点击
Rate按钮,显示示例中的另一个Flyout。在本节稍后讨论使用Flyout控件向用户呈现交互式内容时,我会回到这个控件。
配置弹出控件
正如您现在所期望的,有许多由Flyout控件定义的属性,您可以使用它们来改变它的行为。表 12-8 总结了这些特性。
设置anchor属性没有用,因为该值将被show方法所需的强制参数替换。但是,您可以读取该属性的值来查看Flyout锚定到了哪个元素。
您可以使用placement属性覆盖Flyout的默认位置。这与Tooltip控件的工作方式相同,支持的值为top、bottom、left和right。这些值相对于传递给show方法的锚元素。
如果placement属性是top或bottom,您可以使用alignment属性进一步细化位置,该属性接受值left、right和center。
我在示例中添加到右侧面板的select元素允许您更改由Show和Hide按钮控制的Flyout的placement和alignment属性。选择您需要的数值组合,点击Show按钮,查看Flyout是如何定位的。Flyout将自动重新定位,使其完全适合屏幕——然而,这可能意味着锚定元素可能会被Flyout遮挡。
在图 12-11 的中可以看到alignment的left和right值、placement的top值。
***图 12-11。*放置属性左右值的效果
设计弹出控件的样式
Flyout控件支持一个 CSS 类:win-flyout。您可以使用该类作为设计显示内容样式的起点。没有其他类,因为在Flyout控件中没有固定的结构。我倾向于不使用这个类,更喜欢将样式直接应用于我在Flyout控件中显示的内容。在清单 12-15 中,你可以看到我添加到使用win-flyout类的Flyout.html文件中的style元素。
清单 12-15 。设计弹出控件的样式
`...
.win-flyout { text-align: center }...`
这种样式的效果是将所有Flyout控件中的文本居中。你可以在图 12-12 中看到它的效果。
***图 12-12。*对弹出控件应用样式的效果
处理弹出事件
Flyout控件支持我在表 12-9 中描述的四个事件。我在示例中没有使用这些事件,但是在处理包含交互内容的Flyout控件时,beforehide和afterhide事件会很有用——更多信息请见下一节。
使用弹出按钮进行用户交互
示例应用中的第二个Flyout显示需要用户交互的控件——你可以在图 12-13 中看到这些控件是如何显示的。您可以使用一个Flyout来显示任何内容,但是当您从用户那里收集数据时,有一些特殊的考虑。
***图 12-13。*包含互动内容的弹出按钮
在这个例子中,我的Flyout包含一个Rating控件,允许用户表达他们喜欢苹果的程度。当用户单击示例应用左侧面板中的Rate按钮时,会显示Flyout。你必须小心设计你的Flyout交互来提供一致和流畅的用户体验。在接下来的部分中,我描述了我所遵循的并且推荐您采纳的指导原则。
确保一致的显示和隐藏
我总是小心翼翼地确保用户可以使用与显示相同的交互类型来隐藏Flyout。如果点击button导致Flyout出现,那么我确保在Flyout内容中有一个按钮可以隐藏弹出窗口。这是我在本章的例子中采用的方法:点击Rate按钮会显示Flyout,点击Close按钮会隐藏弹出窗口。我不想干涉灯光消失功能,这使得这个按钮成为隐藏Flyout的其他方式的补充,而不是替代。
保持弹出型按钮简单
我使用Flyout来执行小而简单的用户交互。对于复杂的 HTML 风格的表单,我不使用Flyout s,在这些表单中有大量的数据值需要从用户那里收集,并且这些值之间存在依赖关系。在这种情况下,使用导航到应用的不同部分,并在专用布局中收集数据,让用户清楚这是一个导入交互。
立即响应数据值
一旦用户在Flyout中向我提供信息,我就更新我的视图模型和应用状态。在这个例子中,我会从Rating控件中响应change事件,而不是等到Flyout被解除后才根据用户的意见更新应用。(我两者都没有做过,因为这是一个关于Flyout控件的例子,但是您已经明白了)。
使撤销或重做弹出型交互变得容易
我试图使Flyouts中的交互尽可能无摩擦,并允许用户轻松地更改他们输入的数据或返回到根本没有数据输入的状态。在很大程度上,这意味着我试图让用户非常清楚如何显示特定的Flyout,并且我利用 WinJS 控件特性和数据绑定来允许用户更改或删除数据。在这个例子中,允许用户删除数据需要我将Rating控件的enableClear属性设置为true。
我也避免提示用户“您确定吗?”值被更改或删除时的消息。让更改值变得容易,并通过在应用中立即反映新值,使得检查用户的意图变得多余。
清楚地发出破坏性行动的信号
与使数据值更改变得容易相对应的是,当用户将要执行一个不可撤销的破坏性操作时,比如不可逆地删除一个文件,我使它变得非常明显。在这些情况下,我要求用户使用显示在Flyout中的button给我一个明确的确认,并把一个轻解雇作为一个取消。尽管如此,我还是让用户很容易地将破坏性的动作组合在一起,这样我就不会提示用户确认每个单独的项目应该被销毁。
总结
在这一章中,我已经展示了四个 WinJS UI 控件。这些比我在上一章描述的更复杂,但是它们可以更广泛地应用,并且在Flyout控件的情况下,可以形成你的应用结构的关键部分。在下一章,我将展示如何使用Menu控件,并演示一些不属于 WinJS 名称空间的 UI 控件。
十三、使用菜单和对话框
在这一章中,我将向你展示如何创建两种弹出界面控件。第一个是WinJS.UI.Menu控件,用于创建上下文菜单,允许用户直接在应用布局中的元素上执行操作。Menu控件很灵活,有一些有用的特性,但是我发现用户激活上下文菜单的机制不太明显,这意味着使用这个控件时需要仔细考虑。
我在本章中描述的第二个控件与我在本书中描述的其他控件略有不同。MessageDialog控件是Windows名称空间的一部分,它没有我为 WinJS UI 控件描述的共同特征:例如,它不适用于 HTML 元素,也没有winControl属性。任何 Windows 应用开发语言都可以使用MessageDialog控件,这使得它的使用稍微有些不同。但是,它提供了一个用户交互,这是使用任何 WinJS 控件都无法获得的,这使得掌握它的努力是值得的。表 13-1 对本章进行了总结。
使用菜单控制
Menu控件提供弹出上下文菜单,该菜单被构造为向用户提供一个或多个由MenuCommand控件表示的命令。MenuCommand和Menu控件之间的关系类似于AppBarCommand和AppBar之间的关系,我在第七章的中描述过。
默认情况下不显示Menu控件,所以本例中左侧面板中的主要元素是一个图像。如果用鼠标右键单击或触摸并按住图像,将会触发contextmenu事件。我通过使Menu控件出现来处理这个事件,如图图 13-1 所示。
***图 13-1。*显示菜单控件
何时使用菜单控制
为用户提供命令的主要机制是使用 AppBar,我在第七章中描述过。Menu控件提供了一种回退机制,当用户想要操作的对象不适合 AppBar 模型时,可以使用这种机制。坦率地说,这是一个非常主观的决定,我在项目中遵循的规则是尽可能地支持应用栏,因为并非所有用户都意识到 Windows 应用中可以使用上下文菜单。
命令应该在你的应用中只出现一次,这意味着你不应该在一个Menu上重复应用栏支持的命令。微软建议在一个Menu上最多使用 5 个命令,尽管 Windows 目前没有强制限制。
演示菜单控制
我在示例应用中添加了一个pages/Menu.html文件来演示Menu和MenuCommand控件,你可以在清单 13-1 中看到该文件的内容。这是一个很长的列表,因为在这个例子中有两个Menu控件和许多菜单命令。
清单 13-1 。Menu.html 文件的内容
`
Templates.createControls(rightPanel, menu, "menu", proxyObject);
["placement", "alignment"].forEach(function (propName) { proxyObject.bind(propName, function (val) { menu.winControl.placement = proxyObject.placement; menu.winControl.alignment = proxyObject.alignment; borderMenu.winControl.placement = proxyObject.placement; borderMenu.winControl.alignment = proxyObject.alignment; }); });
targetImg.addEventListener("contextmenu", function (e) { menu.winControl.show(e.target); e.preventDefault(); });
$("#menu, #borderMenu").listen("click", function (e) {
if (WinJS.Utilities.hasClass(e.target, "background")) {
targetImg.style.backgroundColor =
e.target.winControl.label.toLowerCase();
} else if (e.target.winControl
&& e.target.winControl.id == "menuCmdShowBorder") { var showBorder = e.target.winControl.selected;
if (!showBorder) {
targetImg.style.border = "none";
}
this.winControl.getCommandById("menuCmdBorderColor").disabled =
!showBorder
} else if (e.target.hasAttribute("data-color")) { targetImg.style.border = "medium solid " + e.target.getAttribute("data-color"); } }); } });
`为了在图 13-1 的的右面板中创建配置控件,我对清单 13-2 中的文件进行了添加。
清单 13-2 。菜单控件的定义对象
... **menu**: [{ type: "select", id: "placement", title: "Placement", values: ["top", "bottom", "left", "right"], labels: ["Top", "Bottom", "Left", "Right"], useProxy: true}, { type: "select", id: "alignment", title: "Alignment", values: ["left", "center", "right"], labels: ["Left", "Center", "Right"], useProxy: true}], ...
为了允许用户导航到Menu.html文件,我添加了templates.js文件,如清单 13-3 所示。正如你在《??》第七章中回忆的那样,这些条目用于在应用的导航条上生成命令,以支持内容页面之间的导航。
清单 13-3 。将 Menu.html 添加到导航栏
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, { name: "Flyout", icon: "\u0036" }, ** { name: "Menu", icon: "\u0037" },** ]; ...
应用和配置菜单控件
WinJS.UI.Menu控件应用于div元素,并与AppBar和Flyout控件共享一些共同的特性和功能。使用MenuCommand控件在Menu中定义命令,该控件应用于button和hr元素,就像我在第七章的中描述的AppBarCommand控件一样。Menu控件支持与Flyout控件相同的配置属性集。表 13-2 总结了这些特性。
一个Menu的默认位置就在锚元素的正上方(如果有足够的屏幕空间)。您可以使用示例右侧面板中的select元素来更改菜单的位置。在图 13-2 中,您可以看到placement属性的left和right值的效果。
***图 13-2。*菜单放置属性的左右值的效果
注意如果你习惯于 Windows 桌面的上下文菜单,那么
Menu弹出窗口的位置看起来会有点奇怪——就好像Menu与用户交互的元素是断开的。我发现这非常烦人,于是我花了一些时间研究如何移动弹出窗口,使它显示在我点击鼠标的地方旁边。在花了一些时间使用 WinJS Menu控件后,我开始意识到默认位置很有意义,因为它允许用户立即看到他们选择的命令的效果。这是我将在本章后面回到的内容,但是我建议您保持Menu的定位不变。
显示菜单
当用户用鼠标右键单击或触摸并按住屏幕时,Windows 会触发contextmenu事件。不幸的是,这是显示 NavBar 和 AppBar 的同一个事件,所以你需要确保在传递给你的处理函数的事件上调用preventDefault方法,如清单 13-4 所示。重要的是,只在那些你将显示Menu控件的元素上处理contextemenu事件,并让应用显示所有其他元素的导航栏和应用栏。
清单 13-4 。显示菜单控件并阻止默认行为
... targetImg.addEventListener("contextmenu", function (e) { menu.winControl.show(e.target); ** e.preventDefault();** }); ...
定义菜单命令
使用MenuCommand控件定义显示在Menu上的命令。MenuCommand控件支持由AppBarCommand控件定义的属性子集,我在表 13-3 中总结了这些属性。我不打算详述所有这些属性,因为你可以在第七章中看到它们的影响。有几项技术值得指出,我将在接下来的章节中解释这些技术。否则,您可以从清单中看到如何在Menu控件中创建项目。
提示你可以在
Menu中创建一个分隔符,将相关命令组合在一起。为此,将MenuCommand控件应用于hr元素,并使用data-win-options属性将type属性设置为separator。你可以在清单 13-1 中看到一个分隔符的例子。
创建菜单序列
您可以配置一个MenuCommand控件,当它被选中时显示另一个菜单,创建一个菜单链,允许用户在一组复杂的选项中导航。使用type和flyout属性将菜单关联在一起。你可以在清单 13-5 中的示例应用中看到我是如何做到这一点的。
清单 13-5 。使用弹出属性将方法链接在一起
... <button data-win-control="WinJS.UI.MenuCommand" class="border" data-win-options="{id: "menuCmdBorderColor", label:"Border Color", **type:"flyout", flyout:"borderMenu"**}"> </button> ...
对于这个标签为Border Color的MenuCommand,我将type属性设置为flyout,将flyout属性设置为另一个应用了Menu控件的元素的id。你可以在清单 13-6 中看到第二个Menu的定义。
清单 13-6 。应用第二个菜单控件
`...
结果是当您从第一个Menu中选择Border Color项时,显示第二个Menu。你可以在图 13-3 中看到效果。第二个Menu代替了第一个,而不是出现在它旁边。
***图 13-3。*将菜单链接在一起
提示如果你愿意,你可以用一个
Flyout控件代替第二个Menu。这对于向用户提供太复杂而不能用一组菜单项来处理的选项是有用的。也就是说,如果你需要一个Flyout,那么你可能要重新考虑你的策略,看看是否有一种更简单的方式向用户展示你的命令。
创建互斥的菜单项集合
我最常用MenuCommand来创建互斥的菜单项。我想在示例应用中创建两个这样的集合:第一个是主菜单中的Red、White和Green项目,第二个集合由较小的二级菜单中的Red、Black Border和White Border项目组成。
第一组对应于用于img元素的背景颜色,第二组对应于边框颜色。在这两种情况下,我都希望选择代表当前设置的MenuCommand,并在单击其他项目时保持该选择的最新状态。互斥菜单项易于设置,但这是一个手动过程,并且在Menu或MenuCommand控件中没有特定的支持。
技术很简单——我需要将MenuCommand控件上的selected属性设置为用户选择的项目的true,并将同一互斥集合中所有其他MenuCommand的属性设置为false——最有效的方法是使用WinJS.Utilities.query方法定位给定集合中的所有元素。为了帮助我做到这一点,我已经确保每组中的MenuCommand控件都有一个我可以轻松识别的共同特征。对于主菜单中的Red、White和Green项,我将MenuCommand控件应用到的所有按钮元素分配给了背景类,如下所示:
`... <button data-win-control="WinJS.UI.MenuCommand" class="background" data-win-options="{id: "menuCmdRed", label:"Red"}">
<button data-win-control="WinJS.UI.MenuCommand" class="background" data-win-options="{id: "menuCmdWhite", label:"White"}">
<button data-win-control="WinJS.UI.MenuCommand" class="background" data-win-options="{id: "menuCmdGreen", label:"Green"}"> ...`
对于其他菜单中使用的button元素,我添加了一个自定义的data-*属性,如下所示:
... <button data-win-control="WinJS.UI.MenuCommand" **data-color="red"** data-win-options="{id: "menuCmdRedBorder", label:"Red Border"}"> </button> <button data-win-control="WinJS.UI.MenuCommand" **data-color="black"** data-win-options="{id: "menuCmdBlackBorder", label:"Black Border"}"> </button> <button data-win-control="WinJS.UI.MenuCommand" **data-color="white"** data-win-options="{id: "menuCmdWhiteBorder", label:"White Border"}"> </button> ...
您通常会坚持一个可识别的特征,但是我想向您展示我最常用的两种方法。现在我可以很容易地用 CSS 选择器查询文档来定位集合中的所有MenuCommand,我可以更新Menu.html文件中的script元素来在用户选择菜单项时设置selected属性,如清单 13-7 所示。
清单 13-7 。确保菜单命令控件组上的互斥
... $("#menu, #borderMenu").listen("click", function (e) { ` if (WinJS.Utilities.hasClass(e.target, "background")) {
targetImg.style.backgroundColor = e.target.winControl.label.toLowerCase();
** WinJS.Utilities.query("button.background").forEach(function (menuButton) {**
** menuButton.winControl.selected = (menuButton == e.target);**
** });**
} else if (e.target.winControl && e.target.winControl.id == "menuCmdShowBorder") {
var showBorder = e.target.winControl.selected; if (!showBorder) { targetImg.style.border = "none"; } this.winControl.getCommandById("menuCmdBorderColor").disabled = !showBorder
} else if (e.target.hasAttribute("data-color")) { targetImg.style.border = "medium solid " + e.target.getAttribute("data-color"); ** WinJS.Utilities.query("button[data-color]").forEach(function (menuButton) {** ** menuButton.winControl.selected = menuButton == e.target;** ** });** } }); ...`
你可以在图 13-4 中看到在主菜单上选择一个项目然后选择另一个项目的效果。
***图 13-4。*创建互斥的菜单命令控件组
以编程方式管理菜单
Menu控件支持的方法类似于AppBar控件定义的方法,如表 13-4 所示,我在这里已经描述了这些方法。
我倾向于不使用显示和隐藏菜单命令的方法,因为它们让我觉得我的Menu结构太复杂了。我也更喜欢在Menu上保留相同的命令集,并简单地禁用那些目前不可用的命令。这就是我在示例中所做的。在选中边框命令之前,边框颜色命令是未选中的,如图图 13-5 所示。我宁愿清楚地表明命令确实存在,但并不适用,也不愿给出一组不断变化的命令。
***图 13-5。*禁用菜单命令而不是隐藏它
设计菜单控件的样式
Menu控件支持两个 CSS 类进行样式化,如表 13-5 中所述。
要将样式应用于Menu和MenuCommand控件,您需要看看微软用来创建控件的 HTML 和 CSS,并设计一个选择器来覆盖默认情况下添加到 Visual Studio 应用项目中的ui-light.css和ui-dark.css文件中定义的默认值。在清单 13-8 中,你可以看到我如何应用一个样式来改变被禁用的MenuCommand控件的显示颜色。
清单 13-8 。使用 win-menu CSS 类来设计 MenuCommand 控件的样式
`...
**#menu button.win-command:disabled**{ background-color: lightgray; color: gray; }...`
我在示例中使用了一个Menu控件的id属性值,以确保我定义的样式比ui-dark.css文件中的样式更具体。你可以在清单 13-9 中看到默认情况下决定背景颜色的样式(我使用我在第二章中介绍的DOM Explorer窗口来决定)。
清单 13-9 。设置被禁用的 MenuCommand 的背景颜色的默认样式
.win-menu.win-ui-light button:focus, .win-ui-light .win-menu button:focus, .win-menu.win-ui- light button:active, .win-ui-light .win-menu button:active { background-color: rgb(222, 222, 222); }
您可以看到类.win-ui-light是如何被使用的。这并不理想,但是一旦知道它的存在,解决这个额外的特性就足够简单了。你可以在图 13-6 中看到我定义的样式的效果。
***图 13-6。*改变被禁用的菜单命令控件的颜色
处理菜单事件
在处理菜单控件时,感兴趣的主要事件是click事件,当用户点击它时,该事件由MenuCommand控件触发,表示菜单项已被选择——您可以在清单 13-1 中看到我是如何处理该事件的,在清单 13-7 中也是如此。
Menu控件本身支持我在表 13-6 中描述的四个事件,这也是Flyout控件支持的四个事件。我还没有找到这些事件的令人信服的用途,这就是为什么我没有把它们包括在Menu控件的例子中。
使用 MessageDialog 控件
我在本书中描述的所有其他 UI 控件都在WinJS.UI名称空间中,并且是用 JavaScript 编写的。然而,有一个控件在这个名称空间之外,但它在创建遵循 Windows 外观的应用时非常有用。这个控件是Windows.UI.Popups.MessageDialog控件,你用它向用户显示一个对话框。你可以在图 13-7 中看到MessageDialog的例子。
***图 13-7。*消息对话框 UI 控件
注意
Windows.UI.Popups命名空间也包含了PopupMenu控件。我没有在本书中描述这个控件,因为它与我在上一节中描述的WinJS.UI.Menu控件具有相同的功能。
何时使用 MessageDialog 控件
当你需要让用户注意到一些重要的事情或者当你需要做出一个重要的决定时,MessageDialog是很有用的。关键词是重要,因为当使用MessageDialog控件时,应用布局变暗,对话框显示在整个屏幕上。所有用户交互都被阻止,直到用户关闭对话框(通过按下Escape或Enter键)或点击/触摸其中一个对话框按钮。
我认为MessageDialog是最后的 UI 控件,因为它打断了用户的工作流程——这与 Windows 广泛的设计精神背道而驰。
您可以在由MessageDialog控件显示的对话框窗口中添加多达三个按钮,这意味着您无法为用户提供细微的选择。这意味着你不仅在打断用户,而且在强迫他们做出一个清晰明确的决定,通常是一个yes / no的选择。
你应该谨慎而不情愿地使用MessageDialog。在我自己的应用项目中,我只在用户将要启动一个会导致永久、不可恢复的数据丢失的操作时使用MessageDialog,比如删除文件或其他数据。我建议你使用同样的约束——不仅因为MessageDialog是侵入性的,而且因为如果你太自由地使用它,用户将学会不读它就关闭对话框——增加了他们忽略真正重要的信息的机会。
提示你可以使用一个
Flyout向用户显示一个对话框,但是它不会自动使布局的其余部分变暗,也不会像 MessageDialog 控件那样阻止用户交互。
演示消息对话框控件
为了演示MessageDialog控件,我在 Visual Studio 项目的pages文件夹中添加了一个名为MessageDialog.html的新文件。你可以在清单 13-10 中看到这个文件的内容。
清单 13-10 。MessageDialog.html 文件的内容
`
#showButton {font-size: 20pt} + "nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in " + "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " + "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + " culpa qui officia deserunt mollit anim id est laborum.";WinJS.UI.Pages.define("/pages/MessageDialog.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ delay: false, title: true, addcommands: true, });
Templates.createControls(rightPanel, showButton, "messagedialog",
proxyObject).then(function () {
showButton.addEventListener("click", function (e) {
** var md = new winpop.MessageDialog(loremipsum);** if (proxyObject.title) { md.title = "Caution"; } if (proxyObject.delay) { md.options = winpop.MessageDialogOptions.acceptUserInputAfterDelay; }
if (proxyObject.addcommands) { ["Yes", "No", "Help"].forEach(function (text) { md.commands.append(new winpop.UICommand(text)); }); md.defaultCommandIndex = 0; md.cancelCommandIndex = 1; }
md.showAsync().then(function (command) { commandSpan.innerText = command.label; }); }); }); } });
为了在布局的右面板中生成我需要演示的配置控件MessageDialog控件,我已经将清单 13-11 中所示的定义对象添加到了/js/controls.js文件中。
清单 13-11 。MessageDialog 控件的定义对象
... **messagedialog**: [ { type: "toggle", id: "delay", title: "Ignore Input", value: false, labelOn: "Yes", labelOff: "No", useProxy: true }, { type: "toggle", id: "title", title: "Title", value: true, labelOn: "Yes", labelOff: "No", useProxy: true }, { type: "toggle", id: "addcommands", title: "Add Commands", value: true, labelOn: "Yes", labelOff: "No", useProxy: true }, { type: "span", id: "commandSpan", value: "<Ready>", title: "Command" }], ...
最后,我向/js/templates.js文件添加了如清单 13-12 所示的内容,这样用户就可以使用应用的导航条导航到MessageDialog.html文件。
清单 13-12 。启用 MessageDialog.html 文件导航
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, { name: "Flyout", icon: "\u0036" }, { name: "Menu", icon: "\u0037" }, ** { name: "MessageDialog", icon: "\u0038" },** ]; ...
你可以在图 13-8 中看到这些增加的布局。与本书这一部分中的其他控件一样,您可以使用布局右侧面板中的控件来配置MessageDialog控件,以探索我在接下来的章节中描述的特性。
***图 13-8。*MessageDialog.html 文件的布局
使用 MessageDialog 控件
MessageDialog控件的工作方式与 WinJS UI 控件不同。该控件没有应用于 HTML 元素,也没有winControl属性。相反,您创建一个Windows.UI.Popups.MessageDialog对象并使用它定义的属性来配置对话框并显示给用户。MessageDialog控件支持表 13-7 中显示的属性和方法,我将在下面的章节中描述和演示。
创建基本对话框
显示对话框最简单的方法是创建一个新的MessageDialog对象并调用showAsync方法。构造函数MessageDialog有一个强制参数,它是一个字符串,包含应该在对话框中显示的内容。有一个可选参数,用作title属性的值。
您可以使用示例应用看到最基本的对话框。将右侧面板中的所有ToggleSwitch控件设置为No并点击Show MessageDialog按钮。你可以在图 13-9 中看到结果。(我编辑了这一部分的图片,以便更容易看到细节)。
***图 13-9。*用消息对话框控件创建的基本对话框
当您创建一个基本对话框时,MessageDialog控件会为您添加一个Close按钮,当它被点击时会自动关闭对话框。你可以在图 13-10 中看到给对话框添加一个标题的效果——这个效果是我在例子中通过将Title ToggleSwitch设置为Yes实现的。
***图 13-10。*一个带有标题的基本对话框
添加自定义命令
您可以通过放置最多带有三个自定义按钮的Close按钮来自定义对话框。这些按钮使用Windows.UI.Popups.UICommand对象指定,该对象定义了表 13-8 中所示的属性。
在示例应用中,您可以通过将标记为Add Commands的ToggleSwitch设置为Yes来向对话框添加命令。我通过创建新的UICommand对象并对从MessageDialog对象的commands属性中获取的对象使用append方法来添加命令。我重复了清单 13-13 中的例子中的相关语句。
清单 13-13 。向对话框添加命令
... if (proxyObject.addcommands) { ** ["Yes", "No", "Help"].forEach(function (text) {** ** md.commands.append(new winpop.UICommand(text));** ** });** md.defaultCommandIndex = 0; md.cancelCommandIndex = 1; } ...
命令按照添加的顺序显示,你可以在图 13-7 中看到添加这些命令按钮的效果。
确定点击的命令
您可以使用从showAsync方法返回的对象来确定用户单击哪个按钮来关闭对话框。showAsync方法实际上并不返回一个WinJS.Promise对象,但是它返回的对象定义了then、cancel和done方法,通常可以像Promise一样使用。没有取回WinJS.Promise对象的原因是MessageDialog控件不是 WinJS 库的一部分,并且不知道 JavaScript 如何在应用中工作。该对象可以像Promise一样使用的原因是因为微软努力确保所有 Windows 应用开发语言的 API 的一致性,并以 JavaScript 可以使用的方式包装异步操作。
当您对从showAsync方法返回的对象使用then方法时,您的函数被传递给代表用户单击的对话框按钮的UICommand。在清单 13-14 中,你可以看到我如何使用这个特性来设置示例布局右侧面板中span元素的内容,这里我复制了示例应用中的相关代码。
清单 13-14 。确定用户点击了哪个对话框按钮
... md.showAsync().then(function (command) { commandSpan.innerText = command.label; }); ...
从showAsync方法中获得的类似于Promise的对象直到用户关闭对话框后才会实现。
设置默认命令
您可以使用defaultCommandIndex和cancelCommandIndex来指定当用户点击Enter或Escape键时将被传递给then功能的UICommand。这些属性被设置为使用commands属性添加的UICommands对象的索引,你可以在清单 13-15 的例子中看到我是如何设置这些属性的。
清单 13-15 。指定默认命令
... if (proxyObject.addcommands) { ["Yes", "No", "Help"].forEach(function (text) { md.commands.append(new winpop.UICommand(text)); }); md.defaultCommandIndex = 0; md.cancelCommandIndex = 1; } ...
您的then函数将在您指定的索引处被传递给UICommand,即使用户没有明确点击任何对话框按钮。在清单中,我指定的索引值意味着使用Enter键相当于单击Yes按钮,使用Escape键相当于单击No按钮。
注意确保你始终如一地使用默认命令功能,这样
Escape键总是取消,Enter键总是确认你呈现给用户的动作或决定。
延迟响应用户交互
MessageDialog.options属性允许你使用来自Windows.UI.Popup.MessageDialogOptions对象的值配置控件的行为,在表 13-9 中有描述。出于某种原因,MessageDialogOptions对象只定义了一个特定行为的值,即当对话框被MessageDialog对象显示时,在一小段时间内禁止用户交互。
您可以通过将标签为Ignore Input的ToggleSwitch设置为true来应用示例应用中的acceptUserInputAfterDelay行为。当你点击Show MessageDialog时,对话框上显示的命令按钮将被暂时禁用,防止命令被点击。几秒钟后,按钮被激活,允许用户像往常一样与对话框交互。
总结
在下一章,我将返回到WinJS.UI名称空间并描述FlipView控件。这是第一个也是最简单的 WinJS 数据驱动控件,它提供了由数据对象集合支持的功能。正如你将了解到的,数据驱动控件建立在我在第八章中介绍的数据绑定和模板特性的基础上,以便灵活地向用户显示那些数据项。
十四、使用FlipView控件
在这一章中,我将描述FlipView控件,它是 WinJS 数据驱动的 UI 控件之一。数据驱动控件从数据源获取内容,并监控数据源以确保它们显示最新的可用数据。FlipView控件一次显示数据源中的一个项目,并允许用户通过单击按钮或做出翻转触摸手势在它们之间移动。
在本章中,我将向您展示如何为数据驱动控件准备数据源,并将其与用于向用户呈现每一项的模板相结合。您将看到 WinJS 的许多特性是如何在一个数据驱动控件中结合在一起的,包括可观察数组、数据绑定和模板。我还将向您展示这些特性是如何相互作用导致棘手的问题的。最后,我将介绍一下FlipView控件在从一个数据项移动到下一个数据项时使用的动画,作为在第十八章中全面讨论这个主题的前奏。表 14-1 对本章进行了总结
使用 FlipView 控件
数据驱动的 WinJS UI 控件使用模板来呈现数据源中的项。在这一章中,我将使用一个FlipView来执行一个非常普通的任务——一次显示一组照片中的一张图片。你可以在图 14-1 中看到FlipView是如何出现的。如同本书这一部分的其他章节一样,我使用我在第十章中创建的框架创建了如图所示的布局。
***图 14-1。*用来显示一组照片的 FlipView 控件
有三个数据驱动的 WinJS UI 控件,但我选择了FlipView作为第一个描述,因为它相对简单,但基于许多有用和重要的功能。我可以相对快速地处理控件的使用,然后将重点放在一些关键的基础上,这些基础与我稍后描述的更复杂的控件是相同的。
数据源和接口
WinJS 定义了一组指定数据源功能的接口。如果您使用的是标准的数据源对象,您就不需要担心这些接口,这些对象适用于大多数项目。然而,如果您想要创建一个定制的数据源,那么您需要理解这些接口所扮演的角色。
首先要理解的是,接口的概念在 JavaScript 中没有任何意义。这是您可以看到 JavaScript 和其他 Windows 应用编程语言的契合之处之一。在像 C#这样的语言中,定义一个接口意味着列出特定用途所需的抽象功能。实现接口的程序员实际上同意以某种方式实现该功能。编译器检查以确保接口的所有方面都已实现,如果没有实现,则报告一个错误。在用强类型语言创建松散耦合的软件系统时,接口是一种有用的技术。
接口的想法依赖于不适合 JavaScript 的假设,但是微软需要阐明一种可以描述一组预期的属性和方法的方式,所以我们有了这个想法,但是没有接口的实现。当我提到接口时,我指的是一个必需成员的列表,如果你希望它适合特定的用途,比如作为像FlipView这样的 UI 控件的数据源,你必须在你的对象中实现这些成员。
何时使用 FlipView 控件
每当您需要一次显示一项内容时,就可以使用FlipView控件。在这一章中,我展示了这个控件最常见的用法,它允许用户在媒体中翻页——在这个例子中是一些怒目而视者的照片。然而,FlipView控件可以显示任何内容,并且有一个很好的模板系统,可以让你控制每个项目如何呈现给用户。在后面的章节中,你可以看到使用其他类型的数据来驱动 WinJS UI 控件的例子。
创建 FlipView 控件示例
数据驱动控件比WinJS.UI名称空间中的其他控件更复杂,因此我采用了一种稍微不同的方法来创建演示FlipView控件的代码。我将分解示例代码,使 HTML、CSS 和 JavaScript 位于不同的文件中,更重要的是,在本节结束时,示例不会完成。我将在解释数据源和使用它们的 UI 控件的一些核心特征和特性时完成这个例子。
首先,我在 Visual Studio 项目的pages文件夹中创建了一个名为FlipView.html的新文件,其内容可以在清单 14-1 中看到。这个文件包含创建一个基本的FlipView控件的标记,但是它不包含任何对数据源的引用,而数据源是让FlipView显示数据所必需的。
清单 14-1 。FlipView.html 文件的初始内容
`
** ** ** **
您可以在清单中看到我使用data-win-control应用了FlipView控件的元素。您还可以看到我为 CSS 和 JavaScript 文件添加的link和script元素。这个/css/flipview.css文件包含了一些我需要用来控制FlipView控件布局的样式,以及我将用它来显示的数据。您可以在清单 14-2 中看到 flipview.css 文件的内容。
清单 14-2 。/css/flipview.css 文件的内容
#flip { width: 400px; height: 400px } .flipItem img { height: 400px } .flipTitle { position: absolute; color: black; bottom: 2px; font-size: 30pt; width: 100%; padding: 10px; background-color: rgba(255, 255, 255, 0.6)} .renderDiv { border: thick solid white; height: 200px} .renderDiv img { height: 200px; width: 200px } .renderDiv div { text-align: center; font-size: 30pt }
您可以在清单 14-3 中看到 JavaScript 文件的内容。我想将数据驱动控件的代码与 JavaScript 的其余部分分开,所以我创建了/js/pages文件夹,然后在那里添加了flipview.js文件——这就是您可以在清单中看到的文件。
清单 14-3 。flipview.js 文件的内容
`(function() {
WinJS.UI.Pages.define("/pages/FlipView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ itemTemplate: null, customAnimations: false });
Templates.createControls(rightPanel, flip, "flipView", proxyObject) .then(function () {
}); } }); })();`
目前,flipview.js文件只包含代理对象,我将用它来响应一些配置控件和对将创建它们的Templates.createControls方法的调用。我将在本章中添加代码,解释数据驱动控件的不同特性,特别是FlipView控件。在清单 14-4 中,你可以看到我添加到controls.js文件中的定义对象,以创建图 14-1 右侧面板中显示的配置控件。为了与本书这一部分的其他章节保持一致,我将使用这些控件来演示FlipView控件的关键特性。
清单 14-4 为 FlipView 控件添加到 controls.js 文件中的定义对象
... flipView: [ { type: "select", id: "itemTemplate", title: "Template", values: ["HTML", "Function"], useProxy: true}, { type: "select", id: "orientation", title: "Orientation", values: ["horizontal", "vertical"], labels: ["Horizontal", "Vertical"]}, { type: "input", id: "itemSpacing", title: "Item Spacing", value: 10 }, { type: "span", id: "currentPage", value: 0, title: "Current Page" }, { type: "buttons", title: "Move", labels: ["Previous", "Next"] }, { type: "span", id: "itemCount", value: 4, title: "Count" }, { type: "buttons", title: "Change Data", labels: ["Add", "Remove"] }, { type: "toggle", id: "customAnimations", title: "Custom Animations", value: false, useProxy: true, labelOn: "Yes", labelOff: "No" }], ...
本章中设置基本结构的最后一步是启用从导航栏到FlipView.html页面的导航。你可以在清单 14-5 中看到我是如何做到这一点的,它显示了我对/js/templates.js文件所做的添加。
清单 14-5 。通过导航栏导航至 FlipView.html 页面
... var navBarCommands = [ //{ name: "AppTest", icon: "target" }, { name: "ToggleSwitch", icon: "\u0031" }, { name: "Rating", icon: "\u0032" }, { name: "Tooltip", icon: "\u0033" }, { name: "TimePicker", icon: "\u0034" }, { name: "DatePicker", icon: "\u0035" }, { name: "Flyout", icon: "\u0036" }, { name: "Menu", icon: "\u0037" }, { name: "MessageDialog", icon: "\u0038" }, ** { name: "FlipView", icon: "pictures" },** ]; ...
FlipView控件是我描述的第一个 WinJS 数据驱动控件,顾名思义,我需要一些数据来处理。在本章中,我将使用FlipView来显示一些图像文件,这是该控件的典型用法。你可以通过数据驱动的 WinJS 控件使用任何类型的数据,包括FlipView,但是我现在想保持事情简单。
为了将数据图像与应用图像分开,我创建了一 img/data文件夹,并将我的图像文件复制到那里。这些图像是花的照片,你可以在图 14-2 的解决方案浏览器中看到它们。我在本书附带的源代码下载中包含了这些图片(可从Apress.com`获得)。
***图 14-2。*将数据图像添加到 Visual Studio 项目中
如果此时运行应用,您将会看到如图图 14-3 所示的布局。FlipView控件在布局中,但它还不可见——这是因为我还没有设置我想要显示的图像和FlipView控件之间的关系——我将很快解决这个问题。
***图 14-3。*将基础文件添加到 Visual Studio 项目后 app 的状态
创建和使用数据源
使用数据驱动 UI 控件的关键步骤是创建数据源。方法是由数据源实现的属性,数据源由WinJS.UI.IListDataSource接口定义,如果您想要创建自己的数据源,您需要定义一个对象来实现IListDataSource定义的所有方法和属性,以便公开您的数据。
我不打算详细介绍这个界面,因为几乎在每种情况下都可以使用一些预定义的数据源。WinJS.UI.StorageDataSource可用于从设备文件系统加载数据——我将在本书的第四部分中更广泛地回到文件和数据,并且我将在第二十三章中演示StorageDataSource对象。
在本章中,我将使用另一个预定义的数据源,即WinJS.Binding.List对象。我在第八章中解释了如何为数据绑定创建可观察数组,并一直在例子中使用。List对象有一个dataSource属性,它返回一个实现数据源所需的所有方法和属性的对象。这使得List非常适合在处理内存数据时使用数据驱动的 UI 控件。
因此,首先,我将创建一个包含我想要显示的图像细节的List对象。在清单 14-6 中,你可以看到我对/js/viewmodel.js文件所做的添加。
***清单 14-6。*在/js/viewmodel.js 文件中定义列表
`(function () { "use strict";
WinJS.Namespace.define("ViewModel", { ** data: {** ** images: new WinJS.Binding.List([** ** { file:img/aster.jpg", name: "Aster"},** ** { file:img/carnation.jpg", name: "Carnation"},** ** { file:img/daffodil.jpg", name: "Daffodil"},** ** { file:img/lily.jpg", name: "Lilly"},** ** ]),** ** } ** });
})();`
我创建了一个名为images的新的List对象,并将其分配给了ViewModel.data名称空间。List中的每个对象都有一个包含图像文件路径的file属性和一个包含我将向用户显示的名称的name属性。
提示
List只包含四个图像文件的对象——我将在本章后面使用其他图像。
应用数据源
要应用数据源,需要设置两个FlipView属性。属性告诉 ?? 在哪里可以找到数据。itemTemplate属性指定了一个模板,该模板将用于显示数据源中的项目——这与我在第八章的中描述的数据绑定模板类型相同。在清单 14-7 中,你可以看到我是如何定义模板并在/pages/FlipView.html文件中设置FlipView属性的值的。
清单 14-7 。设置数据源和模板
`
**
`
我定义的模板使用数据对象的 file 属性来设置一个img元素的src属性,并显示name属性的值——我用来在模板中布局元素的样式在本章前面列出的/css/flipview.css文件中。
我已经使用data-win-options属性设置了 UI 控件的选项。请注意我是如何为itemTemplate属性指定值的:
itemTemplate: **select('#ItemTemplate')**
select关键字用于定位标记中的元素,它将一个 CSS 选择器作为参数,在本例中,该参数是我应用了WinJS.Binding.Template控件的元素的id属性值。
对于itemDataSource属性,注意我已经指定了我在/js/viewmodel.js文件中创建的WinJS.Binding.List对象的dataSource属性,如下所示:
itemDataSource: ViewModel.data.images.**dataSource**
FlipView控件——实际上是所有数据驱动的 UI 控件——没有关于List对象功能的特殊知识,并期望接收一个定义了在IListDataSource对象中列出的一组方法和属性的对象。这意味着当你告诉一个数据驱动的 UI 控件使用一个List对象作为数据源时,你必须记住使用dataSource属性。
修复第一个图像问题
我已经设置了模板和数据源,但是如果你现在运行这个应用,你会看到一个问题。数据源中的第一项显示不正确。但是,如果您滑动FlipView控件或将鼠标移动到它上面并单击出现的箭头,您将看到第二个和后续项目显示正常。你可以在图 14-4 中看到问题。这是使用FlipView控件时经常遇到的问题,我倾向于把它看作是的第一个图像问题。
***图 14-4。*flip view 第一个图像问题
这个问题是由WinJS.UI.Pages API、WinJS.Binding API 和FlipView控件本身之间不幸的交互引起的。当你点击FlipView控件的NavBar命令时,WinJS.UI.Pages.render方法被用来加载FlipView.html文件。
作为这个过程的一部分,render方法自动调用WinJS.UI.processAll方法,以便正确创建导入内容中的任何 WinJS UI 控件。
对processAll方法的自动调用初始化了FlipView控件。作为初始化的一部分,FlipView定位我使用itemTemplate属性指定的元素,使用这个模板生成显示第一个项目所需的内容,包括处理数据绑定,为img元素设置src属性。
在这个阶段,FlipView已经从模板中创建了 HTML,如下所示:
`...
您可以从代码片段中看到,数据绑定已经过处理,因此来自第一个数据源项的值反映在 HTML 元素中。
然而,您也可以看到(为了强调,我突出显示了它们),模板中最初的data-win-bind属性已经被复制到新内容中。
那些挥之不去的属性是问题的一部分。尽管为导入的内容自动调用了WinJS.UI.processAll方法,但没有调用WinJS.Binding.processAll方法。为了确保我的绑定被解析,我在default.js文件中的WinJS.Navigation事件的处理程序中直接调用这个方法,如下所示:
... WinJS.Navigation.addEventListener("navigating", function (e) { WinJS.UI.Animation.exitPage(contentTarget.children).then(function () { WinJS.Utilities.empty(contentTarget); WinJS.UI.Pages.render(e.detail.location, contentTarget) .then(function () { return **WinJS.Binding.processAll(contentTarget, ViewModel.State)** .then(function () { return WinJS.UI.Animation.enterPage(contentTarget.children) }); }); }); }); ...
在这种情况下,default.js文件中的代码执行一系列常见的操作:导入内容、处理数据绑定并执行动画。
问题是当WinJS.Binding.processAll方法运行时,它遍历 DOM 寻找定义data-win-bind属性的元素,并找到FlipView控件从模板中创建的元素。尽管这些元素的数据绑定已经被解析,但是processAll方法会再次查看它们并尝试理解它们。
WinJS.Binding.processAll方法将undefined插入到任何引用它无法解析的数据值的数据绑定中。它将无法解析的带有innerText绑定的任何元素的内容保留为空。这不是最有帮助的行为,因为processAll方法试图应用ViewModel.State对象,而绑定旨在处理数据源项,所以processAll方法覆盖了模板元素中的值,如下所示:
`...
因此,WinJS 功能的各个部分之间的交互意味着第一个数据项不能正确显示,因为img元素上的src属性被设置为 undefined,而我用于图像名称的 div 元素的内容为空。
FlipView控件按需生成显示数据项所需的元素,这意味着在WinJS.Binding.processAll方法完成其工作很久之后,直到您通过单击或滑动前进到下一项,第二个和后续数据项的元素才从模板中生成——这就是为什么只有第一个数据项受到影响。
解决第一个图像问题
有两种方法可以解决这个问题,这两种方法都不需要我修改WinJS.Navigation事件的处理程序。我用来处理导航的功能模式在其他情况下也很好,使得在导入的内容中使用数据绑定变得轻而易举。在接下来的部分中,我将向您展示解决这个问题的解决方案。
以编程方式指定模板或数据源
第一种解决方案是在内容页面的ready函数中的FlipView控件上设置itemTemplate属性或itemDataSource属性。当您更改任一属性的值时,FlipView控件将重新生成其内容,因为有一个重要的更改需要显示给用户。为了演示这个解决方案,我必须将该语句添加到清单 14-8 中的/js/pages/flipview.js文件中。
清单 14-8 。设置 itemTemplate 属性的值
`(function() {
WinJS.UI.Pages.define("/pages/FlipView.html", { ready: function () { var proxyObject = WinJS.Binding.as({ itemTemplate: ItemTemplate, customAnimations: false });
Templates.createControls(rightPanel, flip, "flipView", proxyObject) .then(function () {
** flip.winControl.itemTemplate = ItemTemplate;** }); } }); })();`
当你现在启动应用时,你会看到第一项显示正确,如图图 14-5 所示。这个解决方案之所以有效,是因为在调用了WinJS.Binding.processAll方法之后执行了ready函数中的代码,这意味着FlipView从模板中生成的元素不会针对data-win-bind属性被再次处理。
***图 14-5。*通过以编程方式设置 itemTemplate 属性来修复第一个图像问题
当您导航到FlipView.html页面时,您可能会注意到轻微的闪烁,在纠正之前,损坏的图像会显示一秒钟。这是因为当页面第一次加载时,itemTemplate属性仍然以声明方式设置和处理。为了解决这个问题,我需要从应用了FlipView控件的元素的data-win-options属性中删除itemTemplate属性的声明值,如清单 14-9 所示,只留下itemDataSource属性的值。
清单 14-9 。移除 itemTemplate 属性的声明值
`...
使用函数作为模板
另一个解决方案是去掉声明性模板,使用一个函数来生成元素,供FlipView控件用来显示每个数据项。你可以看到我是如何在清单 14-10 的/js/pages/flipview.js文件中添加这样一个函数的。
清单 14-10 。添加生成显示元素的功能
`(function() {
** function renderItem(itemPromise) {** ** return itemPromise.then(function (item) {** ** var topElem = document.createElement("div");** ** WinJS.Utilities.addClass(topElem, "renderDiv");** ** var imgElem = topElem.appendChild(document.createElement("img"));** ** imgElem.src = item.data.file;** ** var titleElem = topElem.appendChild(document.createElement("div"));** ** titleElem.innerText = item.data.name;** ** return topElem;** ** });** ** }**
WinJS.UI.Pages.define("/pages/FlipView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ itemTemplate:ItemTemplate, customAnimations: false });
Templates.createControls(rightPanel, flip, "flipView", proxyObject) .then(function () {
** proxyObject.bind("itemTemplate", function (val) {** ** flip.winControl.itemTemplate =** ** val == "HTML" ? ItemTemplate : renderItem;** ** });**
}); } }); })();`
当您使用函数生成项目模板时,传递给您的参数是一个IItemPromise对象。这是一个常规的WinJS.Promise,但是当Promise完成时传递给then函数的参数是一个IItem对象。(这两个I是故意的——开头的I表示一个接口)。IItem对象提供了对数据源项的访问,并定义了我在表 14-2 中描述的属性。
要创建一个模板,您需要在IItemPromise对象上调用then方法,指定一个函数,该函数将在IItem对象准备好时接收它(这允许数据源执行某种后台操作——比如查询数据库或从磁盘读取文件——异步完成)。当您获得IItem对象时,您可以使用表中的属性查询它,以生成向用户显示它所需的元素。
提示使用函数生成模板时,必须返回单个
HTMLElement对象,尽管这个顶级元素可以包含任意多个子元素。这与声明性 HTML 模板的约束相同。
为了帮助演示不同的模板方法,我改变了 JavaScript 文件中设置itemTemplate属性的方式,将它链接到应用布局右侧面板中的select元素。当您从select元素中选取HTML值时,将使用前一节中的声明性模板,当您选取Function值时,将应用清单中的renderItem函数。renderItem功能为数据项产生一种不同的布局样式,以便更容易看出正在使用哪一个,如图 14-6 所示。
***图 14-6。*为 FlipView 控件生成元素的不同技术
这两种方法都解决了第一个图像问题,您可以选择适合您的编程风格的方法。在大多数情况下,我倾向于使用声明性模板,但是使用函数生成元素会更加灵活,尤其是当您希望根据单个数据项来定制元素的内容时。
配置 FlipView 控件
既然我已经解决了第一个图像问题和解决方案,我可以转向FlipView控件支持的其他配置属性,我已经在表 14-3 中总结了这些属性。
itemTemplate和itemDataSource是最重要的属性,在解释如何解决第一个图像问题时,我已经解释了如何使用它们。在接下来的部分中,我将使用我在应用布局的右侧面板中创建的一些配置控件来演示orientation和itemSpacing属性。在本章的后面,当我谈到以编程方式操作FlipView控件时,我将演示currentPage属性。
设置方向
属性允许你改变用户在数据源中前后移动的方向。默认值为horizontal,表示用户在触摸屏上左右滑动或者用鼠标点击控件左右边缘的按钮。另一个支持值是vertical,它要求用户上下滑动,并为鼠标用户改变按钮的位置,使它们出现在控件的顶部和底部。我在右侧面板中添加了一个标记为Orientation的select元素,它允许您更改左侧面板中FlipView控件的orientation属性的值。在图 14-7 中,您可以看到为鼠标用户显示的horizontal和vertical值的不同按钮位置。我在图中突出显示了按钮,因为它们的默认样式很难看到(我将在本章的后面向您展示如何更改按钮样式)。
***图 14-7。*水平和垂直方向值按钮的不同位置
设置项目间距
itemSpacing属性设置当用户使用触摸从一个项目翻转到另一个项目时项目之间显示的间隙。在图 14-8 的中,你可以看到这种差距的两个例子,默认值为 10 像素,较大值为 100 像素。
***图 14-8。*设置触摸交互项目之间显示的间距
我在这个属性的例子中包含了一个配置控件——一个标记为Item Spacing的input元素。itemSpacing属性的影响可能看起来很小,但是设置项目间距可以对FlipView控件的外观和感觉产生显著的影响。特别是,我发现当数据源内容只是松散相关时,使用更大的空间会产生更自然的感觉。当然,这纯粹是主观感觉,你应该做对你自己的应用和偏好有意义的事情。
以编程方式管理动画视图
在很大程度上,FlipView控件为用户提供了与正在显示的内容进行交互所需的一切。当鼠标在FlipView上移动时,鼠标用户会看到弹出按钮,触摸用户可以通过滑动动作从一个项目移动到下一个项目。即便如此,有些时候你需要更直接地控制控件如何操作,对于这些情况,FlipView定义了我在表 14-4 中描述的方法。在接下来的小节中,我将向您展示如何使用这些方法。
移动浏览项目
您可以通过调用next和previous方法以编程方式在数据源中的项目间移动。我在示例的右边面板添加了按钮来演示这个特性,你可以从清单 14-11 中的按钮中看到处理click事件的代码。
清单 14-11 。使用下一个和上一个方法在 FlipView 项目中移动
`... WinJS.UI.Pages.define("/pages/FlipView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ itemTemplate: ItemTemplate, customAnimations: false, });
Templates.createControls(rightPanel, flip, "flipView", proxyObject)
.then(function () { proxyObject.bind("itemTemplate", function (val) {
flip.winControl.itemTemplate =
val == "HTML" ? ItemTemplate : renderItem;
});
** $('#rightPanel button').listen("click", function (e) {** ** var buttonText = e.target.innerText.toLowerCase();** ** switch (buttonText) {** ** case "previous":** ** case "next":** ** flip.winControlbuttonText;** ** currentPage.innerText = flip.winControl.currentPage;** ** break;** ** }** ** });** }); } }); ...`
只有当数据源中有另一个要移动到的项目时,这些方法才会更改显示的项目。这意味着当FlipView显示数据源中的最后一项时,next方法不起作用,当显示第一项时,previous方法不起作用。在我调用上一个或下一个方法之后,我使用currentPage属性的值来更新右侧面板中相应的span元素的值。
提示如果您想直接导航到特定的元素,可以设置
currentPage属性的值。
操纵数据源
数据驱动 UI 控件最有用的功能之一是,当数据源的内容发生变化时,它们会自动做出响应。这使您可以为应用创建动态且适应性强的布局,从而对基础数据的变化做出即时响应。为了展示这种响应性,我向视图模型添加了一些额外的数据项,如清单 14-12 所示。
***清单 14-12。*视图模型中定义的附加数据项
`(function () { "use strict";
WinJS.Namespace.define("ViewModel", {
data: {
images: new WinJS.Binding.List([
{ file:img/aster.jpg", name: "Aster"},
{ file:img/carnation.jpg", name: "Carnation"},
{ file:img/daffodil.jpg", name: "Daffodil"},
{ file:img/lily.jpg", name: "Lilly"},
]), extraImages: [{ file:img/orchid.jpg", name: "Orchid"},
{ file:img/peony.jpg", name: "Peony"},
{ file:img/primula.jpg", name: "Primula"},
{ file:img/rose.jpg", name: "Rose"},
{ file:img/snowdrop.jpg", name: "Snowdrop"}]
}
});
})();`
当应用启动时,这些数据项不是数据源的一部分,但是当您单击示例右侧面板上的Add和Remove按钮时,我会将数据项从List对象移入和移出。我在flipview.js文件中添加了我的click事件处理函数来支持这些按钮,如清单 14-13 中的所示。
清单 14-13 。为点击事件处理程序添加对添加和删除按钮的支持
... $('#rightPanel button').listen("click", function (e) { var data = ViewModel.data.images; var extras = ViewModel.data.extraImages; var buttonText = e.target.innerText.toLowerCase(); switch (buttonText) { ** case "add":** ** case "remove":** ** if (buttonText == "add" && extras.length > 0) {** ** data.push(extras.pop());** ** } else if (buttonText == "remove" && data.length > 1) {** ** extras.push(data.pop());** ** }** ** setImmediate(function () {** ** flip.winControl.count().then(function (countVal) {** ** itemCount.innerText = countVal;** ** });** ** });** ** break;** case "previous": case "next": flip.winControl[buttonText](); currentPage.innerText = flip.winControl.currentPage; break; } }); ...
您可以通过点击Add按钮向List对象添加新的项目,并通过点击Remove按钮删除它们。查看FlipView控件对数据源变化响应的最好方法是移动到最后一个数据项并点击Remove按钮。FlipView将自动移动到前一项,以反映数据源的变化。
获取数据源中的项目数
您会注意到,我在前面的清单中添加了一些调用count方法的代码。正如你所看到的,这个方法需要一点解释。当您调用count方法时,您会得到一个WinJS.Promise对象,当它被满足时,会将数据源中的项目数作为参数传递给then方法。这是数据源和数据驱动的 UI 控件如何被设计成支持异步操作的另一个例子,即使可以同步获得List对象中的项目数。
我将对 count 方法的调用放在传递给setImmediate函数的函数中。正如我在第九章的中解释的那样,setImmediate函数会推迟一个函数的执行,直到已经传递给setImmediate函数的事件和其他函数得到处理。我这样做的原因是因为数据源使用事件来通知 FlipView 控件它包含的数据已经更改,并且当我立即调用count方法时,该事件没有被处理。为了确保显示最新的值,我使用setImmediate推迟了对count方法的调用。在下一节的稍后部分,我将向您展示使用 FlipView UI 控件支持的事件的另一种方法。
响应 FlipView 事件
FlipView定义了我在表 14-5 中描述的三个事件。
只有当数据源中的项目数量改变时,而不是当一个项目被另一个项目替换时,才会触发datasourcecountchanged事件。这意味着当你使用一个WinJS.Binding.List对象作为你的数据源时,使用setAt方法将导致FlipView控件显示你指定的项目,但它不会触发datasourcecountchanged事件。
当显示新页面时,触发pageselected事件。当用户单击导航按钮时,或者当调用next和previous方法时,或者当使用currentPage属性跳转到特定项目时,该事件被触发。当用户在控件上滑动以选择一项时,不会触发该事件,但当释放触摸时会触发该事件。
每当FlipView改变一个项目的可见性时,就会触发pagevisibilitychanged事件。这是一个很好的想法,但它没有以一种特别有用的方式执行。传递给pagevisibilitychanged的处理函数的Event并不包含事件相关条目的细节,所以您只会收到一连串几乎无用的通知。
我可以使用datasourcecountchanged事件来替换我对setImmediate函数的调用,以推迟使用count方法。您可以在清单 14-14 中看到我对flipview.js文件所做的更改。
清单 14-14 。处理 datasourcecountchanged 事件
`... WinJS.UI.Pages.define("/pages/FlipView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ itemTemplate: ItemTemplate,
customAnimations: false,
});
Templates.createControls(rightPanel, flip, "flipView", proxyObject) .then(function () {
proxyObject.bind("itemTemplate", function (val) { flip.winControl.itemTemplate = val == "HTML" ? ItemTemplate : renderItem; });
** flip.addEventListener("datasourcecountchanged", function () {** ** flip.winControl.count().then(function (countVal) {** ** itemCount.innerText = countVal;** ** });** ** });**
$('#rightPanel button').listen("click", function (e) { var data = ViewModel.data.images; var extras = ViewModel.data.extraImages; var buttonText = e.target.innerText.toLowerCase(); switch (buttonText) { case "add": case "remove": if (buttonText == "add" && extras.length > 0) { data.push(extras.pop()); } else if (buttonText == "remove" && data.length > 1) { extras.push(data.pop()); }
break; case "previous": case "next": flip.winControlbuttonText; currentPage.innerText = flip.winControl.currentPage; break; } }); }); } }); ...`
我已经删除了对setImmediate函数的调用,并用一个事件监听器替换它,该监听器调用count方法并更新右面板中span元素的内容。在 FlipView 处理完数据源中的新变化之前,不会触发datasourcecountchanged事件,这意味着我不必再担心推迟方法调用。
样式化 FlipView 控件
FlipView支持使用六个类来设计控件外观的不同方面,如表 14-6 中所总结的。
FlipView不会自动调整自己的大小,所以你需要将控件放入一个自动分配大小的布局中,或者指定明确的宽度和高度值。您可以使用win-flipview类来实现这一点,但是我倾向于将id用于已经应用了控件的类,以便同一页面上的多个FlipView控件被单独处理(出于某种原因,我的项目很少需要相同大小的多个FlipView控件)。
我也倾向于不使用win-item类,因为我更喜欢通过项目模板来处理项目的样式。当然,这只是我的偏好,当您使用一个函数来生成不同的元素集以显示项目,并且希望应用一组总体样式时,win-item类会很有用。
我经常使用的类是那些设计当鼠标移动到FlipView控件上时出现的导航按钮的类。当显示浅色图像时,这些按钮可能很难看到,在清单 14-15 中,你可以看到一个我添加到FlipView.html文件中的style元素,它通过应用边框使它们更加明显。
清单 14-15 。导航按钮样式
`...
** ** ** #flip .win-navleft, #flip .win-navright {** ** border: medium solid black;** ** }** ** ** ...`你可以在图 14-9 中看到这种风格的效果。如果您自己测试这些附加功能,请记住我只对左右按钮应用了样式,如果将FlipView orientation属性设置为vertical,这将不会产生任何效果。
***图 14-9。*让导航按钮在灯光图像上更容易看到
使用自定义动画
如果您查看布局右侧面板中的控件,您会注意到我添加了一个ToggleSwitch来启用自定义动画。许多 WinJS UI 控件以某种形式使用动画,或者显示从一种状态到另一种状态的转换,或者指示内容以某种方式发生了变化。通常,动画是如此的简短,以至于他们几乎看不见,但是他们仍然足够吸引眼球,向用户传递重要的信号。
我在第十八章中深入解释了动画系统,但是FlipView控件为改变它所使用的动画提供了支持,我想在本章中解释如何做到这一点。这意味着我将解释如何使用一个我还没有正确介绍的功能,所以你可能想去阅读第十八章,然后再回到这一节。
在本节中,你需要知道的关于动画系统的所有事情就是动画是由WinJS.UI.Animation名称空间中的函数来表示的,并且两个这样的函数是fadeIn和fadeOut。这两个函数都以一个元素作为参数,它们的名字告诉你它们在这个元素上执行什么样的动画。
应用自定义动画
当用户导航到不同的项目时,FlipView控件使用动画。要改变在一种或多种情况下使用的动画,您可以向setCustomAnimations方法传递一个对象,该方法具有名为next、previous和jump的属性。当用户移动到下一个或前一个数据项时,使用下一个和前一个属性的动画,当用户导航到更远的地方时,使用 jump 属性。
这些属性的值必须是返回一个WinJS.Promise对象的函数,该对象在动画结束时实现。向您定义的函数传递参数,这些参数表示将要删除的元素,以及将替换它作为由FlipView显示的选定项的元素。
在清单 14-16 中,你可以看到我添加到flipview.js文件中的代码,用于为next属性设置自定义动画,当右侧面板中标有Custom Animations的ToggleSwitch控件设置为Yes时,我会应用这些代码。
清单 14-16 。通过 FlipView 控件使用自定义动画
`...
WinJS.UI.Pages.define("/pages/FlipView.html", { ready: function () {
var proxyObject = WinJS.Binding.as({ itemTemplate: ItemTemplate, customAnimations: false, });
Templates.createControls(rightPanel, flip, "flipView", proxyObject) .then(function () {
proxyObject.bind("itemTemplate", function (val) { flip.winControl.itemTemplate = val == "HTML" ? ItemTemplate : renderItem; });
flip.addEventListener("datasourcecountchanged", function () { flip.winControl.count().then(function (countVal) { itemCount.innerText = countVal; }); });
$('#rightPanel button').listen("click", function (e) { var data = ViewModel.data.images; var extras = ViewModel.data.extraImages; var buttonText = e.target.innerText.toLowerCase(); switch (buttonText) { case "add": case "remove": if (buttonText == "add" && extras.length > 0) { data.push(extras.pop()); } else if (buttonText == "remove" && data.length > 1) { extras.push(data.pop()); }
break;
case "previous":
case "next":
flip.winControlbuttonText;
currentPage.innerText = flip.winControl.currentPage; break;
}
});
** proxyObject.bind("customAnimations", function (val) {** ** if (val) {** ** flip.winControl.setCustomAnimations({** ** next: function (pageout, pagein) {** ** return WinJS.Promise.join([WinJS.UI.Animation.fadeOut(pageout),** ** WinJS.UI.Animation.fadeIn(pagein)]);** ** }** ** });** ** } else {** ** flip.winControl.setCustomAnimations({** ** next: null** ** });** ** }** ** });** }); } }); ...`
当视图模型属性被设置为true时,我调用setCustomAnimations方法并向FlipView控件提供我希望它使用的动画的细节以及何时应用它们。我传递给setCustomAnimations方法的对象只定义了next属性,它告诉FlipView控件使用默认的动画用于previous和jump场景。
我想执行两个动画——我想在输出元素上执行fadeOut动画,在输入元素上执行fadeIn动画。我需要返回一个只有当这两个动画都完成时才完成的Promise对象,这就是为什么我使用了Promise.join方法,我在第九章中描述了这个方法。
当ToggleSwitch被设置为No时,我通过向setCustomAnimations方法传递另一个对象来返回默认动画,该方法对于我想要重置的属性具有空值。
您可以通过启动应用并使用控制导航按钮前进到下一个数据项来测试此功能。一个非常简短的动画将创建从右边出现的新项目的效果。当您将ToggleSwitch设置为Yes并再次使用导航按钮时,现有项目将淡出,并被下一个数据项所取代。
注意仔细考虑你选择的动画。虽然动画很快,但与其他 Windows 应用不一致的糟糕选择可能意味着与用户正在执行的交互不同的交互类型。尽可能坚持使用默认的动画,如果不可能的话,试着将用户执行的动作和你选择的动画联系起来。这是我在第十八章中更详细解释 WinJS 动画特性时回到的话题。
总结
在这一章中,我描述了 WinJS 数据控件中的第一个,FlipView。我向您展示了如何使用WinJS.Binding.List对象来创建数据源,以及如何将模板与数据相关联来配置向用户显示项目的方式。我还向您展示了第一图像问题,解释了它是如何产生的,并提供了解决该问题的不同方法。最后,我向你展示了如何用FlipView控件来使用自定义动画——一旦你阅读了深入研究 WinJS 动画系统的第十八章,这些信息将会产生更多的共鸣。在下一章,我将向您展示ListView控件,它是对FlipView控件更大更复杂的补充。