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

26 阅读1小时+

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

原文:Pro Windows 8 Development with HTML5 and JavaScript

协议:CC BY-NC-SA 4.0

六、创建自适应布局

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

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

images

images

创建示例项目

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

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

`

         AppViews

                   

                   

    
        
Top Left
        
Top Right
        
Bottom Left
        
Bottom Right
    
`

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

清单 6-2 。default.css 文件

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

#gridContainer > div {     border: medium solid white;     padding: 10px; margin: 1px; }

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

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

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

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

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

#testImg {     width: 100px;     height: 100px; }`

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

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

images

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

了解地铁景观

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

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

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

images

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

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

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

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

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

使用 CSS 适应视图

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

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

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

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

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

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

    #bottomRight {         display: none;     } }

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

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

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

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

`...

...`

适应填充视图

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

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

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

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

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

images

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

适应快照视图

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

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

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

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

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

images

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

使用 JavaScript 适应视图

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

检测当前视图

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

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

images

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

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

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

`(function () {     "use strict";

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

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

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

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

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

接收视图变化事件

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

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

`(function () {     "use strict";

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

    app.onactivated = function (eventObject) {

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

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

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

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

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

images

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

适应导入内容的视图变化

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

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

`

                      

            WinJS.UI.Pages.define("/content.html", {                 ready: function () { **                    setButtonContent(view.ApplicationView.value);**

**                    window.addEventListener("resize", function () {** **                        setButtonContent(view.ApplicationView.value);** **                    });**                 }             });                               button {                 font-size: 30px;             } **            @media screen and (-ms-view-state: snapped) {** **                #button1 {** **                    font-size: 40px;** **                    font-family: serif;** **                }** **                #button2 {** **                    display: none;** **                }** **            }**                            

            Button One             Button Two         
    

`

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

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

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

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

images

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

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

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

脱离快照视图

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

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

`...

...`

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

适应设备方向

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

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

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

确定和监控设备方位

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

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

`...

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

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

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

`(function () {     "use strict";

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

    app.onactivated = function (eventObject) {

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

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

**        displayOrientation();**

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

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

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

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

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

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

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

images

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

images

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

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

使用 CSS 适应设备方向

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

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

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

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

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

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

    #bottomRight {         display: none;     } }

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

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

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

images

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

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

表达您的设备方向偏好

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

images

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

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

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

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

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

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

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

覆盖清单方向首选项

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

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

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

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

`...

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

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

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

`(function () {     "use strict";

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

    app.onactivated = function (eventObject) {

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

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

        displayOrientation();

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

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

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

    // ... code removed for brevity

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

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

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

适应像素密度

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

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

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

images

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

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

images

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

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

images

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

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

使用自动资源加载

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

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

通过在文件后缀前插入scale-XXX,其中XXX是缩放百分比,您告诉 Metro 运行时这些文件是用于不同密度的图像的变体。当您在 HTML 中使用这些文件时,您排除了缩放信息,正如您在清单 6-15 中看到的,它显示了我添加到default.html文件中的一个img元素。

images 注意仅仅将图像文件复制到磁盘上的图像文件夹是不够的。您还需要在 Visual Studio Solution Explorer窗口中右键单击 images 文件夹,并从弹出菜单中选择Add Existing项。选择图像文件并点击Add按钮。

清单 6-15 。使用一组缩放文件中的图像

`...

    
Top Left         Lock     
    
                               
    
Bottom Left **        **     
    
Bottom Right
...`
测试图像选择

如果没有一系列配备不同像素密度显示屏的设备,测试这项功能是很棘手的。Visual Studio simulator 支持在不同的屏幕分辨率和密度之间切换,但它不能正确处理图像,而是以原始大小显示图像,这就违背了模拟的目的。然而,模拟器确实加载了正确的图像来反映模拟的密度,所以我可以通过限制img元素的大小来得到我想要的效果,我已经使用了我在default.css文件中定义的样式之一,如清单 6-16 所示。对于真实的设备和真实的项目,这不是你需要做的事情。

清单 6-16 。固定图像元素的大小以说明像素密度支持

... #testImg {     width: 100px;     height: 100px; } ...

模拟器窗口边缘的一个按钮改变屏幕尺寸和密度,如图图 6-12 所示。最有用的设置是 10.6 英寸显示屏,可以用四种不同的分辨率进行模拟,覆盖 Windows 8 支持的不同比例级别。

images

***图 6-12。*在 Visual Studio 模拟器中更改屏幕特性

为了测试这种技术,启动应用并在可用的像素密度之间切换。您将看到自动显示针对像素密度的图像。

images 提示每次更改后,你都必须在调试器中重新加载应用才能看到正确的图像——请求的图像名称(img.png)和缩放版本(img.scaled-XXX.png)之间的映射似乎被缓存了。

使用 JavaScript 适应像素密度

您可以通过Windows.Graphics.Display.DisplayProperties对象获得像素密度和缩放工厂的详细信息。为了演示这是如何工作的,我使用名称img100.pngimg140.pngimg180.png将图像文件的副本添加到项目images文件夹中。我已经创建了这些副本,因此它们不受我在上一节中描述的缩放命名方案的约束。此外,我已经从default.html文件的img元素中移除了src属性,如清单 6-17 所示。

清单 6-17 。从 default.html 文件的 img 元素中删除 src 属性

`...

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

我想要的信息可以通过DisplayProperties.resolutionScale属性获得,该属性返回一个对应于Windows.Graphics.Display.ResolutionScale枚举的值。这个枚举中的值是scale100Percentscale140Percentscale180Percent。在清单 6-18 中,您可以看到我是如何基于来自default.js文件中resolutionScale属性的值为img元素设置src属性的值的。

清单 6-18 。基于显示比例因子明确选择图像

`(function () {     "use strict";

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

    app.onactivated = function (eventObject) {

**        switch (display.DisplayProperties.resolutionScale) {** **            case display.ResolutionScale.scale100Percent:** **                testImg.src = "img/img100.png";** **                break;** **            case display.ResolutionScale.scale140Percent:** **                testImg.src = "img/img140.png";** **                break;** **            case display.ResolutionScale.scale180Percent:** **                testImg.src = "img/img180.png";** **                break;** **        };**

        // ...code removed for brevity...     };

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

images 注意你也可以使用 CSS media规则来适应像素密度,但是没有方便的缩放贴图,你必须直接处理像素密度值。这是 Visual Studio 模拟器的问题,因为它以错误的方式舍入了像素密度数字,所以模拟的分辨率不属于正确的类别。此外,由于您只需要使位图图像适应像素密度,而使用 CSS 无法做到这一点,因此这种技术没有什么价值。如果你发现自己在调整布局的任何其他部分以适应像素密度,那你的方法就有问题了。请记住:Windows 将为您缩放应用中的所有其他内容。

总结

在这一章中,我向你展示了让你的应用布局适应设备的不同方法——适应视图、适应方向和适应像素密度。确保您的应用以有意义的方式适应,对于创建流畅、高质量的用户体验非常重要,这种体验可以与设备和操作系统功能完美融合。如果你不仔细考虑你的适应方法,或者更糟的是,完全跳过它们,你将创建一个行为怪异的应用,并且不能真正与 Metro 体验相融合。我的建议是花时间考虑你的应用能做出什么样的最佳反应,特别是在快照视图和纵向视图方向上。

在下一章中,我将介绍一些 UI 控件,它们是 Metro 的独特部分,您可以使用它们来提供一致且简单的应用功能导航。

七、命令和导航

在第五章中,我向你展示了如何使用单页布局来创建一个 Windows 应用的基础。当我这样做的时候,我使用了button和锚(a)元素来导航内容,就像我在普通的 web 应用中所做的一样。在这一章中,我将向你展示应用特有的控件,这些控件专用于提供在应用中导航的命令,并操纵它所呈现的数据或对象:应用工具栏和导航工具栏。这些控件提供了 Windows 应用独特的视觉风格和交互模型的很大一部分,并且存在于大多数应用中(游戏似乎是个例外,在游戏中非标准界面很常见)。在这一章中,我将向你展示如何创建和应用这些控件,并且在这样做的时候,提供更多关于 WinJS 控件模型的细节,我在第五章中使用HtmlControl导入内容的时候提到过。表 7-1 对本章进行了总结。**

*images

创建示例项目

我使用Blank App模板创建的本章示例项目名为AppBars,它遵循单页面导航模型。这一章是关于导航的,所以我已经创建了一个项目,它有一个母版页和两个内容文件,稍后我将向您展示。当应用启动时,我导入其中一个内容文件,然后,在本章的后面,我将添加一些 Windows 应用 UI 控件,以便在其他内容之间导航。default.html文件将作为母版页,如清单 7-1 中的所示。

清单 7-1 。AppBars 项目的 default.html 主页

`

         AppBars

                   

              

    
`

这将是我的母版页,我将把内容导入到属性为contentTargetdiv元素中。在所有其他方面,我保留了这个文件,就像我使用Blank Application项目模板时由 Visual Studio 创建的一样。

定义和加载内容

我从这个项目中的一个内容文件开始,我将其命名为page1.html(我已经将它添加到根项目文件夹中,与default.html放在一起)。正如你在清单 7-2 中看到的,这个文件包含一个h1元素,它清楚地表明哪个文件已经被加载,还有一些span元素(我将在本章后面解释命令时用到它们)。

清单 7-2 。page1.html 文件的内容

`

                                
            

This is Page 1

` `            Command: None         
     `
定义 JavaScript

我已经使用了WinJS.Navigation API 来处理请求和加载js/default.js文件中的内容,你可以在清单 7-3 中看到。我的导航事件处理程序清除母版页中的目标元素,并使用WinJS.UI.Pages.render方法加载指定的内容。我将直接使用文件的名称来请求此应用中的内容。应用的开始位置是加载page1.html,我将使用导航控件切换到本章后面的其他内容。

清单 7-3 。加载 default.js 文件中的初始内容

`(function () {     "use strict";

    var app = WinJS.Application;

    WinJS.Navigation.addEventListener("navigating", function (e) {         WinJS.Utilities.empty(contentTarget);         WinJS.UI.Pages.render(e.detail.location, contentTarget);     });

    app.onactivated = function (eventObject) {         WinJS.Navigation.navigate("page1.html");     };

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

定义 CSS

这个项目中的最后一个文件是css/default.css文件,我用它来定义母版页和单独内容文件的样式。你可以在清单 7-4 中看到 CSS 样式。

清单 7-4 。default.css 文件的内容

`#contentTarget, div.container {     width: 100%;     height: 100%;     text-align: center;     display: -ms-flexbox;     -ms-flex-direction: column;     -ms-flex-align: center;     -ms-flex-pack: center; }

#page1Container {     background-color: #317f42;     }

#page2Container {     background-color: #5A8463;     }

div.container span {     font-size: 30pt; }`

我已经使用了 flexbox 布局来排列元素,正如你在图 7-1 中看到的,它显示了示例应用的初始外观。

images

***图 7-1。*app bars 示例 app 的初始外观

创建应用命令

应用栏出现在屏幕底部,为用户提供对命令的访问。命令对当前范围内的数据或对象执行一些功能,这通常意味着您的命令应该与您当前呈现给用户的内容相关。

AppBar 是 Windows 应用用户体验的重要组成部分。它为关键交互提供了一致和熟悉的锚点,并允许您将 UI 控件从应用的主布局中移出,以便您使用屏幕空间为用户提供更多数据。在这一节中,我将详细介绍 AppBar,向您展示如何创建、填充和管理 UI 控件以及如何响应用户命令,并告诉您在应用中充分利用 app bar 所需要知道的一切。很自然,可以从创建 AppBar 开始,这是通过WinJS.UI.AppBar对象完成的。创建 AppBar 最简单的方法是通过向标准 HTML 元素添加数据属性,以声明的方式完成。应用栏通常被添加到母版页,这样你就不必在每次导入新内容时重新设置它们。考虑到这一点,清单 7-5 展示了在default.html文件中添加一个 AppBar。

images 注意WinJS.UI.AppBar对象是 WinJS UI 控件的一个例子。我在这本书的这一部分中提到的都与应用的基本布局和结构有关。在本书的第三部分的中,我将介绍更通用的 UI 控件对象,并向你展示如何应用它们。

清单 7-5 。给 default.html 文件添加一个 app bar

`

         AppBars

                   

              

    

**    

**

**        <button data-win-control="WinJS.UI.AppBarCommand"** **            data-win-options="{id:'cmdBold', label:'Bold', icon:'bold',** **                section:'selection', tooltip:'Bold', type: 'toggle'}">** **                    **

**        <button data-win-control="WinJS.UI.AppBarCommand"** **            data-win-options="{id:'cmdFont', label:'Font', icon:'font',** **                section:'selection', tooltip:'Change Font'}">** **                    **

**        <hr data-win-control="WinJS.UI.AppBarCommand"** **            data-win-options="{type:'separator', section:'selection'}" />**

**        <button data-win-control="WinJS.UI.AppBarCommand"** **            data-win-options="{id:'cmdCut', label:'Cut', icon:'cut',** **                section:'selection', tooltip:'Cut'}">** **                    **

**        <button data-win-control="WinJS.UI.AppBarCommand"             data-win-options="{id:'cmdCut', label:'Paste', icon:'paste',** **                section:'selection', tooltip:'Paste'}">** **                    **

**        <button data-win-control="WinJS.UI.AppBarCommand"** **            data-win-options="{id:'cmdRemove',label:'Remove', icon:'remove',** **                section:'global', tooltip:'Remove item'}">** **        **

**        <button data-win-control="WinJS.UI.AppBarCommand"** **            data-win-options="{id:'cmdAdd', label:'Add', icon:'add',** **                section:'global', tooltip:'Add item'}">** **        ** **    

**

`

这些元素中有很多东西,我将一步一步地分解它们。图 7-2 显示了这些元素创建的 AppBar,当我解释各个部分是如何组合在一起的时候,它给了你一些上下文。我已经把图中 AppBar 的中间部分切掉了,这样更容易看到各个按钮。

images 提示如果你现在运行这个例子,AppBar 不会出现。您需要首先应用清单 7-6 中的所示的更改。

images

***图 7-2。*清单 7-5 中的元素创建的 app bar

声明 AppBar 控件

AppBar 的起点是创建一个WinJS.UI.AppBar对象,我使用div元素上的data-win-control属性来完成,如下所示:

`...

    // ...elements removed for brevity
...`

这是我在前面章节中使用的 UI 控制模式,也是 WinJS 中使用的模式。您获取一个常规的 HTML 元素,在 AppBars 中是一个div元素,并使用data-win-control属性来指示您想要创建哪个用户控件。正如我之前提到的, Windows 运行时不会自动搜索这些属性,这就是为什么我在default.js文件中添加了对WinJS.UI.processAll方法的调用,如清单 7-6 所示。如果不调用processAll方法,AppBar控件将不会应用于div元素,当用户右击或滑动鼠标时,AppBar 也不会弹出。

清单 7-6 。调用 processAll 方法应用 WinJS UI 控件

`(function () {     "use strict";

    var app = WinJS.Application;

    WinJS.Navigation.addEventListener("navigating", function (e) {         WinJS.Utilities.empty(contentTarget);         WinJS.UI.Pages.render(e.detail.location, contentTarget);     });

    app.onactivated = function (eventObject) {         **        WinJS.UI.processAll().then(function() {** **            WinJS.Navigation.navigate("page1.html");** **        });**     };     app.start(); })();`

processAll方法在后台处理元素,并返回一个WinJS.Promise对象,当处理完成且所有 UI 控件都已创建时,该对象被实现。我在第五章的中介绍了Promise对象,并简要介绍了使用then方法来推迟操作直到Promise完成。我在第九章的中深入讨论了Promise对象。

images 提示注意,我不必亲自处理滑动或右键单击来显示 AppBar 控件。这在应用中是默认连接的,因此应用栏(和导航条,我将在本章后面介绍)会自动出现。

我已经把对WinJS.Navigation.navigate方法的调用放在一个函数中,我把它传递给由processAll方法返回的Promisethen方法。这很重要。虽然我在母版页中设置了 AppBar,但我想从内容页中管理和控制它(我将在本章的后面解释为什么并向您展示如何做到这一点)。我无法通知导入的内容母版页中的 HTML 元素已经被处理并转换为 WinJS UI 控件,这一点很重要,因为我不希望该内容中的代码在准备好之前就开始访问控件功能(这将导致引发异常)。解决方案是确保processAll方法在导入内容之前已经完成了它的工作,这意味着使用 WinJS Promise,如代码所示。

images 提示我说 WinJS 控件是从 HTML 元素创建的,或者应用于 HTML 元素,但实际发生的是 WinJS 用一些 CSS 格式化 HTML 元素,并且在某些情况下,添加一些新元素。该技术非常类似于基于 JavaScript 的 UI 库,如 jQuery UI 和 jQuery Mobile。我不想给你留下正在进行某种魔术的印象——在大多数情况下,WinJS 控件是标准的 HTML 和 CSS。在某些情况下,JavaScript 代码会使用一些 Windows API 调用,但这种情况非常少见,而且这些也是您可以在代码中使用的 Windows API(这也是我在本书中描述的)。

向 AppBar 添加按钮

通过向 AppBar 添加具有data-win-controlWinJS.UI.AppBarCommandbutton元素,向用户显示命令,如下所示:

... <button **data-win-control="WinJS.UI.AppBarCommand"**     data-win-options="{id:'cmdBold', label:'Bold', icon:'bold', section:'selection', tooltip:'Bold', type: 'toggle'}"> </button>             ...

使用包含配置信息的 JSON 字符串,通过data-win-options属性为 AppBar 按钮提供配置。JSON 字符串中的属性对应于WinJS.UI.AppBarCommand对象中的属性。我在表 7-2 中总结了这些配置属性,并在下面的章节中逐一描述。理解这些选项是充分利用 AppBars 的关键。

images

设置命令 ID

id属性标识与按钮相关的命令。没有预定义的命令,因此您可以自由分配在应用环境中有意义的id值。该属性有两种用途。首先,在响应用户交互时,使用id属性来确定请求了哪个命令。其次,您使用id告诉 AppBar 在导入内容时显示哪些命令。我将在本章后面的响应命令部分演示这两种用法。

配置命令按钮的外观

iconlabeltooltip属性定义了按钮的外观。icon属性指定按钮中使用的字形。字形是通过显示一个来自Segoe UI Symbol字体的字符产生的,这是 Windows 8 的一部分。您可以使用 Windows 8 中的Character Map工具查看字体中的图标范围。

icon属性的值通常是来自WinJS.UI.AppBarIcon枚举的值,这为字符代码提供了一个方便的映射方案。分配给我之前展示的按钮的bold值等同于WinJS.UI.AppBarIcon.bold属性,该属性在 Visual Studio 添加到应用的base.js文件中定义为:

... bold:               "\uE19B" ...

(在Solution Explorer窗口中展开References可以看到base.js的内容。)您不需要为您想要的字符指定名称空间或对象名——只需bold即可。如果您想要使用一个在枚举中没有值的字符,您可以简单地使用字体字符代码而不是名称(例如,\uE19B而不是bold)。

images 提示如果找不到适合自己需要的图标,可以将icon属性设置为包含自定义图标的 PNG 文件的名称。

labeltooltip属性指定显示在按钮图标下的字符串,以及当鼠标悬停在按钮上或者当用户在按钮上滑动手指而没有松开时显示的字符串。这些属性的值不是根据icon值自动推断出来的,你可以在你的应用中自由使用任何有意义的文本。然而,重要的是不要给众所周知的图标赋予新的含义,因为你赋予labeltooltip属性的值不会总是显示给用户。在我向您展示的代码片段中的button中,我将icon设置为bold并将labeltooltip都设置为Bold,这产生了如图图 7-3 所示的按钮。

images

***图 7-3。*配置应用栏中命令按钮的外观

图的左侧显示了全屏横向视图中显示的 AppBar 的一部分,其中显示了label文本(我只显示了 AppBar 的一侧,其他按钮都在旁边)。图的右侧显示了快照视图中的 AppBar。在这个视图中没有显示label文本,因为 AppBar 已经通过省略label和将图标打包堆叠在一起来适应缩小的屏幕空间。你也不能依靠tooltip的值来帮助用户理解一个按钮会做什么,因为它们不会显示在只支持触摸的设备上。你可以依靠图标的清晰度来传达按钮的含义——这使得做出适当的选择并尊重与众所周知的图标相关的惯例变得很重要。

images 提示如果你发现自己很难通过图标传达命令,你可能想停下来想想你的应用的设计。Windows 应用的用户体验是关于即时和明显的交互,这可能是因为你试图在一个命令中包含太多的含义。通过将动作分解成一系列命令或者使用上下文菜单(我在本书的第三部分中描述了这一点),你可以为你的用户创造更好的体验。

分组和分隔按钮

AppBar 中有两个部分:选择部分位于 AppBar 的左侧,包含应用于用户选择的数据或对象的命令。全局部分位于应用栏的右侧,包含始终可用且不受单个选项影响的命令。section属性决定了一个按钮被分配到哪个部分,如你所料,支持的值是selectionglobal

当您创建 AppBar 时,您用您的应用在所有情况下支持的所有命令填充每个部分。然后,您可以更改命令集和单个命令的状态,以匹配应用的状态——稍后我将向您展示如何做到这一点。

选择全局命令的位置

微软已经为一些全局命令在应用栏上的位置定义了一些规则。如果你的应用有一个NewAdd命令,那么它应该被放在最右边的全局命令,并且应该用add图标显示(这个图标不应该用于任何其他命令)。

放置在NewAdd左侧的命令应为对应命令。如果你的应用处理的数据或对象具有应用之外的生命(比如照片,它们驻留在设备存储上,可以被其他应用访问),那么你必须使用一个Delete命令(一个Deletelabel值和一个deleteicon值)。如果你的应用只处理自己的数据,那么你应该使用一个Remove命令(一个label值为Remove,一个图标值为remove)。如果该操作将删除多个项目,那么您应该使用一个Clear命令(一个标签值为Clear,一个图标值为clear)。

设置命令类型

有四种类型的命令可以添加到 AppBar,由type属性指定——这些类型对应于值buttontoggleseparatorflyout。如果您没有为type属性显式设置值,那么将使用button值。

buttontoggle类型创建常规按钮,当它们被点击时触发事件。它们之间的区别在于toggle类型创建了一个具有开和关状态的切换按钮。我之前关注的Bold按钮是一个toggle命令,你可以在图 7-4 中看到关闭和打开状态是如何显示的。我将很快向您展示如何以编程方式检查和更改切换状态。

images

***图 7-4。*用分隔符显示的切换命令的不同状态

该图还显示了可以添加到 AppBar 的第三种命令:separator类型。(这在页面上可能很难辨认——分隔符是一条细长的竖线,通过运行示例可以清楚地看到。)其他三种命令类型是从button元素创建的,但是您可以从hr元素创建一个分隔符,如下所示:

... <**hr** data-win-control="WinJS.UI.AppBarCommand"     data-win-options="{type: **'separator'**, section:'selection'}" /> ...

命令按照它们在 HTML 中定义的顺序被添加到它们在 AppBar 中的部分,因此您可以简单地在代表其他类型命令的button元素之间添加分隔符。最后一个命令类型flyout用于将命令与弹出菜单相关联。我将在本章后面的“使用弹出型按钮”部分向您展示如何使用这种命令。

响应命令

通过在 AppBar HTML 元素上为click事件注册一个处理函数来响应来自 AppBar 的命令。处理 click 事件的最佳位置是您导入到母版页的内容中——这允许您针对不同的内容适当地响应命令,而无需诉诸紧耦合。清单 7-7 展示了在page1.html文件中添加一个script元素来响应 AppBar 中的命令。

清单 7-7 。响应 page1.html 文件中的命令

`

              **        **                   
            

This is Page 1

            Command: None         
     `

我使用addEventListener方法为应用了 AppBar 控件的div元素注册一个click事件的处理函数。这依赖于 DOM 事件通过文档中元素的层次结构向上传播的方式,这意味着我可以避免找到单个 command button元素并直接处理它们。清单中的script元素的重要声明是这样的:

... document.getElementById("command").innerText = **e.target.winControl.label**; ...

该语句获取被单击命令的label属性的值,并使用它来设置标记中span元素的innerText属性。当WinJS.UI.processAll方法处理一个具有data-win-control属性的元素时,它将一个winControl属性附加到表示 DOM 中元素的对象上。属性返回一个对象,允许你使用 UI 控件的特性和功能。

images 提示注意,我已经使用了e.target来定位点击事件所源自的元素。该事件来自命令,而不是 AppBar,因此您需要确保处理正确的元素。

winControl属性返回的对象是由data-win-control属性指定的对象。对于 AppBar 命令,winControl是一个WinJS.UI.AppBarCommand对象,我可以访问这个对象定义的所有属性和方法。在本例中,我已经读取了label属性来获取显示在命令按钮下的文本字符串。

AppBarCommand对象非常简单,它定义的大多数成员对应于我在本章前面描述的配置选项(idiconsectiontype等等)。还有一些额外的属性,我已经在表 7-3 中描述了它们,尽管我在本章后面才详细解释其中的几个。

images

使 AppBar 适应特定内容

在声明 AppBar 时,添加应用中需要的所有命令,然后指定在将内容导入母版页时向用户显示哪些命令。这允许您为应用栏提供一组一致的元素,并且仍然适应应用的状态,以便导入内容的功能在应用栏的命令中得到反映。

区分禁用隐藏的命令非常重要。禁用的命令仍然出现在应用栏上,但是命令按钮是灰色的,表示该命令现在不适用,但是以后可能会适用—例如,可能在用户选择了对象或数据项时。

隐藏的命令会从应用栏中完全删除—当命令不适用于当前内容时,您会隐藏命令。您可以使用应用了WinJS.UI.AppBar控件的元素的winControl对象来隐藏和禁用命令。在清单 7-8 的中,我已经添加了page1.html文件来配置 AppBar 命令。

清单 7-8 。定制 AppBar 命令以匹配导入内容的功能

`

             ` `        

                    appbar.addEventListener("click", function (e) {                         command.innerText = e.target.winControl.label;                     });                 }             });                            

            

This is Page 1

            Command: None         
    

`

使用winControl属性,我能够访问WinJS.UI.AppBar控件的方法和属性。在这个清单中,我使用了两种可用的方法:showOnlyCommands方法接受命令id值的数组,并隐藏数组中没有指定的所有命令。我用这个方法来隐藏除了cmdBoldcmdFontcmdAdd命令之外的所有命令。

getCommandById接受一个id值并返回相应的AppBarCommand对象。在清单中,我为cmdBold命令找到了AppBarCommand对象,并将disabled属性设置为true(正如我在上一节中所描述的,它将按钮保留在 AppBar 上,但阻止它被使用)。你可以在图 7-5 中看到这些方法在 AppBar 上的效果。

images

***图 7-5。*为导入的内容定制 app bar

我喜欢使用showOnlyCommands方法,因为这意味着我提供了我想要显示的命令的明确列表,但是在AppBar对象中还有其他方法可以用来为您的内容准备 AppBar。在表 7-4 中,我描述了你可以用来显示、隐藏和定位命令的一整套方法。

使用弹出按钮

基本命令可以用buttontoggle命令类型处理,但是对于复杂的命令,你需要使用flyout类型,它将命令与一个被称为弹出按钮的弹出窗口链接起来。使用WinJS.UI.Flyout UI 控件创建弹出按钮,在这一节中,我将向您展示如何创建和使用弹出按钮,以及如何将它们与您的 AppBar 命令相关联。

images 提示在这一节中,我将解释如何使用 AppBars 的Flyout控件,但是您也可以使用Flyout来创建通用的弹出窗口。更多细节和示例见第十二章。

声明弹出型按钮

声明弹出按钮的最佳位置是在母版页中,这样整个应用中都可以使用相同的元素集。通过将data-win-control属性设置为WinJS.UI.Flyout,弹出按钮被应用于div元素。您可以设置div元素的内容,以适应您的应用的需求——FlyoutUI 控件的作用是处理弹出窗口,对您向用户呈现的内容没有任何限制。清单 7-9 显示了我添加到default.html文件中的一个简单的弹出按钮。

清单 7-9 。向 default.html 文件添加弹出按钮

`

         AppBars

                   

              

    
` `    
                 

        <button data-win-control="WinJS.UI.AppBarCommand"             data-win-options="{id:'cmdFont', label:'Font', icon:'font',                 section:'selection', tooltip:'Change Font', type: 'flyout', **                flyout: 'fontFlyout'**}">         

        

    

**    

** **        

Select a Font

** **        ** **            First Font** **            Second Font** **            Third Font** **        ** **    
**

`

在这个清单中,我定义了一个弹出按钮,它包含一个简单的标题和一个带有三个option元素的select元素。为了将弹出按钮与命令相关联,我将命令的type属性设置为flyout,将flyout属性设置为已经应用了WinJS.UI.Flyout控件的div元素的id

形成弹出按钮的元素是隐藏的,直到用户单击或触摸相关联的命令按钮。此时,弹出按钮显示在按钮上方,如图图 7-6 所示。飞出式按钮被轻轻关闭,这意味着如果用户点击飞出式按钮占据的屏幕区域之外,它们将再次隐藏。这意味着您不必添加任何种类的取消按钮来删除弹出按钮。

images

***图 7-6。*使用带有 AppBar 命令的弹出菜单

样式弹出按钮

弹出型按钮的默认样式非常简单。如果你想让你的弹出按钮与应用的其他部分的视觉主题相适应,你可以覆盖 CSS win-flyout类。清单 7-10 展示了我如何在default.css文件中覆盖这个样式来改变背景颜色和应用边框。如果你遵循这个例子,你需要将这些样式添加到default.css文件中。

清单 7-10 。通过 win-flyout 类设计弹出菜单

`... div.win-flyout {     background-color: #4FCB6A;     border: thick solid black; }

div.win-flyout select {     border: medium solid black; } ...`

响应弹出交互

声明和显示弹出型按钮只是该过程的一部分。您还需要响应用户与您添加到弹出按钮的元素的交互。处理弹出控件交互的最佳位置是在母版页的 JavaScript 代码中——这允许您为弹出控件创建一致的处理方式,不管显示的内容是什么。

然而,您需要使用一个视图模型数据绑定来使这种方法工作,而不会在母版页和导入的内容之间产生紧耦合问题。在第八章之前,我不会描述视图模型和数据绑定,所以我将接受母版页需要有内容的详细知识,这样我就可以演示如何处理弹出按钮,并在本书的后面向您展示如何解决紧耦合问题。清单 7-11 显示了对default.js文件的添加,以响应弹出按钮。

清单 7-11 。响应弹出按钮

`(function () {     "use strict";

    var app = WinJS.Application;

    WinJS.Navigation.addEventListener("navigating", function (e) {         WinJS.Utilities.empty(contentTarget);         WinJS.UI.Pages.render(e.detail.location, contentTarget);     });

    app.onactivated = function (eventObject) {

        WinJS.UI.processAll().then(function() {             WinJS.Navigation.navigate("page1.html");

**            fontSelect.addEventListener("change", function (e) {                 command.innerText = this.value;** **                fontFlyout.winControl.hide();** **            });**         });     };     app.start(); })();`

我已经处理了来自select元素的change事件,就像我在常规 web 应用中一样,并且我将在page1.html中定义的span元素的innerText属性设置为用户选择的值。这是紧密耦合的部分——default.js文件中的代码不应该知道 page1.html 中内容的结构和性质(但是正如我所说的,您可以使用视图模型和数据绑定来解决这个问题,我在第八章中对此进行了描述)。

这个例子的关键部分是这样的陈述:

... fontFlyout.winControl.hide(); ...

一旦我处理了用户交互,我就定位到应用了Flyout控件的元素,并通过winControl属性调用hide方法。仅当用户在弹出窗口之外单击时,弹出窗口灯光消除功能才适用,而当用户使用弹出窗口包含的控件执行操作时,该功能不适用。这意味着在成功交互完成时,您必须明确地从显示屏上移除弹出窗口(使用hide方法)。

对于这个简单的弹出按钮,我可以在用户使用select元素做出选择时立即做出响应,但是对于更复杂的交互,您可以依靠OK按钮或其他类型的明确信号来表明用户已经完成了。

创建导航命令

导航栏从屏幕顶部向下滑动,充当应用栏的对应物。AppBar 提供了对当前内容中的数据和对象进行操作的命令,而 NavBar 提供了在应用的不同区域中移动的方法。NavBars 是使用与 AppBars 相同的WinJS.UI.AppBar UI 控件创建的,但是在向用户呈现命令的方式上,您拥有更大的灵活性。在这一部分,我将向你展示如何在你的应用中添加和管理导航条。为了演示导航控件,我在项目中添加了两个新文件,名为page2.htmlpage3.html。你可以在清单 7-12 中看到page2.html

清单 7-12。【page2.html 档案】??

`

                                
            

This is Page 2

        
     `

这些文件最初将用于演示导航,但是在本章的后面我将使用page2.html文件来演示如何创建一组自定义的导航控件。你可以在清单 7-13 的中看到page3.html的内容。

清单 7-13。【page3.html 档案】??

`

                                
            

This is Page 3

        
     `
使用标准导航条

虽然我发现在母版页中定义 AppBar 更容易,但在 NavBar 中我倾向于采用不同的方法。我创建了一个包含 NavBar 元素的单独文件,并使用WinJS.UI.Pages名称空间将它导入到每个内容文件中,采用了我在第五章的中描述的技术。在这一节中,我将向您展示一个标准的 NavBar,它遵循与我在 AppBar 中使用的相同的基于命令的方法。清单 7-14 显示了我使用 Visual Studio HTML Page项目模板添加到示例项目中的standardNavBar.html文件的内容。(在本章的后面,我将向你展示一个不同的导航条设计。)

清单 7-14 。standardNavbar.html 文件的内容

`

                                         

            <button data-win-control="WinJS.UI.AppBarCommand"                 data-win-options="{id:'cmdPage2', label:'Page 2', icon:'\u0032',                     section:'selection', tooltip:'Page2'}">             

            <button data-win-control="WinJS.UI.AppBarCommand"                 data-win-options="{id:'cmdPage3', label:'Page 3', icon:'\u0033',                     section:'selection', tooltip:'Page3'}">                      

    

`

NavBar 遵循我为 AppBar 使用的命令模式,有两个按钮导航到page2.htmlpage3.html文件。当您使用一个WinJS.UI.AppBar控件作为导航栏时,您必须使用data-win-options属性将placement属性设置为top,就像我在清单中所做的那样。

placement属性是区分用作 AppBars 的WinJS.UI.AppBar控件和用作 NavBars 的控件的方式。当用户点击或触摸其中一个命令按钮时,我接收到click事件,并使用命令的id属性计算出用户想要导航到的页面,并将其传递给WinJS.Navigation.navigate方法。

应用标准导航条

我想在page1.html文件中使用标准的导航条,你可以在清单 7-15 中看到我是如何做的。

清单 7-15 。使用 page1.html 文件中的标准导航条

`

                                         
            

This is Page 1

            Command: None         
    

`

这是render方法的标准用法,除了我正在导入内容的元素没有在page1.html文件中定义。我将在这个示例应用中使用多种样式的 NavBar,这需要对元素进行一些调整,首先是在母版页中有一个公共元素,内容可以在其中加载所需的 NavBar。你可以看到我是如何将这个元素添加到清单 7-16 中的default.html文件中的。(让我再次强调,当我在第八章中介绍视图模型时,我会演示一些更好的技术来处理这种情况。)

清单 7-16 。向 default.html 添加一个通用元素,导航条将被导入其中

`...

    
**    
**     

        

    

    

        

Select a Font

                     First Font             Second Font             Third Font              

...`

page1.html文件被导入时,它将导入standardNavBar.html文件的内容,并将它们插入到default.html文件的公共元素中。你可以在图 7-7 中看到效果。NavBar 与 AppBar 同时显示,并响应相同的交互。同样,我不需要处理任何事件来显示 NavBar——这是在应用控件时自动连接的,当用户滑动或右键单击时,NavBar 和 AppBar 都会显示。

images

***图 7-7。*向示例应用添加导航栏

图中的细节可能很难辨认,但是你可以看到我在图 7-8 的中特写的两个命令按钮。数字没有预定义的图标,所以我将命令的icon值设置为来自Segoe UI Symbol字体的字符代码(\u0032\u0033)。点击命令将导航到相应的页面。

images

***图 7-8。*标准导航条按钮上的命令

使用自定义导航条

在导航条上使用一系列按钮来表示命令并不总是有意义的。在这些情况下,您可以为WinJS.UI.AppBar控件定义一个自定义布局。清单 7-17 显示了customNavBar.html文件的内容,这是我使用 Visual Studio HTML Page项模板添加到项目中的。

清单 7-17 。customNavBar.html 文件的内容

`

                                         
            
                                 

Page Title

            
        
     `

这是另一个自包含的 NavBar,HTML 元素和响应它们的代码定义在同一个文件中。不同的是,我在data-win-options 属性中将layout属性设置为customcustom值告诉WinJS.UI.AppBar控件,你不想要标准的基于命令的布局,你将自己提供和管理导航条中的元素。

images 注意可以在应用栏和导航条上使用自定义布局——但这通常不是个好主意。应用的导航结构可能会有所不同,用户希望导航条能够反映出你的应用的独特特征。另一方面,命令应该是一致的,并使用我前面描述的约定来应用。用户将期待在应用栏中的命令,它们的布局和响应方式与所有其他 Windows 应用一致。如果你必须使用一个非标准的 AppBar 来表达你的应用的独特性,那么你的设计一定有问题。

如清单所示,我已经用一个button和一个h1元素填充了导航栏。我使用了一个叫做win-backbutton的方便的 Windows CSS 类,它创建了一个圆形边框中带有箭头的按钮。导航栏中的元素包含在一个div元素中。我采用了这种结构,这样我可以很容易地设计元素的样式——你可以在清单 7-18 的中看到我添加到default.css文件中的样式。

清单 7-18 。自定义导航栏布局的 default.css 文件中的样式

`... #navBarContent {     display: -ms-flexbox;     -ms-flex-direction: row;     -ms-flex-align: start;     width: 100%;     height: 80px; }

#navBarBack {     margin-top: 18px;     margin-left: 20px; }

#navBarTitle {     padding-top: 3px;     margin-left: 20px; } ...`

我使用 CSS flexbox 布局来定位元素。正如你已经意识到的,我经常使用这种布局,因为它非常适合 Windows 应用的一般风格,其中控件沿着公共轴流动——当我在第十六章中描述语义缩放概念时,你会更清楚地看到这种视觉主题。

应用自定义导航栏

应用带有自定义布局的导航栏的过程与应用常规导航栏的过程是一样的,只是您必须确保布局中的元素得到了正确的配置和准备。对于我的自定义 NavBar 布局,我需要设置h1元素的内容,这是通过用render 方法传递一个数据对象来完成的,这样它就可以被在customNavBar.html文件中定义的ready函数使用。清单 7-19 显示了使用自定义导航条的page2.html文件。

清单 7-19 。page2.html 文件的内容

`

              **        **                   
            

This is Page 2

        
     `

我不需要监听导航栏中按钮的click事件,因为它是使用WinJS.Navigation API 在customNavPage.html文件中处理的。

images 提示我也在项目中添加了page3.html,但它本质上与page2.html相同,所以我不会浪费篇幅列出内容。

调整导航条和应用条的行为

WinJS API 期望一个应用中只有一个 NavBar。当WinJS.UI.processAll方法处理一个已经应用了WinJS.UI.AppBar控件的元素时,它会将该元素移出它在 DOM 中的原始位置,这样它就不会受到导入内容变化的影响。对于我的例子来说,这是一个问题,因为我想在导航条之间自由切换,以反映我正在显示的内容。

我还有第二个问题要解决。当用户导航到应用的另一部分时,应用栏和导航栏不会自动隐藏。我将删除 NavBar,这样我就不用担心隐藏它了——但是我需要确保 AppBar 是隐藏的;否则,它只会在屏幕上徘徊,遮住我新导入的内容。

为了解决这两个问题,我在default.js文件中添加了一些代码来响应导航请求。您可以在清单 7-20 中看到这些变化。

清单 7-20 。移除导航条并隐藏应用条以响应导航事件

(function () {     "use strict"; `var app = WinJS.Application;

    WinJS.Navigation.addEventListener("navigating", function (e) {

**        if (window.navbar) {** **            window.navbar.parentNode.removeChild(navbar);** **        }**

**        if (window.appbar) {** **            window.appbar.winControl.hide();** **        }**

        WinJS.Utilities.empty(contentTarget);         WinJS.UI.Pages.render(e.detail.location, contentTarget);     });

    app.onactivated = function (eventObject) {         WinJS.UI.processAll().then(function () {             WinJS.Navigation.navigate("page1.html");

            fontSelect.addEventListener("change", function (e) {                 command.innerText = this.value;                 fontFlyout.winControl.hide();             });         });     };     app.start(); })();`

在导航到请求的内容之前,我移除了idnavbar的元素,并通过winControl隐藏了appbar元素。这给了我所需的应用状态,以便新内容可以加载自己的导航栏,并且不会被应用栏遮挡。你可以在图 7-9 中看到导航条产生的效果。

images

***图 7-9。*在一个应用中使用两种风格的导航条

制作导航过渡动画

在这一章中,我要做的最后一件事是制作从一页内容到另一页内容的动画。WinJS.UI.Animation名称空间定义了许多动画,您可以在应用状态的关键转换时将这些动画应用于元素。我将向您展示这些动画,因为我描述了它们相关的功能——在这一章中,当一个内容页面离开显示屏以及当另一个内容页面到达时会应用相关的动画。我会在第十八章中详细解释这些动画,但我只想在这里提供一个快速的概述。

应用中的内容转换可能发生得如此之快,以至于人眼并不总是能察觉到它们——特别是如果涉及的两个内容页面共享共同的视觉线索,例如颜色和字体。您希望您的用户知道,由于单击或触摸导航命令,内容已经发生了变化。如果不明显,你将打破好的应用所拥有的流畅的交互模式,并导致用户花一秒钟来检查他期望的动作已经被执行了。

你让用户知道视觉信号发生了变化。我在示例中为内容页面使用了不同的背景颜色,这是一个非常有用和强大的信号,尤其是如果在整个应用中使用一致的颜色来指示不同的功能区域。另一个强大的视觉信号是动画。清单 7-21 展示了我是如何将动画添加到default.js文件中的导航事件处理函数中的——在这个函数中应用动画意味着它们将被一致地应用到整个应用中。

清单 7-21 。将导航动画应用于示例应用

`(function () {     "use strict";

    var app = WinJS.Application;

    WinJS.Navigation.addEventListener("navigating", function (e) {

        if (window.navbar) {             window.navbar.parentNode.removeChild(navbar);         }

        if (window.appbar) {             window.appbar.winControl.hide();         }

        // These statements are commented out         **//**WinJS.Utilities.empty(contentTarget);         **//**WinJS.UI.Pages.render(e.detail.location, contentTarget);

**        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {** **            WinJS.Utilities.empty(contentTarget);** **            WinJS.UI.Pages.render(e.detail.location, contentTarget).then(function () {** **                return WinJS.UI.Animation.enterPage(contentTarget.children)** **            });** **        });**     });

    app.onactivated = function (eventObject) {         WinJS.UI.processAll().then(function () {             WinJS.Navigation.navigate("page1.html");         });     };

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

我在这个清单中使用的两个动画方法是exitPageenterPage。名称解释了每个动画的用途,两种方法的参数都是一个或多个需要动画的元素。为了简单起见,我传递了母版页中承载导入内容的元素的children属性的结果。

请注意,没有为这些动画配置样式或持续时间的选项。通过使用这些方法,您可以应用标准的页面过渡动画,这对于所有 Windows 应用都是一样的。这是一个很好的方法,因为很多非常明智和理性的开发人员在得到一个动画库时会变得有点疯狂,并且往往会忘记动画是为了帮助用户。

我不能给你看截图中的动画,所以你应该运行示例应用来看看效果。由exitPageenterPage方法触发的动画简洁、简单且有效。它们足够简单和快速,当用户第 100 次看到动画时不会感到厌烦,但它们足够丰富,足以引发用户意识到应用中的内容已经发生了变化。

总结

在这一章中,我已经向你展示了如何使用WinJS.UI.AppBar控件来创建应用栏和导航条。这些是 Windows 用户体验中的基本元素,它们为用户与你的应用的交互带来了一致性和清晰性。这并不是说使用 AppBars 和 NavBars 应该受到限制——即使有微软制定的惯例,也有许多不同的方式来组织您呈现给用户的命令,并且您可以选择使用自定义 NavBar 布局来定制您提供的导航体验。

在下一章,我将介绍视图模型数据绑定的主题。现在我们已经了解了 Windows 应用布局和导航的基本结构,是时候考虑如何采用同样的方法来组织和显示应用的数据了。*

八、查看模型和数据绑定

在本章中,我将向您介绍视图模型数据绑定。这是两个基本的技术,可以让您创建可伸缩性好、易于开发和维护、能够流畅地响应数据变化的应用。

您可能已经从设计模式中熟悉了模型和视图模型,如模型-视图-控制器(MVC),模型-视图-视图模型(MVVM 和模型-视图-视图控制器(MVVC))。我不打算在本书中详细讨论这些模式。有很多关于 MVC、MVVM 和 MVVC 的好信息,从维基百科开始,它有一些非常平衡和深刻的描述。

我发现使用视图模型的好处是巨大的,除了最简单的应用项目,其他项目都值得考虑,我建议你认真考虑遵循同样的道路。我不是一个模式狂热者,我坚信应该采用解决实际问题的部分模式和技术,并将它们应用到具体的项目中。最后,你会发现我对如何使用视图模型持相当开放的观点。

我在本章中描述的 WinJS 特性支撑了 Windows 应用支持的一些基本交互模型。为了确保我为更高级的特性打下坚实的基础,我慢慢地开始这一章,并逐步介绍关键概念。理解这些特性是充分利用高级 UI 控件和概念的前提,比如语义缩放,我在第十六章中对此进行了描述。表 8-1 对本章进行了总结。

images

images

重温示例应用

在这一章中,我继续构建我在前一章中创建的AppBars项目。提醒一下,这个应用引入了 NavBars 和 AppBars,并包含了一些简单的内容页面。我将在此基础上展示新的应用特性。

分离应用组件

我将首先应用一个视图模型来修复第七章中的示例应用的一些缺点。在此过程中,我将向您展示我首选的视图模型对象结构,并演示视图模型可以有多简单,同时还能使开发人员的工作更加轻松。

images 注意如我之前所说,我对视图模型的构成持非常开放的态度,它包括不直接呈现给用户的数据。

定义视图模型

视图模型最重要的特征是全局可用性和一致性。在 Windows 应用中,创建基本视图模型最简单的方法是使用WinJS.Namespace特性(我在第三章和第四章中介绍过)来创建视图模型对象并将其导出到全局名称空间。清单 8-1 显示了viewmodel.js文件的内容,我将它添加到了AppBars示例项目的js文件夹中。

清单 8-1 。viewmodel.js 文件的内容

`(function () {     "use strict";

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

    }); })();`

我喜欢创建一个名为ViewModel的顶级名称空间,它包含嵌套的名称空间,代表我想要处理的每一大类数据。在这个例子中,我定义了两个名称空间。ViewModel.State是我定义关于应用状态的数据的地方,我将它粗略地定义为应用的一部分为了与应用的其他部分顺利工作而需要知道的数据。这个名称空间包含三个属性,我将使用它们来解决我在第七章的中引入到示例应用中的挥之不去的紧耦合问题。

我在ViewModel.UserData中定义的第二个名称空间。我使用这个名称空间来存储用户关心的数据。这因应用而异,但它包括用户输入的任何值和我从这些值中导出的任何数据。这是前台数据,而不是我放在ViewModel.State名称空间中的后台数据。最初这个名称空间中没有属性,但是我会在本章的后面添加一些。

images 提示 JavaScript 是一种动态语言,这意味着在给属性赋值之前,我不需要在视图模型中定义属性。我还是这样做了,因为我希望我的视图模型定义成为它包含的数据的规范引用;在我看来,这意味着定义属性并将null分配给它们,而不是在我第一次使用它们时在应用的其他地方创建属性。

我的 Windows 应用通常有一个包含这两个名称空间的视图模型。我根据我的应用支持的契约添加其他契约。我在本书的第四部分解释了契约,但这两个是我最常用的基本契约。我将状态数据从用户数据中分离出来,因为我发现这样更容易确定哪些数据应该持久存储;我会在第二十章中进一步讨论这个问题。

导入并填充视图模型

视图模式是在一个自动执行的 JavaScript 函数中定义的,所以在示例应用中使用它所要做的就是导入带有script元素的代码,并为视图模型包含的属性设置值。我希望视图模型在应用启动时就可用,并且无论导入和显示了哪些内容都可用,这意味着需要将script元素作为示例应用的母版页放在default.html文件中。清单 8-2 显示了添加的script元素。

清单 8-2 。使用脚本元素加载视图模型代码

`...

         AppBars

                         **    **     

...`

我在之前添加了viewmodel.js文件的脚本元素,这让我有机会在初始化应用时引用视图模型。在示例应用中,我想为在default.js文件中包含 AppBar 和 NavBar 控件的元素设置值,如清单 8-3 中的所示。

清单 8-3 。填充视图模型

`(function () {     "use strict";

    var app = WinJS.Application;

    WinJS.Navigation.addEventListener("navigating", function (e) {

**        var navbar = ViewModel.State.navBarControlElement;** **        if (navbar) {** **            navbar.parentNode.removeChild(navbar);** **        }**

**        if (ViewModel.State.appBarElement) {** **            ViewModel.State.appBarElement.winControl.hide();** **        }**

        //WinJS.Utilities.empty(contentTarget);         //WinJS.UI.Pages.render(e.detail.location, contentTarget);

        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {             WinJS.Utilities.empty(contentTarget);             WinJS.UI.Pages.render(e.detail.location, contentTarget).then(function () {                 return WinJS.UI.Animation.enterPage(contentTarget.children)             });         });     });

    app.onactivated = function (eventObject) {         WinJS.UI.processAll().then(function () {

**            ViewModel.State.appBarElement = appbar;** **            ViewModel.State.navBarContainerElement = navBarContainer;**

            WinJS.Navigation.navigate("page1.html");

            fontSelect.addEventListener("change", function (e) {                 command.innerText = this.value;                 fontFlyout.winControl.hide();             });         });     };     app.start(); })();`

消费视图模型

我在视图模型中设置了值,这样,default.html文件中元素结构的细节就不会在整个应用中扩散。如果我更改了default.html文件,我只需在default.js文件中反映这些更改,而不必搜寻并找到所有使用了appbarnavBarContainer值的实例。在包含在内容文件中的代码中,我可以将我想要的导航条导入到视图模型中列出的元素中,如清单 8-4 所示的,它显示了page2.html文件的内容。

清单 8-4 。使用视图模型定位导航条加载到的元素

`

              
        

This is Page 2

    
`

default.js文件中只设置了ViewModel.State名称空间中的两个属性。第三个属性navBarControlElement,由设置应用使用的每种 NavBar 样式的代码设置,如清单 8-5 所示,它显示了来自customNavBar.html文件的script元素——这允许我拥有一个全局可用的属性,该属性被设置为反映当前导入的内容。

清单 8-5 。在 NavBar 代码中设置视图模型属性值

`...

...`

该属性用于default.js,中的导航事件处理函数,这意味着导航条控件不必应用于idnavbar的元素。最后,我对standardNavBar.html文件进行了同样的修改,如清单 8-6 所示。

清单 8-6 。在 NavBar 代码中设置视图模型属性值

`...

...`

这些变化的结果是,视图模型充当了关于应用的状态和结构的信息库,允许各种组件协同工作,而无需事先了解彼此。当我在标记或代码中进行更改时,我只需要确保视图模型属性反映了更改,而不是搜寻所有的依赖项并手动进行更改。

从布局中分离数据

视图模型的最大价值来自于将应用中的数据从 HTML 中分离出来,并呈现给用户。通过将视图模型与数据绑定相结合,您可以使您的应用更容易开发、测试和维护。我将在本节的后面解释数据绑定是如何工作的,但是首先我将向您展示我正在着手解决的问题。在开始之前,我需要为我将在本章中添加的元素添加一些新的 CSS 样式。为此,我创建了一个名为/css/extrastyles.css的新文件,其内容你可以在清单 8-7 中看到。在这个 CSS 中没有新的技术,我列出了新的样式,所以你可以看到这个项目的各个方面。

清单 8-7 。extrastyles.css 文件的内容

`#page2BoxContainer {     display: -ms-flexbox;     -ms-flex-direction: row;     -ms-flex-align: stretch;     -ms-flex-pack: justify;     margin-top: 20px; }

div.page2box {     width: 325px;     padding: 10px;     margin: 5px;     border: medium solid white;     background-color: gray;     display: -ms-flexbox;     -ms-flex-direction: column;     -ms-flex-pack: center; }

div.page2box * {     display: block;     margin: 4px;     font-size: 18pt; }`

这些样式中引用的类和id属性值是针对我稍后将添加的元素的。为了将文件的内容纳入项目范围,我向default.html文件添加了一个link元素,如下所示:

`...

         AppBars

                   

          **    **          

...`
论证问题

没有视图模型,数据项的权威来源是 HTML 元素;也就是说,当您想要一个数据值时,您必须在 DOM 中找到包含它的元素,并从适当的HTMLElement对象中读取该值。作为示范,我对page2.html文件做了一些修改,如清单 8-8 所示。

清单 8-8 。使用布局中的元素作为数据值的权威来源

`

                                         
            

This is Page 2

       **    

** **                
** **                    ** **                    OK** **                
**

**                

** **                    The word is:** **                    ????** **                
**

**                

** **                    The length is:** **                    ????** **                
** **            
**         
    

`

这是一个简单的例子。我在布局中添加了三个div元素。我使用 CSS flexbox 布局对它们进行了定位,并应用了我在extrastyles.css文件中定义的类。

您可以在图 1 中看到新标记和代码的效果。您在左侧面板的input元素中输入一个单词,然后点击OK按钮。您输入的单词显示在中间面板中,单词的长度显示在右侧面板中。在图中,我已经输入了单词press

images

***图 8-1。*向 page2.html 布局添加三个面板

本例中的input元素是用户输入的数据的权威来源。如果我想让用户输入单词,我需要读取代表 DOM 中的input元素的HTMLElement对象的value属性。这种方法的好处是简单,但是它引入了一些深刻的问题。

一个问题是input元素并不是真正的权威。当点击button时,您可以合理地确信input元素包含在精确时刻的用户数据,但是在所有其他时间,用户可能正在改变值。如果你在除了点击button之外的任何时候读取value属性,你不能确定你有有用的数据。

更严重的问题是,在单页内容模型中使用时,数据值不是持久的。当用户导航到新页面时,Windows 应用运行时会丢弃input元素及其内容。当用户导航回该页面时,会生成新元素,并且用户在导航之前输入的任何数据都会丢失。

应用视图模型

当然,解决方案是使用视图模型。清单 8-9 显示了在viewmodel.js文件中添加的两个新属性,代表我向用户显示的两个数据项。因为我正在处理用户输入的数据,所以我在名称空间ViewModel.UserData中定义了这些属性。

清单 8-9 。在视图模型中为用户数据定义属性

`(function () {     "use strict";

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

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

word属性将包含用户输入的值。这个数据项没有默认值,所以我将null赋给了属性。

wordLength属性被称为派生值计算值,这意味着它返回的值基于视图模型中的某个其他值。为了创建这个属性,我使用了一个相对较新的 JavaScript 特性,称为 getter 。Getters 及其对应的setter,允许您使用函数来创建复杂的属性。在这个例子中,我只定义了一个 getter,这意味着该值可以被读取,但不能被修改。

images 提示在视图模型中使用计算值的好处是生成值的逻辑保存在一个地方,而不是嵌入到需要显示值的每个script元素中。如果我以后需要更改wordLength属性,也许是为了只计算元音,那么我只需要更改视图模型。这是向应用添加结构的另一个方面,这样负责配置 HTML 元素的代码就不用负责生成这些值。

视图模型的消费者看不到 getters 和 setters 的使用,他们通过读取ViewModel.UserData.wordLength获得wordLength值,就像它是一个常规属性一样。定义完这些属性后,我需要对page2.html做两组修改,如清单 8-10 所示。

清单 8-10 。使用视图模型存储和检索数据

`...

...`

我通过更新视图模型中的属性来响应被点击的button,这和其他 JavaScript 赋值一样。此时,视图模型对象没有特殊的能力或特性,除了我已经使它成为我的数据的权威来源:

... **ViewModel.UserData.word** = wordinput.value; ...

另一个变化是当ViewModel.UserData.word属性的值改变时,使用视图模型更新中间和右边的面板。我已经将更新面板的语句转移到一个名为updateDataDisplay的函数中,该函数使用视图模型来获取wordwordLength数据值。应用的功能是相同的,但现在视图模型充当数据存储库以及更新属性的代码和响应这些更新的代码之间的中介。

使用数据绑定

在这一点上,我已经达到了我的目标,但不是以一种有益的方式。例如,尽管我已经将更新代码分离到它自己的函数中,并使用视图模型数据进行更新,但我仍然必须直接从与button元素相关联的click事件处理函数中触发更新。

构建应用的下一步是断开更新函数和click事件处理程序之间的链接。我将使用数据绑定来实现这一点,这是两个数据项链接在一起并保持同步的地方。

对于 Windows 应用,数据绑定是通过WinJS.Binding名称空间提供的。设置数据绑定的过程需要两个步骤:使一个数据项可观察,然后观察该数据项,这样当该数据项的值改变时,您就会收到通知。在接下来的小节中,我将向您展示这两个步骤。

使物体可见

一个可观察对象每当它的一个属性值改变时就会发出一个事件。此事件允许相关方监控属性值并做出相应的响应。你通过使用WinJS.Binding.as方法创建一个可观察的对象,你可以看到我是如何将这个方法应用到清单 8-11 中的viewmodel.js文件的。

清单 8-11 。使部分视图模型可见

`(function () {     "use strict";

    WinJS.Namespace.define("ViewModel.State", {         appBarElement: null,         navBarContainerElement: null,         navBarControlElement: null     }); **    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({** **        UserData: {** **            word: null,** **        }** **    }));**

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

创建一个可观察的视图模型并不完全简单,您可以从我在视图模型中所做的结构更改中看到这一点。需要这些更改来解决 WinJS API 中的一系列冲突和限制。我将向您详细介绍每一个问题,以便您理解发生了什么,并且如果您在自己的代码中遇到它们,可以识别它们。

解决命名空间与绑定的冲突

WinJS.Binding.as方法的工作原理是用 getters 和 setters 替换对象中的属性。这允许您继续使用属性,而不必担心它们是如何实现的,并允许可观察对象在设置新值时无缝地发出事件。结果是一个可见的对象,但它仍然与使用它的代码兼容。

为了支持这种方法,WinJS.Binding.as方法使用在成员名前加下划线字符(_)的 JavaScript 约定来创建一些私有成员。

但是当WinJS.Namespace.define方法将一个对象导出到全局名称空间时,它会隐藏任何名称以下划线开头的对象成员。我认为,这个想法是为了加强一些严格性,防止名称空间的消费者访问私有变量和方法。

当您使用WinJS.Binding.as方法创建一个想要使用WinJS.Namespace.define方法导出的可观察对象时,问题就来了——as方法添加的私有成员被define方法隐藏,破坏了对象。结果是,您不能简单地将as方法应用于您想要导出到名称空间的对象,就像这样:

... WinJS.Namespace.define("ViewModel.UserData",     WinJS.Binding.as({         word: null     }) ); ...

define方法只对它所传递的对象的顶层隐藏私有成员,这意味着你可以通过构造不同的名称空间来解决这个问题,就像这样:

... WinJS.Namespace.define("ViewModel", WinJS.Binding.as({ **    UserData: {** **        word: null,** **    }** })); ...

名称空间的UserData部分被传递给as方法,这解决了问题。WinJS API 的不同部分在一些地方不能很好地协同工作,这是新的 Windows 应用开发人员最常遇到的问题。

解决 Getter 冲突

WinJS.Binding.as方法转换对象中的简单值属性,使它们变得可观察。它不对函数进行操作,但是它会导致使用 getters 和 setters 的值出现问题。正是因为这个原因,我将wordLength属性的定义移到了对WinJS.Namespace.define方法的单独调用中:

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

define方法是附加的,这意味着我可以定义属性或函数,它们将被添加到我已经用相同名称定义的任何现有名称空间对象中(而不是替换它们)。在这种情况下,我的wordLength属性被添加到ViewModel.UserData名称空间,但没有通过WinJS.Binding.as方法传递,这意味着我的计算视图模型属性工作正常,并返回从同一名称空间中的word属性派生的值。

消费可观察的对象事件

这个有点复杂的设置过程的结果是我的ViewModel.UserData名称空间中的word属性是可见的。当我更改word属性的值时,ViewModel.UserData对象将向任何感兴趣的方发送一个事件。

您使用bind方法在可观察对象上注册对属性的兴趣。在清单 8-12 的中,你可以看到我是如何在page2.html文件的script元素中使用这个方法的。

清单 8-12 。使用 WinJS。Binding.bind 方法接收属性值更改事件

`...

...`

当我调用WinJS.Binding.as方法时,bind方法被添加到可观察对象中。bind的参数是一个包含您想要观察的属性名称的字符串,以及一个当属性值改变时处理事件的函数。在我的例子中,我已经在ViewModel.UserData对象上调用了bind方法,指定我想要观察word属性,并且变更事件将由updateDataDisplay函数处理。

事件处理函数传递了两个参数——被观察属性的新值和旧值(mu 示例只有一个参数,因为我不关心前一个值)。在这个清单中,我修改了updateDataDisplay函数,以便使用新的 value 参数设置布局中的中间面板,并更新右侧面板以反映单词长度。

使用bind方法创建一个编程绑定(即绑定是在 JavaScript 代码中定义的)。我能够使用编程绑定来简化我的代码,并确保我的应用的不同部分自动保持彼此同步,使用视图模型作为代理或中介。

重构可观察的属性

我的数据绑定还有一个问题需要解决,那就是wordLength属性是不可见的。这样做的效果是,视图模型中值的消费者必须知道wordLength属性是从word属性计算出来的,并使用后者属性中的事件作为触发器来检查前者的变化。

修复这个问题相当简单,只需要稍微修改一下viewmodel.js文件,如清单 8-13 所示。

清单 8-13 。手动发送更新事件使属性可见

`(function () {     "use strict";

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

    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({         UserData: {             word: null, **            wordLength: null**         }     }));

**    ViewModel.UserData.bind("word", function (newVal) {         if (newVal) {** **            ViewModel.UserData.wordLength = newVal ? newVal.length : null;**

**        }** **    });** })();`

在这个清单中,我使用了bind方法让视图模型观察自己。这允许我将为wordLength属性计算值的逻辑移出ViewModel.UserData对象,这意味着wordLength属性本身就可以被观察到。

这并不是一种完美的技术,因为视图模型的消费者有可能接收到word属性的变更事件,并在更新之前读取wordLength属性。然而,对于大多数项目来说,这是一种完全合理的方法,并且只需要少量的工作就可以避免 WinJS 数据绑定支持中的一些问题;我将很快向您展示一种替代方法,用于那些无法接受时间问题的应用。

通过使这两个属性都是可观察的,我使视图模型的消费者能够在其中一个发生变化时收到通知。清单 8-14 显示了来自page2.htmlscript元素,它显示了正在为wordwordLength属性处理的变更事件。

清单 8-14 。绑定到多视图模型属性

`...

...`

事件的处理函数没有传递哪个属性已经改变的细节,这意味着处理改变事件的最简单的方法是使用一个专用于单个属性的函数。您可以在清单中看到这一点,我将updateDataDisplay函数分成了两个独立的函数,每个函数对应一个可观察的属性。结果是,wordLength属性的观察者不需要知道该属性是从word属性派生出来的。

彻底解决问题

在我继续之前,我想向您展示一种使视图模型可观察的不同方法,这种方法没有上一节中潜在的时间问题。如果您使用计算属性,您只需要使用这种方法。在发送任何变更事件之前,更新所有相关的属性值是必要的。对于所有其他情况,你应该使用我在清单 8-14 中展示的方法,这更简单,也更容易操作。清单 8-15 显示了完全解决更新问题所需的更高级的方法。

清单 8-15 。解决 viewmodel.js 文件中的更新排序问题

`(function () {     "use strict";

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

**    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({** **        UserData: {** **            // no properties defined here** **        }** **    }));**

**    WinJS.Namespace.define("ViewModel.UserData", {** **        _wordValue: null,** **        wordLength: null,** **        word: {** **            get: function() {** **                return this._wordValue;** **            },** **            set: function (newVal) {** **                var oldWordVal = this._wordValue;** **                var oldLengthVal = this.wordLength;** **                this._wordValue = newVal;** **                this.wordLength = newVal ? newVal.length : null;** **                this.notify("word", newVal, oldWordVal);** **                if (this.wordLength != oldLengthVal) {** **                    this.notify("wordLength", this.wordLength, oldLengthVal);** **                }** **            }** **        }** **    }); })();**`

以这种方式设置视图模型需要两个步骤。第一种是使用WinJS.Binding.as方法创建一个不包含属性的可观察对象。这在对象中设置了可观察的功能,但是没有用 getters 和 setters 替换任何属性。

第二步是仅使用define方法手工定义需要同步更新的属性,以便将它们添加到您在第一步中作为名称空间导出的对象中。

您可以使用 getters 和 setters 来定义其他值的派生属性。在 setter 中,您执行派生属性的计算,然后使用notify方法发布 changes 属性的更新,如下所示:

... this.notify("word", newVal, oldWordVal); if (this.wordLength != oldLengthVal) {     this.notify("wordLength", this.wordLength, oldLengthVal); } ...

当我调用WinJS.Bind.as方法时,notify方法被添加到了ViewModel.UserData对象中,观察者需要使用bind方法来注册事件。notify方法的参数是发生变化的属性的名称、新值和旧值。notify方法与可观察对象内部使用的机制相同,它允许您完全控制视图模型发出变更事件的方式。在清单中,通过确保在更新完所有属性值之前不调用notify方法,我能够确保在更新完这两个值之前不发送wordwordLength属性的变更事件。

让我重申一下,你只需要在非常特殊的情况下走这么远。我已经向您展示了这种技术,因为这些情况经常令人惊讶地出现,并且因为它允许您获得关于 WinJS 数据绑定机制如何配合的更多细节。

使用声明绑定

到目前为止,在我向您展示的示例中,视图模型中的值是使用编程绑定来消费的,这意味着我在我的 JavaScript 代码中接收属性值更改的通知,并通过更新应用的 HTML 中的一个或多个元素的内容来响应。我可以通过使用声明性绑定来优化这个过程,其中我将数据绑定的细节作为 HTML 元素声明的一部分,并允许 WinJS 为我管理更新过程。

处理文档

使用声明性绑定的前提是调用WinJS.Binding.processAll方法,该方法在 HTML 中定位绑定并激活它们。使用单页内容模型时,将对此方法的调用放在导航事件处理程序函数中很重要,以便在导入内容时处理页面中的声明性绑定;否则,您的绑定将不会被激活。在我的示例应用中,导航事件处理程序在default.js文件中,你可以在清单 8-16 中看到processAll方法调用。

清单 8-16 。打电话给温家。导航事件处理函数中的 Binding.processAll 方法

(function () {     "use strict"; `**    WinJS.Binding.optimizeBindingReferences = true;**

    var app = WinJS.Application;

    WinJS.Navigation.addEventListener("navigating", function (e) {

        var navbar = ViewModel.State.navBarControlElement;         if (navbar) {             navbar.parentNode.removeChild(navbar);         }

        if (ViewModel.State.appBarElement) {             ViewModel.State.appBarElement.winControl.hide();         }

        WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {             WinJS.Utilities.empty(contentTarget);             WinJS.UI.Pages.render(e.detail.location, contentTarget)                 .then(function () { **                    return WinJS.Binding.processAll(document.body, ViewModel);**                 }).then(function() {                     return WinJS.UI.Animation.enterPage(contentTarget.children)                 });         });     });

    app.onactivated = function (eventObject) {        WinJS.UI.processAll().then(function () {

            ViewModel.State.appBarElement = appbar;             ViewModel.State.navBarContainerElement = navBarContainer;

            WinJS.Navigation.navigate("page1.html");

            fontSelect.addEventListener("change", function (e) {                 command.innerText = this.value;                 fontFlyout.winControl.hide();             });         });         };     app.start(); })();`

images 注意当你使用声明式绑定时,你应该总是将WinJS.Binding.optimizeBindingReferences属性的值设置为true,就像我在例子中所做的那样。虽然我没有遇到任何问题,但微软警告说,省略这一步会产生绑定操作导致内存泄漏的风险。我没有在本书的一些例子中应用这个属性,因为我想让他们尽可能专注于手头的功能,但是对于真正的项目,你应该采纳微软的建议。

您需要将对WinJS.Binding.processAll方法的调用放入到Promise.then调用链中,以便在用WinJS.UI.Pages.render方法导入内容之后、向用户显示之前处理绑定。processAll方法返回一个Promise对象,所以很容易得到正确的顺序,尽管嵌套的then调用链可能会变得相当深。

images 提示我在第九章中深入覆盖了WinJS.Promise对象。

WinJS.Binding.processAll方法的参数是处理的开始元素和数据值的来源。我已经将开始元素设置为document.body,这确保了整个布局被处理。对于数据值的来源,我已经指定了ViewModel对象。

声明绑定

一旦安排好用processAll方法处理绑定,就可以继续在 HTML 中声明绑定了。为了演示声明性绑定,我更新了page2.html文件中的面板,如清单 8-17 所示。

清单 8-17 。使用 page2.html 文件中的十进制装订

`

                                         
            

This is Page 2

            

                
                                         OK                 

                

                    The word is:                     <span id="wordspan" **                        data-win-bind="innerText: UserData.word"**>????                 

                

                    The length is:                     <span id="lengthspan" **                        data-win-bind="innerText: UserData.wordLength"**>????                 
            
        
    

`

首先,请注意我已经从script元素中移除了编程绑定——我不再调用bind方法或者在代码中更新span元素的内容。相反,我给span元素添加了data-win-bind属性。该属性是声明性绑定特性的核心,它允许您指定如何设置一个HTMLElement对象的一个或多个属性值。

WinJS 声明性绑定在表示 DOM 中元素的HTMLElement对象上操作。这意味着如果你想设置一个元素的文本内容,你可以指定将innerText属性设置为你传递给processAll方法的对象的UserData.word属性,就像这样:

... <span id="wordspan" **data-win-bind="innerText: UserData.word"**></span> ...

冒号(:)将属性名称与数据属性名称分开。如果与声明性绑定相关的数据项是可观察的,则声明性绑定会自动保持最新。清单中的两个声明性绑定都与可观察的数据项相关,因此我的布局与视图模型保持同步。当您只需要向用户显示值时,这是使用编程绑定的一个很好的替代方法。

images 提示通过用分号(;)分隔绑定,您可以在单个**data-win-bind**属性中绑定多个属性。

创建可观察数组

正如我前面提到的,WinJS.Binding.as方法只能观察简单的值属性。它将忽略对象、函数、日期和数组(尽管在对象的情况下,它将寻找嵌套的简单值属性并使它们可被观察到)。在这一节中,我将向您展示如何创建可观察数组。可观察数组不仅有用,而且是我在本书第三部分中描述的一些 UI 控件的重要基础。

创建可观察数组

通过创建新的WinJS.Binding.List对象来创建可观察数组。你可以看到我是如何在清单 8-18 中的文件中添加这样一个对象的。我将使用这个List来保存用户输入的单词的简单历史。

清单 8-18 。向视图模型添加一个可观察数组

`(function () {     "use strict";

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

    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({         UserData: {             word: null,             wordLength: null,         }     }));

**    ViewModel.UserData.wordList = new WinJS.Binding.List();**

    ViewModel.UserData.bind("word", function (newVal) {         if (newVal) {             ViewModel.UserData.wordLength = newVal ? newVal.length : null; **            ViewModel.UserData.wordList.push(newVal);**         }     }); })();`

WinJS.Binding.List对象受WinJS.Namespace.define方法删除名称以下划线开头的属性和函数的影响——解决方案是手动将List对象添加到名称空间,如清单所示。

images 提示注意,我已经返回到清单中视图模型的更简单形式。我在本章其余部分描述的技术可以应用于任何一种方法,但是这个更简单的视图模型使我更容易说明我所做的更改。

List对象不是 JavaScript 数组的直接替代。它实现了数组中的许多方法和属性,包括我在清单中使用的在word属性更新时向List添加一个项目的push方法。主要的区别是没有数组索引器(像myArray[0]),你必须使用getAtsetAt方法(myArray.getAt(0))来代替。您很快就会习惯它,从积极的方面来看,List对象支持一些很好的特性来排序和过滤它的内容。表 8-2 总结了由List对象定义的最重要的方法。

images

images

这些只是基本的方法,我建议您花些时间看看 API 文档,更详细地探索一下List对象的功能。

images 提示List对象支持的许多功能是它作为一些 WinJS UI 控件的数据源所必需的。你可以在第三部分中了解更多关于数据源和使用它们的控件的信息。

在清单 8-18 中,我创建了一个空的List对象,但是如果你将一个 JavaScript 数组传递给构造函数,你可以预先填充一个List。你也可以传递一个可选的配置对象,它支持表 8-3 所示的属性。

images

您可以按如下方式应用这些选项:

... var myArray = ["Apple", "Orange", "Cherry"]; var myList = new WinJS.Binding.List(myArray, **{** **    proxy: true,** **    binding: false** **}**); ...

我还没有在实际项目中使用过这些选项。proxy属性是危险的,并且binding选项需要在由List发出的事件的处理函数中非常小心地编码,以防止观察到不再是集合的一部分的对象(并且在有用的情况下,我更喜欢自己注意使对象可被观察到)。

观察列表

正如您所料,List对象会在集合内容发生变化时发出事件。有几个事件,最有用的是iteminserteditemchangeditemremoveditemmoved。每个事件的重要性从它的名字就可以看出,通过查看传递给事件处理函数的事件的detail属性,您可以获得插入、更改、删除或移动内容的详细信息。

images更改列表中对象的属性值不会触发事件。

为了演示如何观察一个List对象,我向由page2.html定义的布局添加了一个新面板,并向script元素添加了一些新代码。您可以在清单 8-19 中看到这些变化。

清单 8-19 。观察一个 WinJS。Binding.List 对象

`

         

**                ViewModel.UserData.wordList.addEventListener("iteminserted", function(e){** **                    var newDiv = document.createElement("div");** **                    newDiv.innerText = e.detail.value;** **                    WinJS.Utilities.addClass(newDiv, "word");** **                    wordList.appendChild(newDiv);** **                });**             }         });     

    
        

This is Page 2

        

            
                                 OK             

            

                The word is:                 <span id="wordspan"                     data-win-bind="innerText: UserData.word">????             

            

                The length is:                 <span id="lengthspan"                     data-win-bind="innerText: UserData.wordLength">????             
        

**        

** **            Word List:** **            
** **        
**     

`

您通过调用addEventListener方法来观察一个List对象,指定您感兴趣的事件类型和将处理变更事件的回调函数。在这个清单中,我通过创建一个新的div元素来响应iteminserted事件,将它分配给 CSS word类,并将其添加到新的布局面板中。我将传递给处理函数的Event对象的innerText属性设置为detail.value属性。Event.detail对象还为新添加的项目定义了index属性,这样您就可以知道项目被添加到了List中的什么位置。你可以在图 8-2 中看到结果:每次用户输入一个单词并点击OK按钮时都会添加一个新元素,创建一个简单的历史。

images

***图 8-2。*观察列表对象以创建简单的历史显示

您会注意到,我只展示了List对象的编程绑定。有一些可用的声明性绑定,但是它们被包装在一些 WinJS UI 控件中。我将在本书的第三部分中介绍这些控件并解释它们如何与List对象一起使用。

使用模板

在前面的例子中,我最终创建了一系列的div元素来表示用户输入单词的历史。我使用了 DOM API,它可以工作,但是使用起来很笨拙,而且容易出错。幸运的是,WinJS 提供了一些与数据绑定特性相关的基本模板支持,这使得基于视图模型中的数据创建元素变得更加容易。清单 8-20 显示了我在前面的例子中使用 JavaScript 创建的元素——这些将作为我的模板的(简单)目标。

清单 8-20 。单词历史列表中的代码生成元素

`...

    Word List:     
**        
HTML
** **        
CSS
** **        
JavaScript
**     
...`
定义和使用模板

通过将data-win-control属性设置为WinJS.Binding.Template来表示将成为模板的元素。当文档由WinJS.UI.processAll方法处理时,元素从它在 DOM 中的位置被移除,并准备用作模板。因为这是一个常规的 WinJS 控件,所以在代表 DOM 中主机元素的HTMLElement对象中添加了一个winControl属性。清单 8-21 显示了向page2.html文件添加一个模板以及使用它的 JavaScript 代码。

清单 8-21 。声明和使用模板

`

         

                ViewModel.UserData.wordList.addEventListener("iteminserted",                     function (e) { **                        wordTemplate.winControl.render({ wordValue: e.detail.value },** **                            wordList);**                 });             }         });     

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

    

        

This is Page 2

        

            
                                 OK             

            

                The word is:                 <span id="wordspan"                     data-win-bind="innerText: UserData.word">????             

            

                The length is:                 <span id="lengthspan"                     data-win-bind="innerText: UserData.wordLength">????             
        
        
            Word List:             
        
    

`

您将WinJS.Binding.Template控件应用于包含您的模板的元素。您需要为模板元素定义一个id属性,以便以后可以定位它。在容器元素中,使用data-win-bind属性定义模板内容,以引用要在模板中应用的数据值。

您将为绑定提供值的数据对象直接传递给render方法模板控件,这意味着数据项不必是视图模型的一部分。在清单中,我的模板由单个div元素组成,匹配我在代码中生成元素时使用的模式。我使用了data-win-bind属性来指定将使用传递给模板的数据对象的wordValue属性来设置innerText属性:

`...

    
....`

要使用模板,您需要在 DOM 中找到模板容器元素,并使用winControl属性来访问WinJS.Binding.Template对象。模板对象定义了接受两个参数的render方法——应用于模板的数据和从模板生成的内容将要插入的元素。您需要使数据值与模板期望的一致,所以我创建了一个数据对象,将添加到WinJS.Binding.List对象的新值映射到wordValue属性,然后我将它传递给 render 方法:

... wordTemplate.winControl.render(**{ wordValue: e.detail.value }**, wordList); ...

结果是我的元素是从标记中生成的,而不是纯粹用代码。我喜欢模板方法,我在我的 web 应用中广泛使用模板。在 Windows 应用中,一些更高级的 UI 控件依赖于模板来显示数据,我将在第三部分中向您展示它们是如何操作的(以及它们扮演的角色)。

使用价值转换器

我想回去稍微整理一下。通过使用数据绑定到一个可观察的视图模型值,我引入了一个轻微的修饰问题,你可以在图 8-3 中看到。

images

***图 8-3。*布局的初始状态

我将视图模型中的wordwordLength属性的值设置为null,这样属性就被定义了,并表明还没有设置任何值。问题是null值在布局中显示给用户,这并不美观。

为了解决这个问题,我将使用一个 WinJS 绑定转换器,它是一个 JavaScript 函数,充当视图模型值和数据绑定之间的中介。你可以在清单 8-22 中看到我是如何创建我的转换器的,它显示了对viewmodel.js文件的一些添加。您不必将转换器放在视图模型文件中,但是我采用了厨房水槽的方法,将所有东西放在一起。

清单 8-22 。向 viewmodel.js 文件添加绑定转换器

`(function () {     "use strict";

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

    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({         UserData: {             word: null,             wordLength: null,         }     }));

    ViewModel.UserData.wordList = new WinJS.Binding.List();

    ViewModel.UserData.bind("word", function (newVal) {         if (newVal) {             ViewModel.UserData.wordLength = newVal ? newVal.length : null;             ViewModel.UserData.wordList.push(newVal);         }     });

**    WinJS.Namespace.define("ViewModel.Converters", {** **        defaultStringIfNull: WinJS.Binding.converter(function (val) {** **            return val ? val : "";** **        })** **    });** })();`

我创建了一个名为ViewModel.Converters的新名称空间,并定义了一个名为defaultStringIfNull的转换器。转换器是一个常规的 JavaScript 函数,它接受单个参数并返回转换后的值。这个函数被传递给WinJS.Binding.converter方法,该方法返回一个可以在数据绑定中使用的对象。在这个例子中,我的转换器返回它所传递的值,除非它是null,在这种情况下,返回<No Word>

在声明性绑定中,将转换器的名称放在视图模型属性之后。清单 8-23 展示了我如何更新了page2.html文件中的绑定以使用defaultStringIfNull转换器。

清单 8-23 。应用数值转换器

`...

    The word is:     ????
    The length is:     ????
...`

您必须指定转换器的完整路径,该路径必须通过全局命名空间可用。对于我的示例应用,这意味着我必须将转换器称为ViewModel.Converters.defaultStringIfNull。你可以在图 8-4 中看到结果,当wordwordLength属性为null时,一个更有意义的值呈现给用户。

images

***图 8-4。*使用绑定转换器避免向用户显示空值

使用开放值转换器

我在上一节中使用WinJS.Binding.converter方法创建的值转换器不知道它正在处理的值来自哪里,也不知道转换后的值将应用于哪个元素。这是一个闭值转换器,因为对于给定的输入值,它总是产生相同的转换结果。你可以在图 8-4 中看到这样的效果——我使用转换器的两个地方都显示了<No Word>,我没有办法修改结果来更好地匹配目标元素。我可以获得不同转换值的唯一方法是创建多个转换器,这并不理想,因为大多数转换器代码都非常相似,会导致不必要的重复。

一个更高级的选择是一个开放转换器,它可以看到绑定请求的全部细节。这种转换器更难创建,但这意味着您可以根据所请求的数据值或转换后的值将应用到的元素来定制转换器产生的结果。在清单 8-24 中,我已经用一个开放的转换器替换了前一个例子中的viewmodel.js文件中的转换器,该转换器根据被转换的数据属性调整显示在标记中的值。

小心这是一项先进的技术,在大多数情况下,封闭式捆绑已经足够了。只有在您需要在多个绑定中复制大量代码以生成略微不同的结果的情况下,才使用开放绑定。

清单 8-24 。应用开放式数据转换器

`(function () {     "use strict";

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

    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({         UserData: {             word: null,             wordLength: null,         }     }));

    ViewModel.UserData.wordList = new WinJS.Binding.List();

    ViewModel.UserData.bind("word", function (newVal) {         if (newVal) {             ViewModel.UserData.wordLength = newVal ? newVal.length : null;             ViewModel.UserData.wordList.push(newVal);         }     });

**    WinJS.Namespace.define("ViewModel.Converters", {** **        defaultStringIfNull: function (src, srcprop, dest, destprop) {** **            var srcObject= src;** **            var targetObject = dest;**

**            srcprop.slice(0, srcprop.length -1).forEach(function (prop) {** **                srcObject = srcObject[prop] != undefined ? srcObject[prop] : null;** **            });**

**            destprop.slice(0, destprop.length -1).forEach(function (prop) {                 targetObject = targetObject[prop] == undefined ? null:** **                    targetObject[prop];** **            });**

**            srcObject.bind(srcprop[srcprop.length - 1], function (val) {** **                var value = val != null ? val :** **                    srcprop[srcprop.length - 1] == "wordLength" ? "-" : "";** **                targetObject[destprop[destprop.length - 1]] = value;** **            });** **        }** **    });**

**    ViewModel.Converters.defaultStringIfNull.supportedForProcessing = true;**

})();`

您不需要调用任何WinJS.Binding方法来创建一个开放的转换器。相反,您只需创建一个带有四个参数的函数。您的函数将被调用来转换数据值,并且您可以使用参数来确定需要什么以及它将被应用到哪里。为了解释这些论点,我将基于示例应用中的以下声明性绑定进行描述:

... <span id="lengthspan" data-win-bind="innerText: UserData.wordLength       ViewModel.Converters.defaultStringIfNull">????</span> ...

第一个参数是传递给WinJS.Binding.processAll方法的数据对象——例如,这将是ViewModel对象。第二个参数是被请求的数据值,表示为名称数组。在示例绑定中,我想要将在第二个参数中显示为["UserData", "wordLength"]UserData.wordLength属性。

第三个参数是绑定的目标。对于我的例子,这将是跨度元素,它的idlengthspan。最后一个参数是转换后的值将应用到的属性——这是另一个名称数组,因此我的函数将接收数组["innerText"]作为示例绑定。

这个转换器中的大部分代码处理名称数组,这样我就可以获得数据值并将它们应用到目标对象。

你不只是从一个开放的转换器返回一个值。相反,您必须设置一个编程绑定,以便随着数据值的变化,目标保持最新。正如我前面所描述的,我使用了bind方法。在该示例中,根据所请求的属性,我从转换中返回不同的值,如下所示:

... srcObject.bind(srcprop[srcprop.length - 1], function (val) {     var value = val != null ? val : srcprop[srcprop.length - 1] == **        "wordLength" ? "N/A" : "<No Word>";**     targetObject[destprop[destprop.length - 1]] = value; }); ...

这种技术是用于支持声明性绑定的编程绑定的奇怪组合,它比任何一种技术本身都要多做很多工作。但是灵活性有时是值得努力的。

images 提示请注意,我没有根据所使用的目标元素做出任何决定——这样做是实现紧密耦合的途径,因为标记结构的知识将嵌入到转换器中。如果您必须根据应用的位置来定制转换后的结果,那么就限制自己根据元素类型或它所属的类来做出决定。

默认情况下,除非将supportedForProcessing属性显式设置为true,否则不能从标记中调用函数,如下所示:

... ViewModel.Converters.defaultStringIfNull.supportedForProcessing = true; ...

当您创建封闭转换器时,这个属性是自动为您设置的,但是由于开放转换器需要更多的手动操作,您需要自己负责设置它。如果您忘记了,应用将在执行转换器功能时终止。

总结

在本章中,我向您展示了如何将视图模型引入到您的 Windows 应用中,如何使其可观察,以及如何使用编程和声明性数据绑定来响应更新的数据值。我还向您展示了如何创建可观察数组,以及如何用它们来驱动简单的模板。

通过应用视图模型,您可以将数据从标记中分离出来,确保您的数据在整个应用中都可用,并使长期开发和维护变得更加简单和容易。通过将数据绑定应用到视图模型,您可以创建一个能够流畅地响应数据变化的应用。在下一章,我将深入研究WinJS.Promise对象的细节,并向您展示如何使用它来充分利用 WinJS 和 Windows APIs。