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

原文:Pro Windows 8 Development with HTML5 and JavaScript

协议:CC BY-NC-SA 4.0

二十、使用设置和应用数据

在这一章中,我将向您展示如何向用户展示应用设置,以及如何使它们持久化。几乎每个应用都有一些用户可以自定义的功能,以与其他应用一致的方式向用户提供选项是一种重要的方式,可以确保用户在使用应用时能够建立在他们以前的 Windows 体验上。

对应用有特殊的规定,使设置工作变得简单和相对容易。我将向您展示呈现设置的不同技术以及持久存储设置(和其他数据)的 Windows 特性。表 20-1 提供了本章的总结。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

准备示例 App

我将建立在我在第十九章中用来解释生命周期事件的EventCalc例子之上。在那一章中,我向你展示的一个特性是当应用即将被挂起时如何存储会话状态。我当时提到,您不应该将用户设置存储为会话数据,所以我构建同一个示例应用来告诉您故事的其余部分是合适的。提醒一下,你可以看到EventCalc是如何出现在图 20-1 中的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

准备示例应用

为了准备演示设置的应用,我在项目中添加了一个名为/js/settings.js的新文件,你可以在清单 20-1 中看到。这个文件定义了一个名为ViewModel.Settings的名称空间,我将在这里存储用户首选项。

清单 20-1 。settings.js 文件的内容

`(function () {
    WinJS.Namespace.define(“ViewModel”, {
        Settings: WinJS.Binding.as({
**            backgroundColor: “#1D1D1D”,**
**            textColor: “#FFFFFF”,**
**            showHistory: true**
        })
    });

WinJS.Namespace.define(“ViewModel.Converters”, {
        display: WinJS.Binding.converter(function (val) {
            return val ? “block” : “none”;
        }),
    });

})();`

ViewModel.Settings名称空间中的属性将控制应用布局的背景和前景色,以及显示计算历史的面板的可见性。我修改了default.html文件,将settings.js文件纳入范围,并添加了一些新的数据绑定,以便将ViewModel.Settings属性应用于适当的元素,如清单 20-2 所示。

清单 20-2 。将设置绑定添加到 default.html 文件

`

          EventCalc


    
    


    
    


         Events
        

        

    


         Calculator
        

            
             +
            
             =
                         Calculate
        

    

<div id=“historyContainer” class=“container”
**            data-win-bind=“style.display: showHistory ViewModel.Converters.display”**>
        History
        


        

    

`

我要做的最后一个更改是添加一个对WinJS.Binding.processAll方法的额外调用,以便激活标记中的数据绑定。您可以在清单 20-3 中看到我添加到/js/default.js文件中的附加语句。

清单 20-3 。激活设置数据绑定

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

if (args.detail.kind === activation.ActivationKind.launch) {
        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;
        }

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(document.body, ViewModel.Settings);**
        }).then(function () {
            return WinJS.Binding.processAll(calcElems, ViewModel.State);
        }));

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

这些更改和添加的结果是一组可观察的属性,在ViewModel.Settings名称空间中定义。当这些属性改变时,default.html文件中标记的数据绑定会更新关键元素的 CSS 值。您可以通过使用调试器启动应用(从 Visual Studio Debug菜单中选择Start Debugging)并在JavaScript Console窗口中输入以下语句来测试这些属性:


ViewModel.Settings.showHistory = false **ViewModel.Settings.backgroundColor = "#317f42"**


改变设置值会触发数据绑定的更新,改变背景颜色并隐藏计算历史,如图图 20-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 20-2。*改变设置属性的值

在接下来的小节中,我将向您展示向用户呈现这些设置属性并持久存储用户选择的值的机制。

向用户呈现设置

Windows 提供了一个处理设置的标准机制,它是通过设置魅力(你可以通过魅力栏或使用Win+I快捷方式打开它)来触发的。

设置通过设置窗格呈现,如图图 20-3 所示,默认设置窗格显示了应用的一些基本细节,并包含一个Permissions链接,向用户显示应用在其清单中声明了哪些功能。“设置”面板的底部有一组标准按钮,用于配置系统范围的设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 20-3。*默认设置窗格

如果你点击Permissions链接,你会看到一个设置弹出按钮的例子,如图 20-4 中的所示。这就是设置的处理方式——它们被分组到类别中,在设置窗格中显示为链接,单击链接会显示一个设置弹出按钮,其中包含更多详细信息,通常还包含允许用户更改应用设置的控件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 20-4。*权限弹出按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示您可以通过更改应用清单的Packaging部分中的值来更改Permissions弹出按钮中显示的详细信息。

如图所示,设置弹出按钮有一个标题区,其中包含一个后退按钮——单击此按钮将返回设置窗格。设置窗格和设置弹出按钮都是轻触式的,这意味着用户可以通过单击或触摸屏幕上的其他地方来关闭它们,就像我在本书的前一部分描述的弹出 UI 控件一样。

在这一部分,我将在设置窗格中添加额外的链接,以允许用户查看和更改我在/js/settings.js file中定义的设置。我将向窗格添加两个新链接:Colors链接将打开一个窗格,让用户设置backgroundColortextColor设置属性,History链接将打开一个窗格,让用户设置showHistory设置属性。在这个过程中,我将演示你的应用如何将自己集成到标准设置系统中,以及你如何创建设置弹出按钮,其外观和行为与图 20-4 中的默认Permissions弹出按钮相同。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 通过设置魅惑辅助设置是契约的一个例子。契约是一组定义明确的交互、事件和数据对象,允许应用在标准的 Windows 功能中具体化。设置只是我在本书中解释的契约之一——当我向你展示如何将你的应用集成到 Windows 搜索功能中时,你会在第二十一章中看到一个更复杂的例子。

定义弹出型 HTML

我需要做的第一件事是为每个设置面板创建一个 HTML 文件。首先,我创建了一个名为colorsSettings.html的文件,其内容可以在清单 20-4 中看到。这是一个相对简单的文件,所以我将 CSS 和 JavaScript 放在 HTML 标记所在的文件中。

清单 20-4 。colorsSettings.html 文件的内容

`

               ` `     

这个 HTML 的核心是一个名为SettingsFlyout的 WinJS UI 控件。该控件应用于div元素,仅用于向用户呈现设置。width选项是由SettingsFlyout定义的唯一配置选项,它允许你请求一个标准的弹出按钮(使用narrow值)或一个有额外空间的按钮(使用wide值)。

设置弹出文件的内容通常需要一些 JavaScript 来处理用户输入,您可以看到我已经使用了WinJS.UI.Pages.define方法(我在第七章中描述过)来确保在我设置我的事件处理程序和应用数据绑定之前加载文档中的元素。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示我建议你在创建设置弹出按钮时使用列表中的 HTML 作为模板。它包含了你需要的一切——包括标题和后退按钮的标题,以及一个用于设置内容的区域。

对于这个设置弹出按钮,我定义了两个input元素,它们是绑定到ViewModel.Settings属性的数据,并且在change事件被触发时更新这些属性。对于另一个设置弹出按钮,我已经创建了一个非常相似的文件,叫做historySettings.html,它的内容你可以在清单 20-5 中看到。

清单 20-5 。historySettings.html 文件的内容

`

                   

对于这个弹出按钮,我为width配置选项选择了wide值。ToggleSwitch控件(我在第十一章中描述过)允许用户切换计算历史的可见性。当切换值改变时,ViewModel.Settings名称空间中相应的属性也会更新。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意我的设置弹出按钮有点稀疏,在一个真实的项目中,我可以很容易地在一个弹出按钮上显示所有三个选项。我这样做是为了演示如何添加多个类别,但在实际项目中,如果需要将设置组合在一起,我建议您合并设置并使用弹出按钮中的标准 HTML 元素来创建部分。我在这一章中采用的方法对于一个例子来说是有用的,但是让用户更难配置应用。

响应设置事件

定义了弹出 HTML 文件后,下一步是在设置窗格中注册它们。我这样做是为了响应由WinJS.Application对象发出的settings事件,该事件在用户激活设置符时被触发。

您可以在清单 20-6 的中看到我对此事件的反应。为了将设置代码与应用的其他部分分开,我在settings.js文件中响应这个事件,但在实际项目中,我会在default.js文件中这样做,在那里我处理WinJS.Application发出的其他事件。

清单 20-6 。处理设置事件

`(function () {
    WinJS.Namespace.define(“ViewModel”, {
        Settings: WinJS.Binding.as({
            backgroundColor: “#1D1D1D”,
            textColor: “#FFFFFF”,
            showHistory: true
        })
    });

WinJS.Namespace.define(“ViewModel.Converters”, {
        display: WinJS.Binding.converter(function (val) {
            return val ? “block” : “none”;
        }),
    });

**    WinJS.Application.onsettings = function (e) {**
**        e.detail.applicationcommands = {**
**            “colorsDiv”: { href: “colorsSettings.html”, title: “Colors” },**
**            “historyDiv”: { href: “historySettings.html”, title: “History” }**
**        };**
**        WinJS.UI.SettingsFlyout.populateSettings(e);**
**    };**
})();`

我通过给WinJS.Application.onsettings属性分配一个处理函数来处理settings事件。当用户激活 Settings Charm 时,我的函数将被调用,向我提供向 Settings 窗格添加附加类别设置的机会。

这是一个两步过程。首先,我将一个对象分配给传递给处理函数的对象的detail.applicationcommand属性。我的对象定义的属性的名称对应于我的 HTML 文件中的div元素,其中已经应用了WinJS.UI.SettingsFlyout控件。

我给这些div名称属性中的每一个分配一个具有hreftitle属性的对象。必须将href属性设置为您想要显示的弹出文件的名称,并将title属性设置为您想要包含在设置窗格链接中的文本。每个div名称必须是唯一的,如果div元素名称或href值不正确,您的链接将从设置窗格中被忽略。

第二步是调用WinJS.UI.SettingsFlyout.populateSettings方法,传入传递给处理函数的对象(其detail.applicationcommands属性已经被分配了我想要的链接和弹出按钮的详细信息)。第二步加载弹出 HTML 文件的内容并处理它们。这些增加的结果是当用户激活设置符时,设置窗格包含一些新的链接,如图图 20-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 20-5。*向设置面板添加自定义链接

如果您点击或触摸这些链接,则会显示相应的设置窗格。图 20-6 显示了我创建的两个窗格,展示了width设置的narrowwide值之间的差异。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 20-6。*点击链接时显示的自定义设置弹出按钮

我没有做太多的工作来使设置弹出按钮吸引人,因为我在这一章的重点是窗口设置机制。但是在一个真实的项目中,如果我只有一个ToggleSwitch要显示,我不会使用 wide 设置,我也不会期望用户通过输入十六进制代码或 CSS 颜色名称来选择颜色。这些都是显而易见的,你应该考虑如何分组和安排弹出按钮的内容,以使配置应用的过程尽可能简单和轻松。

使设置持久

我已经关联了“设置”弹出按钮中的控件和元素,以便视图模型在它们发生变化时立即更新。您可以尝试更改设置的效果,并立即看到效果——但下次启动应用时,将使用我在settings.js文件中定义的默认值,您的更改将会丢失。

这就把我带到了应用数据应用数据的话题上,这些数据是你的应用运行所需要的,但你不想让用户直接访问——比如设置。当用户与应用数据交互时,是通过某种形式的中介,比如设置弹出按钮。在接下来的部分中,我将向您展示如何存储和检索应用数据设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示应用数据的替代品是用户数据,用户可以直接使用这些数据——我将在第二十二章–24 章中解释应用如何操作用户数据,届时我将描述 Windows 对使用文件的支持。

存储设置

用于处理应用数据的 Windows 功能非常出色,设置是最容易处理的应用数据。清单 20-7 显示了我对settings.js文件所做的添加,以持久存储设置数据。

清单 20-7 。添加到 settings.js 文件以永久存储设置数据

`(function () {
    WinJS.Namespace.define(“ViewModel”, {
        Settings: WinJS.Binding.as({
            backgroundColor: “#1D1D1D”,
            textColor: “#FFFFFF”,
            showHistory: true
        })
    });

WinJS.Namespace.define(“ViewModel.Converters”, {
        display: WinJS.Binding.converter(function (val) {
            return val ? “block” : “none”;
        }),
    });

WinJS.Application.onsettings = function (e) {
        e.detail.applicationcommands = {
            “colorsDiv”: { href: “colorsSettings.html”, title: “Colors” },
            “historyDiv”: { href: “historySettings.html”, title: “History” }
        };
        WinJS.UI.SettingsFlyout.populateSettings(e);
    };

**    var storage = Windows.Storage;**
**    var settingNames = [“backgroundColor”, “textColor”, “showHistory”];**

**    settingNames.forEach(function (setting) {**
**        ViewModel.Settings.bind(setting, function (newVal, oldVal) {**
**            if (oldVal != null) {**
**                storage.ApplicationData.current.localSettings.values[setting] = newVal;**
**            }**
**        });**
**    });**
})();`

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意除非另有说明,否则我在本节中引用的所有新对象都是Windows.Storage名称空间的一部分。

这项技术的核心是ApplicationDataContainer对象,其中数据存储为键/值对。有两个内置容器——第一个允许您存储数据,以便数据位于运行应用的设备上,另一个存储漫游数据,这些数据会自动复制到用户登录的任何设备上。

稍后我将回到漫游数据容器,但是对于这个例子,我从最简单的选项开始,使用本地容器。为了获得本地容器对象,我读取了ApplicationData.current.localSettings属性,如下所示:

... storage.ApplicationData.current.localSettings.values[setting] = newVal; ...

属性返回一个可以用来存储数据的对象。容器中的数据是永久存储的,这意味着您不必担心显式指示 Windows 保存您的应用数据-一旦您在存储容器中设置了值,您的数据就会被存储。

ApplicationData定义了许多有用的属性,我在表 20-2 中总结了这些属性,并在接下来的章节中进行了描述。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦获得了想要使用的容器,就可以通过values属性存储键/值对,该属性返回一个ApplicationDataContainerSettings对象。在这个例子中,我像使用数组一样使用这个对象,并给它赋值如下:

... storage.ApplicationData.current.localSettings.**values[setting] = newVal;** ...

你可以使用数组符号来赋值和读取值,但是一个ApplicationDataContainerSettings对象并不能实现一个真实数组的所有行为,你可能需要使用我在表 20-3 中描述的方法和属性来代替。

现在您已经理解了所涉及的对象,您可以看到我是如何在示例中持久存储设置值的。我使用 WinJS 编程数据绑定(如第八章中的所述)来监控ViewModel.Settings属性,并在设置改变时存储新值,这一点我在清单 20-8 中已经强调过。

清单 20-8 。绑定查看模型更改以保持用户选择

`…
var storage = Windows.Storage;
var settingNames = [“backgroundColor”, “textColor”, “showHistory”];

settingNames.forEach(function (setting) {
**    ViewModel.Settings.bind**(setting, function (newVal, oldVal) {
        if (oldVal != null) {
**            storage.ApplicationData.current.localSettings.values[setting] = newVal;**
        }
    });
});
…`

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示我忽略了任何可观察属性的旧值为null的更新。这是因为编程数据绑定在首次创建时会被发送一个更新,提供初始值和旧值的null。我只想在值改变时存储它们,因此检查了null

恢复设置

如果你不能在需要的时候读取设置,那么将设置存储为应用数据就没有多大用处。在清单 20-9 中,你可以看到我在/js/settings.js文件中添加的内容,以便在应用首次加载时恢复任何保存的设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意我定义了恢复设置的函数,并在settings.js文件中调用该函数,但在实际项目中,我会从 default.js 文件中调用该函数来响应正在启动的应用。我已经把这个项目中的所有东西放在一起,所以我不必列出代码页来显示简单的变化。

清单 20-9 。加载应用数据设置

