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

38 阅读1小时+

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

原文:Pro Windows 8 Development with HTML5 and JavaScript

协议:CC BY-NC-SA 4.0

十八、使用动画和工具

在这一节中,我将向您展示如何使用 WinJS 动画特性。Windows 应用通常具有的颜色和排版的一致性可能会使用户难以意识到内容已被更新或替换,因此用一个简短的动画来突出显示这种变化可能会很有用。我在前面的章节中提到了一些基本的动画特性,但是现在我要回到这个主题上来。我首先向您展示如何直接使用 WinJS 所依赖的 CSS 特性,然后解释这些特性是如何被 WinJS 动画便利方法打包的。WinJS 包含一些用于常见布局更新场景的预打包动画,我将向您展示如何使用这些动画——包括如何为动画准备元素以及如何在之后进行整理。

在本章的第二部分,我描述了一些在WinJS.Utilities名称空间中有用的方法。我在整本书中一直在使用这些方法,并且我已经描述了当我第一次使用它们时它们做了什么。在这一章中,我给出了一个更完整的使用指南,并演示了一些额外的功能,包括计算布局中元素的大小和位置,以及创建一个灵活的日志记录系统,即使应用没有连接到调试器,您也可以使用该系统。表 18-1 对本章进行了总结。

images

使用动画

出于我不完全理解的原因,当涉及到应用中的动画时,一些程序员变得有点疯狂。不仅仅是 Windows 应用——你可以在网络应用、桌面应用以及任何地方看到这种疯狂的结果。你可以很快疏远你的用户,特别是如果动画阻止用户继续当前的任务。过多的动画是业务线应用中的一种残酷形式,用户将日复一日地重复执行相同的任务。在开发过程中看起来很酷很刺激的动画,当你的用户每天看一百遍的时候,会让他们筋疲力尽。当涉及到 Windows 应用中的动画时,我遵循一套简单的规则,我建议你也这样做:

  1. Only use animation to attract users' attention to changes or results that they may miss.
  2. Keep the animation short to ensure that it won't interfere with the user's work. Use standard animations where they exist, and create subtle effects when they do not exist.

这三条规则会让你的应用看起来不像维加斯的老虎机,让你的应用使用起来更愉快。

创建示例项目

为了演示不同的动画和技术,我创建了一个名为Animations的示例项目。这遵循了我在前面章节中使用的模式,并使用单页模型为本章中的每个示例导入内容,并支持通过 NavBar 导航。你可以在清单 18-1 中看到这个项目的default.html文件的内容,它包含了我将在本章中添加的三个内容页面的导航条命令。

清单 18-1 。动画项目中 default.html 文件的内容

`

         Animations                         ` `     
        

Select a page from the NavBar

    
    

        <button data-win-control="WinJS.UI.AppBarCommand"             data-win-options="{id:'CSSTransitions', label:'CSS Transitions',                 icon:'\u0031', section:'selection'}">           

        <button data-win-control="WinJS.UI.AppBarCommand"             data-win-options="{id:'CoreFunctions', label:'Core',                 icon:'\u0032', section:'selection'}">           

        <button data-win-control="WinJS.UI.AppBarCommand"             data-win-options="{id:'ContentAnimations', label:'Content Animations',                 icon:'\u0033', section:'selection'}">           

    

`

为了布局这个例子中的元素,我在清单 18-2 中定义了样式,它显示了/css/default.css文件的内容。这都是标准的 CSS,在这个文件中没有特定于应用的技术。

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

