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

24 阅读47分钟

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

原文:Pro Windows 8 Development with HTML5 and JavaScript

协议:CC BY-NC-SA 4.0

九、利用承诺

Windows 应用中异步编程的基本前提很简单。您调用一个方法,它执行的工作被安排在以后执行。在未来的某个时刻,工作被执行,并通过回调函数通知您结果。

异步编程在 JavaScript 中已经很成熟了。当您在 web 应用中发出 Ajax 请求时,您可能已经遇到过异步编程。你想从服务器加载一些内容,但不想阻止用户与应用进行交互。因此,您使用XMLHttpRequest对象(或 jQuery 之类的包装器库,使XMLHttpRequest对象更容易使用)进行了一次方法调用,并提供了一个在服务器内容到达时执行的函数。如果您没有使用 Ajax,web 应用在数据从服务器返回之前不会对用户交互做出响应,从而造成应用停滞不前的现象。

Windows 应用中的异步编程以相同的方式工作,并用于相同的目的-允许用户在执行其他操作时与应用进行交互。Windows 应用更广泛地使用异步编程,而不仅仅是 Ajax 请求,这就是为什么有一个通用对象来表示异步操作:WinJS.Promise对象。

术语 promise 表示在未来某个时间执行任务并返回结果的承诺。当这种情况发生时,这个承诺就被说成是兑现了。表 9-1 对本章进行了总结。