`(function () {
    WinJS.Namespace.define(“ViewModel”, {
        Settings: WinJS.Binding.as({
            backgroundColor: “#1D1D1D”,
            textColor: “#FFFFFF”,
            showHistory: true
        })
    });

WinJS.Namespace.define(“ViewModel.Converters”, {
        display: WinJS.Binding.converter(function (val) {
            return val ? “block” : “none”;
        }),
    });

WinJS.Application.onsettings = function (e) {
        e.detail.applicationcommands = {
            “colorsDiv”: { href: “colorsSettings.html”, title: “Colors” },
            “historyDiv”: { href: “historySettings.html”, title: “History” }
        };
        WinJS.UI.SettingsFlyout.populateSettings(e);
    };

var storage = Windows.Storage;
    var settingNames = [“backgroundColor”, “textColor”, “showHistory”];
**    var loadingSettings = false;**

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
                storage.ApplicationData.current.localSettings.values[setting] = newVal;
            }
        });
    });

**    function loadSettings() {**
**        loadingSettings = true;**
**        var container = storage.ApplicationData.current.localSettings;**
**        settingNames.forEach(function (setting) {**
**            value = container.values[setting];**
**            if (value != null) {**
**                ViewModel.Settings[setting] = value;**
**            }**
**        });**
**        setImmediate(function () {**
**            loadingSettings = false;**
**        })**
**    };     loadSettings();**

})();`

我通过读取ApplicationData.current.localSettings属性获得本地设置容器。然后,我使用数组符号来检查是否有我感兴趣的每个设置的存储值。如果有,那么我使用该值来更新相应的ViewModel.Settings属性值,这将触发我定义的数据绑定,将应用返回到其先前的配置。

为了避免存储我正在加载的相同值,我定义了loadSettings变量,在读取存储的设置之前,我将它设置为true。当我收到数据绑定通知时,我会检查这个变量的值,如果加载正在进行,我会放弃更新。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示注意,在我传递给setImmediate方法的函数中,我将变量loadingSettings设置为false。我这样做是为了确保在我再次开始存储值之前,所有的数据绑定事件都得到处理。我在第九章的中解释了setImmediate的方法。

这些添加的结果是对显示在“设置”弹出按钮上的设置的更改现在是持久的。为了测试这一点,启动应用,激活设置符,选择History链接,并将ToggleSwitch控制改为Off位置。

计算历史将立即隐藏。现在重启应用,要么重启调试器,要么使用Alt + F4并再次启动应用。你会看到,当应用启动时,计算历史并没有显示。

使用漫游设置

在前面的例子中,我使用本地应用存储进行设置。这意味着数据仅存储在当前设备上——如果用户在另一台设备上运行该应用,则将使用默认设置。通过这种方式,用户可以在两个设备上运行相同的应用,而每个实例都有完全不同的配置。

如果您想在用户登录的任何地方应用相同的设置,那么您需要使用漫游设置。这是 Windows 应用最有前途的功能之一,对于将其 Windows 帐户与 Microsoft 帐户相关联的用户,应用和用户数据可以无缝复制。

作为一名 Windows 应用程序员,您不必担心帐户登录过程的细节、正在使用的 Microsoft 帐户或如何复制数据的细节。相反,您只需将设置存储在漫游容器中,而不是本地容器中。你可以在清单 20-10 的中看到我对/js/settings.js文件所做的更改,以使用漫游设置。

清单 20-10 。使用漫游设置容器