body {background-color: #5A8463;} .column { display: -ms-flexbox; -ms-flex-direction: column;     -ms-flex-align: center; -ms-flex-pack: center;} div.panel { border: medium white solid; margin: 10px; padding: 20px;} .outerContainer { display: -ms-flexbox; -ms-flex-direction: row;     -ms-flex-align: stretch; -ms-flex-pack: center;} .coloredRect { background-color: black; color: white; width: 300px;     height: 300px; margin: 20px; font-size: 40pt;text-align: center; } .coloredRectSmall { width: 200px; height: 200px;} .buttonPanel button { width: 200px; font-size: 20pt; margin: 20px; height: 85px;}

内容页面之间的导航由/js/default/js文件处理,其内容可以在清单 18-3 中看到。这是我在前一章中使用的相同的基本导航代码。

清单 18-3 。default.js 文件的内容

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

    WinJS.Navigation.addEventListener("navigating", function (e) {         var elem = document.getElementById("contentTarget");

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

    app.onactivated = function (eventObject) {         WinJS.UI.processAll().then(function () {             document.getElementById("navbar").addEventListener("click",                 function (e) {                     var navTarget = "pages/" + e.target.winControl.id + ".html";                     WinJS.Navigation.navigate(navTarget);                 });         })     };

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

直接使用元素

CSS3 定义了对过渡转换的支持,以动画化 HTML 标记中的元素。过渡允许您更改 CSS 属性的值,转换允许您平移、缩放和旋转元素。

images 提示在这一节中,我主要关注与 WinJS 动画直接相关的 CSS3 特性。关于 CSS3 更完整的报道,请参阅我的另一本书html 5的权威指南,这本书也是由 Apress 出版的。

过渡和转换是 WinJS 动画使用的底层机制。通过理解 CSS3 的功能是如何工作的,你将会对 WinJS 的功能有更好的理解,并且如果需要的话能够创建你自己的效果。为了演示如何直接使用这些 CSS 特性,我在示例 Visual Studio 项目的pages文件夹中添加了一个文件。这个文件叫做CSSTransitions.html,如清单 18-4 所示。

清单 18-4 。使用 CSS 过渡和转换

`

` `                  .colorTransition {             color: black;             background-color: white;             font-size: 50pt;             transition-delay: 500ms;             transition-duration: 1000ms;         }          

    
        
            
Transition
        
        
            
Transform
        
    
`

这个例子在布局中有两个明确标记的元素,我将用它们来演示转换和变换。你可以在图 18-1 中看到初始布局,我将在接下来的章节中解释其功能。

images

***图 18-1。*过渡和变换的初始布局示例

应用过渡

当您应用转换时,您告诉浏览器您希望一个或多个属性的值在一段时间内逐渐改变。您可以将过渡应用于颜色和任何具有数值的属性。

images 提示有两种方法可以应用过渡和变换:通过 CSS 类或者使用 JavaScript 直接应用于元素。我使用了基于类的方法进行转换,并在本章的后面向您展示了基于代码的方法进行转换。

有几个 CSS 属性可以用来定义一个过渡的特征,我已经在表 18-2 中描述了这些属性。

images

在这个例子中,我定义了一个 CSS 类,它包含了我的转换。我省略了transition-property,这意味着任何值被改变的属性都服从于transition-directiontransition-delay属性,如清单 18-5 所示。

清单 18-5 。使用 CSS 过渡属性

`...

    .colorTransition {         color: black;         background-color: white;` `        font-size: 50pt; **        transition-delay: 500ms;** **        transition-duration: 1000ms;**     }

...`

当这个类被应用到一个元素时,colorbackground-colorfont-size属性的新值将在 500 毫秒的延迟后 1 秒内被应用。我将这个类应用于目标元素以响应click事件,如清单 18-6 所示。

清单 18-6 。响应于点击事件应用转换类

... WinJS.Utilities.toggleClass(e.target, "colorTransition"); ...

如果指定的元素上没有colorTransition类,则WinJS.Utilities.toggleClass方法会添加它,如果有,则移除它。这意味着元素将随着每个click在正常状态和转换状态之间移动。你可以在图 18-2 的中看到这个效果,它显示了应用过渡时从初始状态的渐进过程。这些只是快照,如果你用示例应用进行实验,你会看到完整的效果是多么平滑。

images

***图 18-2。*一个 HTML 元素的过渡

该图显示了应用过渡时产生的逐渐过渡。请注意,colorTransition类中所有属性的值被同时修改。

如果再次单击该元素,它会立即恢复到原始状态。这是因为transition-delaytransition-duration属性是colorTransition类的一部分,当该类被删除时,应用渐变的指令也被删除。通过在colorTransition类之外设置transition-duration属性,可以确保所有的属性更改都是逐步进行的。

应用变换

在第十七章中,当我修改元素来响应手势时,我使用了一个变换。在这一节中,我将向您展示实现相同结果的不同方法,并演示变换和过渡如何协同工作。变换是通过transform属性来控制的,我在表 18-3 中总结了这个属性,这样你将来可以很容易地找到参考。

images

transform 属性的值可能相当复杂,这取决于您想要进行的更改的性质。我在表 18-4 中列出了不同的变换值。

images

您可以通过将一个或多个单独的转换连接起来作为 transform 属性的值来指定一个转换(这就是我在《??》第十七章中使用的CSSMatrix对象的作用)。例如,我定义了三个不同的transform值,如清单 18-7 所示。

清单 18-7 。定义变换值

... var transforms = ["", "translateX(100px) rotate(45deg)", "translateY(50px) scale(1.2)"]; ...

这个数组中有三个transform值。第一个是空字符串,这意味着没有应用任何转换。第二个将元素沿 X 轴移动 100 个像素,并将其旋转 45 度。旋转用 CSS 度数单位表示(即90deg为 90 度),正值表示顺时针旋转。最后的转换将元素沿 Y 轴移动 50 个像素,并将元素的大小增加 20%。

这些是绝对变换,意味着当你应用其中一个值时,任何先前的变换都被撤销。如果你想累积应用变换,那么你可以使用CSSMatix对象,我在第十七章的中提到过。

在本例中,每次单击元素时,我都会依次遍历这些转换。变换和过渡可以组合在一起,为了演示这一点,我还为transitionDurationbackgroundColor属性应用了一系列值。我使用 DOM 属性应用了这个值,如清单 18-8 所示。

清单 18-8 。使用 DOM 应用变换

... var curr = e.target.style.transform; var index = (transforms.indexOf(curr) + 1) % 3; **e.target.style.transitionDuration = durations[index];** e.target.style.transform = transforms[index]; e.target.style.backgroundColor = colors[index]; ...

我突出显示了为属性transitionDuration赋予新值的语句。当通过 DOM 使用转换和过渡时,在将更改应用到其他属性之前设置这个值是很重要的——如果您不采取这种预防措施,那么更改将会立即应用,不会有任何渐进的效果。你可以看到一组变换和过渡是如何应用到元素图 18-3 中的,尽管你应该用示例应用来体验一下真实的效果有多平滑和优雅。

images

***图 18-3。*通过 DOM 应用变换和过渡

使用 WinJS 核心动画方法

WinJS.UI名称空间包含一组方法,使得在 Windows 应用中应用过渡和变换更加容易。这些是控制 WinJS 动画整体状态的包装器函数,并为我在上一节描述的 CSS 功能提供了一个方便的包装器。表 18-5 总结了核心动画制作方法。

images

前三种方法允许您控制应用中的所有动画。将此功能作为设置向用户公开是一个好主意,这样他们就可以禁用动画——我在第二十章中向您展示如何显示用户设置。

images 提示这里有一个术语不匹配。您可以使用executeAnimationexecuteTransition方法来执行 CSS 过渡和转换。不同之处在于操作完成时元素所处的状态。使用executeAnimation方法,元素被返回到它们的原始状态,而executeTransition使元素保持修改后的状态,这与前面的例子非常相似。微软并不想在这些名字上为难——executeAnimation方法支持一个叫做关键帧的特性,它执行更复杂的效果,被称为动画——这就是方法名称的来源。我没有在这一章描述关键帧,因为它们不太适合更广泛的 Windows UX 主题,但你可以在[www.w3.org/TR/css3-tra…](http://www.w3.org/TR/css3-transforms)获得更多细节。

executeAnimationexecuteTransition方法将 CSS 变换和过渡应用于元素。这些方便的方法比直接使用 CSS 类或style属性更容易使用。为了演示这些方法,我在示例项目中添加了一个名为CoreFunctions.html的新页面,它使用executeAnimationexecuteTransition方法再现了早期的内容。你可以在清单 18-9 中看到这个新文件的内容。

清单 18-9 。使用 executeAnimation 和 executeTransition 方法

`

              

**                            WinJS.UI.executeTransition(e.target, [** **                                { property: "transform",** **                                    to: transforms[index], duration: durations[index],** **                                    delay: 0, timing: "ease"** **                                }, { property: "background-color",** **                                    to: colors[index], duration: durations[index],** **                                    delay: 0, timing: "ease"}** **                            ]);**                             break;                     }                 });             }         });     

    
        
            
Transition
        
        
            
Transform
        
    
`

这两种方法的第一个参数是您想要操作的元素集。这可以是单个元素(这是我在示例中使用的),也可以是元素的数组,这将导致相同的效果应用于数组中的所有元素。第二个参数是一个对象,其属性包含要应用的过渡或变换的详细信息。我已经在表 18-6 中描述了支持的属性名称。

images

使用 executeAnimation 方法

如果您想一次操作多个属性,那么您可以将包含表中属性的对象数组作为第二个参数传递给executeAnimationexecuteTransition方法。这两个方法都返回WinJS.Promise对象,你可以用它们来链接效果。如清单 18-10 所示,我在示例中使用了两种技术,你可以在第九章的中了解更多关于Promise对象的信息。

images 提示当指定属性值时,使用 CSS,而不是 DOM,属性名。所以,比如用background-color而不用backgroundColor。如果使用 DOM 属性名称,效果将无法正确应用。

清单 18-10 。预先制作多个同步效果和连锁效果

... WinJS.UI.executeAnimation(e.target, [{     property: "background-color", to: "white", duration: 500, delay: 0, timing: "ease" }, {     property: "color", to: "black", duration: 500, delay: 0, timing: "ease" }]) .then(function () {     return WinJS.UI.executeAnimation(e.target, {         property: "font-size", to: "50pt", duration: 500, delay: 0, timing: "ease"     }); }); ...

这是处理左边元素的片段——标记为Transition的那个。我首先使用executeAnimation方法来转换background-colorcolor属性——我是通过将两个对象的数组作为第二个参数传递给该方法来完成的。我在返回的Promise上使用了then方法来链接对executeAnimation方法的第二次调用——这次是为了转换font-size属性。结果是background-colorcolor属性被一起转换,当两个转换都完成时,font-size属性被转换。

executeAnimationexecuteTransition方法的区别在于,在动画结束时,元素会返回到其原始状态。最好的方法是使用示例应用并点击Transition元素,但我试图捕捉图 18-4 中的效果。不会逐渐返回到初始状态——元素只是快速返回到调用executeAnimation方法之前的位置。

images

***图 18-4。*使用 executeAnimation 方法

元素的状态在每次调用executeAnimation结束时被重置,您可以在图中看到它的效果。在font-size属性转换之前,background-colorcolor属性的值被重置。

使用 executeTransition 方法

除了被操作的一个或多个元素保持在它们的转换状态之外,executeTransition方法的工作方式与executeAnimation方法类似。你可以在清单 18-11 的中看到我是如何使用executeTransition方法的。

清单 18-11 。使用 executeTransition 方法

... var curr = e.target.style.transform; **var index = (transforms.indexOf(curr) + 1) % 3;**                              WinJS.UI.executeTransition(e.target, [     { property: "transform", to: transforms[index], duration: durations[index],           delay: 0, timing: "ease"     }, { property: "background-color", to: colors[index],           duration: durations[index], delay: 0, timing: "ease"}     ]); ...

使用这个方法就像使用executeAnimation方法一样,您可以从清单中看到,我向该方法传递了一个对象数组,这样就可以转换transformbackground属性。当直接使用 CSS 属性时,使用transform属性具有相同的效果,并允许将变换应用于元素。

事实上,executeTransform方法只是我之前向您展示的 CSS 功能的包装,我在清单中突出显示的语句说明了这一点。我可以从元素中读取transform属性的值,以确定我在循环中的位置。

使用 WinJS 动画

WinJS.UI.Animation名称空间包含一组在应用内容上执行预定义动画的方法。这些是标准的 Windows 动画,您应该在应用中执行与它们相关的活动时使用它们,例如,显示新的内容页面。使用这些方法有两个好处。首先,它们使用起来非常简单,比使用executeTransition方法或直接使用 CSS 要简洁得多。第二个原因是,你的应用将与其他 Windows 应用保持一致,并受益于用户先前对这些动画的意义的体验。表 18-7 描述了 WinJS 内容动画。

images 提示WinJS.UI.Animation名称空间中还有其他方法,但是它们被系统用来在 UI 控件中应用动画,并不是通用的。

images

这些是标准化的动画,通常成对使用。作为一个例子,你可以看到我是如何在清单 18-12 中的default.js中使用enterPageexitPage动画方法的。我调用这些方法来响应navigating事件(我在第七章的中描述过)以引起用户对内容变化的注意。

清单 18-12 。使用 default.js 文件中的 enterPage 和 exitPage 方法

`... WinJS.Navigation.addEventListener("navigating", function (e) {     var elem = document.getElementById("contentTarget");

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

表中的所有方法都返回一个WinJS.Promise对象,当动画完成时,该对象被实现。在接下来的章节中,我将向您展示如何使用其他动画。

使用内容进入和退出动画

为了演示内容动画,我添加了一个名为ContentAnimations.html的新 HTML 页面。我将为每个动画构建这些内容,从enterContentexitContent方法开始。你可以在清单 18-13 中看到ContentAnimations.html文件的初始版本。

清单 18-13 。使用内容输入和内容退出动画

`

         

                $('button').listen("click", function (e) {                     switch (e.target.id) {                         case "content":                             var visible = content1.style.display                                 == "none" ? content2 : content1;                             var hidden = visible == content1 ? content2 : content1;

                            WinJS.UI.Animation.exitContent(visible).then(function () {                                 visible.style.display = "none";                                 hidden.style.display = "";                                 WinJS.UI.Animation.enterContent(hidden);                             });                             break;                     }                 });             }         });     

    
        
            
One
            
Two
            
                Enter/Exit Content             
        
    
`

尽管动画方法是成对出现的,但您仍然要负责协调对这些方法的调用,并准备将被动画化的元素。使用exitContent方法制作动画的元素只需要在页面上可见即可。将由enterContent方法引入的元素需要被添加到 DOM 中,但不可见,这是通过将display属性设置为none来实现的,如下所示:

... content2.style.display = "none"; ...

要协调动画序列,以便从一个元素到另一个元素的过渡是平滑的,请遵循以下顺序:

  1. Call the exitContent method, passing in and out elements as parameters.
  2. Use then method for WinJS.Promise returned by exitContent:
    1. Outgoing elements
    2. Set the display property on to none to clear the incoming element.
    3. The display property on calls the enterContent method, passing in the method as a parameter.

这些方法在改变opacity属性的值时翻译元素。这个过程非常快——exitContent动画耗时 80 毫秒,而enterContent动画耗时 370 毫秒。

images 提示通过在 Visual Studio 项目的References部分的ui.js文件中搜索方法名,可以看到每个动画是如何设置的细节。

点击示例中的button元素触发动画。对于每个click事件,我计算出哪个元素是可见的(因此是传出的),哪个元素是隐藏的(因此是传入的)——这允许示例在元素之间交替。

使用淡入和淡出动画

fadeInfadeOut方法操作动画元素的opacity属性。这意味着在动画开始之前,输出元素需要有一个值为1opacity和一个值为0的输入元素。如果您希望一个元素替换另一个元素,那么您需要确保它们在布局中占据相同的空间-一种方法是使用网格布局并将两个元素分配给同一个网格单元。你可以看到我是如何应用这种技术,并调用动画方法的,在建立在前面例子基础上的清单 18-14 中。

清单 18-14 。使用渐强和渐弱方法

`

         

                $('button').listen("click", function (e) {                     switch (e.target.id) {                         case "content":                             var visible = content1.style.display                                 == "none" ? content2 : content1;                             var hidden = visible == content1 ? content2 : content1;

                            WinJS.UI.Animation.exitContent(visible).then(function () {                                 visible.style.display = "none";                                 hidden.style.display = "";                                 WinJS.UI.Animation.enterContent(hidden);                             });                             break; **                        case "fade":** **                            var visible = fade1.style.opacity  == "0" ? fade2 : fade1;** **                            var hidden = visible == fade1? fade2: fade1;**

**                            WinJS.UI.Animation.fadeOut(visible).then(function () {** **                                WinJS.UI.Animation.fadeIn(hidden);** **                            });** **                            break;**                     }                 });             }         });     

    
        
            
One
            
Two
            
                Enter/Exit Content             
        

**        

** **            
** **                
One
** **                
Two
** **            
** **            
** **                Fade In/Out** **            
** **        
**     

`

通过将display属性设置为–ms-grid,可以很容易地将元素放置在同一位置。如果没有为行或列设置任何值,也没有为单元格分配任何元素,那么结果将是一个 1 x 1 的网格,所有内容元素都在同一个单元格中。在这个例子中,我通过将style属性应用到容器元素来设置这种排列。我通常不直接对元素应用 CSS,但是我破例了,因为这是一个如此简单的例子。

fadeInfadeOut不会变换元素的位置(因此不需要显式地改变动画之间的display属性)。你可以在图 18-5 的中看到示例的布局。

使用交叉渐变动画

crossFade方法转换一对元素的opacity属性,这样一个变得透明,而另一个变得不透明。crossFade方法所需的准备与fadeInfadeOut方法相同,因为您需要为输出元素设置opacity1,为输入元素设置0。不同之处在于两个动画是同时开始的。你可以看到我是如何在清单 18-15 中添加对crossFade方法的支持的。

清单 18-15 。使用交叉渐变方法

`

         

                            WinJS.UI.Animation.exitContent(visible).then(function () {                                 visible.style.display = "none";                                 hidden.style.display = "";                                 WinJS.UI.Animation.enterContent(hidden);                             });                             break;                         case "fade":                             var visible = fade1.style.opacity  == "0" ? fade2 : fade1;                             var hidden = visible == fade1? fade2: fade1;

                            WinJS.UI.Animation.fadeOut(visible).then(function () {                                 WinJS.UI.Animation.fadeIn(hidden);                             });                             break; **                        case "crossfade":** **                            var visible = crossfade1.style.opacity** **                                == "0" ? crossfade2 : crossfade1;** **                            var hidden = visible == crossfade1 ? crossfade2 : crossfade1;**

**                            WinJS.UI.Animation.crossFade(hidden, visible);** **                            break;**                     }                 });             }         });     

    
        
            
One
            
Two
            
                Enter/Exit Content             
        

        

            
                
One
                
Two
            
            
                Fade In/Out             
        

**        

** **            
** **                <div id="crossfade1"** **                    class="coloredRect coloredRectSmall column">One
** **                <div id="crossfade2"** **                    class="coloredRect coloredRectSmall column">Two
** **            
** **            
** **                Cross Fade** **            
** **        **     