images 提示WinJS.Promise对象是CommonJS Promises/A规范的一个实现,你可以在[commonjs.org](http://commonjs.org)读到。这正在成为 JavaScript 异步编程的标准,并在 jQuery 库采用它作为其延迟对象特性的基础时得到了极大的普及。

images

images

创建示例项目

开始异步编程的最佳方式是直接投入进去。为了构建一些熟悉的东西,我将使用用WinJS.Promise包装XMLHttpRequest对象的WinJS.xhr函数。为了演示这个特性,我使用 Visual Studio Blank App模板创建了一个名为Promises的新项目。清单 9-1 显示了default.html文件的内容。

清单 9-1 。default.html 文件的内容

`

         Promises

                                  

    
        
            
                1st Zip:                              
            
                2nd Zip:                              
            
                Go                 Cancel             
            
        
        
Content will go here
        
Content will go here
    

    

        
    

    

        
Zip:
                 
City:
                 
State:
                 
Lat:
                 
Lon:
             

`

这个应用执行邮政编码的网络搜索。布局分成三个面板,你可以在图 9-1 中看到。最左边的面板包含一对input元素,允许你输入邮政编码,旁边是GoCancel按钮。还有一个区域,我将在其中显示关于我发出的 Ajax 请求的消息。

images

***图一。*诺言 app 的初步布局

中间和右侧面板是显示搜索结果的地方。正如您在清单中看到的,我已经定义了一个显示搜索结果的模板,使用了我在第八章中描述的技术。

你可以在清单 9-2 中看到我用来创建这个布局的 CSS,它显示了css/default.css

清单 9-2 。default.css 的内容

`body {     display: -ms-flexbox;     -ms-flex-direction: column;     -ms-flex-align: center; -ms-flex-pack: center;     background-color: #5A8463; color: white;       }

div.container {     display: -ms-flexbox; -ms-flex-direction: row;     -ms-flex-align: center; -ms-flex-pack: center; }

div.panel {     width: 25%; border: thick solid white;     margin: 10px; padding: 10px; font-size: 14pt;         height: 500px; width: 350px;     display: -ms-flexbox; -ms-flex-direction: column;     -ms-flex-align: center; -ms-flex-pack: center; }

#left > div { margin: 10px 0;} #left input {font-size: 14pt; width: 200px;} div.panel label {display: inline-block; width: 100px; text-align: right;} div.panel span {display: inline-block; width: 200px;} #messages {width: 80%; height: 250px; padding: 10px; border: thin solid white;} #middle, #right {font-size: 20pt;} #middle label, #right label {color: darkgray; margin-left: 10px;}`

如您所料,我已经为这个应用定义了一个简单的视图模型。清单 9-3 显示了视图模型的内容,我在一个名为js/viewmodel.js的文件中创建了这个视图模型。

清单 9-3 。承诺应用的视图模型

`(function () {     WinJS.Namespace.define("ViewModel", WinJS.Binding.as({         State: {             zip1: "10036", zip2: "20500",         }     }));

    ViewModel.State.messages = new WinJS.Binding.List(); })();`

最后,清单 9-4 显示了js/default.js文件的内容。这个文件包含为布局中的buttoninput元素定位和设置事件处理函数的代码,但是它不包含实际向 web 服务发出请求的代码——我将在本章后面添加这个代码。

清单 9-4 。default.js 文件

`(function () {     "use strict";

    var app = WinJS.Application;     var $ = WinJS.Utilities.query;

**    function requestData(zip, targetElem) {** **        ViewModel.State.messages.push("Started for " + zip);** **        // ...code will go here...** **    }**

    app.onactivated = function (args) {

        $('input').listen("change", function (e) {             ViewModel.State[this.id] = this.value;         });

        $('button').listen("click", function (e) {             if (this.id == "go") {                 var p1 = requestData(ViewModel.State.zip1, middle);                 var p2 = requestData(ViewModel.State.zip2, right);             };         });

        ViewModel.State.messages.addEventListener("iteminserted", function (e) {                 messageTemplate.winControl.render({ message: e.detail.value }, messages);             });         WinJS.UI.processAll().then(function () {             return WinJS.Binding.processAll(document.body, ViewModel);         });     };

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

至此,应用的基本结构已经完成。你可以在输入元素中输入邮政编码,你可以点击按钮——我所缺少的是做实际工作的代码,这也是我在本章剩余部分要关注的。

这个示例应用通过网络连接向远程服务器请求数据。这需要在应用清单中启用一个功能,让 Windows 和用户知道你的应用能够发出这样的请求。这允许 Windows 实施安全策略(不具备该功能的应用将不被允许发起请求),并且允许用户在考虑从 Windows 应用商店购买应用时对您的应用存在的风险进行评估(尽管很明显用户实际上并不太关注此类信息)。

当您创建新的应用开发项目时,Visual Studio 会自动为您启用这一特定功能。要查看功能,双击Solution Explorer中的package.appxmanifest文件并导航到Capabilities选项卡。你会看到Internet (Client)被选中,如图图 9-2 所示,告诉 Windows 你的 app 能够发起出站网络连接。

images

***图 9-2。*启用呼出网络连接的能力

处理基本异步编程流程

您可以判断何时处理异步操作,因为您调用的方法将返回一个WinJS.Promise对象。返回一个Promise的方法将把它们的工作安排在未来的某个时间发生,工作的结果将通过Promise发出信号。

Scheduled 在这个上下文中不是最有用的词,因为它暗示了未来某个固定的时间。事实上,你所知道的是工作将会完成——你对何时完成没有任何影响,也不知道离任务开始还有多长时间。延迟可能是一个更好的词,也是 jQuery 团队采用的词,但是调度是使用最广泛的术语。

使用异步方法是一种权衡。好处是你的应用可以执行后台任务,同时保持对用户的响应。缺点是你失去了对任务执行的直接控制,你不知道你的任务什么时候会被执行。

然而,在很大程度上,你没有选择。Windows 在整个 WinJS 和 Windows API 中使用异步编程,如果不采用Promise对象和它所代表的编程方法,你就无法创建一流的应用。

使用异步回调

Promise对象使用回调函数为您提供关于异步任务结果的信息。您使用then方法注册这些函数。then方法最多接受三个回调函数作为参数。如果任务成功完成,则调用第一个函数(成功回调),如果任务遇到错误,则调用第二个函数(错误回调),并且在任务执行期间调用第三个函数来通知进度信息(进度回调)。

为了演示Promise对象,我使用了WinJS.xhr方法,它是标准XMLHttpRequest对象的包装器,您可以在 web 应用中使用它来发出 Ajax 请求。WinJS.xhr方法接受一个对象,该对象包含与XMLHttpRequest定义的属性相对应的属性,包括urltypeuserpassworddata,所有这些属性都不加修改地传递给XMLHttpRequest对象。

images 提示您可能使用了 jQuery 等库中的便利包装器来管理您的请求,而没有直接使用XMLHttpRequest对象。你不需要理解XMLHttpRequest的工作原理来理解本章,但是如果你想要更多的信息,那么 W3C 规范是一个很好的起点:[www.w3.org/TR/XMLHttpR…](http://www.w3.org/TR/XMLHttpRequest)

在清单 9-5 的中,你可以看到我是如何在它返回的Promise上使用WinJS.xhr方法和then函数的,这展示了我是如何在default.js文件中实现requestData函数的。如清单所示,我使用 success 回调函数显示来自服务器的数据,使用 error 回调函数显示请求中任何问题的细节。

清单 9-5 。使用 WinJS。约定方法

`... function requestData(zip, targetElem) {

    ViewModel.State.messages.push("Started for " + zip);

    var promise = WinJS.xhr({         url: "gomashup.com/json.php?fd…" + zip     }).then(function (xhr) {         ViewModel.State.messages.push(zip + " Complete");         var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;         WinJS.Utilities.empty(targetElem);         zipTemplate.winControl.render(dataObject[0], targetElem);     }, function (xhr) {         WinJS.Utilities.empty(targetElem);         targetElem.innerText = "Error: " + xhr.statusText;     });     return promise; } ...`

我传递给WinJS.xhr方法的对象有一个url属性,它指定了我想要请求的 URL。WinJS.xhr方法返回一个Promise对象,我使用then方法为成功和错误回调注册函数。

images 提示你没有要用then的方法。如果您不关心异步任务的结果,只需丢弃或忽略该方法返回的Promise来创建一个“一劳永逸”的任务。

WinJS.xhr方法立即返回,Ajax 请求将在未来某个未指定的时间执行。我无法控制请求何时开始,只有当我的一个回调函数被执行时,我才知道请求何时结束。

如果我的成功回调函数被执行,我知道我有一个来自服务器的响应,我处理并显示在布局中。如果我的错误回调函数被执行,那么我就知道出错了,并显示错误的详细信息。你可以在图 9-3 中看到结果,该图展示了点击Go按钮并完成创建的Promise对象后应用的布局。

images

***图 9-3。*使用回调处理程序响应已履行的承诺

WinJS 和 Windows APIs 中的大多数异步方法往往比WinJS.xhr更细粒度,将某种结果对象传递给成功回调函数,将描述性字符串消息传递给错误函数(进度回调函数并不经常使用,尽管在本章后面我向您展示如何创建自己的Promise时,您可以看到演示)。

使用 GOMASHUP 邮政编码服务

我在这个例子中使用的 web 服务来自GoMashup.com,他提供了许多有用的数据服务。我选择 GoMashup 是因为他们的服务快速、可靠,并且不需要在请求中包含任何开发者密钥,这使得他们非常适合用于演示。例如,如果我想要关于邮政编码10036的信息,我使用以下 URL 进行查询:

[gomashup.com/json.php?fds=geo/usa/zipcode/10036](http://gomashup.com/json.php?fds=geo/usa/zipcode/10036)

我得到这样一个字符串:

({"result":[{     "Longitude" : "-073.989847",     "Zipcode" : "10036",     "ZipClass" : "STANDARD",     "County" : "NEW YORK",     "City" : "NEW YORK",     "State" : "NY",     "Latitude" : "+40.759530" }]})

GoMashup 服务旨在与 JSONP 一起使用,其中调用一个函数将数据插入到应用中。这意味着我需要去掉字符串的第一个和最后一个字符来获得一个有效的 JSON 字符串,我可以解析这个字符串来创建一个 JavaScript 对象。

创建链

then方法的一个有趣的方面是它返回一个Promise,当回调函数被执行时,这个 ?? 被实现。

这意味着当 Ajax 请求完成时,我从requestData函数返回的Promise并没有完成。相反,它是一个Promise,当 Ajax 请求已经完成并且成功或错误回调也已经执行时,它就完成了。

使用then方法创建动作序列被称为链接,它允许你控制任务执行的顺序。作为一个例子,我可以改变requestData函数的结构,使它更有用。目前,如果我的请求成功,我只向ViewModel.State.messages对象添加一条消息,但是使用then方法,我可以区分 Ajax 请求的实现和初始回调集的实现,如清单 9-6 所示。

清单 9-6 。使用 then 方法将动作链接在一起

`... function requestData(zip, targetElem) {

    ViewModel.State.messages.push("Started for " + zip); var promise = WinJS.xhr({         url: "gomashup.com/json.php?fd…" + zip     }).then(function (xhr) { **        ViewModel.State.messages.push(zip + " Successful");**         var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;         WinJS.Utilities.empty(targetElem);         zipTemplate.winControl.render(dataObject[0], targetElem);     }, function (xhr) { **        ViewModel.State.messages.push(zip + " Failed");**         WinJS.Utilities.empty(targetElem);         targetElem.innerText = "Error: " + xhr.statusText;     });

**    return promise.then(function () {** **        ViewModel.State.messages.push(zip + " Complete");** **    });**

} ...`

你可以在图 9-4 中看到这些信息的顺序,图中显示了点击Go按钮的结果。

images

***图 9-4。*使用 then 方法控制异步任务的顺序

Promise对象和then方法的一个缺点是,你最终会得到难以阅读的代码。更具体地说,很难确定任务链的执行顺序。为了帮助在清单 9-6 中弄清楚这一点,将Promise赋给一个名为promise的变量,然后分别使用then方法创建一个链。然而,通常情况下,then方法会更直接地应用,如清单 9-7 所示。

清单 9-7 。直接在承诺上使用 then 方法创建链

`function requestData(zip, targetElem) {

    ViewModel.State.messages.push("Started for " + zip);     var promise = WinJS.xhr({         url: "gomashup.com/json.php?fd…" + zip     }).then(function (xhr) {         ViewModel.State.messages.push(zip + " Successful");         var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;         WinJS.Utilities.empty(targetElem);         zipTemplate.winControl.render(dataObject[0], targetElem);     }, function (xhr) {         ViewModel.State.messages.push(zip + " Failed");         WinJS.Utilities.empty(targetElem);         targetElem.innerText = "Error: " + xhr.statusText; **    }).then(function () {** **        ViewModel.State.messages.push(zip + " Complete");** **    });**

    return promise; }`

因为then方法返回一个Promise对象,所以来自requestData方法的结果是一个Promise,当 Ajax 请求已经完成并且一个回调函数已经执行并且Complete消息的函数已经执行时,这个结果被实现。链是组合Promise的一种简单方式,但是它们可能很难阅读。

images 提示Promise.done方法是对then方法的补充。done方法必须作为一个链中的最后一个方法,因为它不返回结果。链中任何未处理的异常在到达done方法时被抛出(然而它们只是通过then方法反映在Promise的状态中)。像这样抛出一个异常并不是特别有用,因为在调用done方法时,应用代码的执行已经开始了。更好的方法是确保在链中使用错误回调函数来处理任何异常。

连锁承诺

注意,图 9-4 中的所示的信息是混合在一起的。这是因为 IE10 能够同时执行多个请求,并且每个请求独立地通过其生命周期。请求之间没有协调,这就是为什么消息是交错的,也是为什么您在运行示例时可能会看到不同的结果。当您看到我是如何在Go按钮的事件处理程序中调用requestData函数时,这是有意义的,如下所示:

... var p1 = **requestData**(ViewModel.State.zip1, middle); var p2 = **requestData**(ViewModel.State.zip2, right); ...

我接收requestData函数返回的Promise对象,但是我不对它们做任何事情,所以请求是独立调度的。这对于我的示例应用来说很好,因为每个请求更新布局的不同部分,我不需要协调结果。

但是,在许多情况下,您会希望将一项任务推迟到另一项任务完成之后。你可以使用then方法创建一个链来完成这个任务,但是你必须注意从第二个请求中返回Promise对象作为你的回调函数的结果,如清单 9-8 所示,它展示了我对default.js文件所做的更改,以确保请求按顺序执行。

***清单 9-8。*连锁承诺

... $('button').listen("click", function (e) {     if (this.id == "go") { **        requestData(ViewModel.State.zip1, middle).then(function () {** **            return requestData(ViewModel.State.zip2, right);** **        });**     }; }); ...

这真的很重要。如果从回调函数中返回Promise对象,那么链中的任何后续动作都不会被调度,直到Promise被完成。这意味着清单 8 中的代码会产生以下效果:

  1. Schedule the first request
  2. Wait until Promise from the first request
  3. Schedule the second request
  4. Wait until Promise from the second request
  5. Add a message to the ViewModel.State.messages object

这通常是所需要的效果——在前一个活动完成之前,不要安排下一个活动。然而,如果您省略了return关键字,您会得到一个非常不同的效果:

  1. Schedule the first request
  2. Wait until the first request Promise is satisfied
  3. Schedule the second request
  4. To the ViewModel.State.messages object

添加消息

如果你没有从回调函数中返回一个Promise,那么后续的活动将会在回调函数执行完成后立即被调度——当你调用一个异步方法时,是在任务被调度后,而不是在任务完成后。

在我的示例应用中,这种差异很容易发现,因为我在请求的整个生命周期中都在编写消息。图 9-5 显示了两种情况下的消息顺序——左图显示了使用 return 关键字的效果,右图显示了没有 return 关键字的效果。指示器是All Requests Complete消息在事件序列中出现的地方。

省略关键字return并不总是错误的。如果您想将某个任务推迟到其前任完成之后,但不关心该任务的结果,那么省略return关键字是完全合适的。只要确保你知道你的目标是什么效果,并根据需要包括或省略return

images

***图 9-5。*在回调函数中省略 return 关键字的影响

取消承诺

您可以通过调用Cancel方法来请求取消Promise。这并不像听起来那么有用,因为不要求Promise支持取消,如果支持,取消是一个请求,并且Promise几乎肯定会在检查取消之前完成当前的工作(当我在本章后面向您展示如何创建自己的Promise时,您可以看到这是如何工作的)。

WinJS.Xhr函数返回的Promise确实支持取消,这也是我在本章一直使用它的原因之一。没有办法在你的应用中发现未实现的Promise对象,所以你需要保存对Promise的引用,就像你想要再次引用任何变量一样。你可以看到我是如何连接Cancel按钮并保存对我在清单 9-9 中创建的Promise对象的引用的。

清单 9-9 。取消承诺

`... var p1; var p2;

$('input').listen("change", function (e) {     ViewModel.State[this.id] = this.value; });

$('button').listen("click", function (e) {     if (this.id == "go") { **        p1 = requestData(ViewModel.State.zip1, middle);** **        p1.then(function () {             p2 = requestData(ViewModel.State.zip2, right);** **            return p2;** **        }).then(function () {** **            ViewModel.State.messages.push("All Requests Complete");** **        });**     } else { **        p1.cancel();** **        p2.cancel();** **        ViewModel.State.messages.push("All Requests Canceled");**     } }); ...`

当按下Cancel按钮时,我在每个我在按下Go按钮时创建的Promise对象上调用cancel方法。这向Promise对象发出信号,我想终止对服务器的请求。

当您取消Promise时,会调用错误回调。传递给函数的对象有三个属性(namemessagedescription),它们都被设置为字符串Canceled。你可以在清单 9-10 中的回调函数中看到我是如何处理这种情况的。如果有值的话,我显示statusText的值,否则显示message属性的值。

清单 9-10 。在错误回调中处理取消

`... function requestData(zip, targetElem) {

    ViewModel.State.messages.push("Started for " + zip);

    var promise = WinJS.xhr({         url: "gomashup.com/json.php?fd…" + zip     }).then(function (xhr) {         ViewModel.State.messages.push(zip + " Successful");         var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;         WinJS.Utilities.empty(targetElem);         zipTemplate.winControl.render(dataObject[0], targetElem);     }, function (xhr) {         ViewModel.State.messages.push(zip + " Failed");         WinJS.Utilities.empty(targetElem);         targetElem.innerText = "Error: "             + (xhr.statusText != null ? xhr.statusText : xhr.message);     }).then(function () {         ViewModel.State.messages.push(zip + " Complete");     });

    return promise; } ...`

测试该功能最简单的方法是重启(而不是刷新应用),点击Go按钮,然后立即点击Cancel按钮。重新启动很重要,因为这意味着请求的任何方面都不会被缓存,只给你足够的时间来执行取消。你可以在图 9-6 中看到效果。

images

***图 9-6。*取消请求的影响

从承诺中传递结果

你可以在图 9-6 的中看到,当发出取消请求的Promise任务被取消时,为每个请求写Complete消息和整个All Requests Complete消息的链式任务仍然被执行。

有时候,这正是你想要的:不管前面的Promise中发生了什么都要执行的任务,但是你经常会想要在面对错误时有选择地进行。在清单 9-11 的中,您可以看到我对default.js文件中的requestData函数所做的修改,以便在请求被取消时不显示Complete消息,这是通过从我的Promise函数返回结果来实现的。

清单 9-11 。从承诺传递结果

`... function requestData(zip, targetElem) {

    ViewModel.State.messages.push("Started for " + zip);

    var promise = WinJS.xhr({         url: "gomashup.com/json.php?fd…" + zip     }).then(function (xhr) {         ViewModel.State.messages.push(zip + " Successful");         var dataObject = JSON.parse(xhr.response.slice(1, -1)).result;         WinJS.Utilities.empty(targetElem);         zipTemplate.winControl.render(dataObject[0], targetElem); **        return true;**     }, function (xhr) {         ViewModel.State.messages.push(zip + " Failed");         WinJS.Utilities.empty(targetElem);         targetElem.innerText = "Error: "             + (xhr.statusText != null ? xhr.statusText : xhr.message); **        return false;     }).then(function (allok) { **        if (allok) { **            ViewModel.State.messages.push(zip + " Complete");** **        }**         return allok;     });

    return promise; } ...`

当由WinJS.xhr函数返回的Promise被满足时,我的成功或错误处理函数将被执行。我已经修改了成功处理程序,使它返回true,表明请求已经完成。我将错误处理程序改为返回 false,表示出现了错误或请求被取消。

来自被执行的处理函数的truefalse值被作为参数传递给链中的下一个then函数。在这个例子中,我使用这个值来判断是否应该显示请求的Complete消息。

您可以通过这种方式沿着Promise对象链传递任何对象,每个then函数可以返回不同的结果,甚至是不同种类的结果。在清单中,当我在default.js文件的其他地方使用时,我从作为参数接收的then函数返回相同的值,如清单 9-12 所示。

清单 9-12 。将结果沿着链传递得更远

... $('button').listen("click", function (e) {     if (this.id == "go") {         p1 = requestData(ViewModel.State.zip1, middle);         p1.then(function () {             p2 = requestData(ViewModel.State.zip2, right);             return p2;         }).then(function (**allok**) { **            if (allok) {**                 ViewModel.State.messages.push("All Requests Complete"); **            }**         });     } else {         p1.cancel();         p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");     } }); ...

记住,requestData函数返回的Promise对象是最后一个then函数返回的对象,所以我从那个函数返回的结果将作为参数传递给链中的下一个函数——如清单所示。我使用true / false值来决定是否应该向用户显示All Requests Complete消息。这是一个简单的数据流经一系列Promise的例子,但是它清楚地展示了这种技术的灵活性。当点击图 9-7 中的Cancel按钮时,您可以看到向用户显示的一组精简的消息。

images

***图 9-7。*通过一个链传递来自承诺对象的结果的效果

协调承诺

then函数并不是协调异步任务的唯一方法。Promise对象支持其他几种方法,这些方法可以用来创建特定的效果,或者使处理多个Promise对象变得更容易。我在表 9-2 中总结了这些方法,并在下面演示了它们的用法。

images

使用任意方法

any方法接受一组Promise并返回一个Promise作为结果。当自变量数组中的Promise对象中的任意一个被满足时,由any方法返回的Promise被满足。你可以在清单 9-13 中看到正在使用的any方法。

清单 9-13 。使用任意方法

... var p1, p2; `$('button').listen("click", function (e) {     if (this.id == "go") {         p1 = requestData(ViewModel.State.zip1, middle);         p2 = requestData(ViewModel.State.zip2, right);

**        WinJS.Promise.any([p1, p2]).then(function (complete) {** **            complete.value.then(function (result) {** **                if (result) {** **                    ViewModel.State.messages.push("Request " + complete.key** **                        + " Completed First");** **                } else {** **                    ViewModel.State.messages.push("Request " + complete.key** **                        + " Canceled or Error");** **                }** **            });** **        });**

    } else {         p1.cancel();         p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");     } }); ...`

如果您使用then方法在由any方法返回的Promise上设置一个回调,您的函数将被传递一个具有两个属性的对象。key属性返回参数数组中被满足的Promise的索引(结果导致 any Promise被满足)。value属性返回一个Promise,当它被满足时,传递来自任务链的结果。您可以看到我是如何使用这两个值向布局中写入消息的,该消息报告了哪个请求首先完成及其结果。您可以在图 9-8 中看到该示例生成的输出。

images

***图 9-8。*用任意方式报告哪个承诺先兑现

images 提示any方法返回的Promise在底层的Promise之一满足后立即满足。any Promise不会等到所有的Promise都实现了才告诉你哪一个是第一个。当任何一个Promise完成时,其他Promise对象可能仍未完成。

使用 join 方法

join方法类似于any方法,但是它返回的Promise直到参数数组中所有Promise对象的都被实现后才被实现。你可以在清单 9-14 中看到正在使用的join方法。传递给then回调函数的参数是一个数组,包含所有原始Promise对象的结果,按照原始数组的顺序排列。

清单 9-14 。使用连接方法

`... var p1, p2;

$('button').listen("click", function (e) {     if (this.id == "go") {         p1 = requestData(ViewModel.State.zip1, middle);         p2 = requestData(ViewModel.State.zip2, right);

        WinJS.Promise.any([p1, p2]).then(function (complete) {             complete.value.then(function (result) {                 if (result) {                     ViewModel.State.messages.push("Request " + complete.key                         + " Completed First");                 } else {                     ViewModel.State.messages.push("Request " + complete.key                         + " Canceled or Error");                 }             });         });

**        WinJS.Promise.join([p1, p2]).then(function (results) {** **            ViewModel.State.messages.push(results.length + " Requests Complete");** **            results.forEach(function (result, index) {** **                ViewModel.State.messages.push("Request: " + index + ": " + result);** **            });** **        });**

    } else {         p1.cancel();         p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");     } }); ...`

注意,我可以在同一套Promise对象上使用anyjoin方法。一个Promise能够支持对then方法的多次调用,并将正确执行多组回调。你可以在图 9-9 中看到使用anythen方法的效果。

images

***图 9-9。*any 和 join 方法显示的消息

使用超时

方法有两种用途,做完全不同的事情。最简单形式的timeout方法采用一个数字参数,并返回一个在指定时间段后实现的Promise。这可能看起来有点奇怪,但是当你想推迟一系列Promise的调度时,它会很有用。你可以在清单 9-15 中看到它是如何工作的。

清单 9-15 。使用超时方法推迟承诺

`... var p1, p2;

$('button').listen("click", function (e) {     if (this.id == "go") {

**        WinJS.Promise.timeout(3000).then(function () {**             p1 = requestData(ViewModel.State.zip1, middle);             p2 = requestData(ViewModel.State.zip2, right);

            WinJS.Promise.any([p1, p2]).then(function (complete) {                 complete.value.then(function (result) {                     if (result) {                         ViewModel.State.messages.push("Request "                             + complete.key + " Completed First");                     } else {                         ViewModel.State.messages.push("Request "                             + complete.key + " Canceled or Error");                     }                 });             });             WinJS.Promise.join([p1, p2]).then(function (results) {                 ViewModel.State.messages.push(results.length + " Requests Complete");                 results.forEach(function (result, index) {                     ViewModel.State.messages.push("Request: " + index + ": " + result);                 });             }); **        });**     } else {         p1.cancel();         p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");     } }); ...`

在这个清单中,我创建了三秒钟的延迟。一旦这段时间过去,由timeout方法返回的Promise将自动完成,我用then方法设置的回调函数被调用——在这种情况下,我对服务器的邮政编码数据的请求直到单击Go按钮三秒后才开始。

为承诺设置超时

timeout方法的另一个用途是设置Promise的到期时间。要使用这个版本的方法,您需要传入一个超时值和您想要应用它的Promise。你可以看到这种形式的timeout方法是如何用在清单 9-16 中的。

清单 9-16 。使用超时方法自动取消承诺

`... var p1, p2;

$('button').listen("click", function (e) {     if (this.id == "go") {

        WinJS.Promise.timeout(250, p1 = requestData(ViewModel.State.zip1, middle));         WinJS.Promise.timeout(2000, p2 = requestData(ViewModel.State.zip2, right));

        WinJS.Promise.any([p1, p2]).then(function (complete) {             complete.value.then(function (result) {                 if (result) {                     ViewModel.State.messages.push("Request "                         + complete.key + " Completed First");                 } else {                     ViewModel.State.messages.push("Request "                         + complete.key + " Canceled or Error");                 }             });         });             } else {         p1.cancel();         p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");     } }); ...`

在这个清单中,我使用timeout方法为一个请求设置 250 毫秒的最大持续时间,为另一个请求设置 2 秒。如果请求在这些时间内完成,那么没有什么特别的事情发生。然而,如果在周期结束时没有实现Promise对象,它们将被自动cancelled(这是通过调用我在本章前面演示的cancel方法来执行的)。为了让这个有用,你需要确保你正在使用的Promise对象支持取消。

对多个承诺应用相同的回调函数

theneach方法是将同一组回调函数应用于一组Promise对象的一种便捷方式。这个方法不会改变Promise的调度顺序,但是它会返回一个Promise,这相当于为回调函数返回的所有Promise调用join方法。清单 9-17 显示了正在使用的theneach方法。

清单 9-17 。使用新方法

`... var p1, p2;

$('button').listen("click", function (e) {     if (this.id == "go") {

        p1 = requestData(ViewModel.State.zip1, middle);         p2 = requestData(ViewModel.State.zip2, right);

**        WinJS.Promise.thenEach([p1, p2], function (data) {** **            ViewModel.State.messages.push("A Request is Complete");** **        }).then(function (results) {** **            ViewModel.State.messages.push(results.length + " Requests Complete");**         });

    } else {         p1.cancel();         p2.cancel();         ViewModel.State.messages.push("All Requests Canceled");     } }); ...`

我只指定了一个成功函数,但是您也可以指定错误和进度回调。没有传递给回调的上下文信息来指示哪个Promise正在被处理,这使得theneach方法没有它本来应该有的用处。

创建自定义承诺

创建异步方法有两种方式。第一种,您已经看到了,是建立在现有的异步方法上,操作它们返回的Promise对象并返回结果。本章第一个示例应用中的requestData函数就是这样创建的异步方法的一个很好的例子。

另一种方法是实现您自己的Promise,并创建一个在未来某个时间执行的定制任务。当您想从头开始创建异步方法时,可以采用这种方法。在这一节中,我将向您展示如何创建您自己的Promise对象。我创建了一个名为CustomPromise的 Visual Studio 项目,其中所有的标记、代码和 CSS 都包含在一个文件中。您可以在清单 9-18 中看到这个文件default.html的内容。

images 注意这是一个高级主题,大多数应用都不需要。也就是说,即使您不需要立即使用这种技术,快速浏览这一部分也会帮助您理解由 WinJS 和 Windows 名称空间中的方法返回的Promise对象是如何工作的。

清单 9-18 。default.html 档案

`

                                      body {             display: -ms-flexbox; -ms-flex-direction: column;             -ms-flex-align: center; -ms-flex-pack: center;                     }         body, button { font-size: 30pt; margin: 5px}         #output { margin: 20px; }          

        WinJS.Application.onactivated = function (args) {             WinJS.Utilities.query("button").listen("click", function (e) {                 displayMessage("Starting");                 var total = calculateSum(10000000);                 displayMessage("Done: " + total);             });         };         WinJS.Application.start();     

    Go     
        Output will appear here     
`

这个简单的应用非常适合演示如何创建定制的Promise。你可以在图 9-10 中看到布局。

images

***图 9-10。*custom promise app 的布局

当点击Go按钮时,我调用calculateSum函数,该函数生成前 10,000,000 个整数的和。这个任务需要几秒钟才能完成,在此期间,应用没有响应。当应用没有响应时,用户界面不会响应用户交互。对于这个简单的例子,您可以看出有问题,因为在点击了button元素之后,直到求和计算完成,它才返回到未按下的状态。这是因为click事件是在 CSS 更改被应用之前被触发和处理的,这意味着计算会阻止任何 UI 更新,直到它完成。这就是异步方法要解决的问题。

images 提示10,000,000 这个值对我的电脑来说很好,但是如果你有一个更快的系统,你可能需要增加它,或者为一个更慢的系统减少它。为了获得问题的本质(和解决方案),您希望任务花费大约 5-10 秒。

实现承诺

第一步是创建Promise对象,通过向Promise对象构造函数传递一个函数来完成。你可以在清单 9-19 中看到我是如何做到的。

清单 9-19 。创建承诺对象

`...

...`

传递给Promise构造函数的函数有三个参数,每个参数都是一个函数。第一个参数是您在完成任务并希望返回结果时调用的参数。如果要报告错误,可以调用第二个参数。当您想要制作进度报告时,会调用最后一个参数。

您可以看到,我在这个清单中添加了对报告错误的支持。如果calculateSum函数的 count 参数小于 5000,我调用fError函数来指出问题。对于其他值,我计算总和并通过fDone函数返回结果。(如果您愿意,可以忽略目标是固定的这一事实——calculateSum函数不知道这一点)。

当您创建一个异步方法时,您返回Promise对象,以便调用者可以使用then方法来接收任务的结果或创建一个链。您可以在示例中看到我是如何这样做的,以便从由calculateSum方法返回的承诺中获得结果。

延期执行

我已经实现了一个Promise,但是我仍然有一个问题:当我点击Go按钮时,应用仍然没有响应。创建一个Promise不会自动推迟任务的执行,这是一个常见的陷阱。要创建一个真正的异步方法,我必须采取额外的步骤,显式地调度工作。我已经用setImmediate函数完成了,如清单 9-20 所示。

清单 9-20 。推迟任务的执行

`...

...`

创建一个好的异步方法有两个基本规则。第一条规则是将任务分成小的子任务,这些子任务只需要很短的时间就能完成。第二个规则是一次只安排一个子任务。如果你偏离了其中任何一条规则,那么你最终会得到一个没有响应的应用创建和管理一个Promise所涉及的费用。

创建子任务的最佳方式会因所做工作的种类而异。对于我的例子,我只需要在较小的块中执行计算,每个块都通过调用清单中的内联calcBlock函数来处理。

我已经使用setImmediate方法安排了我的子任务,这被定义为 IE10 对 JavaScript 支持的一部分。这是一种相对较新的方法,旨在补充常用的setTimeout。当您将一个函数传递给setImmediate时,您要求它在所有未决事件和 UI 更新完成后立即执行。

您需要使用子任务的原因是,一旦 JavaScript 运行时开始执行您的函数,任何新的事件和 UI 更新都会建立起来。通过将工作分解成子任务,并仅在每个任务完成时调用setImmediate,您给了 JavaScript 运行时一个清除事件和更新积压的机会。在完成执行一个子任务和开始下一个子任务之间,运行时能够响应用户输入并保持应用响应。

因为我已经将工作分解成子任务,所以我利用这个机会在每组计算结束时调用fProgress函数来向任何感兴趣的听众报告进度。您可以看到,我在我的then调用中添加了第三个函数来接收和显示这些信息。

WINDOWS 应用中的并行处理

如果你密切关注了这一章,你会注意到我没有使用平行这个词。JavaScript 在单线程运行时中执行,这就是为什么没有关键字来确保原子更新或创建关键部分,就像在 C#和 Java 等语言中一样。当你创建一个异步方法并实现后台任务时,你并没有创建一个新线程;相反,您只是简单地推迟任务,直到主(也是唯一的)线程能够并且愿意执行它。

然而,有可能用 JavaScript 创建真正的并行应用,不同的任务由不同的线程同时执行。一种方法是构建用本机代码编写的异步功能。当我使用一个XMLHttpRequest对象发出 Ajax 请求时,您已经看到了这样一个例子。XMLHttpRequest对象是浏览器的一部分,能够创建和管理多个并发请求。这种并行性对 JavaScript 代码是隐藏的,回调通知被封送到主 JavaScript 线程进行简单处理。Windows API 也是用本机代码编写的,您的调用通常会导致多线程的创建和执行,即使作为 JavaScript 程序员,您并不知道这种复杂性。

如果你想用 JavaScript 创建一个真正的并行应用,那么你应该看看 Web Workers 规范。这是与 HTML5 相关的规范之一,它受 IE10 支持。创建和维护 Web workers 的成本相对较高,这意味着它们只适合于长期任务,应该谨慎使用。我在本书中没有深入讨论 Web Workers 规范,因为它不是一个特定于应用的特性,但是你可以在[msdn.microsoft.com/en-us/library/windows/apps/hh767434.aspx](http://msdn.microsoft.com/en-us/library/windows/apps/hh767434.aspx)阅读更多关于 IE10 支持的内容。

实施取消

您不必在自己的定制中实现对取消的支持,但是这样做是一个好主意,尤其是对于长期任务或资源密集型任务。

这是因为调用cancel方法将触发错误回调来通知取消,即使您的Promise不支持取消。这意味着你的Promise将继续调度工作(并消耗资源),即使回调函数已经被调用,应用已经继续运行。作为最后的侮辱,你的任务结果将被悄悄地丢弃。

要实现取消,您需要向Promise构造函数传递第二个函数。如果在Promise对象上调用了cancel方法,那么你的函数将被执行。你可以看到我是如何在清单 9-21 中的例子中添加取消支持的。

清单 9-21 。在自定义承诺中增加取消支持

`

                                      body {             display: -ms-flexbox; -ms-flex-direction: column;             -ms-flex-align: center; -ms-flex-pack: center;                     }         body, button { font-size: 30pt; margin: 5px;}         #output { margin: 20px; }          

                        if (blockcount == blocks) {                             fDone(total); **                        } else if (!canceled) {**                             fProgress(blockcount * 2);                             setImmediate(function () {                                 calcBlock(start + blocksize, ++blockcount, blocksize)                             });                         }                     };

                    setImmediate(function () {                         calcBlock(0, 1, count / blocks), 1000                     });                 } **            }, function () {** **                canceled = true;** **            });**         };

        var promise;

        WinJS.Application.onactivated = function (args) {             WinJS.Utilities.query("button").listen("click", function (e) {                 if (this.innerText == "Go") {                     displayMessage("Starting");

                    promise = calculateSum(5000000)

                    promise.then(function (total) {                         displayMessage("Done: " + total);                     }, function (err) {                         displayMessage("Error: " + err.message);                     }, function (progress) {                         displayMessage("Progress: " + progress + "%");                     }); **                } else {** **                    if (promise != null) {** **                        promise.cancel();** **                    }** **                }**             });         };

        WinJS.Application.start();     

    Go **    Cancel**     
        Output will appear here     
`

我在 HTML 的default.html中添加了一个Cancel按钮,点击这个按钮会调用在点击Go按钮时创建的Promise的取消方法。注意,我很小心地取消了由calculateSum函数返回的Promise,而不是由then方法返回的Promise——重要的是取消正在工作的Promise,而不是随后将被启动的Promise

images 提示注意,你不必经常检查取消。我发现在安排下一个子任务之前执行检查是一种合理的方法,在响应性和复杂性之间取得了良好的平衡。

创造合成承诺

您会发现,WinJS.Promise对象在 Windows APIs 中被广泛使用,有时您需要创建一个Promise,作为您已经拥有的数据值的包装器。在这种情况下,WinJS.Promise对象定义了一些有用的方法。我已经在表 9-3 中描述了这些方法,并在下面的章节中演示了两个最有用的方法。

images

我已经更新了CustomPromise应用,这样就有一个函数接受一个Promise并使用then方法来设置将向用户显示信息的回调。你可以在清单 9-22 的中看到这个例子的script元素(HTML 和 CSS 没有改变)。

清单 9-22 。创建一个接受承诺的函数

`...

...`

在这种安排下,如果我想显示不在Promise中的数据,我会受到限制。当然,我可以重写函数使之更加灵活,但是这并不总是可能的,尤其是在使用别人的代码时。解决方案是创建一个Promise,它的唯一目的是返回一个值或错误。这样的Promise没有异步方面,这就是为什么它们被称为合成 Promise的原因

写下你自己的承诺

理解这是如何工作的最好方法是从编写你自己的合成Promise开始。对于这个例子,我想在我的代码中处理两种新的情况。如果用户在点击Go按钮之前点击Cancel按钮,我想显示一个错误,我想优化应用,这样如果我已经知道结果,我就不会执行计算(即,如果用户按顺序点击Go按钮两次)。你可以在清单 9-23 的中看到我需要添加的script元素。

清单 9-23 。创建自定义合成承诺对象

`...

...`

我在这个清单中创建的Promise只返回一个结果——没有任务,也没有对setImmediate的调用。在每种情况下,我只是调用其中一个函数来指示我的Promise完成了或者遇到了错误。

使用包装方法

我在上一节中创建的合成Promise对象的问题是,WinJS API 不知道我只是将它们用作适配器,所以我可以使用displayResults函数。当一个Promise被创建时,有许多管道要设置,我承担了设置所有东西的成本,只是为了稍后丢弃它。为了解决这个问题,Promise对象定义了wrapwrapError方法,它们创建轻量级的Promise对象。这意味着它们被明确地用作适配器,并且创建起来不那么复杂和昂贵。当您想要创建一个满足预定结果的Promise时,您可以使用wrap方法;当您想要打包一个错误消息时,您可以使用wrapError方法。清单 9-24 展示了这些方法在示例应用中的应用。

清单 9-24 。使用 wrap 和 wrapError 方法

`...

...`

你应该优先使用wrapwrapError方法来创建你自己的合成Promise物体。它们不仅更便宜,而且代码更简单,因此更容易阅读和维护。

总结

在这一章中,我已经向你展示了WinJS.Promise对象背后的细节。WinJS 和 Windows APIs 中有许多异步方法,对它们的机制有很好的理解对于编写复杂的应用是必不可少的。重要的是要记住,Windows 应用中的 JavaScript 代码是在单线程上执行的,同时还有事件处理程序和 UI 更新。当您创建自定义异步方法时,您管理的是单线程上的工作调度,而不是创建多个并行线程。如果您在应用中遇到异步操作的问题,通常是因为您将代码视为多线程。在本书的第三部分中,我向你展示了 WinJS UI 控件,你可以用它来增强你的应用,并创建一个与其他 Windows 应用一致的外观。

十、创建 UI 控件示例框架

在本书这一部分的章节中,我向您展示了 WinJS UI 控件。这些是 WinJS UI 的重要组成部分,也是我在示例应用中一直使用的标准 HTML 元素的有益补充。这些控件不仅为用户提供了更丰富的体验,还提供了 Metro 应用的部分独特外观。

有各种各样的控制,我把它们都展示给你。每个控件都有许多改变其外观和行为的配置选项,为了尽可能容易地理解这些功能,我希望能够通过一个实例向您展示它们对控件的影响,而不仅仅是描述它们。

演示每个控件所需的标记和代码量很大,单独处理每个控件将需要为每一章列出无尽的页面,这对于我来说没有吸引力,对于您来说也没有吸引力。

相反,我已经构建了一个框架,可以用来简单明了地生成每个 UI 控件所需的示例,我将在接下来的章节中使用这个框架。在这一章中,我将介绍这个框架,并向您展示它是如何运作的,这样您就会理解我提供的较小的列表意味着什么。这种方法的一个好处是,我已经使用了我在前面章节中描述的相同的 WinJS 特性和技术来创建这个示例,因此您可以看到如何将它们应用到更重要的应用中。

images 注意你不需要详细阅读本章来理解后面的章节和它们包含的 WinJS UI 控件的描述。我已经包括了这一章,所以你可以看到我是如何创建这些例子的。这一章有很多代码和标记,可能会很难,所以你可能想浏览一下这些材料,并在你构建了最初的几个 Metro 应用后返回来更深入地阅读。

了解最终应用

如果你能看到我想要的结果,这将有助于理解应用。我的目标是向您展示一个简单的初始布局,带有一个包含每个 UI 控件命令的导航栏。你可以在图 10-1 中看到这个初始布局,它显示了导航条命令。

images

***图 10-1。*应用和导航条的初始布局只需一个命令

每个 NavBar 命令都有一个按钮,点击它会产生一个包含两个面板的页面。左侧面板将包含我正在演示的 UI 控件。右面板将包含其他 UI 控件,您可以使用这些控件来更改左面板中控件的设置。你可以在图 10-2 的中看到一个例子,它展示了我如何演示FlipView控件(我在第十四章的中描述了它)。

images

***图 10-2。*flip view 控件的显示

右侧面板中的每个控件都允许您查看或更改左侧面板中控件的属性。在创建示例框架时,我的目标是能够尽可能简洁地生成这种标记和驱动它的代码。在图 10-2 中,你可以看到我需要生成的不同类型的控件:

  • The select element allows the user to choose from a fixed range of values.
  • The input element allows the user to enter unconstrained values.
  • The span element displays a read-only value to the user.
  • A set of button elements allows users to perform operations.
  • The ToggleSwitch control lets the user select the boolean value.

ToggleSwitch控件是 WinJS UI 控件之一,我在第十一章中描述了它。你可能想读完第十一章然后回到这里,但是我在这一章的重点是生成我需要的标记,我不会深入控件的细节。

images 注意虽然总体结果比冗长重复的清单更简洁,但框架本身相当复杂,这一章包含了大量代码,其中大部分与处理模板有关,我在第八章的中描述过。

创建基本项目结构

首先,我将创建应用的基本结构,以便向用户显示初始消息,并且导航条已经就位。我还将添加导航机制,并将用于视图模型的代码文件放在适当的位置,并保存我需要的每组配置控件的详细信息列表。我首先使用Blank App模板创建一个新的 Visual Studio 项目调用UIControls,并对 default.html 文件做一些基本的添加,如清单 10-1 所示。

清单 10-1。来自 UIControls 项目的初始 default.html 文件

`

         UIControls

                   

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

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

**    

** **        

Select a Control from the NavBar

** **    
**

**    <div id="navbar" data-win-control="WinJS.UI.AppBar"** **        data-win-options="{placement:'top'}">**

`

我添加的script元素指的是我稍后将添加的代码文件。

templates.js文件将包含使用 WinJS 模板生成元素所需的代码。这些是我在第八章中介绍的同类模板,我将在default.html文件中定义它们。事实上,您已经可以看到清单中的第一个模板——其idnavBarCommandTemplate的元素将用于为 NavBar 生成命令,允许用户导航到应用中的内容页面,每个页面将展示一个 WinJS UI 控件。

controls.js文件将包含我需要生成的配置控件的细节,以便演示每个 WinJS UI 控件。viewmodel.js文件将包含我需要的其他部分,比如绑定值转换器和 WinJS UI 控件的数据集,这些控件是由数据驱动的。

添加模板生成代码

我的下一步是创建js/templates.js文件,并用生成 NavBar 命令所需的代码填充它。你可以在清单 10-2 的中看到这个文件的初始版本,我将在整个章节中添加这个文件,为创建我需要的不同种类的配置控件添加支持。

清单 10-2 。templates.js 文件的初始版本

`(function () {

    var navBarCommands = [           { name: "AppTest", icon: "target" },           { name: "ToggleSwitch", icon: "\u0031" },           { name: "Rating", icon: "\u0032" },           { name: "Tooltip", icon: "\u0033" },           { name: "TimePicker", icon: "\u0034" },           { name: "DatePicker", icon: "\u0035" },           { name: "Flyout", icon: "\u0036" },           { name: "Menu", icon: "\u0037" },           { name: "MessageDialog", icon: "\u0038" },           { name: "FlipView", icon: "pictures" },           { name: "ListView", icon: "list" },           { name: "SemanticZoom", icon: "zoom" },           { name: "ListContext", icon: "list" },     ];

    WinJS.Namespace.define("Templates", {

        generateNavBarCommands: function (navBarElement, templateElement) {             navBarCommands.forEach(function (commandItem) {                 templateElement.winControl.render(commandItem, navBarElement);             });         },

    }); })();`

navBarCommands数组包含我想要创建的每个命令的细节。数组中的每一项都有nameicon属性,我在default.html文件中定义的navBarCommandTemplate模板中使用了这些属性。

当我创建完框架后,我将删除其中的第一项,即 name 属性为AppTest的项。我添加它只是为了在我创建示例框架时演示它,在本书这一部分的其余章节中不会用到它。

我使用了WinJS.Namespace.define方法来创建一个名为Templates的新名称空间。这个名称空间将包含我需要从我在default.html文件中定义的模板生成元素的函数。开始只有一个函数,它对应于我已经定义的单个模板。generateNavBarCommands函数有两个参数:第一个参数是 NavBar 元素,生成的命令元素将插入其中,第二个参数是用来生成这些元素的模板。该函数枚举navBarCommands数组中的元素,并使用WinJS.Binding.Template.render方法从数组项中生成元素,并将它们插入到 NavBar 元素中。

了解 WINCONTROL 属性

当我向你展示如何使用模板时,我在第八章中介绍了winControl属性,但是它是 WinJS 的一个更普遍的特征,并且当涉及到 UI 控件时特别重要。您很快就会看到,WinJS UI 控件被应用于常规的 HTML 元素,最典型的是div元素。WinJS 通过向底层元素添加子元素、CSS 样式和事件侦听器来创建控件,这是一种由 jQuery UI 等 web 应用 UI 库共享的技术。

当 WinJS 创建控件时,它会将winControl属性添加到代表底层元素的HTMLElement对象中,并将该属性的值设置为来自WinJS.UI命名空间的对象。从winControl属性中获取的对象让您可以访问由WinJS.UI对象定义的属性、方法和事件,您可以使用它们来配置控件或响应用户交互。在本章和后面的章节中,你会看到我经常使用winControl属性来设置和管理我创建的 WinJS 控件。

WinJS.Binding.Template对象遵循相同的模式。我通过将data-win-control attribute设置为我想要创建的控件的名称来创建一个模板,在本例中是WinJS.Binding.Template。模板没有可视组件,但是 WinJS 仍然创建控件并设置winControl属性。WinJS.Binding.Template控件定义了 render 方法,因此为了访问这个方法,我找到了具有 data -win-control属性的元素,并对由winControl属性返回的对象调用了render方法。这是您将在所有 WinJS 控件中看到的模式。

添加导航码

/js/default.js文件中,我添加了处理WinJS.Navigation.navigating事件的代码,并注册了一个来自导航条的click事件的监听器函数。我还调用了ViewModel.Templates.generateNavBarCommands方法来使用命令填充 NavBar,这些命令将应用导航到各个 UI 控件的内容页面(尽管我还没有创建这些文件,因此单击 NavBar 命令会导致错误)。你可以在清单 10-3 中的文件中看到代码。

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

`(function () {     "use strict";

    var app = WinJS.Application;     window.$ = WinJS.Utilities.query;     WinJS.Binding.optimizeBindingReferences = true;

    WinJS.Navigation.addEventListener("navigating", function (e) {         WinJS.UI.Animation.exitPage(contentTarget.children).then(function () {             WinJS.Utilities.empty(contentTarget);             WinJS.UI.Pages.render(e.detail.location, contentTarget)                 .then(function () {                     return WinJS.Binding.processAll(contentTarget, ViewModel.State)                         .then(function () {                             return WinJS.UI.Animation.enterPage(contentTarget.children)                         });                 });         });     });

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

            Templates.generateNavBarCommands(navbar, navBarCommandTemplate);

            navbar.addEventListener("click", function (e) {                 var navTarget = "pages/" + e.target.winControl.label + ".html";                 WinJS.Navigation.navigate(navTarget);                 navbar.winControl.hide();             });         })

        //.then(function() {         //    return WinJS.Navigation.navigate("pages/AppTest.html");         //})     };

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

navigating事件的处理程序使用WinJS.UI.Animation名称空间来制作从一个内容页面到另一个内容页面的动画,我在第十八章的中描述了这个名称空间。我之所以在这里使用它,是因为内容之间的转换可能太快而无法注意到,而且动画有助于将用户的注意力吸引到内容已经改变的事实上。

NavBar 的click事件处理程序从所使用的命令按钮(通过winControl属性)获取label属性的值,并使用该值导航到pages目录中相应的 HTML 文件(我将很快创建该文件)。这遵循了我在第五章中介绍的相同的导航和内容管理模式,从那以后我已经在几个示例应用中使用过了。

default.html文件中的 JavaScript 非常简单,因为繁重的工作将由我添加到templates.jsviewmodel.js文件中的代码来完成。对于这个应用,default.html 文件只负责设置导航和导航条。

images 提示您会注意到清单中有一些语句被注释掉了。这些是在应用首次加载时自动显示给定内容页面的有用快捷方式,这在您测试和调试示例应用中的页面时非常有用(否则您必须使用 NavBar,如果您像我一样使用短暂的代码然后测试周期,这很快就会变得令人厌倦)。

添加其他 JavaScript 文件

在开始添加内容页面之前,我想让应用的基本结构就位,所以我现在将添加viewmodel.js文件和controls.js文件,尽管它们将只包含创建名称空间的代码。清单 10-4 显示了js/controls.js文件的内容,它创建了一个名为App.Controls的名称空间。我将使用这个名称空间来包含我需要为每个内容页面生成的控件的细节。

***清单 10-4。*controls . js 文件的内容

`(function () {

    WinJS.Namespace.define("App.Controls", {         // ...details of configuration controls will go here     });

})();`

初始版本的js/viewmodel.js文件如清单 10-5 所示,目前它只是创建了一个名为ViewModel的名称空间。

清单 10-5 。js/viewmodel.js 文件的初始内容

`(function () {     "use strict";

    WinJS.Namespace.define("ViewModel", {         // ...code for view model will go here     });

})();`

在本章中我不会用到viewmodel.js文件,但是当我遇到一些更复杂的 WinJS UI 控件,比如FlipViewListView时,我会用到它(我在第十四章和第十五章中描述过)。

添加 CSS

css/default.css文件中,我已经定义了我将用来显示不同 UI 控件的通用样式。有些控件需要额外的 CSS,但是我会在向你展示控件如何工作的时候处理这个问题。您可以在清单 10-6 中看到default.css文件的内容。为了完整起见,我展示了这个文件,但是这些样式中没有新的技术,并且为了简单起见,我没有添加对响应应用视图或方向变化的支持。

清单 10-6 。css/default.css 文件的内容

`body {     background-color: #5A8463; display: -ms-flexbox; -ms-flex-direction: column;     -ms-flex-align: center; -ms-flex-pack: center; }

.inputContainer, .selectContainer, .spanContainer {     width: 100%;}

h2.controlTitle {     margin: 10px 0px; color: white; font-size: 20px; display: inline-block;     padding: 10px; font-weight: bolder; width: 140px;}

.controlPanel .win-toggleswitch { width: 90%; margin: 15px; padding-left: 20px;} .win-toggleswitch .win-title { color: white; font-size: 20px;}

div.flexContainer { display: -ms-flexbox;  -ms-flex-direction: row;     -ms-flex-align: stretch; -ms-flex-pack: center; }

.controlPanel {     display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: center;     -ms-flex-pack: center;    border: medium white solid;     margin: 10px; padding: 20px; min-width: 300px;}

[data-win-control="WinJS.UI.ToggleSwitch"] {     border: thin solid white; margin: 5px; padding: 10px;width: 250px;}

input.controlInput, select.controlSelect, span.controlSpan  {     width: 150px;display: inline-block; margin-left: 20px;}

span.controlSpan {     white-space: nowrap; text-overflow: ellipsis; overflow: hidden; font-size: 14pt;}

div.buttonContainer button {margin: 5px; font-size: 16pt;}

.textPara { display: inline-block; width: 200px; font-size: 18pt;} .win-tooltip { background-color: #8ED09C; color: white;     border: medium solid white; font-size: 16pt;}`

如果你此时运行应用,你会看到类似于图 10-3 的东西。导航栏在图中显示为两行,因为标准 Visual Studio 模拟器分辨率的按钮太多了——如果模拟器设置为更大的分辨率(我在第六章的中描述了这一点),你就不会有这个问题。在这一章的最后,我将把AppTest按钮从导航条上移除,这将把命令放在一行上。AppTest按钮将导航到pages/Apptest.html文件,我只使用它来开发和演示示例框架应用。

images

***图 10-3。*示例 app 的初始状态

创建测试内容页面

应用的基本结构已经就绪,这意味着我可以将注意力转向创建测试内容页面和填充它所需的代码。同样,向您展示完成的内容,然后再向您展示我是如何创建的,会更容易。在图 10-4 中可以看到点击导航栏上AppTest按钮的结果。

images

***图 10-4。*点击导航栏上的 AppTest 按钮时显示的完成内容

在接下来的几节中,我将带您了解我用来创建这些内容的过程以及生成大量内容的代码。

创建内容页面

单击其中一个导航栏按钮会从项目pages文件夹中加载一页内容,所以我的第一步是使用解决方案浏览器实际创建pages文件夹。然后我可以添加AppTest.html文件,这个文件将在点击导航栏上的AppTest按钮时被加载。你可以在清单 10-7 中看到这个文件的初始内容。

清单 10-7AppTest.html的初始内容

`

              

        

                     

        

    

`

这个文件显示了标准模式,我将遵循这个模式来演示每个 WinJS UI 控件。文件中的 HTML 标记包含两个div元素。第一个包含我正在演示的 UI 控件。为了保持本章的简单,我将使用常规的 HTML input元素,而不是 WinJS UI 控件之一——这将允许我专注于我正在构建的框架,而不必深入 WinJS 控件的细节。对于这个例子,我给input元素分配了一个inputElemid值,如下所示:

`...

    
...`

另一个div元素将包含允许用户配置正在演示的控件的元素。这是本章的主要焦点——描述我需要的元素并从模板中生成它们的过程,这样我就不必在的后续章节中重复大量的标记和代码。目标div元素的idrightPanel,因为它是布局中最右边的面板而得名:

`...

...`

内容页面最重要的部分是包含在script元素中的 JavaScript。我使用第五章的中的 WinJS Pages 特性来注册一个ready函数,该函数将在加载内容页面时执行。关键语句是对Templates.createControls方法的调用,如下所示:

... Templates.createControls(rightPanel, inputElem, "apptest"); ...

这是生成我需要的配置元素的方法。此方法的参数是:

  • Element, which will contain the created configuration control.
  • UI control element being demonstrated
  • A key that identifies the set of controls to be generated.

对于AppTest.html文件,目标元素是idrightPanel的元素。input元素是正在演示的元素,我使用的键是apptest。在下一节中,您将看到如何使用这些值。

创建模板系统

现在我已经有了一个要处理的测试内容页面,我可以开始构建代码来生成我需要的元素,以演示input元素的一些特性,正如您所记得的,它是一个真正的 WinJS UI 控件的简单替代。在接下来的小节中,我将向您展示我如何描述所需的配置元素集,以及我创建它们的方法。

我将从创建一个配置元素开始。它将是一个select元素,可用于禁用或启用input元素。你可以在图 10-5 中看到这个选择元素创建后的样子。它非常简单,是我开始描述代码不同部分的好地方。

images

***图 10-5。*生成选择元素来配置输入元素

描述一个选择元素

我必须从描述我想要生成的select元素开始。我在/js/controls.js文件中这样做,你可以看到我在清单 10-8 中添加的内容。select 元素有两个选项——NoYes,,当选择No值时,它们将把input元素的disabled属性的值设置为空字符串(""),当选择Yes值时,它们将设置为disabled

images 注意我花了这么多时间来生成一个简单的可以用四行 HTML 标记编写的select元素,这可能有点奇怪。我试图解决的问题是,我有许多这些select元素要生成,我不想在本书这一部分的其他章节中一遍又一遍地列出本质上相同的标记。此外,您很快就会看到,我不仅仅是生成元素,我还创建了允许配置元素在我演示的 UI 控件上操作的事件处理程序,这是另一个非常重复的代码块,我不必在每章中列出。总的来说,在本章中致力于创建框架可以让我在接下来的章节中花更多的时间关注 WinJS UI 控件和它们的特性。

清单 10-8 。选择元素的初始定义

`(function () {

    WinJS.Namespace.define("App.Controls", {

        apptest: [{             type: "select",             id: "disabled",             title: "Disabled",             values: ["", "disabled"],             labels: ["No", "Yes"]         }]     });

})();`

你可以看到我已经在App.Controls名称空间中创建了一个名为apptest的数组——这是从AppTest.html文件传递给Templates.createControls方法的键。这个数组将包含一系列的定义对象,它们描述了我需要创建的每个配置元素。目前只有一个对象,它描述了你在图 10-4 中看到的select元素。

images 提示在后面的章节中,我将更简洁地列出定义对象。在这个例子中,我使用了在自己的行上显示每个属性,以便于理解这些对象是如何工作的。

我将很快解释对象中的每一个属性,但是首先我将把我用来生成select元素的模板添加到default.html文件中,如清单 10-9 所示。在本章中,我将为我在这个框架中支持的每种配置元素添加一个模板。

清单 10-9 。向 default.html 文件添加用于生成选择元素的模板

`...

` `
                        

**    

** **        
** **            

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

    

        

Select a Control from the NavBar

    

    <div id="navbar" data-win-control="WinJS.UI.AppBar"         data-win-options="{placement:'top'}">

...`

我将使用的模板结构将帮助我解释清单 10-8 中定义对象的属性的用途,我已经在表 10-1 中列出了这些属性。

images 提示此表解释了选择元素的定义对象的属性的含义。定义对象的每个类型都有一些额外的或不同的属性,我将在本章后面生成这类元素时解释这些属性。

images

如果你回头看清单 10-8 ,你可以看到我分配给属性的值是如何与图 10-4 中显示的结果相对应的。type 属性设置为select,表示我想要一个select元素;id 属性设置为 disabled,对应的是我要管理的input元素属性;title 属性被设置为Disabled,这样用户就知道改变配置元素会对正在演示的 UI 控件产生什么影响。

最后,我使用了values labels属性,因为input.disabled属性允许的值不适合显示给用户。通过为这两个属性提供值,我在呈现给用户的内容和分配给 UI 控件属性的值之间创建了一个映射。

添加元素生成代码

我有了select元素的定义,也有了生成select元素的模板。现在我所需要的是将这些结合在一起的代码——我已经将这样做的结构添加到了/js/templates.js文件中,如清单 10-10 所示。

清单 10-10 。使用 templates.js 文件中的定义和模板创建元素的代码

`(function () {

var navBarCommands = [         { name: "AppTest", icon: "target" }, ];

WinJS.Namespace.define("Templates", {

    generateNavBarCommands: function (navBarElement, templateElement) {         navBarCommands.forEach(function (commandItem) {             templateElement.winControl.render(commandItem, navBarElement);         });     },

**    createControls: function (container, uiControl, key) {** **        var promises = [];**

**        App.Controls[key].forEach(function (definition) {** **            var targetObject = uiControl.winControl ? uiControl.winControl : uiControl;** **            promises.push(Templates"create" + definition.type);** **        });**

**        return WinJS.Promise.join(promises).then(function () {** **            $("*[data-win-bind]", container).forEach(function (childElem) {** **                childElem.removeAttribute("data-win-bind");** **            });** **        });** **    },**

**    createtoggle: function (containerElem, definition, uiControl) {** **        // ...code to create ToggleSwitch element will go here** **    },**

**    createinput: function (containerElem, definition, uiControl) {         // ...code to create input element will go here** **    },**

**    createselect: function (containerElem, definition, uiControl) {** **        // ...code to create select element will go here**

**    },**

**    createspan: function (containerElem, definition, uiControl) {** **        // ...code to create span element will go here** **    },**

**    createbuttons: function (containerElem, definition, uiControl) {** **        // ...code to create button elements will go here** **    }** });

})();`

我对这个文件所做的添加是以createControls方法为中心的,我从AppTest.html文件中调用这个方法来创建我想要的配置控件。createControls方法比看起来要简单,但是它是我构建这个框架所采用的方法的核心,所以我将向您详细介绍它,并将代码的工作方式与我在上一节中描述的示例select元素联系起来。

该方法的参数有:创建配置控件时应该放入的容器,将要由新创建的控件配置的 UI 控件,以及引用controls.js文件中定义对象集的键。对于select元素,这些参数将是来自AppTest.html文件中标记的rightPanel元素和input元素以及值apptest,它对应于包含我在清单 10-8 中展示的select元素定义的数组。

处理定义对象

我使用我收到的键从App.Controls名称空间获得关联的定义对象的数组。目前只有一个键(apptest),我得到的数组只包含一个定义对象(对于我的select元素),但是我将在本章后面添加更多的定义(在后续章节中添加更多的键)。

我使用forEach方法来枚举数组中的项目。我做的第一件事是建立我正在工作的目标对象。在后面的章节中,我将使用 WinJS 控件,它们都定义了winControl属性(详见理解 winControl 属性侧栏),但是对于这一章,我将使用没有winControl属性的 input 元素。我是这样弄清楚我在做什么的:

... var targetObject = uiControl.winControl ? uiControl.winControl : uiControl; ...

这很重要,因为我创建了事件处理程序,当我创建配置元素时,它将应用更改目标对象的属性值。当我使用 WinJS 控件时,我想操作控件,而不是应用它的底层 HTML 元素。在这一章中,没有 WinJS 控件,HTML 元素是我必须使用的全部。

确定了我的目标之后,我基于当前定义对象的 type 属性值,调用了Templates名称空间中的其他方法之一,如下所示:

... promises.push(Templates"create" + definition.type); ...

目前我只有一个定义对象,它的 type 属性的值是 select,这意味着将调用Templates.createSelect方法。名称空间中的其他方法负责创建一种元素,并被传递给应该插入元素的容器、当前定义对象和目标对象。我将很快实现其中的第一个方法来演示它们是如何工作的。

元素创建方法返回一个Promise,我将它添加到一个名为 promises 的数组中。我在promises数组上使用了Promise.join方法(我在第九章中描述过)来创建一个Promise,当所有的单个元素都被创建、添加到容器元素并配置好之后,这个方法就完成了。

清理结果

createObjects方法中的最后一步是使用then方法指定一个函数,当所有的单个Promise对象都被满足时,这个函数将被执行。在这个函数中,我从所有拥有属性的元素中移除了属性data-win-bind。当我介绍FlipView元素时,我将详细解释为什么这是一件有用的事情第十四章,但简短的版本是,如果在生成的elements添加到文档后调用WinJS.Binding.processAll方法,将data-win-bind属性留在已生成的元素上会导致问题。通过移除属性,我确保这种情况不会发生。

生成一个选择元素

现在一切就绪,我可以生成我的 select 元素了,我将通过在templates.js文件中实现createselect方法来实现它。你可以在清单 10-11 中看到我是如何做到这一点的。

清单 10-11 。生成选择元素

`... createselect: function (containerElem, definition, uiControl) {     return selectTemplate.winControl.render(definition).then(function (newElem) {

        var selectElem = WinJS.Utilities.query("select", newElem)[0];         selectElem.id = definition.id;         definition.values.forEach(function (value, index) {             var option = selectElem.appendChild(document.createElement("option"));             option.innerText = definition.labels ? definition.labels[index] : value;             option.value = value;         });

        selectElem.addEventListener("change", function (e) {             setImmediate(function () {                 uiControl[definition.id] =                    selectElem.options[selectElem.selectedIndex].value;             });         });         containerElem.appendChild(newElem.removeChild(newElem.children[0]));         uiControl[definition.id] = selectElem.options[0].value;     }); }, ...`

由于这是我生成的第一种类型的元素,我将逐步遍历代码,并解释我如何创建结果。

渲染模板

我首先使用添加到default.html文件中的模板来生成一组新的元素,如下所示:

... createselect: function (containerElem, definition, uiControl) {     return selectTemplate.winControl.render(definition).then(function (newElem) {        // *...code removed for brevity...* }, ...

当你在一个只有一个参数的WinJS.Binding.Template对象上调用render方法时,你得到的Promise产生了当它被实现时已经被创建的元素,并且这些元素没有被插入到应用布局中。在这个清单中,我将定义对象从controls.js文件传递给render方法,这允许定义中包含的细节用于填充模板。提醒一下,下面是我正在使用的模板(我已经使用id属性值在代码中找到了它):

`...

    
        

             
...`

WinJS.Binding.Template对象完成元素的渲染后,我就有了一些从模板生成的分离元素。分离的元素还不是应用内容的一部分,这些元素如下所示:


`

    
        

Disabled

                      

`
配置&填充选择元素

此时,我已经拥有了我需要的元素,但是它们只是部分完成。我的下一步是完成 select 元素并添加代表用户可以做出的选择的option元素,如下所示:

`... createselect: function (containerElem, definition, uiControl) {     return selectTemplate.winControl.render(definition).then(function (newElem) {

**        var selectElem = WinJS.Utilities.query("select", newElem)[0];** **        selectElem.id = definition.id;** **        definition.values.forEach(function (value, index) {** **            var option = selectElem.appendChild(document.createElement("option"));** **            option.innerText = definition.labels ? definition.labels[index] : value;** **            option.value = value;** **        });**

        // ...code removed for brevity...     }); }, ...`

我使用WinJS.Utilities.query方法定位从render方法传递给我的函数的元素集中的select元素。

我的第一个动作是将id属性设置为由定义对象指定的值。然后,我使用定义对象中的valueslabels数组来创建一系列的option元素,我将它们作为子元素添加到 select 元素中。这给了我下面的 HTML:


`

    
        

Disabled

        <select class="controlSelect" id="disabled"> **            No** **            Yes**              

`
创建事件处理程序

当用户在select元素中选择option的 on 选项时,我想更新目标对象的属性值。为了确保这一点,我使用addEventListener方法为change事件注册一个处理函数,如下所示:

`... createselect: function (containerElem, definition, uiControl) {     return selectTemplate.winControl.render(definition).then(function (newElem) {

        var selectElem = WinJS.Utilities.query("select", newElem)[0];

        // ...code removed for brevity... **        selectElem.addEventListener("change", function (e) {** **            setImmediate(function () {** **                uiControl[definition.id] =** **                    selectElem.options[selectElem.selectedIndex].value;** **            });** **        });**

        // ...code removed for brevity...     }); }, ...`

change事件被触发时,我更新目标对象的属性以匹配从select元素中选取的值。请注意,我使用了setImmediate方法来延迟属性更改——这允许选择元素在 UI 控件的属性更改之前完成向新选择的值的转换。如果不调用setImmediate,应用会暂时没有响应,因为控件的属性更改会在select元素完成响应用户选择值之前执行。

整理完毕

当我从模板中生成元素时,我最终得到了一个我不想添加到应用布局中的外部div元素。为此,在我设置了事件处理程序之后,我选择了第一个子元素,并使用传递给该方法的container参数将其添加到应用布局中,如下所示:

`... createselect: function (containerElem, definition, uiControl) {     return selectTemplate.winControl.render(definition).then(function (newElem) {

        // ...code removed for brevity...

**        containerElem.appendChild(newElem.removeChild(newElem.children[0]));** **        uiControl[definition.id] = selectElem.options[0].value;**     }); }, ...`

最后一步是设置 UI 控件的属性,以匹配选择菜单的初始值,这确保了选择控件和所演示的 UI 控件的状态是同步的。

您可以通过启动应用并从导航栏中选择AppTest命令来测试这些附加功能。您将能够通过从右侧面板的select元素中选取值来启用和禁用左侧面板中的input元素。

使用代理对象

不是所有我想在后面章节中展示的特性都可以简单地通过设置属性值来演示。在这些情况下,我需要使用一个代理对象,这样我就可以以一种有用的方式响应对配置元素所做的更改。在这一节中,我将向您展示我是如何将这个特性添加到示例框架中的。

添加定义对象

首先,我将向controls.js文件添加一个新的定义对象,这将提供一个配置选项,它不能被转换成简单的属性更改。你可以在清单 10-12 中看到这个新定义。

清单 10-12 。向 controls.js 文件添加新的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

    apptest: [         { type: "select", id: "disabled", title: "Disabled",             values: ["", "disabled"], labels: ["No", "Yes"] }, **        { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],** **              useProxy: true },**     ] });

})();`

这个定义指定了另一个select元素,带有SmallBig选项。重要的添加是useProxy属性,我已经将它设置为true。这将表明,当用户从select元素中选择一个option时,新值应该应用于代理,而不是直接应用于正在演示的 UI 控件。

创建代理对象

我在内容页面的script元素中创建代理对象,如清单 10-13 中的所示,它展示了我对AppTest.html文件所做的添加。我创建了一个名为proxyObject的可观察对象,并将其作为参数传递给createControls方法。我使用WinJS.Binding.as方法创建可观察对象,我在第八章的中描述过。

清单 10-13 。向 AppTest.html 文件添加代理对象

`

         

                Templates.createControls(rightPanel, inputElem, "apptest", proxyObject);             }         });     

    

        

                     

        

    

`
检测和使用代理对象

接下来,我需要更新templates.js文件中的createControls方法,以便它可以接收代理对象,并在定义对象需要时使用它。你可以在清单 10-14 中看到我为此所做的修改。

清单 10-14 。在 createControls 方法中添加对代理对象的支持

`... createControls: function (container, uiControl, key, proxy) {     var promises = [];

    App.Controls[key].forEach(function (definition) { **        var targetObject = definition.useProxy ? proxy : uiControl.winControl ?** **            uiControl.winControl : uiControl;**         promises.push(Templates"create" + definition.type);     });

    return WinJS.Promise.join(promises).then(function () {         $("*[data-win-bind]", container).forEach(function (childElem) {             childElem.removeAttribute("data-win-bind");         });     }); }, ...`

更改相对简单——我只需扩展为配置控制更改选择目标的语句,以考虑代理对象。

这种添加的效果是,当定义对象指定应该使用代理对象时,从模板生成的select元素的事件处理程序将更改代理上指定属性的值,而不是winControl属性或 HTML 元素本身。

响应代理对象属性变化

最后一步是返回到AppTest.html文件中的 script 元素,并添加一些代码来监控可观察代理对象的变化。你可以在清单 10-15 中看到我是如何做到的。

清单 10-15 。观察代理对象的变化

`...

...`

当用户使用新的select元素选取一个值时,代理对象中的theme属性将会改变。我使用bind方法观察主题属性,我在第八章的中描述过,并改变两个 CSS 属性来创建不同的视觉效果。这是一个简单的演示,说明了我如何使用我的示例框架将配置元素与更复杂的 UI 控件特性绑定在一起——这是我将在接下来的章节中经常用到的。你可以在图 10-6 中看到选择BigSmall值的结果。

images

***图 10-6。*使用代理对象支持更复杂的配置

生成其他元素类型

现在,您已经看到了示例框架中的所有复杂性。剩下的工作就是为不同的元素类型添加剩余的模板,并在controls.js文件中实现使用它们的方法。在接下来的小节中,我将结束这个框架,并创建一个新的定义对象来演示其余类型的配置元素。本章的剩余部分没有新的技术,所以我将列出每种元素类型所需的模板和代码,并展示一个生成每种元素类型的定义对象的例子。

生成输入元素

我使用input元素来允许用户输入不受约束的值。您可以在清单 10-16 的中看到输入配置元素的定义对象。

***清单 10-16。*输入配置元素的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

    apptest: [         { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],             labels: ["No", "Yes"] },         { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],             useProxy: true }, **        { type: "input", id: "value", title: "Value", value: "Hello" },**     ] });

})();`

你可以在表 10-2 中看到该定义对象中属性的含义。

images

您可以从我用来生成清单 10-17 中的input元素的default.html文件中看到模板。

清单 10-17 。来自 default.html 文件的输入元素模板

`...

    
` `        

             
...`

您可以在清单 10-18 中的controls.js文件中看到createinput方法的实现。

***清单 10-18。*controls . js 文件中 createinput 方法的实现

... createinput: function (containerElem, definition, uiControl) {     return inputTemplate.winControl.render(definition).then(function (newElem) {         WinJS.Utilities.query("input", newElem).forEach(function (elem) {             elem.id = definition.id;             elem.addEventListener("change", function (e) {                 setImmediate(function () {                     uiControl[elem.id] = elem.value;                 });             });             uiControl[definition.id] = elem.value;                         });         containerElem.appendChild(newElem.removeChild(newElem.children[0]));     }); }, ...

您输入到我在本节中创建的input配置元素中的值更新了布局左侧面板中的input元素的值,这是 WinJS UI 控件将在后面章节中出现的位置。请注意,这种关系只是单向的——也就是说,在左侧面板的 input 元素中输入文本不会更新右侧面板中 input 元素的内容。

生成一个跨度元素

我使用span元素来显示 UI 控件的一些只读特性,通常是为了支持演示一些其他特性。我在生成span元素时没有创建事件监听器,而是从内容页面的script元素中更新内容。清单 10-19 显示了向controls.js文件添加一个span定义对象。

清单 10-19 。向 controls.js 文件添加 span 定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {     apptest: [         { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],             labels: ["No", "Yes"] },         { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],             useProxy: true },         { type: "input", id: "value", title: "Value", value: "Hello" }, **        { type: "span", id: "value", value: "", title: "Value" },**     ] });

})();`

你可以在表 10-3 中看到该定义对象中属性的含义。

images

您可以从 default.html 文件中看到我用来生成清单 10-20 中的元素的模板。

清单 10-20 。来自 default.html 文件的跨度模板

`...

    
        

             
...`

您可以在清单 10-21 中的templates.js文件中看到createinput方法的实现。

清单 10-21 。templates.js 文件中 createspan 方法的实现

... createspan: function (containerElem, definition, uiControl) {     return spanTemplate.winControl.render(definition).then(function (newElem) {         WinJS.Utilities.query("span", newElem).forEach(function (elem) {             elem.id = definition.id;         });         containerElem.appendChild(newElem.removeChild(newElem.children[0]));     }); }, ...

生成按钮元素

我使用button配置元素让用户触发某种动作——通常是从数据源中添加或删除项目,我会在第十四章的中向您介绍。您可以在清单 10-22 的中看到按钮元素的定义对象。

清单 10-22 。按钮元素的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

    apptest: [         { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],             labels: ["No", "Yes"] },         { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],            useProxy: true },         { type: "input", id: "value", title: "Value", value: "Hello" },         { type: "span", id: "value", value: "", title: "Value" }, **        { type: "buttons", title: "Buttons", labels: ["Add Item", "Delete Item"] },**     ] });

})();`

你可以在表 10-4 中看到该定义对象中属性的含义。

images

我不使用模板来生成按钮元素,我将事件处理程序留给内容页面,这意味着在templates.js文件中实现createbuttons方法,如清单 10-23 所示,特别简单。

清单 10-23 。templates.js 文件中 createbuttons 方法的实现

... createbuttons: function (containerElem, definition, uiControl) {     var newDiv = containerElem.appendChild(document.createElement("div"));     WinJS.Utilities.addClass(newDiv, "buttonContainer");     if (definition.title) {         var titleElem = newDiv.appendChild(document.createElement("h2"))         titleElem.innerText = definition.title;         WinJS.Utilities.addClass(titleElem, "controlTitle");     }     definition.labels.forEach(function (label) {         var button = newDiv.appendChild(document.createElement("button"));         button.innerText = label;     }); } ...

生成 ToggleSwitch 控件

WinJS.UI.ToggleSwitch控件让用户选择真/假值。我将在下一章详细演示这个控件,所以我不想在这一章讨论任何细节。我将按原样呈现定义对象、模板和代码,在您阅读完第十一章后,它们将变得有意义。你可以在清单 10-24 中看到一个ToggleSwitch控件的定义对象。

清单 10-24 。ToggleSwitch 控件的定义对象

`(function () {

WinJS.Namespace.define("App.Controls", {

    apptest: [         { type: "select", id: "disabled", title: "Disabled", values: ["", "disabled"],             labels: ["No", "Yes"] },         { type: "select", id: "theme", title: "Theme", values: ["Small", "Big"],              useProxy: true },         { type: "input", id: "value", title: "Value", value: "Hello" },         { type: "span", id: "value", value: "", title: "Value" },         { type: "buttons", title: "Buttons", labels: ["Add Item", "Delete Item"] }, **        { type: "toggle", id: "disabled", title: "Disabled", value: false},**     ] });

})();`

你可以在表 10-5 中看到该定义对象中属性的含义。

images

你可以在清单 10-25 中看到我用来创建ToggleSwitch控件的default.html文件中的模板。

清单 10-25 。生成 ToggleSwitch 控件的模板

`...

    
    
...`

最后,您可以在清单 10-26 中看到createtoggle方法的实现。我将在第十一章中解释我通过winControl属性设置的属性。

清单 10-26 。templates.js 文件中 createtoggle 方法的实现

... createtoggle: function (containerElem, definition, uiControl) {     return toggleSwitchTemplate.winControl.render(definition).then(function (newElem) {         var toggle = newElem.children[0];         toggle.id = definition.id;         if (definition.labelOn != undefined) {             toggle.winControl.labelOn = definition.labelOn;             toggle.winControl.labelOff = definition.labelOff;         }         toggle.addEventListener("change", function (e) {             setImmediate(function () {                 uiControl[definition.id] = toggle.winControl.checked;             });         });         containerElem.appendChild(newElem.removeChild(toggle));         uiControl[definition.id] = toggle.winControl.checked;     }); }, ...

如果您运行带有所有这些定义对象、模板和方法实现的示例应用,您将会看到如图图 10-4 所示的布局。

打扫卫生

现在剩下的就是从导航条上移除AppTest命令按钮,这样我在下一章就有了一个干净的项目。你可以在清单 10-27 中看到我对templates.js文件所做的最后修改。

清单 10-27 。从导航条上取下测试按钮

... var navBarCommands = [         **//**{ name: "AppTest", icon: "target" },         // *...other commands omitted for brevity...* ]; ...

总结

在这一章中,我已经解释了我是如何创建框架的,我将用它来演示本书这一部分的剩余章节中的 WinJS UI 控件。尽管这个框架本身解释起来相当冗长,但它让我避免了在每章的前十页列出大部分相同的代码和标记。这个项目还有一个额外的好处,就是演示了一些核心的 WinJS 特性如何以更复杂的方式组合起来,以创建更丰富的效果。在接下来的章节中,我将带您浏览一下您可以在 Metro 应用中使用的 UI 控件,以提供更丰富的体验,并为您的用户提供与其他 Metro 应用一致的外观。