(function () {     WinJS.Namespace.define("ViewModel", {         Settings: WinJS.Binding.as({             backgroundColor: "#1D1D1D",             textColor: "#FFFFFF",             showHistory: true         })     });
`    WinJS.Namespace.define(“ViewModel.Converters”, {
        display: WinJS.Binding.converter(function (val) {
            return val ? “block” : “none”;
        }),
    });

WinJS.Application.onsettings = function (e) {
        e.detail.applicationcommands = {
            “colorsDiv”: { href: “colorsSettings.html”, title: “Colors” },
            “historyDiv”: { href: “historySettings.html”, title: “History” }
        };
        WinJS.UI.SettingsFlyout.populateSettings(e);
    };

var storage = Windows.Storage;
    var settingNames = [“backgroundColor”, “textColor”, “showHistory”];
    var loadingSettings = false;

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
                storage.ApplicationData.current.roamingSettings.values[setting] = newVal;
            }
        });
    });

function loadSettings() {
        loadingSettings = true;
        var container = storage.ApplicationData.current.roamingSettings;
        settingNames.forEach(function (setting) {
            value = container.values[setting];
            if (value != null) {
                ViewModel.Settings[setting] = value;
            }
        });
        setImmediate(function () {
            loadingSettings = false;
        })
    };
    loadSettings();

**    storage.ApplicationData.current.addEventListener(“datachanged”, function (e) {**
**        loadSettings();
    });**

})();`

要切换到漫游,而不是本地存储,我只需使用ApplicationData.current.roamingSettings属性。存储和检索单个设置的方法是相同的。

使用漫游设置时,Windows 将在用户登录的任何位置复制你的应用数据,这意味着你可以在多个设备上创建一致的体验。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意如果用户没有与其 Windows 登录相关联的 Microsoft 帐户,则无需处理任何错误——在这种情况下,数据不会被复制。

如果在应用运行时漫游设置被修改,ApplicationData对象将触发datachanged事件。这使您有机会更新您的应用状态以反映新数据,确保用户在其他地方所做的更改尽快得到应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示在示例中,我简单地调用了loadSettings函数来在收到datachanged事件时应用更新,但是您可能想在实际应用中进行更改之前询问用户是否要应用修改后的设置。

要在示例应用中测试对漫游数据的支持,您需要有两台 Windows 8 设备或虚拟机,并使用相同的 Microsoft 帐户登录。在复制应用数据之前,该帐户必须明确信任这两个设备(要信任一个设备,打开设置图标,选择Change PC Settings Users,然后点击Trust this PC链接)。在两台设备上运行应用,然后在其中一台设备上更改应用设置。几分钟后,您将看到同样的更改应用于另一台设备。

了解漫游数据的工作原理

漫游数据功能使用简单,但有一些重要的限制。首先,数据在最大努力的基础上复制。漫游数据旨在让您的应用在不同设备上提供一致的体验,但没有性能保证。当适合 Windows 时,您的数据将被复制,并且您无法控制复制过程(有一个例外,我将在下一节中描述)。Windows 可能会选择无限期推迟数据复制,尤其是在电量或资源不足的情况下。Windows 不承诺在设备关闭或进入睡眠状态之前执行复制,这意味着在存储新设置值和将其复制到其他设备之间可能会经过很长一段时间。

第二,漫游数据是一种低流量、低频率的服务。如果您频繁更新漫游容器,Windows 将暂时停止复制您的数据。这意味着你不应该使用漫游数据来保持一个应用的多个实例同步-在最初几次更新后,Windows 将开始推迟你的应用的更新。

第三,Windows 复制的数据量是有限制的。您可以通过读取ApplicationData.current.roamingStorageQuota属性来确定配额是多少,但在 Windows 8 的初始版本中它被设置为 100KB。这可能看起来很多,但是,正如我在本章后面解释的,您也可以使用漫游存储来复制文件,所以配额可以很快用完。当您超过漫游配额时,不会出现警告,Windows 将会(静默地)停止为您的应用复制数据。此外,由于没有可靠的方法来计算您的应用存储了多少数据,所以在计划哪些数据将被漫游,哪些数据将被存储在本地时,您需要非常保守。

最后,漫游功能旨在让用户在不同设备间移动时获得一致的体验,而不是同时运行同一个应用。对于在不同设备上更改的漫游设置,没有冲突解决方案–Windows 只是丢弃除了最近所做的更改之外的任何更改。

当你考虑到这几点,你就明白漫游数据要慎用了。您应该发送尽可能少的数据,仅在必要时存储更新,并仔细考虑发送数据引用而不是数据本身的机会(例如复制 URL 而不是网页内容)。这并不是说你不应该使用漫游——这是一个很好的功能,它可以改变在多个设备上安装你的应用的用户的体验——但要慎重而谨慎地使用。

使用高优先级设置

漫游设置容器以不同的方式处理一个设置。如果您给HighPriority设置赋值,Windows 将更努力地快速复制该设置。仍然没有性能保证,配额和频繁更新限制仍然适用,但是您可以使用这个特殊设置来尝试确保最重要的信息尽快在其他设备上可用。

HighPriority设置旨在让您创建流畅的用户体验,您应该使用此设置来复制应用状态的关键部分,以便用户在移动到新设备时可以无缝地继续他们的工作流程。这意味着什么将取决于你的应用的性质,但微软给出的一个例子是将一封由部分内容组成的电子邮件从一台设备复制到另一台设备,例如,允许用户开始在家里的 PC 上写邮件,然后在上班途中继续使用平板电脑。

为了演示HighPriority设置的使用,我优先考虑了showHistory设置。当这个设置改变时,我将新值赋给HighPriority,以便尽可能快地复制它。您可以在清单 20-11 中看到我对settings.js文件所做的更改。

清单 20-11 。使用高优先级设置

`(function () {
    WinJS.Namespace.define(“ViewModel”, {
        Settings: WinJS.Binding.as({
            backgroundColor: “#1D1D1D”,
            textColor: “#FFFFFF”,
            showHistory: true
        })
    });

// …statements removed for brevity…

var storage = Windows.Storage;
    var settingNames = [“backgroundColor”, “textColor”, “showHistory”];
    var loadingSettings = false;

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
**                if (setting == “showHistory”) {**
**                    setting = “HighPriority”;**
**                }**
                storage.ApplicationData.current.roamingSettings.values[setting] = newVal;
            }
        });
    });

function loadSettings() {
        loadingSettings = true;         var container = storage.ApplicationData.current.roamingSettings;
        settingNames.forEach(function (setting) {
**            value = container.values[setting == “showHistory” ?**
**                “HighPriority” : setting];**
            if (value != null) {
                ViewModel.Settings[setting] = value;
            }
        });
        setImmediate(function () {
            loadingSettings = false;
        })
    };
    loadSettings();

storage.ApplicationData.current.addEventListener(“datachanged”, function (e) {
        loadSettings();
    });

})();`

我在这里采用的方法是截取对showHistory设置的更新,并使用HighPriority键将新值存储在容器中。它是触发加速行为的密钥名称,并且不需要明确的动作来触发复制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意虽然使用复制时没有性能保证,但我的经验是常规设置大约每 5 分钟复制一次,而HighPriority设置在几秒钟内复制一次。在高峰时间,常规设置的复制通常会减慢到大约每 10 分钟一次。

确保数据一致性

某些应用设置无法自行安全复制。考虑示例应用的以下场景:

  1. On the host computer, the user sets backgroundColor to white and textColor to green. The user was about to leave the house when they turned off the PC before Windows copied the changes. This means that the settings are stored locally, which will affect the application when it runs on the PC, but will not be copied to affect other devices.
  2. The user gets on the train and starts the same application on another device. They changed backgroundColor to green, but did not set the textColor attribute to the default value (white). Windows copies the new backgroundColor value.
  3. The user goes home and starts the application on the PC again. Windows will not copy the setting values in step 1 because they have been replaced by the values changed in step 2.
  4. The application program applies the new value set by backgroundColor, presents green text to the user on the green background, and makes the application program unavailable.

从这种情况中可以吸取几个教训。首先,明确询问用户是否想要应用更新的设置可能很重要——对于我的示例应用,我只是应用更新,在我在本节中提出的场景中,用户不会了解他们几个小时前所做的一系列设置更改已经组合在一起使应用变得无用。同样,您应该花时间确保有一种简单的方法将应用重置为默认设置,或者防止选择会导致应用不可用的设置组合。

然而,最重要的教训是,一些设置需要成组复制,尤其是当这些设置可以以危险的方式组合时。您可以通过使用一个ApplicationDataCompositeValue对象来做到这一点,它允许您将几个设置合并到一个对象中,并确保它们作为一个单一的原子单元被复制。你可以看到我如何在清单 20-12 中使用这个对象来确保我在本节开始描述的场景不会出现。

清单 20-12 。使用 ApplicationDataCompositeValue 对象复制多个值

`(function () {
    WinJS.Namespace.define(“ViewModel”, {
        Settings: WinJS.Binding.as({
            backgroundColor: “#1D1D1D”,
            textColor: “#FFFFFF”,
            showHistory: true
        })
    });

// …statements removed for brevity…

var storage = Windows.Storage;
    var settingNames = [“backgroundColor”, “textColor”, “showHistory”];
    var loadingSettings = false;

settingNames.forEach(function (setting) {
        ViewModel.Settings.bind(setting, function (newVal, oldVal) {
            if (!loadingSettings && oldVal != null) {
**                var container = storage.ApplicationData.current.roamingSettings;**
**                if (setting == “showHistory”) {**
**                    container.values[“HighPriority”] = newVal;**
**                } else if (setting == “backgroundColor” || setting == “textColor”) {**
**                    var comp = new storage.ApplicationDataCompositeValue();**
**                    comp[“backgroundColor”] = ViewModel.Settings.backgroundColor;**
**                    comp[“textColor”] =  ViewModel.Settings.textColor;**
**                    container.values[“colors”] = comp;**
**                }**
            }
        });
    });

function loadSettings() {
        loadingSettings = true;
        var container = storage.ApplicationData.current.roamingSettings;
**        [“HighPriority”, “colors”].forEach(function (setting) {**
**            value = container.values[setting];**
**            if (value != null) {                 if (setting == “HighPriority”) {**
**                    ViewModel.Settings.showHistory = value;**
**                } else {**
**                    ViewModel.Settings.backgroundColor = value[“backgroundColor”];**
**                    ViewModel.Settings.textColor = value[“textColor”];**
**                }**
**            }**
**        });**
        setImmediate(function () {
            loadingSettings = false;
        })
    };
    loadSettings();

storage.ApplicationData.current.addEventListener(“datachanged”, function (e) {
        loadSettings();
    });
})();`

ApplicationDataCompositeValue对象本身就像一个容器,您可以使用数组符号样式为它分配多个设置。然后,您可以将组合的属性集添加为单个设置,并依赖它们一起被复制。我向清单中的ApplicationDataCompositeValue对象添加了两个属性,但是您可以根据需要为您的应用添加任意多个属性(受 Windows 应用于漫游数据的配额和容量限制的限制)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示你可以使用一个ApplicationDataCompositeValue对象作为HighPriority设置的值。这允许您优先选择一组相关属性,以在设备之间保留您的应用状态。

有了这一改变,ViewModel.Settings名称空间中的属性和应用数据容器中的设置名称之间没有直接的联系。我使用特殊的HighPriority设置复制了showHistory属性的值,并将backgroundColortextColor属性复制为一个名为colors的设置。这是一个非常合理的方法,但是您需要确保正确映射您的设置和属性,并进行彻底的测试。请注意,测试是一个痛苦的过程,因为没有办法强制复制-这意味着您必须在一个设备上进行更改,然后等待几分钟才能看到它在其他设备上的反映(当然,除非您正在使用HighPriority设置)。

使用应用数据文件

并不是所有的数据都可以用键/值对来表示。幸运的是,设置并不是应用可以使用的唯一应用数据——你也可以使用文件来存储你需要的数据。为了演示这个特性,我在js项目文件夹中添加了一个名为appDataFiles.js的新文件,其内容可以在清单 20-13 中看到。

清单 20-13 。appDataFiles.js 的内容

(function () {
`    var storage = Windows.Storage;
    var historyFileName = “calcHistory.json”;
    var folder = storage.ApplicationData.current.localFolder;

ViewModel.State.history.addEventListener(“iteminserted”, function (e) {
        folder.createFileAsync(historyFileName,
                storage.CreationCollisionOption.openIfExists)
        .then(function (file) {
            var stringData = JSON.stringify(e.detail.value);
            storage.FileIO.appendLinesAsync(file, [stringData]);
        });
    });
})();`

这段代码使用数据绑定来观察ViewModel.State.history对象,它是一个WinJS.Binding.List对象。每当用户执行一个计算时,一个新的项目被添加到List中,并且appDataFiles.js文件中的代码创建一个用户操作的持久记录。在本章的后面,我将扩展这个例子,并使用这个文件来填充应用启动时的历史记录。但是,首先,我将介绍示例中的代码,并解释所有对象是如何组合在一起的。当你理解了基本的技术,在 Windows 应用中处理文件是非常简单的,但是它不同于我见过的任何其他处理文件系统的方法。在接下来的部分中,我将向您展示每个步骤,并详细解释选项。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意除非我明确声明,否则我在本节中引用的所有新对象都是Windows.Storage名称空间的一部分。

在清单 20-14 中,您可以看到我添加到default.html文件头部分的脚本元素,以将appDataFiles.js文件中的代码引入示例应用。

清单 20-14 。为 appDataFiles.js 文件向 default.html 添加脚本元素

`…

          EventCalc


    
    


    
    

...`
获取文件夹和文件对象

起点是由ApplicationData.current.localFolder属性返回的StorageFolder对象。使用StorageFolder对象时有很多选项,在这一章中我将只关注基本的选项。我将在第二十二章–24 章中向您展示更多可用的功能,届时我将描述如何使用用户数据,而不是应用数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意与设置一样,应用对文件的数据支持可以是本地的,也可以是漫游的,我选择了本地选项。两个实例中的 API 是相同的,不同之处在于漫游文件是复制的。如果要使用漫游 app 数据文件,那么应该使用ApplicationData.current.roamingFolder属性返回的StorageFolder。注意不要超过漫游数据报价,否则您的文件将不会被复制。

表 20-4 总结了使用StorageFolder对象打开、创建或删除文件的基本方法。(我在第二十二章中描述了其他的StorageFolder方法,但是这些是你最常用于 app 数据文件的方法。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Windows 应用中几乎所有与文件相关的操作都是异步执行的,这就是为什么表中的所有方法都返回一个Promise对象。完成后,这些Promise对象将把一个或多个StorageFile对象传递给then方法,代表指定的文件。你可以看到我是如何使用清单 20-15 中的createFileAsync方法得到一个StorageFile对象的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示本章的剩余部分严重依赖于WinJS.Promise对象。如果你还没有阅读第九章,那么你应该现在就阅读,并在完成后返回这里。无法回避Windows.Storage名称空间对象的异步特性。微软为 Windows 应用引入异步支持的主要目标之一是防止应用在执行文件操作时挂起。在处理文件时,你必须使用承诺,即使一开始可能会感觉有点反直觉。

清单 20-15 。获取存储文件夹和存储文件对象

... folder.**createFileAsync**(historyFileName, storage.CreationCollisionOption.openIfExists)     .**then**(function (**file**) {         // statements to operate on the StorageFile go here }); ...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意异步文件操作中出现的任何错误都会传递给then方法的error函数。我在这一节中没有定义错误处理程序,因为我想向您展示如何处理文件,而不是处理错误,但是您应该小心处理真实项目中的错误。关于使用Promise时如何报告错误的详细信息,参见第九章。

createFileAsync方法的可选参数是来自CreationCollisionOption对象的一个值,该对象枚举了一些值,这些值决定了当您试图创建一个已经存在的文件时,Windows 将采取什么操作。表 20-5 描述了可用的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我使用了openIfExists值,这意味着如果没有文件,示例应用将创建一个新文件,如果有,将重用现有文件。

写入文件

StorageFile对象支持对文件的所有操作——你可以重命名或删除它所代表的文件,打开允许你读写数据的流等等。

我不会直接做这些事情,因为FileIO对象定义了一组非常方便的方法,使得执行基本的读写选项变得简单(比直接使用StorageFile对象方法容易得多)。表 20-6 描述了FileIO定义的便利方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示读写缓冲区的方法本身并不是特别有用。它们旨在与来自Windows.Storage.Streams名称空间的对象一起使用,这允许您以更传统的方式执行操作(例如,读取一个字节或一串数据的调用)。FileIO中的便利方法对于大多数情况来说已经足够了,我鼓励你先看看它们是否能满足你的需求。

我发现最有用的方法是appendLinesAsync,因为我可以用它将 JSON 数据写入文件,而不必担心行终止符。你可以看到这是我用来在清单 20-16 中写计算细节的方法,尽管我一次只写一项。

清单 20-16 。使用 FileIO 对象将 JSON 数据写入文件

... ViewModel.State.history.addEventListener("iteminserted", function (e) {     folder.createFileAsync(historyFileName,             storage.CreationCollisionOption.openIfExists)     .then(function (file) { **        var stringData = JSON.stringify(e.detail.value);** **        storage.FileIO.appendLinesAsync(file, [stringData]);**     }); }); ...

请注意,我不必显式地打开我正在处理的文件,将写光标移动到文件的末尾,或者在完成后关闭文件。这些平凡的(并且容易出错的)任务由FileIO对象替我处理。

结果是,每次用户执行计算时,我都会在文件中添加一个字符串。这是一个真实的常规文件,您可以通过在 Visual Studio JavaScript 控制台窗口中输入Windows.Storage.ApplicationData.current.localFolder.path在 Windows 文件系统中找到该文件。对于我的系统,该属性的值是:


"C:\Users\adam\AppData\Local\Packages\a52d9e6e-bba3-4774-a824b26e77499de7_6fxp0bkxjs8ye \LocalState"


当然,在您的系统上,路径会有所不同。如果您使用应用执行一些计算,然后打开文件夹,您将看到calcHistory.json文件,它将包含每个计算的简单 JSON 描述,类似于清单 20-17 中显示的内容。

清单 20-17 。calcHistory.json 文件的内容

{"message":"1 + 2 = 3"} {"message":"1 + 3 = 4"} {"message":"1 + 4 = 5"}

message属性的存在是因为我在default.html文件中使用了一个 WinJS 模板来显示事件消息和计算历史。在这个例子中,为了简单起见,我只是将对象转换为 JSON 并将其写入文件,但是如果需要,您可以重新格式化对象或以完全不同的方式表达数据。

从文件中读取

从文件中读取计算历史所需的代码很容易理解,因为您已经看到了将数据写入文件所涉及的对象。清单 20-18 显示了我在appDataFiles.js文件中添加的内容,以便在应用启动时读取历史记录。(我已经在保存新数据的代码之前插入了将数据添加到视图模型的代码,以便在加载初始数据之前不会对更改做出响应。)

清单 20-18 。从应用数据文件中读取计算历史

`(function () {

var storage = Windows.Storage;
    var historyFileName = “calcHistory.json”;
    var folder = storage.ApplicationData.current.localFolder;

**    function readHistory() {**
**        folder.getFileAsync(historyFileName)**
**        .then(function (file) {**
**            var fileData = storage.FileIO.readLinesAsync(file)**
**            .then(function (lines) {**
**                lines.forEach(function (line) {**
**                    ViewModel.State.history.push(JSON.parse(line));**
**                });**
**            })**
**        });**
**    }**

**    readHistory();**

ViewModel.State.history.addEventListener(“iteminserted”, function (e) {
        folder.createFileAsync(historyFileName,
                storage.CreationCollisionOption.openIfExists)
        .then(function (file) {
            var stringData = JSON.stringify(e.detail.value);
            storage.FileIO.appendLinesAsync(file, [stringData]);
        });
    });

})();`

我使用FileIO.readLinesAsync方法获取字符串数组形式的文件内容。这使我能够很好地将每个 JSON 字符串解析成一个 JavaScript 对象,并将其推送到ViewModel.State.history对象中,从而将每个项目显示给用户。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意如果你在添加readHistory函数之前没有运行这个 app,那么 Visual Studio 会报错。这是因为readHistory函数是在 app 刚启动的时候调用的,文件不存在。readHistory函数处理丢失的文件,您可以单击Continue按钮忽略该文件一次,或者取消选中复选框以防止 Visual Studio 在将来报告相同的异常。

从 App 包中加载文件

您不必从头开始生成设置和文件,您也可以将数据文件包含在您的应用包中,并将其处理为其他应用数据文件。当所有用户都需要相同的数据,并且只需要很少或不需要定制时,这非常有用。

我在本章中作为例子使用的EventCalc应用生成一组存储在缓存中的计算结果。我在第十九章中添加了这个特性,当时我正在演示如何在应用生命周期的范围内执行任务。在本章中,我将更新应用,以便它从数据文件加载预先计算的结果,该数据文件包含在应用部署到用户设备中。首先,我在项目中创建了一个data文件夹,并添加了一个名为calcData.json的新文件。这个文件中的每一行都包含一个计算的 JSON 表示,你可以在清单 20-19 中看到这个数据的例子。在第十九章中,我想要一个需要一段时间才能完成的任务,所以我生成了前 5000 个整数的和。对于这一章,我想要一个可管理的文件大小,所以数据文件只包含前 100 个整数值相加的结果。

清单 20-19 。calcData.json 文件中的 JSON 数据示例

... {"first":3,"second":50,"result":53} {"first":3,"second":51,"result":54} {"first":3,"second":52,"result":55} {"first":3,"second":53,"result":56} {"first":3,"second":54,"result":57} {"first":3,"second":55,"result":58} {"first":3,"second":56,"result":59} {"first":3,"second":57,"result":60} ...

这个文件太长了,我无法在这一章中全部列出,所以如果你想按照这个例子学习,有两种方法可以得到这个文件的内容。首先是下载本书附带的源代码,其中包含了每章中所有示例的所有文件。第二种方法是自己生成数据,您可以在生成示例数据侧栏中找到相关说明。

生成样本数据

如果你不想从apress.com下载calcData.json文件,那么你可以自己轻松生成。在 Visual Studio 中创建一个新的 Windows 应用项目,并用以下内容替换default.html文件的内容:

`

          DataGen               Generate `

这个清单定义了一个简单的应用,其中 HTML、CSS 和 JavaScript 都定义在同一个文件中。应用的布局包含一个单独的Generate按钮,当点击它时,会提示你在你的系统上保存calcData.json文件,然后你可以将它复制到你的 Visual Studio 项目的data文件夹中。结果将被生成并保存到文件中。这个应用使用了我在本章后面才描述的特性,所以为了创建例子所需的数据,把它当作一个黑盒。

为了禁用缓存后台任务并减少预期缓存结果的数量,我更新了default.js文件中的performInitialization函数,如清单 20-20 所示。这些变化缩小了预计算数据的范围,并停止在每次启动应用时生成结果。

清单 20-20 。修改 default.js 文件,准备加载计算结果

... 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 < 100 && second < 100) {
            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;**
**    //});**
};
…`

这个例子的关键部分显示在清单 20-21 中,它详细说明了我对appDataFiles.js文件所做的添加。我添加了一个自执行函数,它打开data/calcData.json文件,解析内容,并将结果作为缓存数据。

清单 20-21 。添加到 appDataFiles.json 文件以加载预先计算的数据

`(function () {

var storage = Windows.Storage;
    var historyFileName = “calcHistory.json”;
    var folder = storage.ApplicationData.current.localFolder;

ViewModel.State.history.addEventListener(“iteminserted”, function (e) {
        folder.createFileAsync(historyFileName,
                storage.CreationCollisionOption.openIfExists)
        .then(function (file) {             var stringData = JSON.stringify(e.detail.value);
            storage.FileIO.appendLinesAsync(file, [stringData]);
        });
    });

function readHistory() {
        folder.getFileAsync(historyFileName)
        .then(function (file) {
            var fileData = storage.FileIO.readLinesAsync(file)
            .then(function (lines) {
                lines.forEach(function (line) {
                    ViewModel.State.history.push(JSON.parse(line));
                });
            })
        });
    }

readHistory();

**    (function () {**
**        storage.StorageFile.getFileFromApplicationUriAsync(**
**            Windows.Foundation.Uri(“ms-appx:///data/calcData.json”))**
**        .then(function (file) {**
**            var cachedData = {};**
**            storage.FileIO.readLinesAsync(file).then(function (lines) {**
**                lines.forEach(function (line) {**
**                    var calcResult = JSON.parse(line);**
**                    if (cachedData[calcResult.first] == null) {**
**                        cachedData[calcResult.first] = {};**
**                    }**
**                    cachedData[calcResult.first][calcResult.second] = calcResult.result;**
**                });**
**            });**
**            ViewModel.State.cachedResult = cachedData;**
**        });**
**    })();**
})();`

这类似于我在上一节中读取历史文件的方式,但是关键的区别是我获取对应于calcData.json文件的StorageFile对象的方式。在这种情况下,我不能使用ApplicationData对象,因为应用包中的文件被不同地处理。

第一步是创建一个Windows.Foundation.Url对象。名称空间包含的对象大部分是。NET 编程语言,并且与 JavaScript 关系不大。Uri对象接受一个 URI 字符串,并以一种可以和Windows.Storage对象一起使用的方式准备它。它不做任何对 JavaScript 程序员有用的事情。

URI 的格式很重要。协议组件必须设置为ms-appx,您必须使用三个/字符,然后包括您想要加载的文件的路径。对于data/calcData.json文件,这意味着我需要使用的字符串是:

ms-appx:///data/calcData.json

一旦有了一个Windows.Foundation.Uri对象,就可以把它作为StorageFile.getFileFromApplicationUriAsync方法的一个参数,该方法返回一个代表应用包中文件的StorageFile。从这一点开始,您可以使用FileIO对象来读取文件的内容,就像读取常规应用数据文件一样。(不要试图写入这些文件——它们是只读的。)这些变化的结果是,我能够将数据作为我的应用分发的一部分进行部署,并在应用启动时加载数据——对于我的示例应用,这意味着我不必依赖用户设备的潜在有限功能来生成缓存数据。

总结

在这一章中,我向你展示了如何实现设置契约,以便将你的应用集成到标准的 Windows 模型中,向用户呈现设置。契约是一种强大的技术,可以确保你的应用向用户呈现一系列一致的交互,正如你将在后面的章节中看到的,有些联系可能非常复杂。

如果您不能持久地存储用户选择的值,那么向用户提供设置是没有意义的。为此,我解释了 app 数据系统的工作原理,允许您存储键/值对和数据文件。Windows 应用可以存储设置和文件,以便它们位于当前设备的本地,或者在用户登录时漫游并跟随用户。漫游功能很容易使用,但在使用上有一些限制,我向您展示了一些高级功能,以帮助您获得特定种类的漫游行为。

本章结束时,我向您展示了如何加载作为应用包的一部分分发给用户的数据。当所有用户都需要相同的数据时,这很有用,并且避免了在应用启动时生成或下载数据的需要。

在下一章,我将向你展示如何使用 Windows 搜索功能,让用户搜索你的应用数据。

二十一、搜索契约

Windows 定义了一系列的契约,Windows 应用可以实现这些契约来集成关键的平台范围的服务。这些契约为特定功能的应用和操作系统之间的交互设置了一个模型。在这一章中,我将向您介绍搜索契约,它允许一个应用无缝地参与 Windows 搜索机制,允许用户在您的应用中查找数据,就像他在操作系统中定位文件和应用一样。

这一章不是关于给你的应用添加搜索功能。这是一款已经有能力处理搜索的应用,并使用契约向用户展示这种能力。您可以提供对您的应用和应用数据有意义的任何类型的搜索功能,并且,正如您将看到的,使用户能够轻松访问和使用它。本章开始时,我将构建一个能够搜索其应用数据的简单应用,然后我将使用该应用来演示如何实现搜索契约。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建示例应用

在《??》第十六章中,我使用了一系列流行的名字来帮助演示SemanticZoom UI 控件。我将在本章中使用相同的数据作为基础,来演示支持搜索契约的不同方式。我为这一章编写的示例应用叫做SearchContract,你可以在的清单 21-1 中看到 default.html 文件的内容。

清单 21-1 。来自 SearchContract 应用的 default.html 文件的内容

`

          SearchContract


    
    


    
**    

    
        
            
        
    


        

    


**        <div id=“nameList” data-win-control=“WinJS.UI.ListView”**
**            data-win-options=“{itemTemplate: NameItemTemplate,**
**                itemDataSource: ViewModel.filteredNames.dataSource}”>**
**        
**
    


**        <div id=“messageList” data-win-control=“WinJS.UI.ListView”**
**            data-win-options=“{itemTemplate: MessageTemplate,**
**                itemDataSource: ViewModel.messages.dataSource,**
**                layout: {type: WinJS.UI.ListLayout}}”>**
**        
**
    

`

这个应用布局的关键部分是两个ListView控件。第一个ListView将显示一组名称,我将使用第二个ListView控件来显示一系列消息,类似于我在第十九章中向您展示 UI 中的应用生命周期事件时采用的方法。该标记还包含几个我将用于ListView内容项的模板和一个导入viewmodel.js文件的脚本元素,我将很快创建该文件。

为了创建这个应用的布局,我在/css/default.css文件中定义了一些样式,如清单 21-2 所示。这些样式是使用常规 CSS 属性构建的,不依赖于任何特定于 Windows 的功能。

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

`body { background-color: #5A8463; display: -ms-flexbox;
    -ms-flex-direction: row; -ms-flex-align: center; -ms-flex-pack: center;}
.listContainer {height: 80%; margin: 10px; border: medium solid white; padding: 10px;}

#nameContainer { width: 50%;}
#messageContainer {width: 25%;}
#nameList, #messageList {height: 100%; margin-bottom: 10px;}

*.ListData, .message { background-color: black; text-align: center;
    border: solid medium white; font-size: 20pt; padding: 10px; width: 140px;}

.message { width: 95%; font-size: 18pt; padding: 5px;}
#buttonContainer > button {font-size: 20pt; margin: 10px;}`

定义视图模型

为了定义数据并准备好它,以便它可以与ListView控件一起使用,我添加了一个名为js/viewmodel.js的文件,其内容可以在清单 21-3 中看到。

清单 21-3 。viewmodel.js 文件的初始内容

`(function () {

var rawData = [‘Aaliyah’, ‘Aaron’, ‘Abigail’, ‘Abraham’, ‘Adam’, ‘Addison’, ‘Adrian’,
        ‘Adriana’, ‘Aidan’, ‘Aiden’, ‘Alex’, ‘Alexa’, ‘Alexander’, ‘Alexandra’, ‘Alexis’,
        ‘Allison’, ‘Alyssa’, ‘Amelia’, ‘Andrew’, ‘Angel’, ‘Angelina’, ‘Anna’, ‘Anthony’,
        ‘Ariana’, ‘Arianna’, ‘Ashley’, ‘Aubrey’, ‘Austin’, ‘Ava’, ‘Avery’, ‘Ayden’,
        ‘Bella’, ‘Benjamin’, ‘Blake’, ‘Brandon’, ‘Brayden’, ‘Brian’, ‘Brianna’, ‘Brooke’,
        ‘Bryan’, ‘Caleb’, ‘Cameron’, ‘Camila’, ‘Carter’, ‘Charles’, ‘Charlotte’, ‘Chase’,
        ‘Chaya’, ‘Chloe’, ‘Christian’, ‘Christopher’, ‘sClaire’, ‘Connor’, ‘Daniel’,
        ‘David’, ‘Dominic’, ‘Dylan’, ‘Eli’, ‘Elijah’, ‘Elizabeth’, ‘Ella’, ‘Emily’,
        ‘Emma’, ‘Eric’, ‘Esther’, ‘Ethan’, ‘Eva’, ‘Evan’, ‘Evelyn’, ‘Faith’, ‘Gabriel’,
        ‘Gabriella’, ‘Gabrielle’, ‘Gavin’, ‘Genesis’, ‘Gianna’, ‘Giovanni’, ‘Grace’,
        ‘Hailey’, ‘Hannah’, ‘Henry’, ‘Hunter’, ‘Ian’, ‘Isaac’, ‘Isabella’, ‘Isaiah’,
        ‘Jack’, ‘Jackson’, ‘Jacob’, ‘Jacqui’, ‘Jaden’, ‘Jake’, ‘James’, ‘Jasmine’,
        ‘Jason’, ‘Jayden’, ‘Jeremiah’, ‘Jeremy’, ‘Jessica’, ‘Joel’, ‘John’, ‘Jonathan’,         ‘Jordan’, ‘Jose’, ‘Joseph’, ‘Joshua’, ‘Josiah’, ‘Julia’, ‘Julian’, ‘Juliana’,
        ‘Julianna’, ‘Justin’, ‘Kaitlyn’, ‘Katherine’, ‘Kayla’, ‘Kaylee’, ‘Kevin’,
        ‘Khloe’, ‘Kimberly’, ‘Kyle’, ‘Kylie’, ‘Landon’, ‘Lauren’, ‘Layla’, ‘Leah’, ‘Leo’,
        ‘Liam’, ‘Lillian’, ‘Lily’, ‘Logan’, ‘London’, ‘Lucas’, ‘Luis’, ‘Luke’,
        ‘Mackenzie’, ‘Madeline’, ‘Madelyn’, ‘Madison’, ‘Makayla’, ‘Maria’, ‘Mason’,
        ‘Matthew’, ‘Max’, ‘Maya’, ‘Melanie’, ‘Mia’, ‘Michelle’, ‘Miriam’, ‘Molly’,
        ‘Morgan’, ‘Moshe’, ‘Naomi’, ‘Natalia’, ‘Natalie’, ‘Nathan’, ‘Nathaniel’,
        ‘Nevaeh’, ‘Nicholas’, ‘Nicole’, ‘Noah’, ‘Oliver’, ‘Olivia’, ‘Owen’, ‘Paige’,
        ‘Patrick’, ‘Peyton’, ‘Rachel’, ‘Rebecca’, ‘Richard’, ‘Riley’, ‘Robert’, ‘Ryan’,
        ‘Samantha’, ‘Samuel’, ‘Sara’, ‘Sarah’, ‘Savannah’, ‘Scarlett’, ‘Sean’,
        ‘Sebastian’, ‘Serenity’, ‘Sofia’, ‘Sophia’, ‘Sophie’, ‘Stella’, ‘Steven’,
        ‘Sydney’, ‘Taylor’, ‘Thomas’, ‘Tristan’, ‘Tyler’, ‘Valentina’, ‘Victoria’,
        ‘Vincent’, ‘William’, ‘Wyatt’, ‘Xavier’, ‘Zachary’, ‘Zoe’, ‘Zoey’];

WinJS.Namespace.define(“ViewModel”, {
        allNames: [],
        filteredNames: new WinJS.Binding.List(),
        messages: new WinJS.Binding.List(),
        writeMessage: function (msg) {
            ViewModel.messages.push({ message: msg });
        },
        searchTerm: “”
    });

rawData.forEach(function (item, index) {
        var item = { name: item, firstLetter: item[0] };
        ViewModel.allNames.push(item);
    });

ViewModel.search = function (term) {
        ViewModel.writeMessage("Searched for: " + (term == “” ? “empty string” : term));
        term = term.toLowerCase();
        ViewModel.filteredNames.length = 0;
        ViewModel.allNames.forEach(function (item) {
            if (item.name.toLowerCase().indexOf(term) > -1) {
                ViewModel.filteredNames.push(item)
            }
        });
        ViewModel.searchTerm = term;
    };
})();`

rawData数组包含作为一组字符串的名字列表。这些对我来说没有多大用处,所以我处理这些值来创建两组对象,我可以将它们用于数据绑定模板。第一个集合ViewModel.allNames,包含一个完整的对象集合——这将是我的参考数据,我将根据它执行搜索。第二组对象是ViewModel.filteredNames,我将它用作布局中左侧ListView控件的数据源。在本章中,我将使用数据源来显示一些搜索的结果。

我还使用viewmodel.js文件来定义ViewModel.search函数,该函数通过依次检查每个名字来执行简单的搜索——这是一种低效的搜索技术,但对于我的示例应用来说已经足够了。

定义 JavaScript 代码

现在剩下的就是实现default.js文件。我只使用了最小生命周期事件处理代码,在本章中我不会担心应用暂停或终止的影响。您可以在清单 21-4 中看到我修改后的default.js文件的内容。

清单 21-4 。初始 default.js 文件

`(function () {
    “use strict”;

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

app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState
                    != activation.ApplicationExecutionState.suspended) {

args.setPromise(WinJS.UI.processAll().then(function () {
**                    ViewModel.writeMessage(“App Launched”);**
**                    ViewModel.search(“”);**
                }));
            }
        }
    };

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

当应用启动时,我调用ViewModel.search方法来搜索空字符串——这样可以在应用首次启动时匹配所有名称并显示完整的名称集。你可以在图 21-1 中看到应用的布局以及首次启动时显示的数据和消息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-1。*示例 app 的初始布局和数据

添加应用图片

我在项目的images文件夹中添加了一些文件,如图图 21-2 所示。我已经使用了文件名以tile开头的文件来设置应用清单的应用 UI 部分的字段,就像我在第四章的中所做的一样。这些图像包含如图所示的放大镜图标。另一个文件,user.png,包含一个人物图标,我将在本章后面使用 Windows 显示我的搜索结果时使用这个文件。您可以在图中看到这两个图标(我添加了黑色背景以使图标可见—实际文件是透明的)。我已经将这些文件包含在本书的源代码下载中,您可以从apress.com获得。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-2。*向示例应用添加磁贴和用户图标

测试示例应用

正如我在介绍中提到的,这一章不是关于你如何在你的应用中实现搜索,而是关于你如何将搜索功能集成到 Windows 中,以便你的用户有一致和丰富的搜索体验。我在示例应用中执行搜索的方式非常简单,您可以通过使用 Visual Studio JavaScript Console窗口看到它是如何工作的。启动应用(确保使用Start Debugging菜单项)并进入 JavaScript 控制台窗口。在提示符下输入以下内容:


ViewModel.search("jac")


(JavaScript 控制台也会说Undefined——你可以忽略这个。)当你按下 Enter 键时,你会看到左边的ListView显示的名字集合将被限制为包含搜索词jac的名字,如图图 21-3 所示。右边的ListView将显示一条新消息,报告所请求的搜索词。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-3。*测试 app 搜索能力

你必须使用JavaScript Console来执行搜索,因为我没有在布局中添加任何搜索元素或控件——当我将应用搜索功能集成到更广泛的 Windows 搜索体验中时,这将由操作系统提供。在接下来的小节中,我将向您展示如何执行这种集成,并解释您可以采用的不同方法。

实施搜索契约

第一步是更新应用清单,以声明您打算支持搜索契约。为此,双击 Visual Studio Solution Explorer窗口中的package.appxmanifest文件,并选择Declarations选项卡。从Available Declarations菜单中选择Search并按下Add 按钮。你会看到Search被添加到Supported Declarations列表中,如图图 21-4 所示。你可以忽略页面的Properties部分——只有当你想使用一个单独的应用来处理搜索时,这些才是有用的,在这一章,我将向你展示如何直接向应用添加搜索支持。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-4。*宣布支持搜索契约

处理激活事件

Windows 通过发送一个activated事件通知你的应用它需要执行一个搜索操作。这与您的应用启动时发送的事件相同,为了区分这两种情况,您必须读取事件的kind属性。处理一个契约的激活需要一种不同的方法来实现你的应用中的onactivated功能,如清单 21-5 中的所示。

清单 21-5 。default.js 文件中的契约激活处理

`(function () {
    “use strict”;

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

app.onactivated = function (args) {

**        var searchTerm;**
**        var promise;**

**        switch (args.detail.kind) {**
**            case activation.ActivationKind.search:**
**                ViewModel.writeMessage(“Search Activation”);**
**                searchTerm = args.detail.queryText;**
**                break;**
**            case activation.ActivationKind.launch:**
**                ViewModel.writeMessage(“Launch Activation”);                 searchTerm = “”;**
**                break;**
**        }**

**        if (args.detail.previousExecutionState**
**            != activation.ApplicationExecutionState.suspended) {**
**            ViewModel.writeMessage(“App was not resumed”);**
**            promise = WinJS.UI.processAll();**
**        } else {**
**            ViewModel.writeMessage(“App was resumed”);**
**            promise = WinJS.Promise.as(true);**
**        }**

**        args.setPromise(promise.then(function () {**
**            ViewModel.search(searchTerm);**
**        }));**
    };

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

为了解释这是如何工作的,我将详细地遍历代码,并向您展示它适合的场景范围。这一点也不复杂,但有一些细节需要考虑,如果你理解如何处理这个契约,你会发现处理其他契约更简单、更容易。

启动应用

首先要做的是启动 app。如何做并不重要——您可以使用 Visual Studio Debug菜单中的Start Debugging项,或者,如果您之前已经运行了该示例,可以使用开始屏幕上的磁贴。当应用启动时,您会在右侧的ListView控件中看到如图图 21-5 所示的消息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-5。*应用正常启动时显示的消息

在这种情况下,我在onactivated处理函数中的switch语句将读取detail.kind属性并获得launch值。在这种情况下,我将搜索字符串设置为空字符串(""),如下所示:

... switch (args.detail.kind) {     case activation.ActivationKind.search:         ViewModel.writeMessage("Search Activation");         searchTerm = args.detail.queryText;         break;     case activation.ActivationKind.**launch**:         ViewModel.writeMessage("Launch Activation"); **        searchTerm = "";**         break; } ...

这给了我两样我需要的东西之一:搜索词。为了得到Promise,我需要查看之前的执行状态。由于 app 已经重新启动,之前的状态会是notRunning。对于除suspended之外的每个州,我想调用WinJS.UI.processAll方法来执行应用的初始设置:

... if (args.detail.previousExecutionState     != activation.ApplicationExecutionState.**suspended**) {     ViewModel.writeMessage("App was not resumed"); **    promise = WinJS.UI.processAll();** } else {     ViewModel.writeMessage("App was resumed");     promise = WinJS.Promise.as(true); } ...

其效果是,搜索将匹配所有数据项(因此最初显示所有名称),并且应用被初始化,以便 WinJS UI 控件被激活。你可以看到我是如何在整个代码中编写消息来显示我正在处理的情况,这就是我如何得到如图 21-4 所示的消息。

执行搜索

启动应用当然很好,但是你已经知道怎么做了。要查看新的东西,选择搜索符,通过键入Win+Q或激活符栏并选择Search图标,如图图 21-6 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-6。*选择搜索符

当你激活搜索功能时,一个名为搜索窗格的新显示会覆盖在应用上,允许你进行搜索。有一个搜索词的文本输入框,在它下面你会看到一系列图标。这些图标代表搜索目标的范围,包括SearchContract示例 app,如图图 21-7 所示。(您可能需要向下滚动列表才能看到该应用。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示如果搜索窗格报告无法搜索到应用,您需要停止 Visual Studio 调试器,卸载SearchContract应用,然后再次启动 Visual Studio 调试器。在开发过程中,Windows 并不总是能够正确响应明显的更改,但对于通过 Windows 应用商店安装的应用来说,这不是问题。(我在本书的第五部分中向你展示了将你的应用发布到商店的过程。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-7。*搜索窗格

您会注意到列表中已经选择了SearchContract应用,这向用户表明搜索请求将被传递给该应用进行处理。

输入jac和搜索词,点击回车或点击文本输入右侧的图标进行搜索。该应用将执行指定术语的搜索,并显示匹配的名称,但搜索窗格将保持可见。

单击应用上的任意位置关闭搜索窗格,您将看到右侧ListView控件中显示的消息已经更新,如下所示:


Launch Activation App was not resumed Searched for: empty string **Search Activation** **App was not resumed** **Searched for: jac**


感兴趣的是我用粗体标记的新条目。当你提交搜索时,Windows 向应用发送了另一个activated事件,但这次args.detail.kind属性被设置为search。众所周知,当执行搜索激活时,系统包含用户正在搜索的字符串作为detail.queryText属性的值。当我收到一个搜索激活事件时,这是我在我的onactivated处理程序中搜索的术语:

... switch (args.detail.kind) {     case activation.ActivationKind.**search**:         ViewModel.writeMessage("Search Activation"); **        searchTerm = args.detail.queryText;**         break;     case activation.ActivationKind.launch:         ViewModel.writeMessage("Launch Activation");         searchTerm = "";         break; } ...

当然,此时应用正在运行,所以我不需要调用WinJS.UI.processAll方法,因为我所有的 UI 控件都已经应用并正常工作。我仍然希望有一个Promise对象来处理,所以我使用Promise.wrap方法(我在第九章中描述过)来创建一个将被立即实现的Promise,如下所示:

... if (args.detail.previousExecutionState     != activation.ApplicationExecutionState.suspended) {     ViewModel.writeMessage("App was not resumed");     promise = WinJS.UI.processAll(); } else {     ViewModel.writeMessage("App was resumed"); **    promise = WinJS.Promise.as(true);** } ...

每当用户执行额外的搜索时,该应用将接收额外的activated事件,其kindsearch。出于这个原因,我一直小心地构建我的代码,这样我就不会对我将要处理的事件的数量或种类做任何假设。

应用不运行时进行搜索

我想探索的最后一个场景要求应用已经关闭。停止调试器或在应用中按Alt+F4关闭应用。无需重启应用,从开始屏幕选择搜索符。

当您从开始屏幕中选择 Search Charm 时,搜索范围将设置为安装在设备上的应用的名称(视觉提示是在文本输入框下的列表中选择了Apps)。输入jac作为搜索词,并按回车键执行搜索。你会在图 21-8 中看到结果(除非你刚好有一个 app 的名字包含jac)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-8。*通用视窗搜索

现在点击列表中SearchContract应用的图标。(如果你一直遵循本章中的示例,应用将是列表中的第一项,因为 Windows 根据使用情况对应用进行排序——你或许可以从图中看出这一点。)

单击列表中的应用条目会将搜索范围更改为示例应用。该应用将被启动,显示在左侧ListView的名字将是那些匹配的搜索词。点击应用布局,关闭搜索窗格,查看右侧ListView显示的消息,如下所示:


Search Activation App was not resumed Searched for: jac


需要注意的重要一点是,应用没有接收到launch激活事件,只有search激活是由系统发送的。如果当用户执行针对该应用的搜索时,该应用没有运行,则系统将启动该应用,但它不会发送与通过其磁贴或 Visual Studio 调试器启动该应用时相同的事件。

知道了这一点,我需要确保当我得到一个搜索激活事件并且应用之前没有运行时,我可以正确地初始化应用——对于这个简单的应用,这仅仅意味着调用WinJS.UI.processAll方法,然后进行搜索。您现在可以理解为什么我把对前一个执行状态的检查从检查我接收到的事件的kind的代码中分离出来了:

... if (args.detail.previousExecutionState     != **activation.ApplicationExecutionState.suspended**) {     ViewModel.writeMessage("App was not resumed"); **    promise = WinJS.UI.processAll();** } else {     ViewModel.writeMessage("App was resumed");     promise = WinJS.Promise.as(true); } ...

总结搜索契约激活场景

重要的是要确保你知道什么时候初始化你的应用,什么时候你可以通过queryText属性获得一个搜索词。您已经在前面的章节中看到了各种场景,为了将来快速参考,我在表 21-2 中总结了这些排列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用搜索窗格

我在前面几节中向您展示的技术是搜索契约的基本实现,一旦您的应用中有了搜索功能,大部分工作就是确保您正确处理激活事件。

您还可以通过直接使用搜索窗格来创建定制的搜索体验,搜索窗格可通过Windows.ApplicationModel.Search名称空间中的对象获得。这个名称空间中的关键对象是SearchPane,它允许您访问 Windows 搜索窗格并与之集成。SearchPane对象定义了表 21-3 中所示的方法和属性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此外,SearchPane对象支持几个事件,我已经在表 21-4 中描述过了。这些事件在搜索过程的关键时刻触发,允许您以比使用基本契约实现更复杂的方式做出响应。

在接下来的部分中,我将向您展示一些使用这些方法、属性和事件的高级搜索技术,包括如何从应用布局中触发搜索过程,并提供 Windows 可以用来在搜索过程中帮助用户的不同类型的建议。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意除非我另有说明,否则我在本节中引用的对象都在Windows.ApplicationModel.Search名称空间中。

激活搜索

用户并不总是理解 Windows 8 搜索是如何工作的,这是一个遗憾,因为从系统范围的窗格中触发特定应用搜索的想法是一个很好的想法。如果搜索是应用功能的一个关键部分,您可能需要在应用布局中为用户提供一个用于打开搜索窗格的控件。为了演示这一点,我在default.html文件的标记中添加了一个button元素,如清单 21-6 所示。

清单 21-6 。添加一个打开搜索窗格的按钮

`…

    
        
            
        
    


        

    


        

        
    

**    

**
**        Show Search**
**    
**


        

        

    

...`

这并没有创造出最优雅的应用布局,但对我来说已经足够了。你可以在图 21-9 中看到结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-9。*在应用布局中添加显示搜索按钮

我已经将清单 21-7 中显示的语句添加到default.js文件中,以响应被点击的button

清单 21-7 。响应被点击的按钮

`(function () {
    “use strict”;

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

app.onactivated = function (args) {
        var searchTerm;
        var promise;

switch (args.detail.kind) {
            case activation.ActivationKind.search:
                ViewModel.writeMessage(“Search Activation”);                 searchTerm = args.detail.queryText;
                break;
            case activation.ActivationKind.launch:
                ViewModel.writeMessage(“Launch Activation”);
                searchTerm = “”;
                break;
        }

if (args.detail.previousExecutionState
            != activation.ApplicationExecutionState.suspended) {
            ViewModel.writeMessage(“App was not resumed”);
            promise = WinJS.UI.processAll().then(function () {
**                showSearch.addEventListener(“click”, function (e) {**
**                    search.SearchPane.getForCurrentView().show(ViewModel.searchTerm);**
**                });**
            });
        } else {
            ViewModel.writeMessage(“App was resumed”);
            promise = WinJS.Promise.as(true);
        }

args.setPromise(promise.then(function () {
            ViewModel.search(searchTerm);
        }));
    };

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

使用SearchPane对象的第一步是调用getForCurrentView方法,该方法返回一个SearchPane对象,您可以在其上执行操作。这意味着,如果您想要显示搜索窗格,就像我在示例中所做的那样,您必须使用:

... search.SearchPane.**getForCurrentView()**.show(ViewModel.searchTerm); ...

如果你试图在一个SearchPane对象上使用一个不是通过getForCurrentView方法获得的方法或属性,你将会创建一个异常。在清单中,我通过使用show方法显示搜索窗格来响应新添加的button中的click事件。我可以通过向show方法传递一个字符串来设置搜索窗格中的初始查询字符串,这允许我确保搜索窗格与应用布局左侧ListView中显示的数据一致。

如果您运行示例应用并单击Show Search按钮,将会显示标准的 Windows 搜索窗格。Search示例应用将被自动选择为搜索范围,就像您在本章前面通过 Search Charm 显示搜索窗格一样。

提供查询建议

使用搜索窗格时,我最喜欢的功能是直接在窗格中显示结果。这让用户可以在你的应用上执行渐进式搜索,每次查询框中的文本改变时,可能匹配的范围都会更新。

当搜索窗格可见,用户正在输入搜索词时,SearchPane对象将触发suggestionsrequested事件,这是邀请你向用户提供建议。你可以在清单 21-8 中的示例应用中看到我是如何做到这一点的,我已经为这个事件在default.js文件中添加了一个处理函数。

清单 21-8 。添加对建议请求事件的支持

`(function () {
    “use strict”;

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

app.onactivated = function (args) {
        var searchTerm;
        var promise;

switch (args.detail.kind) {
            case activation.ActivationKind.search:
                ViewModel.writeMessage(“Search Activation”);
                searchTerm = args.detail.queryText;
                break;
            case activation.ActivationKind.launch:
                ViewModel.writeMessage(“Launch Activation”);
                searchTerm = “”;
                break;
        }

if (args.detail.previousExecutionState
            != activation.ApplicationExecutionState.suspended) {
            ViewModel.writeMessage(“App was not resumed”);
            promise = WinJS.UI.processAll().then(function () {
**                var sp = search.SearchPane.getForCurrentView();**
                showSearch.addEventListener(“click”, function (e) {
                    sp.show(ViewModel.searchTerm);
                });
**                sp.addEventListener(“suggestionsrequested”, function (e) {**
**                    var query = e.queryText;**
**                    var suggestions = ViewModel.search(query, true);**
**                    suggestions.forEach(function (item) {**
**                        e.request.searchSuggestionCollection**
**                            .appendQuerySuggestion(item.name);**
**                    });**
**                });**
            });
        } else {
            ViewModel.writeMessage(“App was resumed”);
            promise = WinJS.Promise.as(true);
        }         args.setPromise(promise.then(function () {
            ViewModel.search(searchTerm);
        }));
    };

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

注意,我在从SearchPane.getForCurrentView方法返回的对象上调用了addEventListener方法。为了能够提供建议,我必须更新viewmodel.js文件,这样我的search方法就可以执行更新ListView控件的搜索以及仅仅生成建议的搜索。您可以在清单 21-9 的中看到search方法的新版本。

清单 21-9 。更新搜索方法以生成建议

... ViewModel.search = function (term, **suggestions**) {     ViewModel.writeMessage("Searched for: " + (term == "" ? "empty string" : term));     term = term.toLowerCase(); **    var target = suggestions ? [] : ViewModel.filteredNames;**     target.length = 0;     ViewModel.allNames.forEach(function (item) {         if (item.name.toLowerCase().indexOf(term) > -1) {             target.push(item)         }     }); **    if (!suggestions) {** **        ViewModel.searchTerm = term;** **    }**     return target; }; ...

如果suggestions参数存在并且true,那么该方法将生成并返回一个匹配数组,而不更新ListView使用的数据源。这允许我添加对建议的支持,而不必修改任何对search方法的现有调用。

对于本节,向您展示结果,然后解释所有涉及的对象是如何组合在一起的会更容易。要查看这些更改的效果,请启动应用,打开搜索窗格,然后键入jac。当您输入每个字母时,您会看到在文本输入框下方显示一个可能匹配的列表,如图图 21-10 所示。Windows 将显示您的应用提供的最多五个建议。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-10。*搜索窗格中显示的建议

每当您键入一个额外的字母时,该列表都会被优化。当你输入所有三个字母时,会显示四个建议,所有建议都包含术语jac。示例应用中的ListView在你输入时不会受到影响——但是如果你点击这些建议中的任何一个,系统将触发搜索激活,这将具有搜索该术语的效果。

理解建议示例

要想给系统提供建议,需要一长串的对象,但是请相信我——它并不像看起来那么复杂。一个SearchPaneSuggestionsRequestedEventArgs对象被传递给suggestionrequired事件的处理函数。除了有一个长得离谱的名字,这个对象定义了两个有用的只读属性,我已经在表 21-5 中描述过了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每次用户修改搜索文本时,queryText属性都会更新。当用户缩小搜索范围时,通常会收到一系列事件——可能从jqueryText值开始,然后是ja,最后是jac。您读取queryText属性的值,并使用 request 属性返回的SearchPaneSuggestionsRequest对象向系统提供可以呈现给用户的建议。SearchPaneSuggestionsRequest定义了我在表 21-6 中描述的方法和属性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们已经到达了这个链中的重要对象,即SearchSuggestionCollection对象,它可以通过传递给处理函数的事件对象的request.searchSuggestionCollection属性获得。SearchSuggestionCollection对象定义了四个方法和一个属性,我已经在表 21-7 中进行了总结。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最简单的方法是appendQuerySuggestion,它将一个字符串作为参数,并在搜索窗格中作为一个可能的查询呈现给用户。这是我在例子中使用的方法,正如你在清单 21-10 中看到的,在那里我重复了关键语句。

清单 21-10 。为用户提供查询建议

... sp.addEventListener("suggestionsrequested", function (e) {     var query = e.queryText;     var suggestions = ViewModel.search(query, true);     suggestions.forEach(function (item) {         e.request.searchSuggestionCollection.**appendQuerySuggestion**(item.name);     }); }); ...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示我为从ViewModel.search方法得到的每个建议调用appendQuerySuggestion方法。这对于我的示例应用来说是可行的,因为搜索 200 个名字没有任何显著的成本。但是,Windows 将显示不超过五个建议,因此如果生成结果的成本很高,您可以通过只生成五个匹配来节省资源。

给建议添加分隔符

方法允许你给你的建议增加一些结构。如果将对appendSearchSeparator的调用与对appendQuerySuggestion方法的调用交错,就可以创建一组结构化的建议。然而,在搜索窗格上仍然只有五个位置用于建议,所以您向建议添加的每一个分隔符都意味着您可以少提供一个结果。清单 21-11 显示了我对default.js文件中的suggestionsrequested处理函数所做的修改,以演示appendSearchSeparator方法的使用。

清单 21-11 。使用 appendSearchSeparator 方法向建议添加结构

`…
sp.addEventListener(“suggestionsrequested”, function (e) {
    var query = e.queryText;
    var suggestions = ViewModel.search(query, true);
**    var lastLetter = null;**
    suggestions.forEach(function (item) {
**        if (item.firstLetter != lastLetter) {**
**            e.request.searchSuggestionCollection.appendSearchSeparator(item.firstLetter);**
**            lastLetter = item.firstLetter;**
**        }**
        e.request.searchSuggestionCollection.appendQuerySuggestion(item.name);

});
});
…`

搜索建议没有固定的结构,所以你可以在应用中以任何有意义的方式应用分隔符。在这个例子中,我根据名字的第一个字母来分隔名字(这样做时,我依赖于这样一个事实,即由ViewModel.search方法返回的名字是按字母顺序排序的)。你可以在图 21-11 中看到结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-11。*搜索建议添加分隔符的效果

要查看示例应用中的效果,您需要找到一个匹配以不同字母开头的名称的搜索字符串。在示例中,我使用了字符串aa,它匹配AaliyahAaronIsaac。您可以在图中看到,这些名称已经使用我传递给appendSearchSeparator方法的值进行了分隔。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示我建议你在提供搜索结果时使用分隔符之前仔细考虑一下,因为它们会占用宝贵的空间,而这些空间可以用来给用户提供额外的建议。只有当没有分隔符的建议对用户没有意义时,才使用分隔符。

提供结果建议

当用户正在搜索的术语与你的应用中的数据项完全匹配时,你可以给出一个结果建议。这为用户提供了该项目的概述,并帮助他决定他是否已经找到了他正在寻找的东西。你可以在图 21-12 中看到一个例子,我在这里搜索过alexAlex是我正在处理的列表中的一个名字,也是其他名字的一部分,比如AlexaAlexander,这就是为什么你会看到混合的查询建议(到目前为止我一直在做的那种建议)和结果建议。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 21-12。*搜索窗格中显示的结果建议

使用SearchSuggestionCollection.appendResultSuggestion方法提出结果建议,该方法需要表 21-8 中描述的参数。这些附加参数用于提供您可以在图中看到的有关该项目的附加信息。

所有参数都是必需的。对于这个例子,我在示例项目中添加了一个简单的图像作为img/user.png,这个图像显示在建议的Alex结果旁边。您可以在清单 21-12 中看到我是如何创建建议的,它显示了我对default.js文件所做的更改。

清单 21-12 。增加对结果建议的支持

... sp.addEventListener("suggestionsrequested", function (e) {     var query = e.queryText;     var suggestions = ViewModel.search(query, true);     var lastLetter = null;     suggestions.forEach(function (item) { **        if (query.toLowerCase() != item.name.toLowerCase()) {**             e.request.searchSuggestionCollection.appendQuerySuggestion(item.name); **        } else {** **            var imageSource = Windows.Storage.Streams.RandomAccessStreamReference.** **                createFromUri(Windows.Foundation.Uri("ms-appx:img/user.png"));** **            e.request.searchSuggestionCollection.appendResultSuggestion(** **                item.name,** **                "This name has " + item.name.length + " chars",** **                item.name, imageSource, item.name);** **        }**     }); }); ...

我的数据并不完全适合这个模型——这并不罕见。我发现数据项要么太复杂而不能用作结果建议,要么太简单,导致我不得不添加一些填充数据。在这个例子中,数据过于简单。

对于textdetailText参数,我使用了匹配的名称和一个报告名称中有多少字符的字符串——这不是有用的数据,但它是您最终使用的填充类型。如果您真的没有什么有用的东西要说,您可以将detailText参数设置为空字符串,但是这样会产生一个看起来有点奇怪的建议。

我将暂时跳过tag参数,来看看image参数。我包含了我想作为项目一部分使用的图像,但是图像参数必须是一个Windows.Storage.Streams.IRandomAccessStreamReference对象。

我在《??》第二十二章中介绍了 Windows 对文件处理的支持,所以现在我将把image参数所需的代码作为黑盒咒语呈现出来。从应用包加载图像所需的咒语类似于我在加载应用数据文件时向您展示的技术,但是将Windows.Foundation.Uri对象传递给Windows.Storage.Streams. RandomAccessStreamReference.createFromUri方法,以创建加载建议图像所需的对象类型:

... Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(     Windows.Foundation.Uri("**ms-appx:img/user.png**")); ...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意不要忘记在指定图像文件的 URL 中有三个///字符——一个常见的错误是只使用两个。这不会产生错误,但是您不会看到作为结果建议的一部分显示的图像。

选择建议后做出回应

您可以在清单中看到,我也为tag参数使用了这个名称。此参数必须唯一标识您建议的数据项。系统并不关心这个值是什么——如果用户点击了建议,它就把它返回给你。当这种情况发生时,SearchPane对象触发一个resultsuggestionchosen事件,并通过事件对象的tag属性将您提供的tag值传递回您的应用。清单 21-13 显示了为resultsuggestionchosen事件添加一个处理函数,其中我将标签值传递给ViewModel.search方法以反映用户的选择。

清单 21-13 。为 resultsuggestionchosen 事件添加事件处理程序

... sp.addEventListener("suggestionsrequested", function (e) {     var query = e.queryText;     var suggestions = ViewModel.search(query, true);     var lastLetter = null;     suggestions.forEach(function (item) {         if (query.toLowerCase() != item.name.toLowerCase()) {             e.request.searchSuggestionCollection.appendQuerySuggestion(item.name);         } else {             var imageSource = Windows.Storage.Streams.RandomAccessStreamReference                 .createFromUri(Windows.Foundation.Uri("ms-appx:img/user.png"));             e.request.searchSuggestionCollection.appendResultSuggestion(                 item.name,                 "This name has " + item.name.length + " chars",                 item.name, imageSource, item.name);         }     }); }); **sp.addEventListener("resultsuggestionchosen", function (e) {** **    ViewModel.search(e.tag);** **});** ...

处理建议历史

Windows 维护用户接受的建议结果和查询的历史记录,并在再次执行相同的搜索时给予他们优先权。要了解这是如何工作的,请启动应用并搜索jo。您将看到搜索窗格中显示的建议是:


Joel John Jonathan Jordan Jose


点击Jordan完成搜索——你会看到应用布局中的ListView控件被更新以匹配你的搜索,搜索框也被更新以显示Jordan。清除搜索框,再次输入jo。这一次,名字出现的顺序有所不同:


**Jordan** Joel John Jonathan Jose


这对用户来说是一个有用的帮助,并且搜索历史是持久的,这意味着当用户做出更多选择时,它将向用户提供更好的建议。

您可以通过将SearchPane.searchHistoryEnabled属性设置为false来禁用建议历史。这可以防止用户的选择被添加到历史记录中,并确保您的建议按照您提供的顺序呈现给用户。您可以在清单 21-14 中看到我是如何使用searchHistoryEnabled属性的。

清单 21-14 。禁用搜索历史

... if (args.detail.previousExecutionState     != activation.ApplicationExecutionState.suspended) {     ViewModel.writeMessage("App was not resumed");     promise = WinJS.UI.processAll().then(function () {         var sp = search.SearchPane.getForCurrentView(); **        sp.searchHistoryEnabled = false;**         showSearch.addEventListener("click", function (e) {             sp.show(ViewModel.searchTerm);         });         sp.addEventListener("suggestionsrequested", function (e) {             // *... code removed for brevity...*         });         sp.addEventListener("resultsuggestionchosen", function (e) {             ViewModel.search(e.tag);         });     }); } else {     ViewModel.writeMessage("App was resumed");     promise = WinJS.Promise.as(true); } ...

如果您启动应用并再次运行对jo的搜索,您将会看到Jordan在建议列表中没有被优先考虑。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示如果你的应用支持不同的数据搜索方式或者支持搜索不同的数据集,你可以使用SearchPane.searchHistoryContext属性为每个数据集保存单独的搜索历史。给这个属性分配一个代表用户将要执行的搜索类型的值,下次使用相同的值给searchHistoryContext属性时,Windows 将只考虑用户所做的选择。

异步提出建议

到目前为止,我已经同步生成了我的所有建议,这很好,因为我的所有数据都存储在内存中,可以立即使用。

如果生成建议所依赖的代码返回一个Promise而不是直接给你数据,你就需要采取不同的方法。当您的数据包含在文件或一个WinJS.UI.IListDataSource对象中时,您会经常发现这种情况,该对象从它的许多方法中返回Promise对象,并在满足Promise时产生数据。

为了演示这个问题,我在我的viewmodel.js类中添加了一个ViewModel.asyncSuggest方法。这个方法使用了我在这一章中一直使用的数据,但是它通过一个Promise来呈现结果,这个结果只有在搜索完成后才能实现。为了更真实地展示这一点,搜索是作为一系列小操作来执行的,这些小操作与对setImmedate方法的调用交织在一起,以允许 JavaScript 运行时执行其他操作。你可以在清单 21-15 中看到增加的内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示关于WinJS.Promise对象和setImmediate方法的详细信息,请参见第九章。

清单 21-15 。一种异步搜索方法

`…
ViewModel.asyncSuggest = function (term) {
    return new WinJS.Promise(function (fDone, fError, fProgress) {
        var index = 0;
        var blockSize = 10;
        var matches = [];
        term = term.toLowerCase();

function searchBlock() {
            for (var i = index; i < index + blockSize; i++) {
                if (ViewModel.allNames[i].name.toLowerCase().indexOf(term) > -1) {
                    matches.push(ViewModel.allNames[i].name);
                }
            }
            index += blockSize;
            if (index < ViewModel.allNames.length) {                 setImmediate(searchBlock);
            } else {
                fDone(matches);
            }
        }
        setImmediate(searchBlock);
    });
}
…`

该方法返回一个WinJS.Promise对象,当查询词的所有名称都被搜索到时,该对象被满足。搜索本身是在 10 个名字的块中执行的,并且在每个块之后调用setImmediate方法,以便 JavaScript 运行时可以执行其他未完成的工作,比如流程事件。

要使用异步方法生成建议,您必须调用SearchPaneSuggestionRequest.getDeferral方法。通过传递给suggestionsrequested事件处理函数的对象的request属性可以获得SearchPaneSuggestionRequest对象。getDeferral方法返回一个SearchPaneSuggestionsRequestDeferral对象,该对象定义了一个方法:complete。当异步建议生成方法返回的Promise被满足时,调用complete方法。清单 21-16 展示了我是如何将这项技术应用到例子中的suggestionsrequested处理程序的。

清单 21-16 。使用异步方法生成建议

... sp.addEventListener("suggestionsrequested", function (e) { **    var deferral = e.request.getDeferral();**     ViewModel.asyncSuggest(e.queryText).then(function (suggestions) {         e.request.searchSuggestionCollection.appendQuerySuggestions(suggestions); **        deferral.complete();**     }); }); ...

当满足Promise时,记住调用complete方法是很重要的。如果您忘记这样做,不会报告错误,但是用户不会看到您提供的建议。

总结

在这一章中,我已经向你展示了如何实现第一个 Windows 契约,它允许一个应用与系统范围的特性相集成。我向你展示了搜索契约,通过实施这一契约,一个应用能够无缝地参与搜索,提供其内容和数据以及其他应用的内容和数据。

我还向您展示了如何通过直接使用搜索窗格来定制搜索体验。使用搜索窗格,我向您展示了如何提供查询和结果建议,以帮助用户在您的应用中找到他想要的东西。Windows 对搜索的支持非常灵活,我建议你花时间以一种有用和有意义的方式将其集成到你的应用中。在下一章,我将向你展示如何在 Windows 应用中处理文件。这是一个重要的功能领域,我将在接下来的三章中全面介绍。

二十二、使用文件

这是关注用户文件的两章中的第一章,也就是说用户创建、更改和存储的文件不同于我在第二十一章中描述的应用数据和文件。

在这一章中,我将向您展示如何直接操作文件和文件夹来执行基本的文件操作,例如复制和删除文件。接下来,我将向您展示 Windows 应用的功能,这些功能可让您更全面地查看文件,包括对文件列表进行排序、过滤文件夹内容、执行复杂的文件搜索、创建虚拟文件夹以将相关文件分组在一起,以及监控文件夹以便在添加新文件时通知您。

这些技术让我为下一章做好了准备,下一章将展示如何将您的应用及其与文件和文件夹相关的功能集成到更广泛的 Windows 体验中。因此,简而言之,这一章的所有内容都是关于处理你的应用中的文件系统,而下一章是关于向用户展示这些功能。表 22-1 对本章进行了总结。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建示例应用

对于这一章,我已经创建了一个名为UserFiles的新示例项目。我需要一些示例文件,所以我创建了一个img/flowers文件夹,并添加了一些 JPG 图片。这些是我在第二十一章中使用的相同图像,我已经将它们包含在本书附带的源代码下载中(可从apress.com获得)。您可以在图 22-1 中看到文件名列表,它显示了UserFiles项目的解决方案资源管理器窗口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-1。*向示例应用添加图像文件

该项目的其余部分是一个简单的应用。您可以在清单 22-1 中看到default.html文件的内容。布局中有两个面板——一个包含一系列触发本章示例代码的按钮,另一个包含一个我用来显示消息的WinJS.UI.ListView控件。WinJS.Binding.Template控件用于显示ListView中的信息。

清单 22-1 。用户文件项目的 default.html 文件

`

          UserFiles                ` `     **
**         **
**     **
**


         Copy Files
         Copy Files (Ordered)
         Delete Files
         Sort Files
         Filter (Basic)
         Filter (AQS)
         Common Query
         Group (Type)
    


         <div id=“list” data-win-control=“WinJS.UI.ListView”
             data-win-options="{
                 itemDataSource: ViewModel.State.messages.dataSource,
                 itemTemplate: template,
                 layout: {type:WinJS.UI.ListLayout}
           ** }">**
        

    

`

你可以看到我在css/default.css文件中定义的 CSS 来管理这些元素在清单 22-2 中的布局。

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

`body {
    display: -ms-flexbox;
    -ms-flex-direction: row;
    -ms-flex-align: center; -ms-flex-pack: center;
}

.container {
    height: 80%; margin: 10px; padding: 10px;
    border: medium solid white;
}

#buttonsContainer button, .message {
    display: block; font-size: 20pt;
    width: 100%; margin-top: 10px;
}

#listContainer {width: 30%;}
#list { height: 100%;}`

我在js/viewmodel.js文件中定义了一些基本的应用状态和一些实用函数,你可以在清单 22-3 中看到。这些支持显示在ListView控件中的信息。

清单 22-3 。示例应用的 viewmodel.js 文件

`(function () {

WinJS.Namespace.define(“ViewModel”, {
        State: WinJS.Binding.as({
            messages: new WinJS.Binding.List()
        })
    });

WinJS.Namespace.define(“App”, {
        writeMessage: function (msg) {
            ViewModel.State.messages.push({ message: msg });
        },
        clearMessages: function () {
            ViewModel.State.messages.length = 0;
        }
    });

App.writeMessage(“Ready”);
})();`

最后,你可以在清单 22-4 中看到js/default.js文件。在这里,我将通过响应default.html文件中按钮元素的click事件来添加示例代码,但目前该文件只包含基本的应用管道。

清单 22-4 。示例应用的初始 default.js 文件

`(function () {

var $ = WinJS.Utilities.query;
    var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    var storage = Windows.Storage;
    var search = Windows.Storage.Search;

var imageFileNames = [“astor.jpg”, “carnation.jpg”, “daffodil.jpg”,
        “lily.png”,“orchid.jpg”, “peony.jpg”, “primula.jpg”, “rose.png”, “snowdrop.jpg”];

app.onactivated = function (args) {
        if (args.detail.previousExecutionState !=
            activation.ApplicationExecutionState.suspended) {

args.setPromise(WinJS.UI.processAll().then(function() {
                $(‘#buttonsContainer > button’).listen(“click”, function (e) {
                    App.clearMessages();
                    switch (e.target.id) {
                        // …code for examples will go here…
                    }                 });
            }));
        }
    };
    app.start();
})();`

我定义了一个名为imageFileNames的数组,它包含了img/flowers文件夹中的文件名。在接下来的几节中,我将在switch语句中添加代码来响应各个按钮。如果你现在启动示例应用,你会看到如图图 22-2 所示的基本布局。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-2。*示例 app 的布局

执行基本的文件操作

在接下来的小节中,我将向您展示如何执行一些基本的文件操作。这将允许我演示使用Windows.Storage对象处理用户文件而不是应用文件的区别,并提供一些关于应用在处理文件系统时所受限制的细节。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示我在本章中没有提到的一个基本文件操作是读取文件的内容。你可以在第二十一章(我演示了如何读取应用数据文件的内容)和第二十三章(我展示了如何在应用布局中使用文件内容)中看到这一点。

复制文件

在这一节中,我将向您展示如何将应用包中包含的花卉图像文件复制到用户的My Pictures文件夹中。您可以在清单 22-5 的中看到我对default.js文件中的switch语句所做的补充。在 Windows 应用中处理文件的代码相当冗长,所以我将只展示我对每个示例所做的修改。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意除非我另外明确说明,否则我在本章中描述的新对象都在Windows.Storage名称空间中,在本例中我将其别名为storage

清单 22-5 。增加从应用包复制文件到文件系统的支持

... switch (e.target.id) {     **case 'copyFiles':**         **storage.KnownFolders.picturesLibrary.createFolderAsync("flowers",**             **storage.CreationCollisionOption.replaceExisting)**         **.then(function(folder) {**             **imageFileNames.forEach(function (filename) {**                 **storage.StorageFile.getFileFromApplicationUriAsync(**                     **Windows.Foundation.Uri("ms-appx:img/" + filename))**                 **.then(function (file) {**                     **file.copyAsync(folder).then(function () {**                         **App.writeMessage("Copied: " + filename);**                     **}, function (err) {**                         **App.writeMessage("Error: " + err);**                     **});**                 **});**             **});**         **});**         **break;** } ...

这比看起来要复杂得多,因为大多数与文件相关的方法都使用了Promise对象。我将一步一步地浏览这段代码,并解释发生了什么。一旦理解了基本结构,您会发现本节中的其余示例更容易解析。

定位文件夹

我想做的第一件事是找到我的目标文件夹。当涉及到使用文件系统时,Windows 应用被置于严格的限制之下,只有某些位置你可以在没有用户明确许可的情况下写入(这种许可是通过文件选择器来表达的,我在第二十三章中进行了演示)。在第二十三章之前,我不会向您介绍文件拾取器,这意味着我被限制在一个非常小的预定义位置集合中,可以通过KnownFolder对象访问这些位置。该对象定义了表 22-2 中所示的属性,这些属性与常见的 Windows 文件位置相关。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示甚至访问由KnownFolder对象定义的位置集也受到限制。想要读写这些位置的应用必须在它们的清单中做一个声明,我将在本章后面演示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个例子中,我将应用包中的文件复制到用户的Pictures文件夹中,如清单 22-6 所示。

清单 22-6 。使用用户的图片文件夹

... storage.**KnownFolders.picturesLibrary**.createFolderAsync("flowers",     storage.CreationCollisionOption.replaceExisting) ...

每个属性返回一个代表文件系统位置的StorageFolder对象。一旦我得到代表Pictures文件夹的StorageFolder,我就调用createFolderAsync方法来创建一个flowers文件夹。这将是我复制图像文件的位置。在第二十一章中,我向你展示了StorageFolder对象定义的创建或定位StorageFile对象的基本方法,但是StorageFolder也为其他文件夹定义了有用的方法。表 22-3 描述了这些方法中最有用的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当使用createFoldersAsyncrenameAsync方法时,你可以使用一个由CreationCollisionOption对象定义的值来提供一个可选参数,我在表 22-4 中总结了这个值。

在示例中,我使用了replaceExisting选项,这将删除任何名为flowers的现有文件夹,并用一个新的空文件夹替换它。由createFolderAsync方法返回的Promisethen方法传递一个StorageFolder对象,该对象代表我感兴趣的flowers文件夹。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 警告删除文件夹当然是破坏性的。在运行此示例应用之前,您应该确保您的Pictures文件夹中没有名为flowers的文件夹;否则你会丢失你的文件。

复制文件

现在我有了一个代表文件目标的StorageFolder对象,我可以开始复制它们了。我通过代表单个文件的StorageFile对象来实现。首先,我为我的应用包中的每个文件获取一个StorageFile,然后依次为它们调用copyAsync方法,如清单 22-7 所示。

清单 22-7 。将 app 包中的每个文件复制到目标文件夹

... switch (e.target.id) {     case 'copyFiles':         storage.KnownFolders.picturesLibrary.createFolderAsync("flowers",             storage.CreationCollisionOption.replaceExisting)         .then(function(folder) {             **imageFileNames.forEach(function (filename) {**                 **storage.StorageFile.getFileFromApplicationUriAsync(**                     **Windows.Foundation.Uri("ms-appx:img/" + filename))**                 **.then(function (file) {**                     **file.copyAsync(folder).then(function () {**                         **App.writeMessage("Copied: " + filename);**                     **}, function (err) {**                         **App.writeMessage("Error: " + err);**                     **});**                 **});**             **});**         });         break; } ...

这是一个很好的例子,说明当您使用许多异步方法来执行相关操作时,会产生冗长的代码。确定给定的Promise对象上运行的函数可能需要一段时间——尽管随着你习惯于编写 Windows 应用,这变得更加容易。

我在清单中突出显示的代码列举了我在imageFileNames数组中定义的每个文件名,并使用StorageFile.getFileFromApplicationUriAsync方法获得一个依次代表每个图像的StorageFile对象。这是我在《??》第二十一章中展示的技术,依靠Windows.Foundation.Uri物体。

StorageFile对象是对StorageFolder对象的补充,代表文件系统中的单个文件。一旦我有了一个StorageFile对象,我就可以调用copyAsync方法。这个方法的参数是StorageFolder对象,它代表文件应该被复制到的位置。我提供了我在上一节中创建的flowers文件夹。我已经在由copyAsync方法返回的Promise上使用了then方法来编写成功和错误消息,这些消息将显示在应用布局的ListView控件中。我在表 22-5 中总结了常用的StorageFile方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

启用清单中的功能

如果您在此时运行示例应用并单击Copy Files按钮,您将会看到 Visual Studio 中显示的一条消息,如图 22-3 中的所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-3。*试图访问文件系统时显示错误

正如我前面提到的,应用对文件系统的访问非常有限。如果你想访问一个不是由用户通过文件选择器提供的位置(我会在第二十三章中解释),那么你必须使用由KnownFolders对象定义的一个位置。您还必须在应用清单中声明您的应用能够访问该位置。如果您不这样做,一旦您的应用尝试访问文件系统,您就会看到如图所示的错误。

要声明该功能,在 Visual Studio Solution Explorer窗口中双击package.appxmanifest文件打开清单,并导航到Capabilities选项卡。你会看到能力列表包括KnownFolders位置的项目,我在图 22-4 中突出显示了这些项目。如图所示,我已经检查了与KnownFolders.picturesLibrary位置相对应的Pictures Library能力。(Internet (Client)功能在您创建新的 Windows 应用项目时默认选中。)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-4。*允许访问应用清单中的位置

您必须为每个想要使用的位置启用该功能。这将为您的应用提供该位置的访问权限,并向您的应用的权限窗格(可通过 Settings Charm 获得)添加一个条目,如果您通过 Windows 应用商店分发您的应用,该条目将显示给用户。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示您应该只检查您需要的功能。这个想法是为了给用户提供他们需要的信息,让他们在把你的应用安装到他们的设备上之前,对他们的信任程度做出明智的选择。实际上,用户在任何平台上都不会太关注这些信息,他们会下载并安装任何看起来有趣的东西。即使如此,要求您需要的最低能力也是一个好的做法。

通过添加响应按钮点击的代码并在清单中声明该功能,我已经可以将项目中的文件复制到设备文件系统中了。

为了测试这个例子,启动应用并点击Copy Files按钮。你会在布局中间面板的ListView控件中看到一系列消息,报告每个文件都被复制,如图图 22-5 所示。如果您打开文件浏览器并导航到您的Pictures库,您将看到一个包含样本图像的 flowers 文件夹。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-5。*将文件从 app 包复制到文件系统

在我继续之前,有几点需要注意。首先,我已经从应用包中复制了文件,因为这是我设置和分发示例的最简单的方法。虽然这在实际项目中可能是一项有用的技术,但是您当然可以在您的应用清单中声明的任何已知位置上执行文件操作(我将在第二十三章中向您展示如何获得用户许可来处理其他位置的文件)。

要注意的第二点是,由于我在使用createFolderAsync方法时指定的碰撞选项,单击Copy Files按钮将删除flowers文件夹,创建一个新文件夹,并再次复制文件。

确保复制顺序

如果您重复点击Copy Files按钮,您将会看到ListView控件中显示的消息以不同的顺序显示。你可以在图 22-6 中看到效果,这里我已经显示了三次点击按钮的消息顺序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-6。*消息顺序的变化

在第九章的中,当我解释Promise对象如何工作时,我说过 JavaScript 维护一个要完成的工作队列,使用Promise s 通过将工作放在那个队列中进行后续处理来推迟工作——这就是setImmediate方法所做的。

然而,当您使用 Windows API(包括Windows.Storage)时,您的工作可以并行完成,因为 API 的这一部分中的对象是使用支持并行执行任务的语言实现的。JavaScript 隐藏了这方面的细节,它没有对并行任务的内置语言支持,但这确实意味着操作完成的顺序可以不同。

如果您不关心操作执行和完成的顺序,那么您不必采取任何特殊的操作。如果你真的在乎,那么你需要改变你调用异步方法的方式,这样你只需要在前一个操作完成时复制一个文件,如清单 22-8 所示。我添加了这段代码,以便在点击Copy Files (Ordered)按钮时执行。

清单 22-8 。强制按给定顺序执行复印操作

... switch (e.target.id) {     case 'copyFiles':         // *...statements removed for brevity...*         break;     **case 'copySeq':**         **var index = 0;**         **function copyFile(index, folder) {**             **return storage.StorageFile.getFileFromApplicationUriAsync(**                 **Windows.Foundation.Uri("ms-appx:img/" +**                    **imageFileNames[index]))**             **.then(function(file) {**                 **return file.copyAsync(folder).then(function () {**                     **App.writeMessage("Copied: " + imageFileNames[index]);**                 **}).then(function() {**                     **index++;**                     **if (index < imageFileNames.length) {**                         **return copyFile(index, folder);**                     **}**                 **})**             **});**         **}**         **storage.KnownFolders.picturesLibrary.createFolderAsync("flowers",**             **storage.CreationCollisionOption.replaceExisting)**         **.then(function (folder) {**             **copyFile(index, folder).then(function () {**                 **App.writeMessage("All files copied");**             **});**         **});**         **break;** } ...

在这个清单中,我使用我在第二十一章中描述的技术将复制操作链接在一起,其效果是按照在imageFileNames数组中定义的顺序复制图像。这是另一段看起来曲折的代码,但是基本模式——定义一个返回Promise并递归调用自身的函数——与《??》第九章中的链接示例是一样的。

只有在一致性非常重要的情况下,才应该强制异步操作的顺序,因为 Windows 能够并行执行多种操作,这种技术会强制序列化,因此会严重影响性能。

删除文件

既然您已经看到了几个文件操作示例,您会发现本节的其余部分更容易理解。如果你还没有理解使用异步方法处理文件的想法,你应该考虑重温一下第九章——如果你还没有很好地掌握Promise对象如何工作,你会发现例子越来越难理解。

在这一节中,我将演示如何删除文件。这很简单,但是我想强调这种方法的共性——一旦你理解了一个文件操作是如何工作的,你就可以很快地创建其他的文件操作。在清单 22-9 中,您可以看到我添加到default.js文件中的代码,以响应被点击的Delete Files按钮。

清单 22-9 。删除文件

... switch (e.target.id) {    // *...statements omitted for brevity...*    **case 'deleteFiles':**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**         **.then(function (folder) {**             **folder.getFilesAsync().then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **storageFile.deleteAsync(storage.StorageDeleteOption.default);**                         **App.writeMessage("Deleted: " + storageFile.name);**                     **});**                 **}**             **});**         **})**         **break;** } ...

在这个清单中,我使用了getFilesAsync方法来获取代表 flowers 目录中文件的一组StorageFile对象。如果数组是空的,那么我知道文件夹中没有文件,并在ListView控件中显示一条适当的消息。如果数组中有StorageFile对象,那么我使用forEach方法枚举数组内容并调用deleteAsync方法,这将删除文件。该方法的参数是来自StorageDeleteOption对象的一个值,它指定了执行的删除类型。我在表 22-6 中描述了这些值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例如,我指定了default值,这意味着如果你启动应用并单击Delete Files按钮,你将能够在 Windows 回收站中找到已删除的文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示您会注意到,在这个例子中,当我向ListView写消息时,我使用了StorageFile.name属性。StorageFile对象定义了许多属性,您可以用它们来获取关于文件的信息——我将在下一节更详细地解释这一点。

整理和过滤文件

在前一个例子中,我称为StorageFolder.getFilesAsync方法的StorageFile对象数组有两个定义特征。第一个是我得到了文件夹中所有文件的和第二个是数组中按字母顺序排列的对象,基于文件名。

在这一节中,我将向您展示如何执行文件查询,这允许您对检索的文件进行更多的选择,并以不同的方式对结果进行排序。本节中的新对象位于Windows.Storage.Search名称空间中,在示例中我将其别名为search

对文件进行排序和过滤有两个原因。第一个是应用中的通用工具——你可能只想对满足某种标准的文件执行操作,对从StorageFolder中获得的文件进行过滤和排序是一种很好的方式。对文件进行分类和过滤的另一个原因是,这些技术支撑了一些关键的接触点,使你能够将你的应用集成到更广泛的 Windows 平台中,并向你的用户呈现一个处理文件的一致模型——这是我在第二十三章中再次提到的主题。

整理文件

在 Windows 应用中,对文件的选择顺序进行排序是文件查询的一种简单形式。在后面的小节中,我将向您展示一些功能,这些功能更像是与单词 query 相关联的东西,但是从排序开始,让我介绍所需的对象,并解释它们是如何组合在一起的。首先,清单 22-10 显示了我添加到default.js文件中的内容,以便在点击Sort Files按钮时做出响应。(如果您最近删除了文件,您需要再次点击Copy Files按钮,以便在flowers文件夹中有要处理的文件。)

清单 22-10 。使用文件查询对文件进行分类

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'sortFiles':**         **var options = new search.QueryOptions();**         **options.sortOrder.clear();**         **options.sortOrder.push({**             **ascendingOrder: false,**             **propertyName: "System.ItemNameDisplay"**         **});**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**         **.then(function (folder) {**             **folder.createFileQueryWithOptions(options).getFilesAsync()**             **.then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **App.writeMessage("Found: " + storageFile.name);**                     **});**                 **}**             **});**         **});**         **break;** } ...

您使用一个QueryOptions对象控制文件的选择和排序方式,创建其中一个是我在清单中采取的第一个动作。我创建了这个没有任何构造函数参数的对象,它使用默认设置。

属性返回一个对象数组,这些对象按顺序应用来对文件夹中的文件进行排序。每个对象都有一个propertyName属性和一个ascendingOrder属性,前者指定执行排序的文件的属性,后者指定排序的方向。

在示例中,您可以看到我已经通过调用QueryOptions.sortOrder.clear方法开始了。这是因为默认的QueryOptions对象是用一个sortOrder对象创建的,该对象根据文件名对文件进行升序排序。如果不调用clear方法,您定义的任何自定义排序都将仅在默认排序之后应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示sortOrder属性是只读的。这意味着您需要通过更改属性返回的数组的内容来创建排序指令,而不是分配一个新的数组。

清除默认排序后,我添加自己的排序,如下所示:

... options.sortOrder.push({     ascendingOrder: false,     propertyName: "**System.ItemNameDisplay**" }); ...

该语句向sortOrder数组添加了一个新对象,该对象按照名称以逆序对文件进行排序,这是默认排序的一个简单变体。propertyName属性值System.ItemNameDisplay是 Windows 定义的一组广泛的文件属性之一。这样的房产有 125 个,太多了,我无法在此一一列举。相反,我在表 22-7 中列出了常用的。对于列表中的每一个条目,我都详细描述了当示例应用中的snowdrop.jpg文件被复制到Pictures库中并且拥有完整的路径C:\Users\adam\Pictures\flowers\snowdrop.jpg时,属性会显示什么。

除了 125 个基本属性(包括表中的属性)之外,还有数百个更具体的属性。例如,您可以在音频文件中找到一整套的System.Music属性,在视频文件中可以找到System.Media属性。你可以在[msdn.microsoft.com/en-us/library/windows/desktop/ff521735(v=vs.85).aspx](http://msdn.microsoft.com/en-us/library/windows/desktop/ff521735(v=vs.85).aspx)获得完整名单的详细信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示这些是字符串值,尽管它们看起来像是对System名称空间中对象的引用。在将这些值分配给propertyName属性时,必须用引号将它们括起来。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦我定义并配置了我的QueryOptions对象,我就调用我对其内容感兴趣的StorageFolder对象的getFilesAsync方法,并返回一个由按照我指定的顺序排序的StorageFile对象组成的数组。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示应用排序时,使用有效的文件系统属性名很重要。如果您提供的名称无效,应用将被终止,不会发出警告。

在示例中,我枚举了排序数组的内容,并显示了每个文件的名称。为了得到这个名字,我使用了一个由StorageFile对象定义的描述性属性,我在表 22-8 中总结了这个属性。我再次展示了每个属性为snowdrop.jpg文件返回的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中一些属性提供了用户友好的值,您可以直接将其包含在应用布局中,而其他属性则更具技术性和细节性,旨在用于您的代码中。

文件系统属性和通过StorageFile属性可用的属性之间有很好的映射,但是并不是所有 125 个文件系统属性都可用。如果你想得到一个特定的值,那么你可以使用StorageFile.properties.retrievePropertiesAsync方法,如清单 22-11 所示。

清单 22-11 。从 StorageFile 对象获取文件系统属性值

... file.properties.retrievePropertiesAsync(["System.ItemType"]).then(function (props) {      var value = props["System.ItemType"]; }); ...

我没有将这种技术结合到例子中,因为StorageFile属性足以满足我的需要。如果你启动 app,点击Sort Files按钮,你会看到文件按名字降序排列(即按字母顺序倒序排列),如图图 22-7 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-7。*列出文件时应用自定义排序顺序

过滤文件

除了排序,您还可以使用QueryOptions对象来过滤从getFilesAsync方法获得的StorageFile对象。QueryOptions对象定义了许多支持过滤的属性,我已经在表 22-9 中描述过了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些过滤属性分为两类——基本AQS——我将在接下来的章节中解释这两种属性。三个基本的过滤属性是fileTypeFilterfolderDepthindexOption。您可以在清单 22-12 中看到这些正在使用的属性,它列出了我添加到default.js文件中的代码,以响应被点击的Filter (Basic)按钮。

清单 22-12 。执行基本过滤

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'filterBasic':**         **var options = new search.QueryOptions();**         **options.fileTypeFilter.push(".doc", ".jpg", ".pdf");**         **options.folderDepth = search.FolderDepth.shallow;**         **options.indexerOption = search.IndexerOption.useIndexerWhenAvailable;**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**         **.then(function (folder) {**             **folder.createFileQueryWithOptions(options).getFilesAsync()**             **.then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **App.writeMessage("Found: " + storageFile.name);**                     **});**                 **}**             **});**         **});**         **break;** } ...

这个例子在结构上类似于我之前展示的排序例子,除了我使用了基本的过滤属性。当然,您可以将排序和过滤属性结合起来——我将它们分开显示,只是因为这样便于解释属性值。

过滤文件类型

在清单中,我从QueryOptions.fileTypeFilter属性开始,它过滤掉没有您指定的文件前缀的任何文件。这是另一个需要修改从属性获取的数组内容的实例,而不是分配一个新的数组。

在这个例子中,我使用了由 JavaScript 数组定义的push方法来指定我想要的文件扩展名为.doc.jpg.pdf的文件。我的示例应用只有.jpg.png文件,因此效果是从查询中排除了.png文件,但是我已经指定了多个值来向您展示这是如何做到的。该属性的默认值是一个空数组,表示不应根据文件扩展名排除任何文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 提示文件扩展名以前导句点指定,即。.pdf而不是pdf

设置搜索深度

使用由FolderDepth对象定义的值之一来设置QueryOptions.folderDepth属性,我已经在表 22-10 中描述过了。

我在清单中选择了shallow值,它将我通过getFilesAsync方法接收的文件限制在flowers目录中(当然,虽然没有子目录可供我操作我的示例文件)。使用deep值时要小心——您最终可能会查询大量文件,这可能是一个耗时且消耗资源的操作。

使用以前的索引数据

Windows 为用户的内容编制索引以加快搜索速度,从而避免了通过文件系统来获取文件属性和内容的详细信息。问题是,索引是作为后台任务完成的,对文件的最新更改可能不会反映在索引中—本质上,索引数据是在更快的搜索和准确性之间的权衡。您可以使用QueryOptions.indexerOption属性指定查询是否使用索引数据,该属性设置为来自IndexerOption对象的值。我已经在表 22-11 中描述了可用值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我在示例中使用了useIndexerWhenAvailable值。当您对文件的内容感兴趣时,索引数据的影响最大,访问文件系统意味着依次搜索每个文件。使用以前的索引数据可以大大加快这种搜索的速度。

查看过滤结果

我为各种筛选器属性选择的值的效果是,我的查询选择了正在搜索的文件夹中的 JPG、DOC 和 PNG 文件,并且 Windows 应该使用缓存的文件数据(如果可用)。你可以在图 22-8 中看到结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-8。*文件过滤结果

使用高级查询语法属性

高级查询语法 (AQS)允许表达复杂的查询,超出了使用其他QueryOptions属性所能管理的范围。有两个属性可用于指定 AQS 查询:applicationSearchFilter属性用于您在应用中定义的 AQS 查询,而userSearchFilter属性用于用户定义的查询。这种分离不是强制性的,无论如何,当查询文件系统时,两个查询字符串会自动合并。

清单 22-13 显示了添加到default.js文件中的switch语句,我添加它是为了在点击Filter (AQS)按钮时做出响应。这段代码执行了一个相当简单的 AQS 查询。

清单 22-13 。执行 AQS 查询

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'filterAQS':**         **var options = new search.QueryOptions();**         **options.folderDepth = search.FolderDepth.deep;**         **options.applicationSearchFilter**             **= 'System.ItemType:=".jpg" AND System.Size:>300kb AND folder:flowers';**         **storage.KnownFolders.picturesLibrary.createFileQueryWithOptions(options)**             **.getFilesAsync().then(function (files) {**                 **if (files.length == 0) {**                     **App.writeMessage("No files found");**                 **} else {**                     **files.forEach(function (storageFile) {**                         **App.writeMessage("Found: " + storageFile.name);**                 **});**             **}**         **});**         **break;** } ...

对于这个例子,我在代表Pictures库的StorageFolder对象上调用了getFilesAsync方法。这允许我将flowers文件夹指定为 AQS 查询的一部分,如下所示:

System.ItemType:=".jpg" AND System.Size:>300kb AND folder:flowers

这是一个典型的 AQS 查询,包含两个搜索词和一个约束。搜索词基于我前面描述的文件系统属性。我正在查询其System.ItemType属性为.jpg并且其System.Size属性大于300kb的文件。条款和约束与关键字AND组合在一起,该关键字必须总是大写(您也可以在查询中使用ORNOT)。

请注意,每个搜索属性后跟一个冒号,然后是比较符号,如下所示:

System.ItemType:=".jpg" AND System.Size:>300kb AND folder:flowers

约束使用folder关键字表示,并将查询限制在路径包含名为flowers的文件夹的文件上。(这匹配路径中任何名为flowers的文件夹,而不仅仅是直接的父文件夹)。

如果运行应用并点击Filter (AQS)按钮,AQS 查询将用于过滤Pictures库中的文件,产生如图图 22-9 所示的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-9。*使用 AQS 过滤文件

AQS 也可以在 Windows 搜索窗格中使用。我想我从来没有见过用户使用 AQS,但它可以在应用开发过程中测试你的查询。图 22-10 显示了先前清单 22 中的 AQS 查询——在搜索窗格中使用。如果您的文件系统包含 Visual Studio 项目中图像的额外副本,您可能会看到更多结果(我清理了我的文件以获得此结果)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-10。*在 Windows 搜索窗格中使用 AQS 查询查找文件

在我的例子中,我使用了基于文件属性和路径的 AQS 查询,但是您也可以使用 AQS 来搜索文件内容,只需在查询中包含一个带引号的短语。因此,例如,如果您想要查找包含短语“我喜欢苹果”的所有 PDF 文件,您的查询应该是:

System.ItemType:=".pdf" AND "I like apples"

AQS 可以用来创建极其精确的查询,在[msdn.microsoft.com/en-us/library/aa965711(v=VS.85).aspx](http://msdn.microsoft.com/en-us/library/aa965711(v=VS.85).aspx)可以找到一个了解 AQS 更多信息的好起点。

使用便捷查询

CommonFileQuery对象定义了六个常用的查询,您可以使用它们来创建预配置的QueryOptions对象。清单 22-14 显示了我添加到default.js文件中的switch语句中的内容,以响应点击Common Query按钮。

清单 22-14 。使用 CommonFileQuery 对象

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'commonQuery':**         **var options = new search.QueryOptions(**             **search.CommonFileQuery.orderByName, [".jpg", ".png"]);**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**                 **.then(function (folder) {**                     **folder.createFileQueryWithOptions(options).getFilesAsync()**                     **.then(function (files) {**                         **if (files.length == 0) {**                             **App.writeMessage("No files found");**                         **} else {**                             **files.forEach(function (storageFile) {**                                 **App.writeMessage("Found: " + storageFile.name);**                             **});**                         **}**                     **});**                 **});**         **break;** } ...

为了使用方便的查询,可以将一个值从CommonFileQuery对象作为构造函数参数传递给QueryOptions对象。您还必须提供一个文件扩展名数组,用于设置fileTypeFilter属性。

在这个例子中,我使用了CommonFileQuery.orderByName属性,该属性将QueryOptions对象配置为包含StorageFolder中的所有文件,并按照名称的字母顺序对它们进行排序。我已经过滤了文件,只接受了jpgpng文件扩展名(在示例文件夹中只有这种类型的文件,但是您已经明白了)。我已经在表 22-12 中描述了所有六个CommonFileQuery值。其中一些查询只有在应用于音乐文件时才有意义,因为它们依赖于System.Music文件属性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用虚拟文件夹

对象过着双重生活。在本章的前面,我向您展示了可以用来在文件系统上定位或创建新文件夹的StorageFolder方法。这是文件夹的传统用法,也是你对一个叫做StorageFolder的对象的期望。

然而,StorageFolder对象也可以在文件查询的结果中使用,其中它们表示用于按照共同特征或排序顺序将文件分组在一起的虚拟文件夹。清单 22-15 展示了我是如何使用这个特性将我的示例图像文件按照年份分组到虚拟文件夹中,以响应点击Group (Type)按钮。

清单 22-15 。将文件分组到虚拟文件夹中

... switch (e.target.id) {     // *...statements omitted for brevity...*     **case 'groupType':**         **storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")**             **.then(function (flowersFolder) {**                 **flowersFolder.getFoldersAsync(search.CommonFolderQuery.groupByType)**                 **.then(function (typeFolders) {**                     **var index = 0;**                     **(function describeFolders() {**                         **App.writeMessage("Folder: " + typeFolders[index].displayName);**                         **typeFolders[index].getFilesAsync().then(function (files) {**                             **files.forEach(function (file) {**                                 **App.writeMessage("--File: " + file.name);**                             **});**                             **if (index < typeFolders.length -1) {**                                 **index++;**                                 **describeFolders();**                             **}**                         **});**                     **})();**                 **});**         **});**         **break;** } ...

清单中的代码有点复杂,因为我想以特定的顺序显示输出,但是我执行的操作是异步的。解释发生了什么的最简单的方法是将代码分成两部分。建议你运行 app,点击Group (Type)按钮。您将看到图 22-11 中所示的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 注意如果您已经删除了示例文件,您需要先点击Copy Files按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-11。*根据类型对示例文件进行分组

将文件分组

将文件分组到虚拟文件夹的过程非常简单,你可以在清单 22-16 中看到重要的陈述。这是完整清单中语句的子集,这样我就可以专注于分组特性。

清单 22-16 。分组文件

... storage.KnownFolders.picturesLibrary.getFolderAsync("flowers")     .then(function (flowersFolder) {         **flowersFolder.getFoldersAsync(search.CommonFolderQuery.groupByType)**         .then(function (typeFolders) {            // …code removed...         }); }); ...

我通过对由KnownFolders.picturesLibrary属性返回的StorageFolder对象调用getFolderAsync方法来获得flowers文件夹,就像我在前面的例子中所做的一样。

这给了我一个代表flowers文件夹的StorageFolder对象。为了对文件夹包含的文件进行分组,我调用了getFoldersAsync方法并传入了由CommonFolderQuery对象定义的一个值。CommonFolderQuery对象定义了许多允许你以不同方式对文件进行分组的值,如表 22-13 所总结的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个例子中,我使用了groupByType值,顾名思义,它根据文件扩展名对文件进行分组。用一个CommonFolderQuery值调用getFoldersAsync方法的结果是一个Promise对象,它在完成时将一个StorageFolder对象数组传递给 then 函数。

每个StorageFolder对象代表为组属性找到的一个值。在这个例子中,我按照类型对文件进行了分组,有两种类型的示例文件——JPG 文件和 PNG 文件。我将收到一个包含所有 JPG 文件的StorageFolder和一个包含所有 PNG 文件的StorageFolder

处理分组后的文件

一旦我获得了虚拟StorageFolder对象的数组,我需要在应用布局中显示结果。这是示例代码的第二部分,我已经在清单 22-17 中展示过了。

清单 22-17 。显示每个虚拟文件夹的内容

... .then(function (typeFolders) {     **var index = 0;**     **(function describeFolders() {**         **App.writeMessage("Folder: " + typeFolders[index].displayName);**         **typeFolders[index].getFilesAsync().then(function (files) {**             **files.forEach(function (file) {**                 **App.writeMessage("--File: " + file.name);**             **});**             **if (index < typeFolders.length -1) {**                 **index++;**                 **describeFolders();**             **}**         **});**     **})();** }); ...

您使用getFilesAsync方法获取每个虚拟文件夹中的文件。我已经调用了不带参数的方法,但是您可以使用QueryOptions对象过滤或排序文件,如本章前面所述。

这段代码解决的问题是,getFilesAsync方法返回一个提供文件数组的Promise,但是我需要确保在移动到下一个虚拟文件夹之前,我已经为一个虚拟文件夹中的所有文件调用了App.writeMessage方法。

为了解决这个问题,我使用了我在第二十一章中介绍的技术的一个小变体,通过定义一个自我执行的函数,当它遍历虚拟文件夹时调用它自己。这段代码的结果是,我对虚拟文件夹及其包含的文件的处理进行了序列化,这样我就可以确保描述性消息以正确的顺序显示,从而产生您在本节开始的图 22-11 中看到的结果。

监控文件夹中的新文件

我要展示的最后一个功能是监视文件夹中的新文件。为了演示这一点,我已经将清单 22-18 中的代码添加到了default.js文件中。与本章中的其他例子不同,这个添加的代码不是响应按钮点击而触发的,而是作为onactivated函数的一部分执行的。

清单 22-18 。监控文件

... app.onactivated = function (args) {     if (args.detail.previousExecutionState         != activation.ApplicationExecutionState.suspended) {
`        args.setPromise(WinJS.UI.processAll().then(function () {

storage.KnownFolders.picturesLibrary.getFolderAsync(“flowers”)
            .then(function (folder) {
                var query = folder.createFileQuery();
                query.addEventListener(“contentschanged”, function (e) {
                    App.writeMessage(“New files!”);
                });

setTimeout(function () {
                    query.getFilesAsync();
                }, 1000);
            });

$(‘#buttonsContainer > button’).listen(“click”, function (e) {
                App.clearMessages();
                switch (e.target.id) {
                    // … statements removed for brevity
                }
            });
        }));
    }
};
…`

监控一个文件夹的起点是获得一个代表它的StorageFolder对象。我将再次监控flowers文件夹,所以我首先对由KnownFolders.picturesLibrary属性返回的StorageFolder调用getFolderAsync

下一步是创建一个StorageFileQueryResult对象,通过调用您想要监控的StorageFolder上的createFileQuery方法可以获得这个对象。

当一个新文件被添加到被监控的目录中时,StorageFileQueryResult对象发出contentschanged事件。在这个例子中,我提供了一个函数来处理这个事件,在应用布局的ListView控件中向用户显示一条消息。

最后一步是调用StorageFileQueryResult.getFilesAsync方法,开始监控文件夹。我使用setTimeout函数在一秒钟的延迟后调用这个方法——这是因为contentschanged功能不是特别可靠,我发现这是增加工作几率的最好方法(尽管它仍然不时地失败,并且我没有在应该得到通知的时候得到通知)。

要在示例中测试该功能,请启动应用,单击Delete Files按钮从 flowers 文件夹中删除文件,然后单击Copy Files将新文件添加到受监控的文件夹中。几秒钟后你会在应用布局中看到一条消息,就像图 22-12 中的一样。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

***图 22-12。*监控文件夹中的新文件

这种技术有一些严重的局限性。首先,contentschanged事件只有在新文件被添加到文件夹中时才会被触发。当文件被删除或修改时,您不会收到该事件。其次,在添加新文件和触发事件之间可能会有几秒钟的延迟。第三,这不是一个健壮的特性,并且contentschanged事件并不总是在它应该触发的时候被触发,并且对于一个单独的改变经常被触发多次。但如果你能忍受这些问题,那么监控文件夹可能是一种有用的方法,可以确保你的应用让用户了解最新的内容。

总结

在这一章中,我向您展示了如何使用文件和文件夹,从复制和删除文件等基本功能开始。然后,我向您展示了如何对文件进行排序、过滤和查询,如何创建虚拟文件夹来将相关文件分组在一起,以及如何监视文件夹中的新文件。

所有这些特性和技术的共同点是它们都是在你的应用内部实现的,用户看不到。但由于我们处理的是用户的文件和内容,所以我们以一种清晰、明显、与其他应用和 Windows 本身一致的方式来表达文件和文件夹处理功能是很重要的。在下一章中,我将向您展示如何做到这一点,使用 Windows 为处理设备文件系统提供的大量集成功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值