`

交叉淡入淡出动画非常快。淡入和淡出动画都持续 167 毫秒,因此过渡是即时的。我发现效果有点快,倾向于将fadeOutfadeIn方法链接在一起。你可以在图 18-5 中看到触发动画的元素和按钮的布局。我建议您花一些时间对这三个元素进行实验,以感受用户将如何看到从一个元素到另一个元素的转换。

images

***图 18-5。*使用 enterContent 和 exitContent 动画

使用 WinJS 工具

在本书这一部分的例子中,我一直在使用WinJS.Utilities名称空间的特性。在这一节中,我将描述其中最有用的特性,如果您使用过 jQuery 之类的 DOM 操作库,就会对其中的许多特性很熟悉。

查询 DOM

在本书的许多例子中,我给符号$起了别名,这样它就可以引用WinJS.Utilities.query方法。这是一个类似 jQuery 的方法,它在 DOM 中搜索匹配 CSS 选择器字符串的元素。结果作为一个QueryCollection对象从query方法返回——该对象定义了表 18-8 中描述的方法。

images

到目前为止,我已经在本书的例子中使用了几乎所有这些方法,并且在接下来的章节中也使用了它们。在这一章中,我不打算给出任何具体的例子,因为这些方法是不言而喻的,而且大多数 web 程序员至少对 jQuery 或类似的库有一点熟悉。

images 提示如果你正在做 web 开发而没有使用 jQuery 之类的东西,那么你就错过了。更多细节请见我的书 Pro jQuery,这本书也是由 Apress 出版的。你的网络开发将会改变。

您可以在 Windows 应用项目中使用 jQuery。不过,在很大程度上,我倾向于坚持使用WinJS.Utilities方法。它们完成了 jQuery 支持的大部分基本功能,我发现它们又快又可靠。

确定元素的大小和位置

除了 DOM 查询之外,WinJS.Utilities名称空间还包含帮助确定应用布局中元素的大小和位置的方法。这些方法在表 18-9 中描述。这些方法对单个元素进行操作,这些元素可以使用 DOM 方法如getElementById或从QueryCollection对象中获得。

为了演示这些方法,我创建了一个名为SizeAndPosition的新 Visual Studio 项目。整个项目包含在default.html文件中,如清单 18-16 所示,我已经移除了 Visual Studio 默认添加到新项目中的其他文件。

清单 18-16 。来自 SizeAndPosition 项目的 default.html 文件

`

         SizeAndPosition                             body {             background-color: #5A8463;         }         #container {             margin: 20px;              display: -ms-grid;              -ms-grid-columns: 0.1fr 0.25fr 1fr 0.55fr;              -ms-grid-rows: 0.2fr 0.5fr 1fr 0.1fr;         }         #content {             -ms-grid-row: 2;             -ms-grid-row-span: 2;             -ms-grid-column: 3;             font-size: 30pt; padding: 20px; margin: 20px;             border: thick solid white; text-align: center;         }                      console.log("Content Height: " + WinJS.Utilities.getContentHeight(elem));             console.log("Content Width: " + WinJS.Utilities.getContentWidth(elem));             console.log("Total Width: " + WinJS.Utilities.getTotalWidth(elem));             console.log("Total Height: " + WinJS.Utilities.getTotalHeight(elem));

            var position = WinJS.Utilities.getPosition(elem);             console.log("Position Top: " + position.top);             console.log("Position Left: " + position.left);             console.log("Position Width: " + position.width);             console.log("Position Height: " + position.height);

            var parent = document.getElementById("container");             console.log("Rel Left: " + WinJS.Utilities.getRelativeLeft(elem, parent));             console.log("Rel Top: " + WinJS.Utilities.getRelativeTop(elem, parent));         };         WinJS.Application.start();     

    
        
Here is some content
    
`

在这个例子中,我把所有的东西都放在了一起,所以你可以看到 CSS 如何影响元素的布局,以及这个布局如何影响WinJS.Utilities方法。我定义了一个包含嵌套的div元素的简单布局。外部元素设置为网格布局,其中可用空间以不同的数量分配给行和列。网格中空间的部分分配以及填充和边距的使用使得很难从标记中计算出内部元素的位置。在script元素中,我使用了表 18-9 中的方法来获取职位的详细信息,并将它们写入 JavaScript 控制台窗口。你可以在图 18-6 中看到布局是如何出现的。

images

***图 18-6。*创建展示 WinJS 的布局。使用方法

运行这个示例应用会产生下面的输出。根据您使用的设备或模拟器配置,您的结果会有所不同。该输出将显示在 Visual Studio JavaScript 控制台窗口中。


Content Height: 55 Content Width: 608 Total Width: 698 Total Height: 145 Position Top: 59 Position Left: 284 Position Width: 658 Position Height: 105 Rel Left: 264 Rel Top: 39


记录消息

WinJS.Utilities名称空间包含三种方法,可用于记录来自应用的消息。乍一看这似乎没什么用——但是有一个巧妙的技巧可以让这些方法比乍看起来更有趣。这些方法用于通过WinJS.log方法设置日志记录。表 18-10 总结了这些方法,我将在下面的章节中解释。

images 提示注意,log方法在WinJS名称空间中,但是其他方法在WinJS.Utilities中。

images

编写日志消息

在这种情况下,您最常用的方法是WinJS.log,它有三个参数:一个写入日志的消息,一个包含一个或多个标签(用空格分隔)的字符串,这些标签对消息进行分类,以及一个消息类型,如infoerrorwarn。您可以使用任何字符串作为标签和类型,只要对您的应用有意义。

WinJS.log方法不会总是被定义(原因我将很快解释),所以您需要在编写日志消息之前确保它存在。你可以在清单 18-17 的中看到我是如何做到的,在那里我修改了来自SizeAndPosition项目的 default.html 文件,使用了WinJS.log方法。

清单 18-17 。将 WinJS.log 方法应用于 SizeAndPosition 示例

`

         SizeAndPosition                             body { background-color: #5A8463; }         #container { margin: 20px; display: -ms-grid;` `             -ms-grid-columns: 0.1fr 0.25fr 1fr 0.55fr;              -ms-grid-rows: 0.2fr 0.5fr 1fr 0.1fr;}         #content {             -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 3;             font-size: 30pt; padding: 20px; margin: 20px;             border: thick solid white; text-align: center;}          

        WinJS.Application.onactivated = function () {

            var elem = document.getElementById("content");

**            logSizeAndPos("Content Height: " + WinJS.Utilities.getContentHeight(elem));** **            logSizeAndPos("Content Width: " + WinJS.Utilities.getContentWidth(elem));** **            logSizeAndPos("Total Width: " + WinJS.Utilities.getTotalWidth(elem));** **            logSizeAndPos("Total Height: " + WinJS.Utilities.getTotalHeight(elem));**

            var position = WinJS.Utilities.getPosition(elem); **            logSizeAndPos("Position Top: " + position.top);** **            logSizeAndPos("Position Left: " + position.left);** **            logSizeAndPos("Position Width: " + position.width);** **            logSizeAndPos("Position Height: " + position.height);**

            var parent = document.getElementById("container"); **            logSizeAndPos("Rel Left: " + WinJS.Utilities.getRelativeLeft(elem, parent));             logSizeAndPos("Rel Top: " + WinJS.Utilities.getRelativeTop(elem, parent));**         };         WinJS.Application.start();     

    
        
Here is some content
    
`

我发现使用WinJS.log方法最简单的方法是通过应用代码中定义的函数,这就是我在示例中使用logSizeAndPos函数所做的。传递给该函数的任何消息都使用标签winjsappinfo写入日志,并指定类型info。如果在这种状态下运行应用,您将看不到任何输出,因为默认情况下,WinJS.log 方法尚未定义。我将在下一节解释如何设置。

启动日志

在调用WinJS.Utilities.startLog方法之前,没有定义WinJS.log方法。这将告诉系统您对哪种类型的日志消息感兴趣,从而允许您过滤被记录的内容。startLog方法的参数是一个对象,它的属性有特殊的含义——我已经在表 18-11 中列出了公认的属性名。

images

从本质上讲,startLog方法设置了一个过滤器来捕获某些日志消息,并在默认情况下将它们写入 JavaScript 控制台。清单 18-18 显示了对添加到SizeAndPosition项目的脚本块中的startLog的调用。

清单 18-18 。启动日志

`...

...`

在清单 18 中——我调用了startLog方法,指定我对类型为info且标签为appbugsinfo的消息感兴趣。一个日志消息只需要有一个您指定给startLog方法的标签就可以被捕获和处理。对startLog的调用设置了过滤器,在中,创建了WinJS.log方法。如果您现在运行应用,您将在 JavaScript 控制台窗口中看到输出。以下是输出的示例行:

winjs:app:info: Position Top: 59

Windows 应用日志记录系统已格式化输出,以便包含标记。如果您没有看到该消息,请检查JavaScript Console窗口顶部的按钮。这些按钮可用于过滤控制台中显示的消息类型,您可能会发现有一个或多个按钮未按下。您可以在图 18-7 中看到按钮。

images

***图 18-7。*确保显示日志信息

创建自定义日志记录操作

JavaScript 控制台的问题在于,当应用在调试器之外部署和运行时,它是不可用的。这就是传递给startLog方法的对象的action属性发挥作用的地方——它允许你创建一个定制的日志记录方法,该方法与你的应用的其余部分集成,并且可以在调试器之外工作。action属性被设置为一个函数,该函数被传递了日志消息、标签和类型,如清单 18-19 中的所示,这里我向SizeAndPosition示例的script块添加了一个自定义日志动作。

清单 18-19 。添加自定义日志动作

`

         SizeAndPosition                             body { background-color: #5A8463; }         #container { margin: 20px; display: -ms-grid;              -ms-grid-columns: 0.1fr 0.25fr 1fr 0.55fr;              -ms-grid-rows: 0.2fr 0.5fr 1fr 0.1fr;}         #content {             -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 3;             font-size: 30pt; padding: 20px; margin: 20px;             border: thick solid white; text-align: center;} **        #logMessages {** **            font-size: 30pt; text-align: center;** **        }**          

**    WinJS.Utilities.startLog({** **        type: "info", tags: "app",** **        action: function (msg, tags, type) {** **            if (msg.indexOf("Position") == 0) {** **                var fMsg = WinJS.Utilities.formatLog(msg, tags, type);** **                var newElem = document.createElement("div");** **                newElem.innerText = fMsg;** **                logMessages.appendChild(newElem);** **            }** **        }** **    });**

    WinJS.Application.onactivated = function () {

        var elem = document.getElementById("content");

        logSizeAndPos("Content Height: " + WinJS.Utilities.getContentHeight(elem));         logSizeAndPos("Content Width: " + WinJS.Utilities.getContentWidth(elem));         logSizeAndPos("Total Width: " + WinJS.Utilities.getTotalWidth(elem));         logSizeAndPos("Total Height: " + WinJS.Utilities.getTotalHeight(elem));

        var position = WinJS.Utilities.getPosition(elem);         logSizeAndPos("Position Top: " + position.top);         logSizeAndPos("Position Left: " + position.left);         logSizeAndPos("Position Width: " + position.width);         logSizeAndPos("Position Height: " + position.height);

        var parent = document.getElementById("container");         logSizeAndPos("Rel Left: " + WinJS.Utilities.getRelativeLeft(elem, parent));         logSizeAndPos("Rel Top: " + WinJS.Utilities.getRelativeTop(elem, parent));     };     WinJS.Application.start();     

    
        
Here is some content
    
**    
    ** `

在这个例子中,我使用了startLog来创建一个动作,该动作捕获那些类型为info、标签为app并且消息以工作Position开始的消息。对于每个匹配的消息,我创建一个新的div元素,并将其作为子元素添加到我添加到布局中的新容器元素中。这并不是一种特别有用的显示日志消息的方式,但是它确实证明了您可以对您需要的日志信息做几乎任何事情——这可能包括向用户显示它、将它保存到一个文件或者将它上传到一个服务器。

您可以通过调用formatLog方法来创建与写入 JavaScript 控制台的字符串格式相同的字符串——这与默认操作使用的方法相同,并生成包含消息和标签细节的字符串。当然,您可以完全忽略这个方法,生成对您的应用有意义的任何消息格式。你可以在图 18-8 的中看到这些增加的结果,它显示了作为布局一部分的日志信息。(布局元素没有正确对齐,因为我在最初的示例中使用了奇怪的网格布局。)

images 提示注意,我在示例中保留了对startLog的原始调用。WinJS 日志记录系统支持多种过滤器和操作,这意味着消息仍然被写入 JavaScript 控制台,这在开发和测试过程中非常有用。

images

***图 18-8。*在应用布局中显示选中的日志信息

总结

在这一章中,我通过描述动画特性结束了对 WinJS UI 特性的介绍。我向您展示了如何直接使用 CSS3 特性来转换属性和元素。我转到了 WinJS 便利功能,它可以使 CSS 功能更容易使用,并且包含您希望向用户发出信号的常见情况的预定义效果,例如应用布局中出现的新内容。一如往常的效果,WinJS 动画应该简短,简单,少用。

本章结束时,我详细介绍了WinJS.Utilities名称空间中最有用的方法。我向您展示了如何查询元素、操作 DOM、获取应用布局中元素的大小和位置,以及如何创建一个灵活的日志系统,供您在整个应用生命周期中使用。我在前面的章节中解释了这些方法的用途,并把它们包含在这里,这样你将来可以很快地引用它们。在本书的下一部分,我将向您展示如何将您的应用集成到 Windows 中,并通过这样做来改善您的应用向用户提供的体验。

十九、了解应用生命周期

我将关注的第一个集成领域是 Metro 应用生命周期。到目前为止,在本书中,我一直在掩饰应用的启动和管理方式,我一直依赖于 Visual Studio 在创建新项目时添加到default.js文件中的一部分代码,为了简洁起见,我删除了其他部分。

在这一章中,我将解释 Metro 应用生命周期中的不同阶段,解释它们是如何发生的以及为什么会发生,并向您展示如何理解您的应用何时从一个阶段进入另一个阶段。在这个过程中,我将解释为什么 Visual Studio 添加的代码不是那么有用,向您展示如何修复它,并向您展示如何确保您的应用适合整体 Metro 和 Windows 8 生命周期模型。表 19-1 对本章进行了总结。

images

了解应用生命周期

Windows 8 积极管理 Metro 应用,以保持设备内存空闲。这样做是为了确保最大限度地利用可用内存,并减少电池消耗。作为这一战略的一部分,Metro 应用拥有明确的生命周期。除了最简单的应用,所有应用都需要知道 Windows 8 何时将应用从生命周期的一个阶段转移到另一个阶段,这是通过侦听一些特定的系统事件来完成的。我将描述这些事件,并向您展示如何处理它们,但在这一部分,我将描述生命周期,以便您了解您的 Metro 应用运行的环境。

激活

一个应用在启动时被激活,通常是当用户点击开始屏幕上的图标时。这是 Metro 应用的基本初始化,就像任何类型的应用启动时一样,您需要创建布局、加载数据、设置事件监听器等等。简而言之,激活的应用从不运行变为运行,并负责引导其状态以向用户提供服务。

images 注意并非所有的激活都是为了启动一个应用——正如我将在本章后面解释的,Windows 也会激活你的应用,这样它就可以执行其他任务,包括那些由契约定义的任务。契约是本书这一部分的主题,你将在第二十四章开始看到它们是如何工作的。

暂停

当一个应用不被使用时,它会被 Windows 8 暂停,最常见的原因是用户已经切换到使用另一个应用。这就是应用管理中积极的部分:在用户切换到另一个应用后几秒钟,一个应用就被挂起了。简而言之,一旦你的应用对用户不再可见,你就可以期待它被暂停。

暂停的应用被冻结。应用的状态被保留,但应用代码不会被执行。用户不会意识到某个应用已被暂停,该应用的缩略图仍会出现在正在运行的应用列表中,以便可以再次选择它。

恢复

当用户选择一个暂停的应用并再次显示它时,暂停的应用被恢复。Windows 会保留应用的布局和状态,因此在恢复应用时,您不需要加载数据和配置 HTML 元素。

终止

如果 Windows 需要释放内存,暂停的 Metro 应用将被终止。暂停的应用在终止时不会收到任何通知,应用的状态和任何未保存的数据都会被丢弃。应用布局的快照将从向用户显示的运行应用列表中移除,并替换为作为占位符的闪屏。如果用户再次启动应用,应用将返回激活状态。

images 注意 Windows 没有为 Metro 应用开发者提供关于应用何时终止的明确政策。这是最后的手段,但 Windows 可以在任何需要的时候自由终止应用,并且您不能根据可用的系统资源来假设应用被终止的可能性。

与 WinJS 合作。应用

既然您已经理解了生命周期的不同阶段,我将向您展示如何在应用 JavaScript 代码中处理它们。这个部分的关键对象是WinJS.Application,它提供了对 JavaScript Metro app 的生命周期事件和一些相关特性的访问。

images 提示 WinJS.Application只是包装了Windows.UI.WebUI.WebUIApplication对象中的功能。使用WinJS.Application的价值在于它以一种更符合 JavaScript 和 web 应用开发的方式呈现了生命周期特性。你可以直接使用WebUIApplication类,但是我发现有一些有用的WinJS.Application特性值得使用。

我将从WinJS.Application支持的事件开始,我已经在表 19-2 中描述过了。对于处理应用生命周期,重要的事件是activatedcheckpoint。事实上,我很少使用其他事件。

images

当您创建一个新的 Metro 项目时,Visual Studio 会向js/default.js文件添加一些代码,使用WinJS.Application对象提供一些基本的生命周期事件处理。对于这一章,我已经创建了一个名为EventCalc的新 Visual Studio Metro 应用项目,你可以在清单 19-1 中看到 Visual Studio 创建的default.js文件。(我已经编辑了清单中的注释,并突出显示了与WinJS.Application对象相关的部分。)

清单 19-1 。Visual Studio 添加到 default.js 文件中的代码

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

    WinJS.Binding.optimizeBindingReferences = true;

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

    app.onactivated = function (args) {         if (args.detail.kind === activation.ActivationKind.launch) {             if (args.detail.previousExecutionState !==                 activation.ApplicationExecutionState.terminated) { **                // app has been launched**             } else { **               // app has been resumed**             }             args.setPromise(WinJS.UI.processAll());         }     };

    app.oncheckpoint = function (args) { **        // app is about to be suspended**     };

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

你可以使用addEventListener方法来设置你的事件监听器函数,但是微软已经为 Visual Studio 生成的代码使用了on*属性,我将在本章中遵循这个约定。

images 提示注意,清单中的最后一条语句是对WinJS.Application.start方法的调用。WinJS.Application对象将事件排队,直到start方法被调用,此时事件被传递给你的代码。调用stop方法会导致WinJS.Application再次开始对事件进行排队。一个常见的问题是忘记调用start,创建一个激活时不做任何事情的应用。

这个清单中有几个问题。首先,Visual Studio 添加到新项目中的代码不是很有帮助。第二,WinJS.Application对象不会转发WebUIApplication对象发送的所有事件。如果你使用/js/default.js文件中的代码构建一个复杂的应用而不解决这两个问题,你迟早会碰壁。在我构建出EventCalc示例应用后,我将解释这两个问题并演示它们的解决方案。

构建示例应用

示例应用是一个非常简单的计算器,允许您将两个数字相加。该应用还显示其接收的生命周期事件的详细信息。在图 19-1 中可以看到没有任何内容的 app 布局。

images

***图 19-1。*event calc 应用

定义视图模型

这是一个基本的简单应用,但如果我添加一个简单的视图模型,它将帮助我演示应用的生命周期。清单 19-2 显示了viewmodel.js文件的内容,我将它添加到了 Visual Studio 项目的js文件夹中。

清单 19-2 。EventCalc 应用的视图模型

`(function () {

    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({         State: {             firstNumber: null,             secondNumber: null,             result: null,             history: new WinJS.Binding.List(),             eventLog: new WinJS.Binding.List()         }     }));

    WinJS.Namespace.define("ViewModel.Converters", {         calcNumber: WinJS.Binding.converter(function (val) {             return val == null ? "" : val;         }),         calcResult: WinJS.Binding.converter(function (val) {             return val == null ? "?" : val;         }),     }); })();`

ViewModel.State对象为相加的两个值和计算结果定义属性。我还使用了两个WinJS.Binding.List对象来记录计算和保存应用接收的生命周期事件的细节。除了状态数据之外,我还定义了一对简单绑定转换器,当视图模型属性没有赋值时,它们将阻止null显示在布局中。

定义标记

我的示例应用只包含一组内容,所以我在default.html文件中定义了标记,而不是使用单页内容模型和导航 API。你可以在清单 19-3 的中看到default.html的内容。我没有在这个文件中使用任何新技术。布局分为三个部分。第一部分和最后一部分包含ListView控件,该控件使用视图模型列表对象作为数据源,中间部分包含基本的 HTML 元素,用于捕获用户的计算输入并显示结果。

清单 19-3 。default.html 文件的内容

`

         EventCalc

                   

                   

    
        
    

    

        Events         <div id="eventList" data-win-control="WinJS.UI.ListView"             data-win-options="{                 itemTemplate: template,                 itemDataSource: ViewModel.State.eventLog.dataSource,                 layout: {type: WinJS.UI.ListLayout},             }">         
    

    

        Calculator         
                         +                          =                          Calculate         
    

    

        History         <div id="historyList" data-win-control="WinJS.UI.ListView"             data-win-options="{                 itemTemplate: template,                 itemDataSource: ViewModel.State.history.dataSource,                 layout: {type: WinJS.UI.ListLayout},             }">         
    

`
定义 CSS

为了管理 HTML 元素的布局,我在/css/default.css文件中定义了如清单 19-4 所示的样式。这些样式依赖于标准的 CSS 属性,没有使用特定于 Metro 的特性。

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

`body {display: -ms-flexbox;-ms-flex-direction: row;     -ms-flex-align: stretch;-ms-flex-pack: center;} div.container {width: 30%; display: inline-block;margin: 0 20px;         text-align: center;font-size: 35pt;}

#eventList, #historyList, #calcElems {     border: thin solid white;height: 85%;padding-top: 20px; }

#calcButton {font-size: 20pt; margin: 20px;} #calcContainer input {font-size: 25pt;width: 100px;}

*.calcSymbol, #result {font-size: 35pt;} .message {font-size: 20pt;margin: 5px;text-align: left;}`

定义 JavaScript

这个应用所有有趣的部分都发生在 JavaScript 代码中。清单 19-5 建立在 Visual Studio 添加到default.js文件的代码上,因此生命周期事件的细节显示在一个ListView控件中,并且可以执行计算。

清单 19-5 。构建在 Visual Studio 创建的 default.js 文件上

`(function () {     "use strict";

    var app = WinJS.Application;     var activation = Windows.ApplicationModel.Activation;     WinJS.strictProcessing();

**    function writeEventMessage(msg) {** **        ViewModel.State.eventLog.push({ message: msg });** **    };**

    app.onactivated = function (args) {         if (args.detail.kind === activation.ActivationKind.launch) {             if (args.detail.previousExecutionState !==                 activation.ApplicationExecutionState.terminated) { **                // app has been launched** **                writeEventMessage("Launched");** **                performInitialization();**             } else { **                // app has been resumed** **                writeEventMessage("Resumed");**             }

            args.setPromise(WinJS.UI.processAll().then(function () { **                return WinJS.Binding.processAll(calcElems, ViewModel.State);**             }));         }     };

**    app.oncheckpoint = function (args) {** **        // app is about to be suspended** **        writeEventMessage("Suspended");** **    };**

**    function performInitialization() {** **        calcButton.addEventListener("click",** **            function (e) {** **                var first = ViewModel.State.firstNumber = Number(firstInput.value);** **                var second = ViewModel.State.secondNumber = Number(secondInput.value);** **                var result = ViewModel.State.result = first + second;** **            });**

**        ViewModel.State.bind("result", function (val) {             if (val != null) {** **                ViewModel.State.history.push({** **                    message: ViewModel.State.firstNumber + " + "** **                        + ViewModel.State.secondNumber + " = "** **                        + val** **                });** **            }** **        });**     };

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

writeEventMessage函数向视图模型添加一个项目,报告生命周期事件的接收,格式化消息,以便它可以与我在default.html文件中定义的模板一起工作。

performInitialization函数包含我希望在应用启动时执行的代码——对于这个应用,这意味着为由button元素发出的click事件设置一个处理程序,并设置一个编程数据绑定,以便在视图模型result属性更改时生成计算历史。

images 提示注意,在onactivated处理程序中,我对传递给函数的参数对象调用了setPromise方法。我将在本章后面的处理闪屏部分解释这个方法。

触发生命周期变更

将应用从生命周期的一部分转移到另一部分的最简单方法是使用 Visual Studio。如果你查看工具栏,你会看到一个标有Suspend的菜单项。如果你点击标签右边的小箭头,你会看到菜单也包含了ResumeSuspend and?? 的选择,如图图 19-2 所示。(您可能需要从 Visual StudioViewToolbars菜单中选择Debug位置才能看到工具栏。)

images

***图 19-2。*使用 Visual Studio 控制应用生命周期

菜单项迫使应用从一个生命周期阶段转移到另一个阶段。它们很有用,因为它们允许您在附加调试器的情况下测试代码。在应用开发的早期阶段,我经常使用这个功能。

然而,Visual Studio 只能模拟生命周期事件,这意味着调试器运行时和不运行时会有细微的差别。这意味着您应该花时间通过直接从操作系统生成事件来测试您的应用的行为方式,而不依赖于 Visual Studio 调试器。

问题是,当调试器没有被使用时,一个影响是没有JavaScript Console窗口可用于显示调试消息,这使得更难弄清楚发生了什么。正是因为这个原因,我在示例应用中添加了一个ListView控件,这样我就可以在应用本身的布局中记录生命周期事件的到来——这是我经常用于应用最终测试的一种技术。

在 Windows 8 中生成生命周期事件

生成生命周期事件实际上非常简单,只要您耐心等待,看看 Windows 8 显示的指示生命周期变化的指示。在接下来的小节中,我将向您展示如何在不使用 Visual Studio 的情况下生成每个activatedresumingsuspending事件。正如我所说的,这是测试你的用户将会看到什么的唯一现实的方法。

启动应用

触发activated事件最简单的方法是启动应用,尽管重要的是不要用 Visual Studio 调试器来做这件事。你可以从开始屏幕中选择应用的磁贴,或者从 Visual Studio Debug菜单中选择Start Without Debugging

images 提示在 Visual Studio 中至少启动一次之前,示例应用的磁贴不会添加到开始屏幕。之后,您应该会看到应用的磁贴列在屏幕的最右侧。有时磁贴会不可见,特别是如果你一直在模拟器和本地机器之间切换以运行应用——在这种情况下,我发现通过键入应用名称的前几个字母并从结果列表中选择它来搜索应用足以使文件正确显示。

当你启动EventCalc应用时,你会看到默认的闪屏(因为我没有改变所用颜色或图标的清单设置),然后看到如图图 19-3 所示的应用布局。请注意,左侧的ListView控件中已经记录了activated事件。我用消息Launched记录了这个事件,以表明接收到的事件是一个启动应用的请求——这一点我将在本章后面详细解释。

images

***图 19-3。*在没有调试器的情况下启动示例应用

暂停应用

让 Windows 暂停应用最简单的方法就是按Win+D切换到桌面。您可以通过启动Task Manager,切换到Details选项卡并找到WWAHost.exe进程来跟踪应用生命周期的进程(这是用于运行 JavaScript Metro 应用的可执行文件的名称——因此,您启动的每个 Metro 应用都会有一个进程)。您可能需要点击任务管理器中的More Details按钮才能看到Details选项卡。

images 注意您需要在本地机器上启动任务管理器,即使示例应用正在模拟器中运行。任务管理器不能在模拟器中运行,但是WWAHost.exe进程在本地机器的任务管理器中仍然可见。

几秒钟后,Windows 将暂停应用,进程状态将在Task Manager中变为Suspended。你可以在图 19-4 中看到一个暂停的应用是如何显示在任务管理器中的。

images

***图 19-4。*使用任务管理器观察一个 Metro 应用被挂起

images 注意连接到 Visual Studio 调试器的应用永远不会在Task Manager中显示为挂起,因为它们保持活动状态,以便调试器可以控制应用。如果你没有看到挂起的消息,通常是因为应用是用调试器启动的。

我需要使用任务管理器来检查应用的状态,因为我再也看不到应用的布局,而且由于我没有使用调试器,我也看不到任何调试消息。在下一节中,当我恢复应用时,您将看到收到了suspending事件的证据。

恢复应用

你可以通过将应用带回到前台来恢复应用,最简单的方法是将鼠标移动到左上角(或在触摸屏上从左边缘滑动)并单击缩略图。应用将返回并填满屏幕,您将看到在应用布局的左侧ListView控件中记录了一个新事件,如图图 19-5 所示。

images

***图 19-5。*app 收到暂停和恢复事件

图中显示suspending事件被 app 接收。这发生在应用对用户隐藏之后,但在进程在Task Manager中显示为暂停之前。这个小间隔是我将在本章后面返回的东西,因为它为应用提供了一个准备被挂起的机会,这对某些类型的应用来说是无价的。

没有显示的是resuming事件的接收。这是 Visual Studio 添加到项目中的代码的问题之一。我将向您展示如何确保您的应用很快获得该事件。

终止应用

你可以通过输入Alt + F4来终止应用的执行。应用会突然终止,并且不会发送警告事件。以这种方式退出应用允许您确保您的应用在下次启动时正确恢复,并检查它使用的任何远程服务(web 服务、数据库等。)能够正确地恢复资源。

这是终止你的应用的两种方式之一。另一种情况发生在应用暂停,Windows 需要释放一些资源的时候。在这两种情况下,您的应用将被终止,而不会收到任何通知事件。在这一章的后面,我将向你展示当你的应用被暂停时如何准备终止,我也将向你展示当你的应用下次启动时如何判断它是如何被终止的。

您可以通过使用 Visual Studio 工具栏菜单中的Suspend and Shutdown项(包含SuspendResume项的菜单)来模拟 Windows 终止应用的情况。

获取激活类型和以前的应用状态

在我可以修复default.js文件中的代码以便获得所有的生命周期事件之前,我需要做一些准备工作,以便我可以弄清楚当事件到来时我被要求做什么。为此,我需要深入研究事件的细节,以发现激活类型和我的应用在事件被调度之前所处的状态。

当一个应用被发送activated事件时,传递给onactivated函数的事件对象有一个detail属性,该属性返回一个Windows.ApplicationModel.Activation.IActivatedEventArgs对象。

IActivatedEventArgs对象定义了我在表 19-3 中描述的三个属性,这些属性为你提供了所有你需要的信息,让你知道 Windows 要求你的应用做什么。我将在接下来的章节中描述其中的两个属性,并在本章的后面返回到第三个属性。

images

确定激活的种类

一个应用可以被激活用于一系列不同的目的,例如在应用之间共享数据,从设备接收数据,以及处理搜索查询。这些激活目的是由 Windows 契约定义的,它允许你将你的应用集成到 Windows 中,我将在这一章的后面返回——你将能够在后面的章节中看到我如何实现契约的例子。对于这一章,我关心的是启动激活,这发生在用户已经启动应用或者应用在暂停后已经恢复的时候。

您可以通过从activated事件中读取kind属性来确定您正在处理的激活类型。kind属性返回由Windows.ApplicationModel.Activation.ActivationKind枚举定义的值之一。我在launch的这一章中寻找的唯一值,它告诉我应用已经被启动或恢复。你可以看到我如何在清单 19-6 的函数中检查这一点。

清单 19-6 。检查启动激活类型

... app.onactivated = function (args) {     if (**args.detail.kind === activation.ActivationKind.launch**) {        // the app has been launched        writeEventMessage("Launched");     } }; ...

确定以前的应用状态

在处理一个启动激活请求时,你需要知道应用在激活前是什么状态,这可以通过读取previousExecutionState属性来确定。该属性返回由Windows.ApplicationModel.Activation.ApplicationExecutionState枚举定义的值之一,我已经在表 19-4 中列出了这些值。

images

如果你的应用之前的状态是notRunningclosedByUser,那么你正在处理一个的重新开始的发布。您需要初始化您的应用(设置 UI 控件、事件监听器等。)并为应用的首次用户交互做准备。

如果之前的状态是suspended,那么应用将已经被初始化,并且应用的状态将与应用被挂起时一样。terminated状态是两种情况的奇怪组合。应用是由系统终止的,而不是用户,所以这个想法是,如果用户再次启动应用,他们应该能够像暂停和终止发生之前一样继续。但是,应用的状态在终止时会丢失。为了实现正确的行为,您需要存储应用挂起时的状态,以防发生终止。我将在本章的后面解释如何做到这一点。

当 Windows 希望您的应用履行其契约义务时,通常会遇到running状态,并且该应用已经在运行,因为用户之前已经启动了它。当您使用 Visual Studio Refresh按钮重新加载应用的内容时,您也会遇到这种情况——当然,这不是您的应用在部署给用户时会遇到的情况。你如何响应运行状态将取决于你的应用和它所支持的契约,但是在这一章中,我将把running状态视为与notRunning相同的状态,只是为了确保应用与 Visual Studio Refresh按钮一起正常工作。这可能对所有的应用都没有意义,但是因为我的用户——也就是你——可能会在 Visual Studio 中运行应用,所以这是最明智的做法。

应对不同的投放类型

现在您已经理解了 Windows 如何在activated事件中提供细节,我可以在我的/js/default.js文件中添加一些代码来响应不同的情况。你可以在清单 19-7 中看到我添加的内容。

清单 19-7 。区分激活类型和以前的执行状态

`... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) { **        switch (args.detail.previousExecutionState) {** **            case activation.ApplicationExecutionState.suspended:** **                writeEventMessage("Resumed from Suspended");** **                break;** **            case activation.ApplicationExecutionState.terminated:** **                writeEventMessage("Launch from Terminated");** **                performInitialization();** **                break;** **            case activation.ApplicationExecutionState.notRunning:** **            case activation.ApplicationExecutionState.closedByUser:** **            case activation.ApplicationExecutionState.running:** **                writeEventMessage("Fresh Launch");** **                performInitialization();** **                break;** **        }**

        args.setPromise(WinJS.UI.processAll().then(function () {             return WinJS.Binding.processAll(calcElems, ViewModel.State);         }));     } }; ...`

当我收到一个启动激活事件时,我使用一个 switch 语句来检查所有的ApplicationExecutionState值。我将notRunningclosedByUserrunning的值视为相同,并调用performInitialization函数来设置我的事件监听器和数据绑定。我已经更改了传递给writeEventMessage函数的消息,以使我收到的事件的意义更加清晰。

我也是在前一个状态是terminated的时候调用performInitialization函数。我将在本章后面添加一些额外的代码,以区分我处理这种状态的方式与notRunningrunningclosedByUser。对于所有这些先前的状态,我需要初始化我的应用,以确保我的事件处理程序和数据绑定已经设置好。

我还没有对suspended值做任何事情,除了调用writeEventMessage函数来记录事件的接收。我不需要初始化我的应用,因为系统正在恢复执行,我的事件处理程序和数据绑定已经存在。当我谈到后台活动时,我会在这一部分添加一些代码,但是目前,什么也不做是将对suspended状态的响应区分开来的原因。

images 提示你会注意到我调用了WinJS.UI.processAllWinJS.Binding.processAll方法,而不管我正在处理哪种激活。我将在本章的后面回到这段代码。

你可以在图 19-6 中看到我显示的三个激活事件消息中的两个。您可以使用 Visual Studio 非常容易地自己重新创建这些事件。首先,使用Debug菜单中的Start Debugging项启动应用——这将产生如图左侧所示的Fresh Launch消息。

images

***图 19-6。*显示启动激活事件的详细信息

现在从工具栏菜单中选择Suspend and Shutdown项,这将终止应用。再次启动应用,activated事件的先前执行状态将被设置为terminated,导致出现图中右侧所示的消息。

捕获恢复事件

我的default.js文件中的代码可以区分不同类型的激活事件,并根据应用之前的状态做出响应——但当应用恢复时,我仍然没有收到事件。为了补救这一点,我在default.js文件中添加了如清单 19-8 所示的内容。

清单 19-8 。捕捉恢复事件

`(function () {     "use strict";

    var app = WinJS.Application;     var activation = Windows.ApplicationModel.Activation;     WinJS.strictProcessing();

    function writeEventMessage(msg) {         ViewModel.State.eventLog.push({ message: msg });     };

    app.onactivated = function (args) {         // ...code removed for brevity...     };

    app.oncheckpoint = function (args) {         // app is about to be suspended         writeEventMessage("Suspended");     };

    function performInitialization() {         // ...code removed for brevity...     };

**    Windows.UI.WebUI.WebUIApplication.addEventListener("resuming", function (e) {** **        WinJS.Application.queueEvent({** **            type: "activated",             detail: {** **                kind: activation.ActivationKind.launch,** **                previousExecutionState: activation.ApplicationExecutionState.suspended** **            }** **        });** **    });**

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

正如我之前提到的,WinJS.Application对象是由Windows.UI.WebUI.WebUIApplication对象提供的功能的包装器。出于我不明白的原因,WinJS.Application类在由WebUIApplication对象发送恢复事件时不会将其转发给应用,我对default.js文件的添加修复了这一遗漏:我监听由WebUIApplication对象发出的resuming事件,并通过调用queueEvent对象将其送入由WinJS.Application维护的事件队列。

我传递给queueEvent方法的对象符合被激活事件的模式,即传递——类型属性被设置为activated,detail属性具有返回预期值的kindpreviousExecutionState属性。

这并不理想,但是如果您希望能够响应整个生命周期的变化,这是很重要的。图 19-7 展示了修复的效果,您可以通过启动示例应用,然后从 Visual Studio 工具栏菜单中选择SuspendResume项来复制这个结果。

images

***图 19-7。*说明捕捉恢复事件的效果

响应生命周期变化

当然,应用响应生命周期变化的方式会有所不同,但是无论你的应用提供什么工具,你都需要考虑一些核心行为。在接下来的章节中,我描述了一些关键技术,当你响应生命周期事件时,这些技术可以让你的应用适应生命周期模型。

处理闪屏

你可能已经注意到我在本章的onactivated处理函数中使用了setPromise方法,如清单 19-9 所示。这是一个有用的WinJS.Application功能,可以防止应用初始化前闪屏消失。

清单 19-9 。使用 setPromise 方法保留闪屏

`... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) {         switch (args.detail.previousExecutionState) {             case activation.ApplicationExecutionState.suspended:                 writeEventMessage("Resumed from Suspended");                 break;             case activation.ApplicationExecutionState.terminated:                 writeEventMessage("Launch from Terminated");                 performInitialization();                 break;             case activation.ApplicationExecutionState.notRunning:             case activation.ApplicationExecutionState.closedByUser:             case activation.ApplicationExecutionState.running:                 writeEventMessage("Fresh Launch");                 performInitialization();                 break;         }

**        args.setPromise(WinJS.UI.processAll().then(function () {** **            return WinJS.Binding.processAll(calcElems, ViewModel.State);** **        }));**     } }; ...`

闪屏将一直显示,直到传递给args.setPromise方法的Promise完成。在这个清单中,我使用了一系列的Promise对象,它们分别调用WinJS.UI.processAllWinJS.Binding.processAll方法。只有当这两种方法都完成时,应用布局才会替换闪屏。

images 提示我在第十章中描述了WinJS.UI.processAll法,在第八章中描述了WinJS.Binding.processAll法。

目前,示例应用中没有任何东西会延迟闪屏的移除,因此,为了演示这个特性,我将在接下来的部分中添加一些新功能。

添加到示例应用

我的示例应用非常简单,但是在真正的应用中可能需要大量的初始设置。为了模拟这种情况,我更改了示例应用,以便在应用首次启动时计算前 5000 个整数值相加的结果,然后在用户执行计算时使用这些数据来产生计算结果。如果你愿意的话,忘记这样做没有什么好的理由,除了这是一个有用的演示。首先,我向视图模型添加了一个新值来包含缓存的结果,如清单 19-10 所示。

清单 19-10 。为缓存数据添加视图模型属性

... WinJS.Namespace.define("ViewModel", WinJS.Binding.as({     State: {         firstNumber: null,         secondNumber: null,         result: null,         history: new WinJS.Binding.List(),         eventLog: new WinJS.Binding.List(), **        cachedResult: null**     } }));

为了生成缓存的结果,我在项目中添加了一个名为tasks.js的新文件,其中包含一个定制的WinJS.Promise实现。你可以在清单 19-11 中看到tasks.js文件的内容,它展示了doWork函数的实现。(我在第九章中解释了Promise对象如何工作以及如何实现自己的对象)。

清单 19-11 在 tasks.js 文件中实现自定义承诺

`(function () {     WinJS.Namespace.define("Utils", {         doWork: function (count) {             var canceled = false;

            return new WinJS.Promise(function (fDone, fError, fProgress) {                 var blockSize = 500;

                var results = {};

                (function calcBlock(start) {                     for (var first = start; first < start + blockSize; first++) {                         results[first] = {};                         for (var second = start; second < start + blockSize; second++) {                             results[first][second] = first + second;                         }                     }                     if (!canceled && start + blockSize < count) {                         fProgress(start);                         setImmediate(function () {                             calcBlock(start + blockSize);                         });                     } else {                         fDone(results);                     }                 })(1);

            }, function () {                 canceled = true;             });         }     }); })();`

定制的Promise代码可能很难阅读,但是清单中的代码在调用setImmediate函数之前一次将 500 个块中的成对数字加在一起,以避免锁定 JavaScript 运行时。我将calcBlock函数定义为一个自执行函数,并在用参数1定义后立即调用它来计算第一组结果。

此代码创建的数据结构是一个对象,它具有每个数值的属性,每个属性的值是另一个对象,它的属性对应于第二个数值,其值是总和,如下所示:

results = {     1 = {         1: 2,         2: 3,         3: 4,     } }

完整的结果集被传递给Promise完成函数,这意味着可以使用doWork函数返回的Promise对象的then方法来访问它。我已经将tasks.js文件添加到了default.html文件的头部分,如清单 19-12 所示。

清单 19-12 。将 tasks.js 文件添加到 default.html 头文件

`...

         EventCalc

                   

               **    **     

...`
生成缓存的结果

要查看setPromise对象解决的问题,它有助于查看当不使用方法时会发生什么。为此,我在onactivated处理程序中调用了doWork方法,但是没有使用setPromise对象。你可以在清单 19-13 中看到对default.js文件的添加。

清单 19-13 。不使用 setPromise 对象生成缓存结果

`... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) {         switch (args.detail.previousExecutionState) {             case activation.ApplicationExecutionState.suspended:                 writeEventMessage("Resumed from Suspended");                 break;             case activation.ApplicationExecutionState.terminated:                 writeEventMessage("Launch from Terminated");                 performInitialization();                 break;             case activation.ApplicationExecutionState.notRunning:             case activation.ApplicationExecutionState.closedByUser:             case activation.ApplicationExecutionState.running:                 writeEventMessage("Fresh Launch");                 performInitialization();                 break;         }

**        Utils.doWork(5000).then(function (data) {** **            ViewModel.State.cachedResult = data;** **        });**

        args.setPromise(WinJS.UI.processAll().then(function () {             return WinJS.Binding.processAll(calcElems, ViewModel.State);         }));     } }; ...`

只有在计算完所有值后,才会将缓存的结果分配给视图模型属性。风险在于,在所有数据可用之前,用户将看到应用的布局。

images我发现一项任务花费大约 5-10 秒钟是合适的。

你可以在performInitialization函数中看到潜在的问题,我在清单 19-14 中修改了这个函数以使用缓存的结果。

清单 19-14 。使用 performInitialization 函数中的缓存数据

... function performInitialization() {     calcButton.addEventListener("click", function (e) {         var first = ViewModel.State.firstNumber = Number(firstInput.value);         var second = ViewModel.State.secondNumber = Number(secondInput.value); `**        if (first < 5000 && second < 5000) {** **            ViewModel.State.result = ViewModel.State.cachedResult[first][second];** **        } else {** **            ViewModel.State.result = first + second;** **        }**     });

    ViewModel.State.bind("result", function (val) {         if (val != null) {             ViewModel.State.history.push({                 message: ViewModel.State.firstNumber + " + "                     + ViewModel.State.secondNumber + " = "                     + val             });         }     }); }; ...`

如果用户试图在缓存数据准备好之前执行计算,试图从视图模型中读取结果将会产生一个异常,如图 19-8 所示。在缓存数据准备就绪之前,尝试执行 1 + 1 计算后,显示了图中的错误消息。click事件处理程序中的代码,如清单 19-14 所示,试图访问一个名为 1 的变量,但它还不存在。

images

***图 19-8。*试图在缓存结果生成之前使用它们

维护闪屏

另一种方法是使用setPromise方法,这将确保闪屏一直显示到它所经过的Promise完成。你可以在清单 19-15 中看到我是如何为示例应用做这些的。

清单 19-15 。使用 setPromise 方法保持闪屏显示

`... app.onactivated = function (args) {     if (args.detail.kind === activation.ActivationKind.launch) {         switch (args.detail.previousExecutionState) {             case activation.ApplicationExecutionState.suspended:                 writeEventMessage("Resumed from Suspended");                 break;             case activation.ApplicationExecutionState.terminated:                 writeEventMessage("Launch from Terminated");                 performInitialization();                 break;             case activation.ApplicationExecutionState.notRunning:             case activation.ApplicationExecutionState.closedByUser:             case activation.ApplicationExecutionState.running:                 writeEventMessage("Fresh Launch");                 performInitialization();                 break;         }

**        var cachedPromise = Utils.doWork(5000).then(function (data) {** **            ViewModel.State.cachedResult = data;** **        });**

**        var processPromise = WinJS.UI.processAll().then(function () {** **            return WinJS.Binding.processAll(calcElems, ViewModel.State);** **        });**

**        args.setPromise(WinJS.Promise.join([cachedPromise, processPromise]));**     } }; ...`

doWork函数生成缓存的结果时,WinJS.UI.processAllWinJS.Binding.processAll方法没有理由不能对标记进行操作,所以我使用了WinJS.Promise.join方法来创建一个Promise,让这两个活动交错进行——正是这个组合的Promise被我传递给了setPromise方法,它确保了闪屏将一直显示,直到doWork和两个processAll调用都完成了它们的工作。

当然,我还没有完全正确的行为——每当我的应用启动时,我的结果都会被计算,即使它是从suspended状态恢复的。我需要更好地选择何时执行初始化,最简单的方法是开始在performInitialization函数中生成缓存的结果,当应用还没有从挂起状态恢复时就会调用这个函数。你可以在清单 19-16 中看到我是如何做到这一点的,它显示了对default.js文件的进一步修改。

清单 19-16 。确保恢复应用时不会生成缓存结果

`(function () {     "use strict";

    var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation;     WinJS.strictProcessing();

    function writeEventMessage(msg) {         ViewModel.State.eventLog.push({ message: msg });     };

    app.onactivated = function (args) { **        var promises = [];**

        if (args.detail.kind === activation.ActivationKind.launch) {             switch (args.detail.previousExecutionState) {                 case activation.ApplicationExecutionState.suspended:                     writeEventMessage("Resumed from Suspended");                     break;                 case activation.ApplicationExecutionState.terminated:                     writeEventMessage("Launch from Terminated"); **                    promises.push(performInitialization());**                     break;                 case activation.ApplicationExecutionState.notRunning:                 case activation.ApplicationExecutionState.closedByUser:                 case activation.ApplicationExecutionState.running:                     writeEventMessage("Fresh Launch"); **                    promises.push(performInitialization());**                     break;             }

**            promises.push(WinJS.UI.processAll().then(function () {** **                return WinJS.Binding.processAll(calcElems, ViewModel.State);** **            }));**

            args.setPromise(WinJS.Promise.join(promises));         }     };

    app.oncheckpoint = function (args) {         // app is about to be suspended         writeEventMessage("Suspended");     };

    function performInitialization() {         calcButton.addEventListener("click", function (e) {             var first = ViewModel.State.firstNumber = Number(firstInput.value);             var second = ViewModel.State.secondNumber = Number(secondInput.value);             if (first < 5000 && second < 5000) {                 ViewModel.State.result = ViewModel.State.cachedResult[first][second];             } else {                 ViewModel.State.result = first + second;             }         });         ViewModel.State.bind("result", function (val) {             if (val != null) {                 ViewModel.State.history.push({                     message: ViewModel.State.firstNumber + " + "                         + ViewModel.State.secondNumber + " = "                         + val                 });             }         });

**        return Utils.doWork(5000).then(function (data) {** **            ViewModel.State.cachedResult = data;** **        });**     };

    Windows.UI.WebUI.WebUIApplication.addEventListener("resuming", function (e) {         WinJS.Application.queueEvent({             type: "activated",             detail: {                 kind: activation.ActivationKind.launch,                 previousExecutionState: activation.ApplicationExecutionState.suspended             }         });     });

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

了解闪屏何时关闭

如果初始化需要几秒钟,保持闪屏可见是可以接受的。超过这一点,看起来你的应用在启动时就挂起了,变得没有响应。如果您有很多初始化要执行,那么您应该避免使用setPromise对象,而是向用户显示某种进度显示。要做到这一点,你需要知道闪屏什么时候被关闭,什么时候被你的应用的布局所取代。

当闪屏被替换为应用布局时,IActivatedEventArgs对象的splashScreen属性发出一个dismissed事件。你可以通过传递给onactivated处理函数的Event对象的detail.splashScreen属性获得这个值,如清单 19-17 所示。

清单 19-17 。收到闪屏已被取消的通知

`... app.onactivated = function (args) {     var promises = [];

    if (args.detail.kind === activation.ActivationKind.launch) {         switch (args.detail.previousExecutionState) {             case activation.ApplicationExecutionState.suspended:                 writeEventMessage("Resumed from Suspended");                 break;             case activation.ApplicationExecutionState.terminated:                 writeEventMessage("Launch from Terminated");                 promises.push(performInitialization());                 break;             case activation.ApplicationExecutionState.notRunning:             case activation.ApplicationExecutionState.closedByUser:             case activation.ApplicationExecutionState.running:                 writeEventMessage("Fresh Launch");                 promises.push(performInitialization());                 break;         }

**        if (args.detail.splashScreen) {** **            args.detail.splashScreen.addEventListener("dismissed", function (e) {** **                writeEventMessage("Splash Screen Dismissed");** **            });** **        }**

        promises.push(WinJS.UI.processAll().then(function () {             return WinJS.Binding.processAll(calcElems, ViewModel.State);         }));

        args.setPromise(WinJS.Promise.join(promises));     } }; ...`

应用恢复时没有闪屏——不仅仅是因为 Metro 没有使用闪屏,还因为我将恢复事件提供给了WinJS.Application,并且没有定义splashScreen属性。这意味着您需要检查一下splashScreen属性是否存在。

如果您启动该应用,您将看到在左侧ListView控件中显示一条通知消息,报告闪屏已被取消,如图图 19-9 所示。如果您仍然有应用初始化要执行,那么这将提示您向用户显示一些临时消息或内容。

images

***图 19-9。*闪屏关闭时显示消息

处理 App 终止

处理一个终止后又启动的 app,需要做一些工作。一个应用不会被警告即将被终止——系统会暂停应用,然后,如果资源紧张,终止应用进程以释放内存。Windows 无法告诉应用它将被终止,除非让它退出挂起状态,这将需要一些 Windows 试图回收的资源。

处理应用终止需要代表应用做一些仔细的工作,因为微软想对用户隐藏终止。这是一个明智的想法,当一个应用遵循这种模式时,它会为用户创造更好的整体地铁体验,用户不会在意有限的设备资源导致的应用终止。用户没有关闭应用,当他们从“正在运行”的应用列表中选择闪屏占位符时,他们会期望应用会让他们从停止的地方继续。

要做到这一点,您需要存储应用挂起时的状态。这就像购买应用终止保险一样——你希望你的应用只是被恢复,你不必恢复状态,但你需要采取预防措施,以防你的应用被终止。

WinJS.Application对象通过它的sessionState属性帮助你存储你的应用状态。通过将一个对象分配给该属性来存储状态,该对象将作为 JSON 数据永久存储。使用的 JSON 解析器非常简单,不会处理复杂的对象,包括可观察的对象,这意味着您通常需要创建一个新的对象来表示您的应用的状态——当然,如果您的应用被终止然后被激活,请准备好使用该对象来恢复该状态。

为了演示这项技术,我在viewmodel.js文件中添加了两个新函数来保存和恢复应用状态数据,如清单 19-18 所示。

images 注意sessionState属性应该只用于存储应用状态,比如用户输入到 UI 控件中的值,用户在应用中导航到的点,以及将应用恢复到其先前状态所需的其他数据。用户数据和设置应该而不是使用sessionState属性存储。我将在第二十章中向您展示如何处理设置,以及如何在第二十二章–24 章中使用文件系统。

清单 19-18 。创建和恢复状态对象的功能

`(function () {

    WinJS.Namespace.define("ViewModel", WinJS.Binding.as({         State: {             firstNumber: null,             secondNumber: null,             result: null,             history: new WinJS.Binding.List(),             eventLog: new WinJS.Binding.List(),             cachedResult: null         }     }));

    WinJS.Namespace.define("ViewModel.Converters", {         calcNumber: WinJS.Binding.converter(function (val) {             return val == null ? "" : val;         }),         calcResult: WinJS.Binding.converter(function (val) {             return val == null ? "?" : val;         }),     }); **    WinJS.Namespace.define("ViewModel.State", {** **        getData: function () {** **            var data = {** **                firstNumber: ViewModel.State.firstNumber,** **                secondNumber: ViewModel.State.secondNumber,** **                history: [],** **                events: []** **            };** **            ViewModel.State.history.forEach(function (item) {** **                data.history.push(item);** **            });** **            ViewModel.State.eventLog.forEach(function (item) {** **                data.events.push(item);** **            });** **            return data;** **        },** **        setData: function (data) {** **            data.history.forEach(function (item) {** **                ViewModel.State.history.push(item);** **            });** **            data.events.forEach(function (item) {** **                ViewModel.State.eventLog.push(item);** **            });** **            ViewModel.State.firstNumber = data.firstNumber;** **            ViewModel.State.secondNumber = data.secondNumber;** **        }** **    });**

})();`

请注意,我对包含的数据是有选择性的。例如,我不包括result属性。如果我这样做了,并在应用再次启动时恢复了该值,那么设置该属性将触发我在default.js中定义的数据绑定,并在历史记录中创建一个新的(意外的)条目。不设置这个值的缺点是,最后提供给用户的结果不会显示在中间的列中。

这是处理应用状态数据的典型方式——你通常可以非常接近地恢复到应用暂停前的状态,但总有一些事情很难恢复。至于你在处理这些问题时能走多远,这是一个判断的问题。

images 提示我在这个例子中也省略了 app 状态下缓存的计算数据。决定在应用状态中包含相对大量的数据是另一个判断。如果数据是我可以快速再现的东西,就像在这种情况下,那么我倾向于从状态中忽略它,因为我预计终止是一种相对罕见的情况,我宁愿承受再现数据的代价,而不是存储数据的代价,后者无限期地保留在设备上,可能会给用户带来其他问题。这个问题没有简单的答案,您必须查看您正在处理的数据,并找出给用户最佳体验的方法。在你阅读了第二十章之后,你将能够就如何存储更大数量的数据做出明智的决定,我在其中描述了存储应用数据的其他方法。

为了保存和恢复应用状态,我从default.js文件中调用视图模型方法,以响应适当的生命周期事件,如清单 19-19 所示。

清单 19-19 。存储和恢复状态数据

`... app.onactivated = function (args) {     var promises = [];

    if (args.detail.kind === activation.ActivationKind.launch) {         switch (args.detail.previousExecutionState) {             case activation.ApplicationExecutionState.suspended:                 writeEventMessage("Resumed from Suspended");                 break;             case activation.ApplicationExecutionState.terminated: **                ViewModel.State.setData(app.sessionState);**                 writeEventMessage("Launch from Terminated");                 promises.push(performInitialization());                 break;             case activation.ApplicationExecutionState.notRunning:             case activation.ApplicationExecutionState.closedByUser:             case activation.ApplicationExecutionState.running:                 writeEventMessage("Fresh Launch");                 promises.push(performInitialization());                 break;         }

        if (args.detail.splashScreen) {             args.detail.splashScreen.addEventListener("dismissed", function (e) {                 writeEventMessage("Splash Screen Dismissed");             });         }

        promises.push(WinJS.UI.processAll().then(function () {             return WinJS.Binding.processAll(calcElems, ViewModel.State);         }));

        args.setPromise(WinJS.Promise.join(promises));     } };

app.oncheckpoint = function (args) {     // app is about to be suspended **    app.sessionState = ViewModel.State.getData();**     writeEventMessage("Suspended"); }; ...`

就在应用挂起之前,调用了oncheckpoint处理函数,这提示我通过将getData方法的结果赋给sessionState属性来存储我的应用状态。如果我的应用启动了,并且之前的执行状态是terminated,我通过读取sessionState属性的值来恢复状态数据。

这些变化的效果是,当应用终止时,用户获得了(几乎)无缝的体验。你可以在图 19-10 的中看到状态是如何恢复的,以及生命周期事件通知,显示应用被启动之前已经被终止。

images

***图 19-10。*保存和恢复 app 状态的效果

处理 App 暂停

除了存储应用的状态,你还可以使用oncheckpoint处理函数来释放应用正在使用的资源,并控制应用正在执行的任何后台任务。

例如,在资源方面,您可能需要关闭与服务器的连接或安全地关闭文件。你不知道你的应用将被挂起多长时间,也不知道它是否会在挂起时被终止,所以使用oncheckpoint处理程序让你的应用进入安全状态是有意义的。如果您使用远程服务器,最好尝试关闭连接,以便其他用户可以使用它们–大多数服务器最终会发现您的应用已经消失,但这可能需要一段时间,而且仍然有一些企业服务器对并发连接的数量有严格的限制,即使这些连接没有主动处理请求。

就后台任务而言,如果在应用暂停时,您有使用setImmediatesetIntervalsetTimeout方法延迟的工作,那么这项工作将在应用再次恢复时执行——这并不总是有用的,尤其是当应用恢复时,状态可能会发生变化,这使得处理被延迟执行的函数的结果更加困难。

在这一节中,我将向您展示如何在应用挂起之前进行准备。我将使用一个简单的后台任务来完成这项工作,因为这意味着您不必按照示例设置服务器。后台任务将计算出自应用上次激活以来已经过了多少秒——这在现实世界中并不是一件有用的事情,但它是一个有用的示例,因为它允许我向您展示当后台活动的上下文变得陈旧时会发生什么。清单 19-20 显示了我对tasks.js文件所做的更改,以定义新的活动。

清单 19-20 。为示例应用定义后台活动

`(function () {     WinJS.Namespace.define("Utils", {         doWork: function (count) {             // ...statements removed for brevity...         },

**        doBackgroundWork: function () {** **            var interval = 1000;** **            var canceled = false;** **            var timeoutid;             return new WinJS.Promise(function (fDone, fError, fProgress) {** **                var startTime = Date.now();** **                (function getElapsedTime() {** **                    var elapsed = Date.now() - startTime;** **                    fProgress(elapsed / 1000);** **                    if (!canceled) {** **                        timeoutid = setTimeout(getElapsedTime, interval);** **                    } else {** **                        fDone();** **                    }** **                })();** **            }, function () {** **                canceled = true;** **                if (timeoutid) {** **                    clearTimeout(timeoutid);** **                }** **            });** **        }**     }); })();`

当调用doBackgroundWork函数时,我创建了一个新的Promise,它对当前时间进行快照,然后生成进度消息来指示自任务开始以来已经过去了多少秒。我将更新的时间间隔设置为一秒钟,这样在测试生命周期事件时,我就不必为更新等待太长时间。

我还在default.js文件中添加了代码,以启动后台工作并显示结果。您可以在清单 19-21 中看到这些变化。

清单 19-21 。开始后台工作并显示进度信息

`... function performInitialization() {     calcButton.addEventListener("click", function (e) {         var first = ViewModel.State.firstNumber = Number(firstInput.value);         var second = ViewModel.State.secondNumber = Number(secondInput.value);         if (first < 5000 && second < 5000) {             ViewModel.State.result = ViewModel.State.cachedResult[first][second];         } else {             ViewModel.State.result = first + second;         }     });

    ViewModel.State.bind("result", function (val) {         if (val != null) {             ViewModel.State.history.push({                 message: ViewModel.State.firstNumber + " + "                     + ViewModel.State.secondNumber + " = "                     + val             });         }     }); **    startBackgroundWork();**

    return Utils.doWork(5000).then(function (data) {         ViewModel.State.cachedResult = data;     }); };

var backgroundPromise;

function startBackgroundWork() { **    backgroundPromise = Utils.doBackgroundWork();** **    var updatedExistingEntry = false;** **    backgroundPromise.then(function () { }, function () { }, function (progress) {** **        var newItem = {** **            message: "Activated: " + Number(progress).toFixed(0) + " seconds ago"** **        };** **        ViewModel.State.eventLog.forEach(function (item, index) {** **            if (item.message.indexOf("Activated:") == 0) {** **                updatedExistingEntry = true;** **                ViewModel.State.eventLog.setAt(index, newItem);** **            }** **        });** **        if (!updatedExistingEntry) {** **            ViewModel.State.eventLog.push(newItem);** **        }** **    });** } ...`

startBackgroundWork函数调用doBackgroundWork并使用then方法接收进度更新,每个更新包含自激活以来的秒数。我将这些信息写入视图模型中的事件日志,但是因为我不想让日志中每一秒都充满消息,所以我在日志中创建了一个对象,并在每次收到进度更新时替换它。

doBackgroundWork函数中,我已经用后台任务开始的时间来表示 app 被激活的时刻。为了做出合理的估计,我从performInitialization函数中调用了startBackgroundWork函数,该函数在应用启动时被调用,并且之前不处于suspended状态。你可以在图 19-11 中看到这些变化的结果,图中显示了一个已经运行了一段时间的应用实例。

images

***图 19-11。*显示应用激活后经过的时间

要查看过时的上下文问题,请启动应用并运行一段时间。然后暂停和恢复应用。应用暂停时不会进行任何后台工作,但应用恢复后会立即重新开始。从字面上看,该应用会从停止的地方继续运行,这意味着后台任务生成的进度更新是基于任务首次启动的时间,而不是该应用最近一次激活的时间。

示例中的这个问题是微不足道的,但是现实世界中经常会出现类似的问题。执行oncheckpoint函数是停止后台任务的机会,这些任务依赖于应用再次启动时将失效的数据。有两种停止后台工作的方法—取消并忘记停止并等待。我将在接下来的章节中解释这两个问题。

使用取消和忘记技术

这是最简单的技术——您只需调用执行后台工作的Promisecancel方法。你可以在清单 19-22 中的函数中看到我是如何做到这一点的——快速、简单、容易。

清单 19-22 。取消 oncheckpoint 处理函数中的承诺

... app.oncheckpoint = function (args) {     // app is about to be suspended     app.sessionState = ViewModel.State.getData(); **    backgroundPromise.cancel();**     writeEventMessage("Suspended"); }; ...

当应用从暂停状态恢复时,你可以再次开始工作,如清单 19-23 所示。

清单 19-23 。恢复应用时开始后台工作

... switch (args.detail.previousExecutionState) {     case activation.ApplicationExecutionState.suspended: **        startBackgroundWork();**         writeEventMessage("Resumed from Suspended");         break;     case activation.ApplicationExecutionState.terminated:         ViewModel.State.setData(app.sessionState);         writeEventMessage("Launch from Terminated");         promises.push(performInitialization());         break;     case activation.ApplicationExecutionState.notRunning:     case activation.ApplicationExecutionState.closedByUser:     case activation.ApplicationExecutionState.running:         writeEventMessage("Fresh Launch");         promises.push(performInitialization());         break; } ...

这种方法的缺点是,在Promise检查应用是否被取消之前,或者在工作正在进行期间,应用可能会被暂停。这可能意味着当应用恢复时,将有一个或多个过时的更新。您可以通过在您的Promise代码中添加一个额外的检查来轻松地解决这个问题,如清单 19-24 所示,它显示了我对tasks.js文件所做的更改。

清单 19-24 。执行额外检查,以避免应用恢复时更新过时

`... doBackgroundWork: function () {     var interval = 1000;     var canceled = false;     var timeoutid;

    return new WinJS.Promise(function (fDone, fError, fProgress) {         var startTime = Date.now();         (function getElapsedTime() {             var elapsed = Date.now() - startTime; **            if (!canceled) {** **                fProgress(elapsed / 1000);**                 timeoutid = setTimeout(getElapsedTime, interval);             } else {                 fDone();             }         })();     }, function () {         canceled = true;         if (timeoutid) {             clearTimeout(timeoutid);         }     }); } ...`

我已经将调用转移到了进度更新函数,这样只有在Promise没有被取消的情况下才会调用它。这降低了向用户显示过时更新的可能性,因为即使在应用暂停时正在处理一个工作单元,当应用恢复时也不会显示该工作的结果。

使用停止和等待技术

我对自定义Promise中调用 progress 函数的方式所做的更改减少了过时更新的机会,但并没有完全消除它——当应用暂停时,可能会调用 progress 处理程序,当应用恢复时,调用将会完成。这意味着只有当一个过时的更新不会导致严重的问题时,才应该使用“取消并忘记”技术。

如果您需要确保不使用过时的数据,那么您需要停止并等待技术。向oncheckpoint处理函数传递一个支持setPromise方法的对象。如果您将执行后台工作的Promise传递给此方法,应用的暂停将被延迟最多 5 秒钟,以便让Promise完成。

在这种情况下,你不能取消Promise。对Promise.cancel方法的调用立即返回并将Promise置于错误状态,而Promise正在执行的后台工作将继续,直到取消状态被下一次检查——这挫败了目标。

相反,你必须向Promise发出信号,表示它应该终止,但要以一种将承诺留在常规完成状态的方式进行。我对tasks,js文件做了一些进一步的修改来演示这种技术,你可以在清单 19-25 中看到。

清单 19-25 。修改提前完工的背景承诺

`... doBackgroundWork: function () {     var interval = 1000;     var canceled = false;     var timeoutid;

    var p = new WinJS.Promise(function (fDone, fError, fProgress) {         var startTime = Date.now();         function getElapsedTime() {             var elapsed = Date.now() - startTime; **            if (!canceled && !p.stop) {**                 fProgress(elapsed / 1000);                 timeoutid = setTimeout(getElapsedTime, interval);             } else {                 fDone();             }         }; **        setImmediate(getElapsedTime);**     }, function () {         canceled = true;         if (timeoutid) {             clearTimeout(timeoutid);         }     });

**    p.stop = false;** **    return p;** } ...`

我在由doBackgroundWork函数返回的Promise对象上定义了一个附加属性,并在自定义的Promise代码中引用它。这允许我有一个 per- Promise标志,表示后台工作应该停止,Promise应该表明它已经完成。

为了做到这一点,我改变了最初执行getElapsedTime函数的方式,从自执行函数切换到调用setImmediate方法。推迟执行getElapsedTime函数意味着在最初调用getElapsedTime之前执行在Promise对象上创建stop属性的代码,这意味着我可以检查函数中stop属性的值,安全地知道到那时它已经被定义了。

您可以在清单 19-26 的中看到我是如何使用stop属性的,它显示了我对default.js文件中的oncheckpoint函数所做的修改。

清单 19-26 。使用 setPromise 方法推迟应用暂停

... app.oncheckpoint = function (args) {     // app is about to be suspended     app.sessionState = ViewModel.State.getData(); **    backgroundPromise.stop = true;** **    args.setPromise(backgroundPromise);**     writeEventMessage("Suspended"); }; ...

oncheckpoint函数中的args对象上可用的setPromise方法与onactivated函数中的同名方法扮演着不同的角色:当你将一个Promise传递给该方法时,它会在你的应用被挂起之前给它片刻的宽限,等待承诺的兑现。

使用这种技术时,有几个要点需要记住。首先,使用setPromise方法只会推迟暂停你的应用 5 秒钟。在此之后,应用将被暂停,当应用恢复时,您将面临状态更新的风险——这意味着您需要确保您的后台工作在短时间内执行,并且您需要足够频繁地检查stop标志,以确保没有超过 5 秒期限的机会。第二点是,你不能用这种方法取消Promise——只有当你能安排好事情,使Promise在不进入错误状态的情况下完成时,这种方法才会起作用——否则,在Promise.cancel方法被调用之后,在Promise正在做的工作被暂停之前,你的应用会被挂起。

总结

在这一章中,我解释了 Metro 应用的不同生命周期阶段,并向您展示了这些阶段是如何由 Windows 发出信号的。我向您展示了如何解决 Visual Studio 添加到新项目中的WinJS.Application对象和代码的问题,以便您的应用能够获得全方位的生命周期通知并做出适当的响应。我演示了应用基于其先前状态启动时所需的不同操作,并向您展示了如何确保在应用初始化时显示闪屏,以及如何存储和恢复状态数据,以便您可以在应用被 Windows 终止后启动时正确响应。

应用生命周期的主题贯穿全书的这一部分。我将向您展示用于支持契约的不同类型的激活事件,这是 Windows 平台的一个关键特性。首先,我将在下一章向您展示如何使用用户设置和应用数据。