Windows-商店应用开发入门指南-五-

128 阅读1小时+

Windows 商店应用开发入门指南(五)

原文:Beginning Windows Store Application Development – HTML and JavaScript Edition

协议:CC BY-NC-SA 4.0

十六、文件

有了从 IndexedDB 到会话状态到本地设置等各种选项,在决定如何保存某些内容以备后用时,您有很多选择。在这一章中,我将介绍使用一种完全不同的数据类型:文件。

也许您正在构建的应用可以处理照片、电子表格、文档或任何其他类型的文件。虽然您可能会发现可以使用其他技术保存文件,例如在 IndexedDB 数据库中,但 Windows 8 应用可以在用户的文件系统中读取、写入和删除文件。计算机将文件存储在文件系统中已经有很长一段时间了,除非您有明确的要求,否则在考虑处理文件时,文件系统是最合理的起点。

窗户。存储命名空间

如果你要在 Windows Store 应用中处理文件和文件夹,你会花很多时间在Windows.Storage命名空间 中。这个名称空间包含创建、编辑、删除、读取或列出文件或文件夹所需的所有类。一些最常见的类是StorageFileStorageFolderFileIO 类,我将在本章中一一演示。

StorageFileStorageFolder类代表用户计算机上的文件和文件夹。在任何需要处理文件或文件夹的时候,您都会用到这些类。每当用户在文件选择器或文件夹选择器中进行选择时,都会返回这些类型之一。这两个类都提供了许多有用的方法,例如,复制、创建、删除和打开文件和文件夹。虽然StorageFile类提供了在用户计算机上处理文件的方法,但是FileIO类提供了许多处理StorageFile对象的内容的方法。一旦你有了一个StorageFile对象,你可以使用FileIO类来读取文件的内容或者改变文件的内容。

image 注意除了操作StorageFile对象的FileIO类之外,如果你有文件路径但没有StorageFile对象,你也可以使用PathIO类。两个类中存在相同的方法。

在 Windows 应用商店应用中处理文件和文件夹时,请记住一点。正如 WinRT 或 WinJS 库中任何潜在的高开销操作一样,StorageFileStorageFolderFileIO类中的所有方法都是异步的。它们都返回Promise对象来表示被请求的操作。

除了StorageFileStorageFolderFileIO类之外,Windows.Storage名称空间还提供了对存储应用数据的位置的访问点。该访问由第十五章中用于设置和检索本地和漫游设置的同一个ApplicationData类提供。具体来说,您可以使用Windows.Storage.ApplicationData.current对象的localFolder属性来引用存储在您的应用范围内的文件。毫不奇怪,localFolderStorageFolder类的一个实例。

image 注意 Windows Store 应用开发人员有两种方式来访问其应用的本地存储文件夹。如前所述,可以使用Windows.Storage.ApplicationData.current.localFolder。此外,在 WinJS 应用中,您可以使用 WinJS 的等效项WinJS.Application.local.folder

在这一章中,我将介绍这些类的用法,展示我是如何在 Clok 中实现两个新特性的。首先是备份存储在 Clok 数据库中的项目和时间条目的能力。第二个是文档库,用户可以在其中存储与他们的项目相关的文件。

数据备份

在任何应用中,能够备份数据都是一个有用的功能。因此,这是我添加到 Clok 中的一个特性。Clok 用户将在 Clok 选项设置弹出菜单中访问此功能。从那里,他们将能够点击保存备份按钮,这将保存他们在 Clok 中保存的项目和时间条目数据的副本。

添加这个功能并不需要太多,代码也很短。虽然这段代码很短,但它将说明如何使用前一节中提到的一些常见的类。您将会看到我已经使用了localFolder属性来访问 Clok 的本地文件存储,还使用了来自StorageFileStorageFolderFileIO类的方法。在我向您介绍这段代码之前,您必须先对options.html做一个小小的改动。将清单 16-1 至options.html中的代码添加到调试部分的代码之后。

清单 16-1。 向 Clok 选项设置弹出按钮添加控件

<div class="win-settings-section">
    <h3>Backups</h3>
    <p>Backup Projects and Time Entries</p>
    <p>
        <button id="saveBackupButton">Save Backup</button>
        <span id="backupConfirmation"
            class="win-type-xx-small"
            style="display: none;">Backup saved</span>
    </p>
    <p class="win-type-xx-small">Backup location: <span id="backupPath"></span></p>
</div>

这段代码添加了一个按钮,用于启动备份过程,还添加了一个标签,用于向用户指示在哪里可以找到备份文件。您可以通过将清单 16-2 中的代码添加到options.jsready函数中来配置这两个控件。

清单 16-2。 配置备份控件

saveBackupButton.onclick = this.saveBackupButton_click;
backupPath.innerText = appData.localFolder.path + "\\backups";

完成这个过程的最后一步是实现saveBackupButton_click处理函数。将清单 16-3 中的代码添加到options.js

清单 16-3。 编写备份文件

saveBackupButton_click: function(e) {
    var dateFormatString = "{year.full}{month.integer(2)}{day.integer(2)}"
        + "-{hour.integer(2)}{minute.integer(2)}{second.integer(2)}";
    var clockIdentifiers = Windows.Globalization.ClockIdentifiers;

    var formatting = Windows.Globalization.DateTimeFormatting;
    var formatterTemplate = new formatting.DateTimeFormatter(dateFormatString);
    var formatter = new formatting.DateTimeFormatter(formatterTemplate.patterns[0],
                formatterTemplate.languages,
                formatterTemplate.geographicRegion,
                formatterTemplate.calendar,
                clockIdentifiers.twentyFourHour);

    var filename = formatter.format(new Date()) + ".json";

    var openIfExists = Windows.Storage.CreationCollisionOption.openIfExists;

    appData.localFolder
        .createFolderAsync("backups", openIfExists)
        .then(function (folder) {
            return folder.createFileAsync(filename, openIfExists);
        }).done(function (file) {

            var storage = Clok.Data.Storage;
            var backupData = {
                projects: storage.projects,
                timeEntries: storage.timeEntries
            };
            var contents = JSON.stringify(backupData);
            Windows.Storage.FileIO.writeTextAsync(file, contents);

            backupConfirmation.style.display = "inline";
        });
},

该函数的第一部分使用DateTimeFormatter类 根据当前日期为备份文件生成一个名称。因为DateTimeFormatter类中的一个限制阻止了指定 12 小时或 24 小时时钟的Clock属性在该类的实例创建后被设置,所以我首先创建了一个formatterTemplate 对象,使用所有的默认值。然后我基于formatterTemplate创建formatter对象,在构造函数中指定Clock。因为构造函数是唯一可以指定使用哪个Clock的地方,并且所有其他参数都是必需的,这允许formatter对象对所有其他构造函数参数使用系统默认值。

这个函数的核心是函数后半部分的Promise链。正如我上面提到的,appData.localFolder对象是StorageFolder的一个实例。对createFolderAsync 的调用将在应用的本地数据文件夹中创建一个backups文件夹,如果它还不存在的话。这个文件夹被传递给then函数,该函数通过createFileAsync调用创建一个StorageFile对象。这个文件被传递给done函数,该函数通过调用JSON.stringify将我们的应用数据序列化为一个字符串,并使用writeTextAsync 调用将其保存到文件中。最后一行只是向用户显示一条确认消息。

要了解这是如何工作的,请运行 Clok 并打开“Clok 选项设置”弹出按钮。点击保存备份按钮后(参见图 16-1 ,使用 Windows 资源管理器导航到指定的备份位置查看文件(参见图 16-2 )。

9781430257790_Fig16-01.jpg

图 16-1 。在“时钟选项设置”弹出菜单中保存的备份

9781430257790_Fig16-02.jpg

图 16-2 。备份文件

该文件包含 Clok 数据库中所有项目和时间条目的 JSON 表示。您可以在文本编辑器中打开该文件,如记事本(见图 16-3 ),查看其内容。虽然这不是查看数据的最方便的格式,但是您应该能够看到所有的内容。

9781430257790_Fig16-03.jpg

图 16-3 。备份文件的内容

Clok 项目的文档库

我们最初的目标是为用户提供一种简单的方式来跟踪他们在项目上工作的时间。当我们在第十四章中完成这个目标时,是时候创建一些新的需求,添加一些其他有用的相关功能了。在这一节中,我将向您展示如何开始构建一个文档库,用户可以在其中存储与他们正在处理的项目相关联的文档。最终,用户将能够导出和删除文件,但是我们将从允许他们向项目添加文档开始。

创建文档库页面控件

首先要做的是在 Visual Studio 项目的pages文件夹中创建一个名为documents的文件夹。在documents文件夹中,添加一个名为library.html的新页面控件(参见图 16-4 )。

9781430257790_Fig16-04.jpg

图 16-4 。解决方案浏览器为文档提供了新的页面控件

我们将在本章的剩余部分构建文档库屏幕,您要做的第一个更改是更新屏幕的标题以反映正确的页面标题,以及当前选定项目的名称。用清单 16-4 中高亮显示的代码更新library.html

清单 16-4。 在文档库屏幕上显示当前项目的名称

<div class="library fragment">
    <header aria-label="Header content" role="banner">
        <button class="win-backbutton" aria-label="Back" disabled type="button"></button>
        <h1 class="titlearea win-type-ellipsis">
            <span class="pagetitle">Document Library</span>
            <span class="win-type-x-large" id="projectName">[Project Name]</span>
        </h1>
    </header>
    <section aria-label="Main content" role="main">
        <p>Content goes here.</p>
    </section>
</div>

当然,[Project Name]只是一个占位符。您必须添加 JavaScript 代码来设置projectName span元素的正确值。在library.js顶部定义了现在熟悉的storage别名后,用清单 16-5 中突出显示的代码更新文件。

清单 16-5。 设置项目名称

ready: function (element, options) {
    this.projectId = options && options.projectId;
    this.setProjectName();
},

setProjectName: function () {
    if (this.projectId) {
        var project = storage.projects.getById(this.projectId);
        projectName.innerText = project.name + " (" + project.clientName + ")";
    }
},

目前,用户无法导航到文档库。用户应该能够从项目详细信息屏幕到达该屏幕。在detail.htmldetail.js中进行所需的更改,向应用栏添加一个新按钮。按照您添加按钮以导航到时间表屏幕的方式,确保通过当前项目的id(回头参考清单 12-32 )并且仅在查看现有项目时启用该按钮。完成后,项目细节的应用栏应该类似于图 16-5 。

9781430257790_Fig16-05.jpg

图 16-5 。项目详细信息应用栏带有新建文档按钮

image 注意通过将AppBarButtonicon属性 设置为attach可以显示回形针图标。本章描述的工作的完整版本可以在本书附带的源代码中找到。您可以在该书的 press product 页面的 Source Code/Downloads 选项卡上找到本章的代码示例(www.apress.com/9781430257790)。

立即运行 Clok 并导航到文档库屏幕。屏幕仍然是空的,但是项目和客户名称现在应该显示在标题中(见图 16-6 )。将这些信息添加到屏幕上是一个很小的功能,但是像这样的事情将会使用户在使用应用时保持方向感。

9781430257790_Fig16-06.jpg

图 16-6 。项目名称为的文档库标题

向项目中添加文档

现在我们已经创建了文档库页面,下一个任务是允许用户向 Clok 中的项目添加文档。在 UI 中实现时,向应用栏添加一个添加命令是最自然的选择。将清单 16-6 中的代码添加到body元素之后。

清单 16-6。 向文档库屏幕添加应用栏

<div id="libraryAppBar"
    class="win-ui-dark"
    data-win-control="WinJS.UI.AppBar"
    data-win-options="{ sticky: true }">

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

在这一节中,我将向您介绍FileOpenPicker ,它允许用户选择一个或多个要在您的应用中使用的文件。除了FileOpenPickerWindows.Storage.Pickers名称空间还提供了FileSavePickerFolderPicker类。我不会详细介绍FileSavePicker类,但是它允许用户在他们计算机上的指定位置用指定的名称保存文件。顾名思义,FolderPicker类允许用户选择一个文件夹,这将在本章后面介绍。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.pickers.aspx)上有关于Windows.Storage.Pickers名称空间及其所有类的更多信息。

接下来,在library.jsready函数中为addDocumentsCommand连接click事件处理程序。另外,将清单 16-7 中突出显示的别名添加到library.js的顶部。

清单 16-7。 向 library.js 添加别名

var storage = Clok.Data.Storage;
var appData = Windows.Storage.ApplicationData.current;
var createOption = Windows.Storage.CreationCollisionOption;
var pickerLocationId = Windows.Storage.Pickers.PickerLocationId;

本章和本书中使用的别名并不是必需的,但它们允许我们在使用它们时使用更短的语法,编写更少的代码,我相信这使它更容易阅读。我之前已经讨论过Windows.Storage.ApplicationData,在本章中我们将使用它来访问应用的本地文件夹。枚举用于指定当你的代码试图创建的文件或文件夹已经存在时,Windows 应该如何处理。选项包括让 Windows 生成一个新文件名、覆盖现有文件、使用现有文件或直接失败。Windows.Storage.Pickers.PickerLocationId枚举用于指定向用户显示的文件或文件夹选择器的首选位置。它包括许多公共位置,如用户的桌面文件夹或图片库。在清单 16-8 中,你可以看到这些别名是如何使用的。将下面的代码添加到library.js中。

清单 16-8。 向库中添加文档

getProjectFolder: function() {
    if (this.projectId) {
        var projectId = this.projectId;

        return appData.localFolder
            .createFolderAsync("projectDocs", createOption.openIfExists)
            .then(function (folder) {
                return folder.createFolderAsync(projectId.toString(), createOption.openIfExists)
            });
    } else {
        return WinJS.Promise.as();
    }
},

canOpenPicker: function () {
    var views = Windows.UI.ViewManagement;

    var currentState = views.ApplicationView.value;
    if (currentState === views.ApplicationViewState.snapped &&
            !views.ApplicationView.tryUnsnap()) {

        return false;
    }
    return true;
},

addDocumentsCommand_click: function (e) {
    if (!this.canOpenPicker()) {
        return;
    }

    var filePicker = new Windows.Storage.Pickers.FileOpenPicker();
    filePicker.commitButtonText = "Add to Document Library";
    filePicker.suggestedStartLocation = pickerLocationId.desktop;
    filePicker.fileTypeFilter.replaceAll(["*"]);

    filePicker.pickMultipleFilesAsync().then(function (files) {
        if (files && files.size > 0) {

            this.getProjectFolder().then(function (projectFolder) {
                var copyPromises = files.map(function (item) {
                    return item.copyAsync(
                        projectFolder,
                        item.name,
                        createOption.replaceExisting);
                });

                return WinJS.Promise.join(copyPromises);
            });
        } else {
            return WinJS.Promise.as();
        }
    }.bind(this));
},

我在这里定义了三个函数。第一个是getProjectFolder ,返回一个Promise,代表用于存储当前所选项目文档的文件夹。文档将存储在一个名为与当前项目的id相匹配的文件夹中。该文件夹将位于名为projectDocs的本地数据文件夹中。当你的应用被抓拍时,如果你试图显示一个FileOpenPicker ,或者上面提到的任何一个选择器,就会抛出一个异常。第二个函数canOpenPicker ,检查应用的当前视图状态,是截图还是全屏。如果应用被抓拍,那么对tryUnsnap的调用将尝试取消它的抓拍。

第三个函数是addDocumentsCommand_click 处理函数。如果对canOpenPicker的调用成功,那么文件选择器在显示给用户之前被初始化。我已经将commitButtonText设置为合适的值,而不是默认值“打开”我建议选取器在用户的桌面上启动,但是如果用户最近选择了另一个位置,则不强制执行该设置。最后,我指定应该显示所有文件类型。当显示选取器时,如果用户选择一个或多个文件,则使用从FileOpenPicker返回的每个StorageFile对象的copyAsync函数复制所选择的文件。

现在,运行 Clok 并导航到项目的文档库。激活应用栏(用鼠标右键单击或从触摸屏底部边缘滑动),然后单击添加按钮。在图 16-7 中,我从桌面上名为Import Folder的文件夹中选择了四个文件。当您单击“添加到文档库”按钮时,选定的文件将被添加到项目的文档库中。

9781430257790_Fig16-07.jpg

图 16-7 。选择了四个文件的 FileOpenPicker

image 注意除了指定他们计算机上的文件夹,通过展开“文件”下的菜单,用户还可以指定实现文件选取器契约的其他应用。例如,他们可以从他们的 SkyDrive 帐户中选择文件,甚至直接从他们计算机上的相机中导入图片。

探索项目文件

此时,您可以运行 Clok 并向项目中添加任意数量的文件。虽然目前还没有办法在 Clok 中查看它们,但是你可以在 Windows 资源管理器中查看它们。在图 16-8 中,你可以看到我给一个项目添加了四个图标。

9781430257790_Fig16-08.jpg

图 16-8 。将文件添加到项目中

我电脑上 Clok 的本地数据文件夹的路径是


C:\Users\sisaacs\AppData\Local\Packages\068d38c6-5cdc-44d8-a832-f96ab138e866_0bzpj67fjc6np\LocalState

该路径在您的计算机上会有所不同,但它会在%USERPROFILE%\AppData\Local\Packages内的某个文件夹中。您很可能在Packages文件夹中有许多文件夹,并且您的应用的数据将在其中的一个中。有几种方法可以找到你的应用的本地数据文件夹:通过反复试验,通过检查appData.localFolder.path的值,或者通过匹配你的 Visual Studio 项目中package.appxmanifest文件的打包选项卡上的“包名”字段(参见图 16-9 )。

9781430257790_Fig16-09.jpg

图 16-9 。package.appxmanifest 中的包名是本地数据文件夹路径的一部分

作为开发人员,我们有工具来帮助我们轻松地找到这个路径并在 Windows 资源管理器中查看文件。当然,对于在 Visual Studio 中没有检查变量或package.appxmanifest文件的优势的用户来说,这是不一样的。因此,让我们进行构建文档库的下一步:添加查看已添加文件的功能。

配置列表视图

如图图 16-7 所示的文件拾取器使用一个ListView来选择文件。为了与默认的 Windows 行为保持一致,我们还将使用一个ListView来向用户显示文档库中的文件。用清单 16-9 中的代码替换library.html中的主section

清单 16-9。 添加用于显示文件的 ListView】

<section aria-label="Main content" role="main">
    <div id="libraryTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
        <div class="libraryItem" data-win-bind="item: item Clok.Library.bindLibraryItem">
            <div class="libraryItem-icon-container">
                <img class="libraryItem-icon" />
            </div>
            <div class="libraryItem-details">
                <h3 class="libraryItem-filename win-type-ellipsis"></h3>
                <h6 class="libraryItem-modified-container win-type-ellipsis">
                    <strong>Modified:</strong> <span class="libraryItem-modified"></span>
                </h6>
                <h6 class="libraryItem-size-container win-type-ellipsis">
                    <strong>Size:</strong> <span class="libraryItem-size"></span>
                </h6>
            </div>
        </div>
    </div>

    <div id="libraryListView"
        class="win-selectionstylefilled"
        data-win-control="WinJS.UI.ListView"
        data-win-options="{
            itemTemplate: select('#libraryTemplate'),
            selectionMode: 'multi',
            swipeBehavior: 'select',
            tapBehavior: 'directSelect'
        }">
    </div>
    <div id="noDocuments" class="hidden">No documents found for this project.</div>
</section>

libraryListView 允许用户选择多个文件,每个文件根据libraryTemplate 定义的Template显示。对于每个文件,ListView将显示一个文件图标、文件名、文件类型和文件上次修改的日期。你可能已经注意到这个Template与之前的ListView例子有所不同。这次我没有单独绑定每个值,而是在顶层指定了绑定,并且指定了一个绑定初始化器Clok.Library.bindLibraryItem。这将允许我们在 JavaScript 代码中实现更复杂的绑定,我将在下一节更详细地展示这一点。

CSS 现在应该很熟悉了,因为它非常类似于项目、方向和时间表屏幕中使用的内容。将清单 16-10 中的 CSS 代码添加到library.css

清单 16-10。 CSS 为文档库

.hidden {
    display: none;
}

#libraryListView {
    height: calc(100% - 88px);
}

    #libraryListView .win-container {
        background-color: #46468C;
    }

    #libraryListView .libraryItem {
        display: -ms-grid;
        -ms-grid-columns: 80px 350px;
        height: 80px;
    }

    #libraryListView .libraryItem-icon {
        -ms-grid-column: 1;
        margin: 8px;
        width: 64px;
        height: 64px;
        text-align: center;
    }

    #libraryListView .libraryItem-details {
        -ms-grid-column: 2;
        margin: 5px;
    }

    #libraryListView .libraryItem-filename {
        font-size: 1.25em;
    }

在我们之前所有的ListView例子中,我们已经将ListView绑定到了一个WinJS.Binding.List对象。我们可以使用StorageFileStorageFolder类上的函数来构建我们自己的List,但是有一种更好的方式在ListView中显示文件系统信息。

配置存储数据源

一个ListView可以绑定到任何实现IListDataSource接口的类。虽然我们可以用关于文件和文件夹的信息填充一个WinJS.Binding.List对象,但是StorageDataSource已经存在了。因为它实现了IListDataSource,我们可以直接绑定到它。除了比其他选择更简单之外,使用StorageDataSource还提供了文件和文件夹的“实时视图”的额外好处。因此,如果您添加了一个新文件,或者删除了一个现有的文件,数据源将立即反映这一点并更新ListView

在这一节中,我将演示如何创建一个简单的StorageDataSource对象来反映项目文档库的内容。我将把这些数据绑定到上一节中创建的ListView,然后在下一节中,我将向您展示如何允许您的用户直接从 Clok 打开文件。

创建一个绑定到我们的ListView的对象只需要几行代码。将清单 16-11 中的代码添加到library.js中。

清单 16-11。 创建存储数据源对象

bindProjectLibraryFiles: function () {
    if (this.projectId) {
        var resizeThumbnail = thumbnailOptions.resizeThumbnail;
        var singleItem = thumbnailMode.singleItem;

        this.getProjectFolder().then(function (folder) {
                var fileQuery = folder.createFileQuery();

                var dataSourceOptions = {
                    mode: singleItem,
                    requestedThumbnailSize: 64,
                    thumbnailOptions: resizeThumbnail
                };

                var dataSource = new WinJS.UI.StorageDataSource(fileQuery, dataSourceOptions);

                dataSource.getCount().then(function (count) {
                    if (count >= 1) {
                        libraryListView.winControl.itemDataSource = dataSource;
                        WinJS.Utilities.addClass(noDocuments, "hidden");
                        WinJS.Utilities.removeClass(libraryListView, "hidden");
                    } else {
                        WinJS.Utilities.removeClass(noDocuments, "hidden");
                        WinJS.Utilities.addClass(libraryListView, "hidden");
                    }
                });
            });
    }
},

StorageDataSource构造函数接受一个查询对象和一个选项对象。查询参数可以是用户计算机上常见 Windows 库列表中的字符串(“音乐”、“图片”、“视频”或“文档”),也可以是实现IStorageQueryResultBase的对象。例如,如果你专门处理用户的图片或音乐库,那么只需将其中一个字符串作为第一个参数传递给StorageDataSource构造函数就非常简单了。如果您正在使用用户计算机上的另一个位置,则必须创建一个查询对象。

在清单 16-11 中,因为我已经引用了一个StorageFolder 对象——当前项目的文档库——我可以调用createFileQuery函数来获得一个有效的查询对象。StorageDataSource构造函数的第二个参数定义了一些附加选项,主要与查询结果中包含的缩略图信息相关。关于StorageDataSource类和这两个构造函数参数的更多信息可以在 MSDN 网站(http://msdn.microsoft.com/en-us/library/windows/apps/br212651.aspx)上找到。

image 注意createFileQuery函数构建了一个查询,允许您处理StorageFolder对象顶层的所有文件。除了createFileQuery之外,StorageFolder类还定义了其他五个返回不同类型的文件或文件夹查询的函数,以及两个可以用来同时查询文件和文件夹的函数。有关这些其他StorageFolder功能的更多信息,请访问 MSDN 网站(http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.storagefolder.aspx)。

创建数据源后,我调用了getCount函数,检查当前项目的文档库中是否有文件。如果有,那么我将ListViewitemDataSource属性设置到这个数据源,并使结果可见。如果不存在文件,我会向用户显示一条消息。

我在上一节中指出了我如何只将数据绑定到用于格式化文件信息的WinJS.Binding.Template的顶层。我没有单独绑定每个元素,而是选择使用名为bindLibraryItem的绑定初始化函数来处理一些更复杂的绑定需求。将清单 16-12 中突出显示的代码添加到library.js中的页面定义之后。

清单 16-12。 绑定初始化函数

(function () {
    "use strict";

    // SNIPPED

    WinJS.UI.Pages.define("/pages/documents/library.html", {
        // SNIPPED
    });

    function bindLibraryItem(source, sourceProperty, destination, destinationProperty) {
        var filenameElement = destination.querySelector(".libraryItem-filename");
        var modifiedElement = destination.querySelector(".libraryItem-modified");
        var sizeElement = destination.querySelector(".libraryItem-size");
        var iconElement = destination.querySelector(".libraryItem-icon");

        filenameElement.innerText = source.name;

        modifiedElement.innerText = source.basicProperties
            && source.basicProperties.dateModified
            && formatDateTime(source.basicProperties.dateModified);

        var size = source.basicProperties &&**source.basicProperties.size;**
        **if (size > (Math.pow(1024, 3))) {**
            **sizeElement.innerText = (size / Math.pow(1024, 3)).toFixed(1) + " GB";**
        **}**
        **else if (size > (Math.pow(1024, 2))) {**
            **sizeElement.innerText = (size / Math.pow(1024, 2)).toFixed(1) + " MB";**
        **}**
        **else if (size > 1024) {**
            **sizeElement.innerText = (size / 1024).toFixed(1) + " KB";**
        **}**
        **else {**
            **sizeElement.innerText = size + " B";**
        **}**

        **var url;**

        **if (source.thumbnail &&** **isImageType(source.fileType)) {**
            **url = URL.createObjectURL(source.thumbnail, { oneTimeOnly: true });**
        **} else {**
            **url = getIcon(source.fileType);**
        **}**

        **iconElement.src = url;**
        **iconElement.title = source.displayType;**
    **}**

    **WinJS.Utilities.markSupportedForProcessing(bindLibraryItem);**

    **WinJS.Namespace.define("Clok.Library", {**
        **bindLibraryItem: bindLibraryItem,**
    **});**

`})();`

当 WinJS 为ListView中的每一项调用我们的绑定初始化器时,它将数据源对象——文件——作为参数source传递给这个函数,并将 HTML 元素作为参数destination。可以为每个数据绑定值指定不同的初始化器,如果是这样的话,我们也可以利用sourcePropertydestinationProperty 参数。然而,在这种情况下,我忽略了这些。相反,我使用查询选择器来查找应该向用户显示数据绑定信息的元素,并根据我的自定义逻辑设置这些元素的innerText`属性。例如,根据文件的大小,文件大小可以显示为千兆字节、兆字节、千字节或简单的字节。

文件图标略有不同。StorageDataSource提供了将在 Windows 资源管理器中使用的图标,我可以选择在ListView中显示这些图标。相反,只有当库中的文件是图像时,我才显示提供的图标。这将导致图像的缩略图版本显示在列表视图中。在所有其他情况下,我更喜欢显示与 Clok 的现代风格相匹配的自定义文件类型图标,并且我使用getIcon函数来确定该图标应该是什么,稍后我会介绍这个函数。

image 注意我用于文件类型图标的图片可以在本书附带的源代码中找到。你可以在本书的 press 产品页面(www.apress.com/9781430257790)的 Source Code/Downloads 选项卡上找到本章的代码示例。

在定义了绑定初始化器之后,我通过调用markSupportedForProcessing 并将其公开为Clok.Library.bindLibraryItem,使得它可以从library.html开始使用。此外,还需要一些其他函数来使这个绑定初始化器工作。在library.js中的bindLibraryItem定义后添加清单 16-13 中的代码。

清单 16-13。 函数支持绑定初始化器

function formatDateTime(dt) {
    var formatting = Windows.Globalization.DateTimeFormatting;
    var dateFormatter = new formatting.DateTimeFormatter("shortdate");
    var timeFormatter = new formatting.DateTimeFormatter("shorttime");
    return dateFormatter.format(dt) + " " + timeFormatter.format(dt);
}

function isImageType(fileType) {
    fileType = (fileType || "").toLocaleUpperCase();

    return fileType === ".PNG"
        || fileType === ".GIF"
        || fileType === ".JPG"
        || fileType === ".JPEG"
        || fileType === ".BMP";
}

function getIcon(fileType) {
    fileType = (fileType || "").replace(".", "");

    var knownTypes = ["WAV", "XLS", "XLSX", "ZIP",
        "AI", "BMP", "DOC", "DOCX", "EPS", "GIF",
        "ICO", "JPEG", "JPG", "MP3", "PDF", "PNG",
        "PPT", "PPTX", "PSD", "TIFF", "VSD", "VSDX"];

    if (knownTypes.indexOf(fileType.toLocaleUpperCase()) >= 0) {
        return "/img/fileTypes/" + fileType + ".png";
    }

    return "/img/fileTypes/default.png";
}

雄心勃勃的开发人员可能会用一个动态检查img/fileTypes文件夹内容的定义来替换getIcon的定义,以确定哪些图标可用。因为这些图标包含在用户下载 Clok 时安装的包中,所以在本地数据文件夹中找不到这些图标。相反,你应该在Windows.ApplicationModel.Package.current.installedLocation中寻找这些文件,它也是一个StorageFolder对象。

要显示当前项目文档库中的文件,还需要两个简单的步骤。您必须在library.js的顶部添加两个别名(参见清单 16-14 中的,并在ready函数中添加对this.bindProjectLibraryFiles的调用。

清单 16-14。 再加两个别名

var storage = Clok.Data.Storage;
var appData = Windows.Storage.ApplicationData.current;
var createOption = Windows.Storage.CreationCollisionOption;
var pickerLocationId = Windows.Storage.Pickers.PickerLocationId;
var thumbnailOptions = Windows.Storage.FileProperties.ThumbnailOptions;
var thumbnailMode = Windows.Storage.FileProperties.ThumbnailMode;

立即运行 Clok 并导航到文档库。您将看到您之前添加的文件,如果您现在添加更多文件,StorageDataSource 将自动用新文件更新ListView(参见图 16-10 )。

9781430257790_Fig16-10.jpg

图 16-10 。文档库中有各种图片和文档

正在启动文件

现在文件显示在文档库中,一个有用的特性是允许用户直接从 Clok 打开文档。在这一节中,我将演示如何添加一个新的应用栏按钮。将清单 16-15 中的代码添加到library.html中的AppBar定义中。

清单 16-15。 向应用栏添加打开按钮

<button
    data-win-control="WinJS.UI.AppBarCommand"
    data-win-options="{
        id:'openDocumentCommand',
        label:'Open',
        icon:'openfile',
        section:'selection',
        tooltip:'Open',
        disabled: true}">
</button>

为了防止一次意外启动多个文件,我添加了一次只能打开一个文件的要求。为了加强这一点,我处理了ListViewonselectionchanged事件,并且只有在ListView中选择了一个项目时,打开按钮才被激活。将清单 16-16 中的代码添加到library.js中以实现这一点。

清单 16-16。 根据列表视图中选择的项目数量,启用或禁用打开按钮

libraryListView_selectionChanged: function (e) {
    // Get the number of currently selected items
    var selectionCount = libraryListView.winControl.selection.count();

    if (selectionCount <= 0) {
        openDocumentCommand.winControl.disabled = true;
        libraryAppBar.winControl.hide();
    } else if (selectionCount > 1) {
        openDocumentCommand.winControl.disabled = true;
        libraryAppBar.winControl.show();
    } else { // if (selectionCount === 1) {
        openDocumentCommand.winControl.disabled = false;
        libraryAppBar.winControl.show();
    }
},

当用户点击这个按钮时,调用Windows.System.Launcher.launchFileAsync将在默认的文件查看器中打开文件。将清单 16-17 中的click事件处理程序添加到library.js中。

清单 16-17。 打开按钮的 Click 事件处理程序

openDocumentCommand_click: function (e) {
    libraryListView.winControl.selection.getItems()
        .then(function (selectedItems) {
            if (selectedItems && selectedItems[0] && selectedItems[0].data) {
                return Windows.System.Launcher.launchFileAsync(selectedItems[0].data);
            }
        })
        .then(null, function error(result) {
            new Windows.UI.Popups
                .MessageDialog("Could not open file.", "An error occurred. ")
                .showAsync();
        });
},

代码将只打开第一个被选择的文件,但是因为你在清单 16-16 中添加了代码,无论如何不应该有多于一个的项目被选择。如果文件可以打开,则启动该文件类型的默认应用;否则,将显示一条错误消息。一个重载版本的launchFileAsync 允许你在启动时指定额外的选项。例如,您还可以添加一个带有。。。应用栏按钮,并指定应该让用户选择为所选文件启动的应用。这对于图像尤其有用,因为默认应用可能是不允许用户编辑图像的应用。通过允许用户指定他们希望启动的应用,他们可以选择编辑文件。因为代码几乎与我刚刚介绍的相同,所以我不会在本书中演示,但是我已经在本书附带的源代码中实现了这个功能。(见该书的 Apress 产品页的源代码/下载标签[ www.apress.com/9781430257790 ]。)

允许用户在默认应用中启动文档的最后一步是在ready函数中连接事件处理程序。将清单 16-18 中突出显示的代码添加到library.js

清单 16-18。 连接起新的事件处理器

ready: function (element, options) {
    this.projectId = options && options.projectId;

    this.setProjectName();
    this.bindProjectLibraryFiles();

    libraryListView.winControl.onselectionchanged =
        this.libraryListView_selectionChanged.bind(this);

    openDocumentCommand.winControl.onclick = this.openDocumentCommand_click.bind(this);
    addDocumentsCommand.winControl.onclick = this.addDocumentsCommand_click.bind(this);
},

立即运行 Clok 并导航到文档库。当您选择一个文件时,您将能够使用该文件类型的默认应用打开它(参见图 16-11 )。

9781430257790_Fig16-11.jpg

图 16-11 。单击打开按钮将使用 Microsoft ExcelT3 启动所选电子表格

管理项目文件

我们的文档库正在整合,已经成为 Clok 的一个有用的补充。在我结束这一章之前,我想再增加两个特性。为了说明从 Clok 的本地数据文件夹中复制文件,我将实现一个导出特性,为了说明如何从StorageFolder中删除文件,我将添加一个删除特性。

image 注意那些认为自己是超级用户的用户会很高兴地发现,他们在 Windows 资源管理器中使用的键盘快捷键也适用于ListView控件中的项目。如果一个ListView当前处于焦点上,您可以使用 Ctrl+A 选择一个ListView中的所有项目,使用 ESC 键取消选择所有项目,使用箭头键浏览ListView中的项目。此外,您可以按住 Shift 键或 Ctrl 键单击ListView中的项目来选择多个项目。

使用文件夹选择器导出文件

允许用户向 Clok 添加任意多的文档,但不提供任何获取文档的方法,这有多大用处?在本节中,我将向您展示如何向文档库添加导出功能。首先,让我们在应用栏中添加一个新按钮。将清单 16-19 中的代码添加到library.html中。

清单 16-19。 向应用栏添加导出按钮

<button
    data-win-control="WinJS.UI.AppBarCommand"
    data-win-options="{
        id:'exportDocumentsCommand',
        label:'Export',
        icon:'savelocal',
        section:'selection',
        tooltip:'Export',
        disabled: true}">
</button>

毫不奇怪,从文档库中导出文件类似于向其中添加文件。StorageFile对象的copyAsync方法执行实际的文件复制操作。最大的区别在于,不是用FileOpenPicker挑选文件添加到文档库中,也不是用getProjectFolder函数确定StorageFolder,而是从ListView对象selection属性中获取对一组StorageFile对象的引用,并允许用户用FolderPicker指定一个StorageFolder。一旦你有了一个StorageFolderStorageFile对象的集合,基本过程是一样的。将清单 16-20 中的代码添加到library.js中。

清单 16-20。 导出按钮的点击处理程序

exportDocumentsCommand_click: function (e) {
    if (!this.canOpenPicker()) {
        return;
    }

    var folderPicker = new Windows.Storage.Pickers.FolderPicker;
    folderPicker.suggestedStartLocation = pickerLocationId.desktop;
    folderPicker.fileTypeFilter.replaceAll(["*"]);

    folderPicker.pickSingleFolderAsync().then(function (folder) {
        if (folder) {
            return libraryListView.winControl.selection.getItems()
                .then(function (selectedItems) {
                    var copyPromises = selectedItems.map(function (item) {
                        return item.data.copyAsync(
                            folder,
                            item.data.name,
                            createOption.generateUniqueName);
                    });

                    return WinJS.Promise.join(copyPromises);
                });
        } else {
            return WinJS.Promise.as();
        }
    }).then(function error(result) {
        new Windows.UI.Popups
            .MessageDialog("All files successfully exported.", "File export is complete.")
            .showAsync();
    }, function error(result) {
        new Windows.UI.Popups
            .MessageDialog("Could not export all selected files.", "An error occurred. ")
            .showAsync();
    });
},

image 注意记住在ready函数中绑定这个click事件处理程序,并修改libraryListView_selectionChanged,当列表中的一个或多个文档被选中时,激活导出按钮。

虽然过程与添加文档相同,但我在这里做了一处更改。如果存在命名冲突,copyAsync函数将为复制的文件生成一个新名称,而不是替换已经存在的文件。您可以自己实现的一个有用的功能是允许用户指定他或她是否想要替换现有文件,保留文件的两个副本,或者取消导出操作。

当您添加代码以在ListView中选择一个或多个项目时启用导出按钮,并在library.jsready函数中绑定此click事件处理程序后,文档导出功能就完成了。但是,在测试之前,让我们继续添加删除功能。

从本地应用文件夹中删除文件

“呜呜!我不是故意加那个文件的。”"这个模型已经过时了。""我的硬盘快满了。"

用户希望从文档库中删除文件的原因有很多,这些只是其中的几个。幸运的是,删除文件就像导出文件一样简单。首先,让我们在应用栏中添加一个新按钮。将清单 16-21 中的代码添加到library.html中。

清单 16-21。 向应用栏添加删除按钮

<button
    data-win-control="WinJS.UI.AppBarCommand"
    data-win-options="{
        id:'deleteDocumentsCommand',
        label:'Delete',
        icon:'delete',
        section:'selection',
        tooltip:'Delete',
        disabled: true}">
</button>

在删除一个文件之前,这是一个永久的操作,您将提示用户确定他或她想要点击删除按钮。将清单 16-22 中的代码添加到library.js中。

清单 16-22。 删除按钮的点击处理程序

deleteDocumentsCommand_click: function (e) {
    var msg = new Windows.UI.Popups.MessageDialog(
        "This cannot be undone.  Do you wish to continue?",
        "You're about to permanently delete files.");

    var buttonText = (libraryListView.winControl.selection.count() <= 1)
        ? "Yes, Delete It"
        : "Yes, Delete Them";

    msg.commands.append(new Windows.UI.Popups.UICommand(buttonText, function (command) {
        libraryListView.winControl.selection.getItems()
            .then(function (selectedItems) {
                var deletePromises = selectedItems.map(function (item) {
                    return item.data.deleteAsync();
                });

                return WinJS.Promise.join(deletePromises);
            })
            .then(null, function error(result) {
                new Windows.UI.Popups
                    .MessageDialog("Could not delete selected files.", "An error occurred.")
                    .showAsync();
            });
    }));

    msg.commands.append(new Windows.UI.Popups.UICommand(
        "No, Don't Delete Anything",
        function (command) { }
    ));

    msg.defaultCommandIndex = 0;
    msg.cancelCommandIndex = 1;

    msg.showAsync();
},

如果用户确认他或她确实希望继续,只需为选择中的每个StorageFile调用deleteAsync即可。在ListView中选择一个或多个项目时,不要忘记启用删除按钮,并且一定要在library.jsready功能中绑定这个click事件处理程序。

现在是时候看看你的工作成果了。现在,您已经有了一个功能完整的文档库。立即运行 Clok 并导航至文档库(参见图 16-12 )。浏览所有的函数,看看它们是如何工作的。添加一些文件,打开它们,导出它们,然后删除它们。你应该会发现这种体验非常自然,任何使用过 Windows 8 一段时间的人都应该很熟悉。

9781430257790_Fig16-12.jpg

图 16-12 。已建成的文档库

结论

处理文件是不可避免的。虽然StorageFileStorageFolder函数的异步特性改变了您与文件和文件夹交互的方式,但是如果您已经用其他编程语言进行过任何涉及用户计算机文件系统的开发,这些差异就很容易理解了。在Windows.Storage名称空间中有许多高级功能。如果您发现这一介绍性章节不能满足您的所有需求,请务必在线阅读 MSDN 文档(http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.aspx)。

当我介绍共享契约时,我们将在第十九章中再次访问文档库,当我讨论如何使用用户的相机时,我们将在第二十二章中再次访问。`

十七、处理状态变化

可以说,任何应用最重要的要求是它在任何情况下都能按照用户的期望工作。显然,正确地执行核心任务对您的应用来说很重要,但这不是我在这里讨论的内容。

你添加到你的应用中使其看起来“正常工作”的细节或小改动会让用户一次又一次地回来。类似于我在第十章中描述的微妙的动画,这种对细节的关注对大多数用户来说可能从来都不明显,但是如果你忽略它,他们肯定会注意到。在这一章中,我将讨论两个方面,你可以在这两个方面做些小工作,使你的应用看起来更加完美。

首先要考虑应用的激活状态。你的应用是如何开始的?用户以前是如何与你的应用交互的?由于资源限制,用户是否认为你的应用仍然在后台运行,即使 Windows 已经终止了它?第二个方面是考虑应用的视图状态。用户是横向还是纵向浏览你的应用?当他们看着你的应用旁边的另一个应用时,他们有没有在一个狭窄的窗格中抓取你的应用?当他们看着你旁边的应用时,他们是否在一个狭窄的窗格中抓拍了另一个应用?

在构建应用时考虑这些类型的问题将允许您做出有助于满足用户期望的决策。幸运的是,所需的代码通常很容易实现。因此,只需相对较小的努力,您就可以解决任何应用最重要的需求。

应用激活状态

有几种方法可以让用户激活你的应用。最简单也是最常见的方式是通过点击开始屏幕上的一个磁贴来启动它。但是,您的应用也可以通过以下方式之一启动:

  • 当用户使用 Windows Search charm 在您的应用中查找内容时
  • 当用户使用 Windows Share charm 与您的应用共享某些内容时
  • 当用户在FileOpenPickerFileSavePicker中指定您的应用时,从另一个应用打开或保存文件

这些只是你的应用被激活的许多方式中的一部分。有十几种方法可以激活 Windows 应用商店应用。枚举定义了所有不同类型的应用激活方式。正如我提到的,从开始屏幕上的磁贴启动你的应用是最常见的一种激活方式,也是我在本章中讨论的唯一一种。在第十九章,我将演示如何在你的应用中搜索,以及如何与你的应用共享文档。

image MSDN(http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.activation.activationkind)上有Windows.ApplicationModel.Activation.ActivationKind枚举的文档。

当你在第九章的中第一次从导航应用模板创建 Clok Visual Studio 项目时,default.js被添加到你的项目中,包含了许多样板代码。这段代码包括处理应用的activated事件。很容易忽略添加的内容,但现在让我们仔细看看。如果你打开default.js,你会看到激活的事件处理程序(见清单 17-1 )。

清单 17-1。 应用的激活事件处理程序

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

// SNIPPED

app.addEventListener("activated", function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        // SNIPPED
    }
});

该处理程序的第一行检查应用是如何激活的。在这个生成的代码中,事件处理程序只在应用启动时做一些事情,很可能是从开始屏幕启动。

扩展闪屏

每个 Windows Store 应用都有一个闪屏。当你在 Visual Studio 中使用第四章中讨论的项目模板创建一个新项目时,Visual Studio 会在灰色背景上添加一个占位符图像。这是默认的 Clok 闪屏,可以在图 17-1 中看到。在第二十三章中,我将向你展示如何用定制的闪屏来打造你的应用。

9781430257790_Fig17-01.jpg

图 17-1 。默认锁定闪屏

在应用的第一个屏幕可见之前,Windows 会自动显示该初始屏幕一小段时间。但是,有时您可能需要在用户能够与您的应用交互之前执行一些任务。例如,您可能需要将数据加载到内存中,从 web 服务请求数据,或者打开一个大文档。在这些情况下,如果这些任务没有在您的第一个屏幕可见之前完成,您的应用可能会出现故障。这正是我们在第十四章中遇到的问题。

当 Clok 启动时,数据从我们的 IndexedDB 数据库加载到WinJS.Binding.List 中,用于填充 Clok 仪表板上的项目下拉列表。因为这些数据是异步加载的,所以仪表板屏幕通常在必要的数据出现在List之前就加载了。在第十五章中,我将home.js改为先初始化List,然后将ready函数的内容包装在Promisedone函数中(见清单 17-2 )。

***清单 17-2。***home . js 中的当前就绪功能

ready: function (element, options) {
    storage.initialize().done(function () {
        // SNIPPED
    }.bind(this));
},

在这一节中,我将向您展示如何在 Clok 的激活过程中使用扩展闪屏来加载这些数据。一个扩展的闪屏并不像名字最初暗示的那样。这不是对 Windows 默认显示的闪屏的更改。它也不一定是一个单独的屏幕,尽管理论上你可以在一个单独的屏幕上实现它。实现扩展闪屏最常见的方式是在default.htmldiv元素中定义它。div被配置成和默认的闪屏一模一样,可以选择显示额外的控件,比如一个进度条来告诉用户应用还在加载。

这是我们将在 Clok 中使用的技术。首先从home.js中删除清单 17-2 中高亮显示的代码行。然后将清单 17-3 中的代码从添加到default.html,紧跟在开始的body标签之后。

清单 17-3。 标记为扩展闪屏

<div id="extendedSplash" class="hidden">
    <img id="splashImage" src="/img/splashscreen.png" />
    <progress id="splashProgress" style="color: white;"></progress>
</div>

参考图像splashscreen.png 与图 17-1 中默认启动画面上使用的图像相同。这个文件可以在package.appxmanifest中配置,我会在第二十三章中介绍。在图像下方,我添加了一个不确定的进度条,当扩展闪屏可见时,它将继续显示动画。为了使扩展闪屏与默认闪屏的外观相匹配,将清单 17-4 中的 CSS 添加到default.css中。

清单 17-4。 CSS 为扩展闪屏

.hidden {
    display: none;
}

#extendedSplash {
    background-color: #3399aa;
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0px;
    left: 0px;
    text-align: center;
}

    #extendedSplash #splashImage {
        position: absolute;
    }

我们将利用绝对定位来确保splashscreen.png显示在扩展闪屏中与默认闪屏相同的位置。我一会儿会展示这个。此外,我还设置了扩展闪屏的背景色,以匹配 Clok 的背景色。如果你没有好的理由,微软的建议是让扩展闪屏的背景色与默认闪屏的背景色匹配,默认闪屏在package.appxmanifest中指定。目前,Clok 的默认闪屏是灰色背景,但我会在第二十三章中告诉你如何改变。因此,虽然背景颜色目前与默认启动屏幕的颜色不匹配,但最终会在 Clok 准备好用于 Windows 商店之前匹配。

image 微软的闪屏指南可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465338.aspx)上获得。

我将向您展示如何创建一个管理扩展闪屏的类。这个新类将负责在需要时显示和隐藏闪屏,并确保长时间运行的初始化代码在进程中的正确位置运行。为了做好准备,在default.js中需要一点重构。为了帮助确保我们的代码在本节结束时仍然可读和可理解,从清单 17-1 中截取的代码必须被移到它自己的函数中。在default.js中,修改activated事件处理程序并创建一个新函数,如清单 17-5 所示。

清单 17-5。 重构激活事件处理程序

app.addEventListener("activated", function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        launchActivation(args);
    }
});

var launchActivation = function (args) {
    if (args.detail.previousExecutionState
            !== activation.ApplicationExecutionState.terminated) {
        // TODO: This application has been newly launched. Initialize
        // your application here.
    } else {
        // TODO: This application has been reactivated from suspension.
        // Restore application state here.
    }

    if (app.sessionState.history) {
        nav.history = app.sessionState.history;
    }

    initializeRoamingSettings();

    // add our SettingsFlyout to the list when the Settings charm is shown
    WinJS.Application.onsettings = function (e) {
        e.detail.applicationcommands = {
            "options": {
                title: "Clok Options",
                href: "/settings/options.html"
            },
            "about": {
                title: "About Clok",
                href: "/settings/about.html"
            }
        };

        if (roamingSettings.values["enableIndexedDbHelper"]) {
            e.detail.applicationcommands.idbhelper = {
                title: "IndexedDB Helper",
                href: "/settings/idbhelper.html"
            };
        }

        WinJS.UI.SettingsFlyout.populateSettings(e);
    };

    args.setPromise(WinJS.UI.processAll().then(function () {
        configureClock();

        if (nav.location) {
            nav.history.current.initialPlaceholder = true;
            return nav.navigate(nav.location, nav.state);
        } else {
            return nav.navigate(Application.navigator.home);
        }
    }));
}

到目前为止,这在功能上等同于您刚才替换的代码。现在,我们将更改activated事件处理程序来实例化一个新类Clok.SplashScreen.Extender ,它将管理扩展的闪屏。用清单 17-6 中突出显示的代码更新default.js

清单 17-6。 修改激活事件处理程序

app.addEventListener("activated", function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {

        var extender = new Clok.SplashScreen.Extender(
            extendedSplash,
            args.detail.splashScreen,
            function (e) {
                args.setPromise(Clok.Data.Storage.initialize());
                simulateDelay(2000);
                launchActivation(args);
            });
    }
});

这个新类的构造函数有三个参数。第一个是对div的引用,代表default.html中扩展的闪屏。第二个是对默认闪屏的引用,可以从activated事件的参数中获得。第三个参数是初始化应用的函数。在此功能完成之前,将显示扩展的闪屏。

image 注意清单 17-6 中的代码引用了一个名为simulateDelay的函数。实际上,Clok 的初始化非常快。此功能用于模拟更长的初始化过程,应在扩展闪屏经过全面测试后删除。本书附带的源代码中提供了simulatedDelay函数的定义。你可以在该书的 press 产品页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790)。

当然,Clok.SplashScreen.Extender类还不存在。在 Visual Studio 的js文件夹中添加一个名为extendedSplash.js 的 JavaScript 文件。将清单 17-7 中的代码从添加到extendedSplash.js

清单 17-7。 定义闪屏扩展器类

(function () {
    "use strict";

    var util = WinJS.Utilities;

    var extenderClass = WinJS.Class.define(
        function constructor(extendedSplash, defaultSplash, loadingFunctionAsync) {
            this._extendedSplash = extendedSplash;
            this._defaultSplash = defaultSplash;
            this._loadingFunctionAsync = loadingFunctionAsync;

            this._defaultSplash.ondismissed = this._splash_dismissed.bind(this);

            this._show();
        },
        {
            _splash_dismissed: function (e) {
                WinJS.Promise.as(this._loadingFunctionAsync(e))
                    .done(function () {
                        this._hide();
                    }.bind(this));
            },

            _show: function () {
                this._updatePosition();
                util.removeClass(this._extendedSplash, "hidden");
            },

            _hide: function () {
                if (this._isVisible()) {
                    util.addClass(this._extendedSplash, "hidden");
                }
            },

            _isVisible: function () {
                return !util.hasClass(this._extendedSplash, "hidden");
            },

            _updatePosition: function () {
                var imgLoc = this._defaultSplash.imageLocation;

                var splashImage = this._extendedSplash.querySelector("#splashImage");
                splashImage.style.top = imgLoc.y + "px";
                splashImage.style.left = imgLoc.x + "px";
                splashImage.style.height = imgLoc.height + "px";
                splashImage.style.width = imgLoc.width + "px";

                var splashProgress = this._extendedSplash.querySelector("#splashProgress");
                splashProgress.style.marginTop = (imgLoc.y + imgLoc.height) + "px";
            },
        },
        { /* no static members */ }
    );

    WinJS.Namespace.define("Clok.SplashScreen", {
        Extender: extenderClass,
    });
})();

当默认启动画面关闭时,执行清单 17-6 中指定的初始化功能_loadingFunctionAsync ,当该功能完成时,扩展启动画面被隐藏。用WinJS.Promise.as包装对_loadingFunctionAsync的调用允许Extender类将初始化函数视为已经返回了一个Promise,即使它没有返回。_updatePosition功能用于根据图像在默认闪屏上的位置,确定扩展闪屏中的图像和进度条在屏幕上的位置。

完成扩展闪屏的最后一步是添加对extendedSplash.jsdefault.html的引用。此外,其他脚本引用的顺序可能需要修改,以确保在尝试使用它们之前定义了类和数据。用来自清单 17-8 的代码更新default.htmlhead元素。

**清单 17-8。【default.html】**剧本参考文献

<head>
    <meta charset="utf-8" />
    <title>Clok</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <!-- Clok references -->
    <link href="/css/default.css" rel="stylesheet" />
    <link href="/css/themeroller.css" rel="stylesheet" />

    <!-- Clok generic scripts -->
    <script src="/js/extensions.js"></script>
    <script src="/js/utilities.js"></script>
    <script src="/js/navigator.js"></script>

    <!-- Clok data and extended splash screen -->
    <script src="/data/project.js"></script>
    <script src="/data/timeEntry.js"></script>
    <script src="/data/storage.js"></script>
    <script src="/data/bingMapsWrapper.js"></script>
    <script src="/js/extendedSplash.js"></script>

    <!-- Clok controls -->
    <script src="/controls/js/timerControl.js"></script>
    <script src="/controls/js/clockControl.js"></script>

    <script src="/js/default.js"></script>
</head>

现在,当您启动 Clok 时,在初始闪屏关闭后,会显示带有进度条的扩展闪屏(参见图 17-2 )。

9781430257790_Fig17-02.jpg

图 17-2 。带有进度条的扩展闪屏

一旦Clok.Data.Storage类被初始化,先前的激活码,现在在launchActivation函数中,被执行,然后闪屏被隐藏。

先前执行状态

当用户启动你的应用时,让他们得到他们期望的体验是很重要的。最有可能的是,他们的期望是基于他们最后一次看到你的申请。在第十五章中,我介绍了会话状态,并展示了如何将应用的当前状态保存到会话状态,这样当用户返回到你的应用时,它可以被恢复。恢复终止的会话将大大有助于提供预期的体验,尤其是因为用户可能没有意识到会话在一开始就被终止了。

当您的应用被激活时,用户可能会有其他期望。如果他们通过在触摸屏上向下滑动或按下键盘上的 Alt+F4 来手动关闭应用,他们可能希望在下次启动应用时看到主屏幕。如果激活是点击第二个磁贴(我将在第二十一章中讨论这个话题)或 Windows Search charm 的结果,他们可能不希望看到主屏幕。此外,最明智的行为可能会有所不同,这取决于他们是否已经在使用该应用。

launchActivation函数中的第一段代码演示了如何确定用户最后一次与应用交互的方式。我已经将该块复制到清单 17-9 的中。

清单 17-9。 检查上一次执行状态

if (args.detail.previousExecutionState
        !== activation.ApplicationExecutionState.terminated) {
    // TODO: This application has been newly launched. Initialize
    // your application here.
} else {
    // TODO: This application has been reactivated from suspension.
    // Restore application state here.
}

您可以使用previousExecutionState 的值来决定如何处理应用的激活。虽然我们还没有在 Clok 中实现任何基于previousExecutionState的定制逻辑,但是这段代码展示了如果它的previousExecutionStateterminated,我们可以如何不同地初始化 Clok。在Windows.ApplicationModel.Activation.ApplicationExecutionState枚举中定义了五个选项。除了terminated,其他可能的previousExecutionState值如下:

  • notRunning:应用尚未运行,因为用户登录到了 Windows,或者从未运行过(新安装)。
  • 应用最后一次运行时,用户通过向下滑动或按 Alt+F4 关闭了它。
  • running:当用户通过例如从 Windows Search charm 中进行搜索或其他类型的激活(例如点击次级磁贴或通知)来激活应用时,该应用当前正在运行。
  • suspended:当用户通过例如从 Windows Search charm 进行搜索或其他类型的激活(例如点击辅助磁贴或通知)来激活应用时,该应用当前被挂起。

MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.activation.applicationexecutionstate.aspx)上提供了Windows.ApplicationModel.Activation.ApplicationExecutionState枚举的完整定义。查看清单 17-9 中的代码,你会注意到,默认情况下,从 Visual Studio 项目模板创建的项目对所有这些都一视同仁,除了terminated。这是一个合理的缺省值,但是在开发应用时要记住这个属性。当您的应用按预期运行时,您将改善用户体验。特别是,我将在第十九章中讨论一些情况,对于不同的previousExecutionState值,您的应用应该有不同的行为。

处理应用暂停

当用户离开您的应用时,Windows 会挂起它。当他或她切换回来时,Windows 将恢复您的应用,对用户来说它将是无缝的。但是,有时如果您的应用被挂起,并且当前使用的应用需要额外的资源,Windows 将终止您的应用。当用户切换回你终止的应用时,你有责任利用会话状态让它看起来无缝,正如在第十五章中所讨论的。

正如我在《??》第十五章中提到的,当 Windows 挂起你的应用时会引发WinJS.Application.oncheckpoint事件。但是,当应用终止时,不会引发任何事件。理想情况下,当用户在应用中进行更改时,您应该保持会话状态最新。然而,oncheckpoint事件在应用被挂起时被调用,允许您保存对会话状态的任何未保存的更改。

需要明确的是,当保存会话状态并在应用挂起时处理oncheckpoint事件时,目的不是为了恢复一个挂起的应用。Windows 会自动为您处理这些问题。因为您的应用在终止时没有得到通知,保存会话状态并处理oncheckpoint事件使您能够恢复一个终止的应用。

当 Visual Studio 第一次创建 Clok 项目时,它在default.js中包含了一个用于oncheckpoint事件的处理程序,该处理程序可用于在挂起时将应用级设置存储到会话状态。例如,用户的导航历史被保存到default.js中的会话状态。此外,如第十五章中的所示,您可以在页面控件中处理oncheckpoint事件,并在那里解决页面级会话状态问题。更多关于oncheckpoint事件的信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br229839.aspx)上找到。

应用视图状态

应用的另一种状态是视图的状态。您的应用可以处于几种不同的视图状态之一:横向(图 17-3 )、纵向(图 17-4 )、对齐或填充(图 17-5 )。

9781430257790_Fig17-03.jpg

图 17-3 。天气在景观中的应用

9781430257790_Fig17-04.jpg

图 17-4 。16 纵向视图中的天气应用

9781430257790_Fig17-05.jpg

图 17-5 。快照视图中的天气应用和填充视图中的地图应用

查看图 17-3 、图 17-4 和图 17-5 中的天气应用,您会看到应用的布局随着视图的变化而变化。在横向视图中,天气预报从左到右显示。在纵向视图中,从上到下显示。在捕捉视图中,它也从上到下显示,但细节较少。虽然这些更改是必须遵循的规则,但考虑用户可能与应用交互的不同方式并相应地更改它也很重要。

到目前为止,在 Clok 中,我们只考虑了用户在横向视图中运行 Clok 的选项。如果在 Windows 模拟器中运行 Clok,可以在纵向视图中模拟运行 Clok(图 17-6 )。

9781430257790_Fig17-06.jpg

图 17-6 。纵向视图中的 Clok 仪表板屏幕

这看起来不太好,是吗?不幸的是,它在快照视图中看起来更糟(见图 17-7 )。

9781430257790_Fig17-07.jpg

图 17-7 。在快照视图中锁定仪表板屏幕

在纵向视图和快照视图中都无法访问核心功能。正如你可能想象的那样,Clok 中的其他屏幕在除了风景以外的视图中看起来同样糟糕。

用 CSS 媒体查询更新布局

更新 Clok Dashboard 屏幕以在其他视图状态下工作可以完全在 CSS 中完成,使用 CSS 媒体查询。我在第九章中简要介绍了 CSS 媒体查询,当时我描述了如何根据用户屏幕的宽度以不同的尺寸在背景中显示 Clok 徽标。简而言之,媒体查询允许您指定仅在满足特定条件时才生效的 CSS 规则。在第九章中,你添加了仅在屏幕宽度小于等于 1400 像素时应用的 CSS 规则(参见清单 17-10 )。

清单 17-10。 CSS 媒体查询示例来自第九章

@media screen and (max-width: 1400px) {
    #contenthost {
        background-size: 40%;
    }
}

除了使用屏幕宽度作为条件,我们还可以使用当前视图状态。清单 17-11 中的 CSS 规则仅在 Clok 以纵向视图运行时适用。将此 CSS 添加到home.css的末尾。

清单 17-11。

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

    .homepage section[role=main] {
        margin-left: 100px;
        display: block;
    }

        .homepage section[role=main] #rightPane {
            display: none;
        }

    .homepage #mainMenu {
        height: 424px;
        -ms-flex-direction: column;
    }

    .homepage #elapsedTime #elapsedTimeClock {
        font-size: 6em;
        font-weight: 200;
    }
}

因为 Clok Dashboard 屏幕的大部分样式在纵向视图中仍然有效,所以您只需要指定正在变化的规则。例如,当 Visual Studio 创建一个新的页面控件时,它会在 CSS 文件中包含媒体查询,以更改主section的边距。在此媒体查询之前,我们可能对主section的 CSS 所做的任何其他更改仍然适用,即使屏幕处于纵向视图。因此,在清单 17-11 中,我不需要指定所有的 CSS 来配置mainMenu元素来使用 flexbox 布局。相反,我只需要改变 flexbox 的高度和方向。

在 Windows 模拟器中运行 Clok now,并将其旋转至纵向视图(参见图 17-8 )。

9781430257790_Fig17-08.jpg

图 17-8 。在纵向视图中修改了 Clok 仪表板屏幕

通过对 flexbox 定义的一个简单更改,相机、项目和时间表菜单选项现在显示在开始/停止时钟菜单选项旁边,而不是在它下面。

在您的 CSS 媒体查询中,-ms-view-state的其他选项包括fullscreen-portraitsnappedfilledfullscreen-landscape。此外,可以使用许多其他媒体查询,如纵横比。媒体可能提出的问题可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh453556.aspx)上找到。

在本书附带的源代码中,我对快照和填充视图中的 Clok Dashboard 屏幕进行了类似的更改,还对项目、项目细节和文档库屏幕进行了更改。我不会在本书中讨论这些变化,因为使用 CSS 媒体查询的技术是相同的,CSS 覆盖本身也非常简单。

用 JavaScript 更新布局

一些适应不同视图状态的更改可以很容易地完全通过 CSS 更改来完成。然而,其他更改也需要更改 JavaScript 代码。回到第七章的,当我第一次介绍ListView控件时,我展示了一个将layout属性设置为GridLayoutListLayout 的例子。在GridLayout中,如果没有指定layout属性,这是默认设置,条目以网格格式从上到下,然后从左到右填充ListView。使用ListLayout时,ListView中的项目在一列中从上到下显示。天气应用使用ListLayout在纵向视图(参见图 17-4 )和抓图视图(参见图 17-5 )中显示天气预报,在本节中,我将向您展示如何更新 Clok 的项目屏幕以在抓图视图中使用ListLayout,尽管它将继续在纵向视图中使用GridLayout

为了实现这一改变,在list.html中不需要更新。在list.css的媒体查询中需要一些 CSS 规则,但是我不会在这里讨论这些。它们类似于前一节中所做的 CSS 更改,包含在本书附带的源代码中。为项目屏幕添加肖像和快照支持所需的大部分工作将在list.js完成。将清单 17-12 中高亮显示的代码添加到list.js

清单 17-12。 修改 ListView 绑定,基于视图状态

filter_value_changed: function (e) {
    this.filteredProjects = storage.projects.getGroupedProjectsByStatus(this.filter.value);

    listView.winControl.itemDataSource = this.filteredProjects.dataSource;
    listView.winControl.groupDataSource = this.filteredProjects.groups.dataSource;
    zoomedOutListView.winControl.itemDataSource = this.filteredProjects.groups.dataSource;

    this.configureListViewLayout();
},

configureListViewLayout: function () {
    var viewState = Windows.UI.ViewManagement.ApplicationView.value;

    if (viewState === Windows.UI.ViewManagement.ApplicationViewState.snapped) {
        listView.winControl.layout = new WinJS.UI.ListLayout();
        semanticZoom.winControl.enableButton = false;
    } else {
        listView.winControl.layout = new WinJS.UI.GridLayout();
        zoomedOutListView.winControl.layout = new WinJS.UI.GridLayout();
        semanticZoom.winControl.enableButton = true;
    }
},

在为ListView控件设置数据源后,调用一个新函数configureListViewLayout来确定ListView应该如何显示。当前视图状态在Windows.UI.ViewManagement.ApplicationView.value中可用。如果 Clok 是snapped,那么SemanticZoom控件被禁用,并且ListView对象的layout属性被设置为ListLayout。否则,ListView控件将继续使用GridLayout,并且SemanticZoom被启用。

您需要采取的最后一步是在视图状态改变时处理这种情况。如果你添加一个名为updateLayout 的函数到你的页面控件定义中,当屏幕尺寸改变时,该方法将被调用。将清单 17-13 中的代码从添加到list.js

清单 17-13。 处理视图状态变化

updateLayout: function (element, viewState, lastViewState) {
    this.configureListViewLayout();
},

现在,当屏幕大小发生变化时,比如当 Clok 被抓取或取消抓取时,将调用configureListViewLayout函数来确定ListView是否应该用ListLayoutGridLayout来渲染。

现在运行 Clok 并导航到项目屏幕。ListView现在将在一个单列列表中显示所有项目(参见图 17-9 )。

9781430257790_Fig17-09.jpg

图 17-9 。项目屏幕现在捕捉时使用列表布局

在本书附带的源代码中,您会发现对文档库屏幕的类似更改。因为用于更新该屏幕的技术与我刚才介绍的技术相同,所以我不会在本书中再次介绍。

不支持捕捉视图时

有时候,由于某种原因,应用中的某些屏幕,或者整个应用,可能无法在快照视图中实现。一个常见的例子是必须在风景视图中玩的游戏。甚至商店应用在快照视图中也被禁用(见图 17-10 )。

9781430257790_Fig17-10.jpg

图 17-10 。商店应用在快照视图中被禁用

在 Clok 的情况下,有几个屏幕在 snapped 视图中不受支持:方向屏幕、时间表屏幕和时间表图表屏幕。类似于图 17-10 中所示的商店应用,当你不打算为一个特定的屏幕启用快照视图时,你应该显示一些东西来告诉用户这个屏幕不可用。如果不可用,你不应该保留图 17-7 中的默认行为。此外,您不应该将用户重定向到另一个屏幕。当他们取消你的应用的快照时,他们希望仍然在他们快照前的屏幕上。

我们可以为快照视图中不支持的每个屏幕单独实现“不可用屏幕”功能。这将为在每种情况下显示不同的内容提供一些灵活性。然而,在 Clok 中,我们将构建这一功能,以便可以轻松地重用它。第一步是定义用户在捕捉视图中不支持的屏幕时应该看到的内容。将清单 17-14 中的代码添加到default.html中,在本章前面添加的扩展闪屏之后。

清单 17-14。 【屏幕不可用】消息

<div id="snappedNotSupported" class="hidden">
    <img id="notSupportedImage" src="/img/logo.png" />
    <div>This screen is not available while Clok is snapped.</div>
</div>

类似于图 17-10 中的商店应用,Clok 将显示其徽标,我们的信息位于徽标下方的中心。将清单 17-15 中的代码添加到default.css中,以设置消息的样式。

清单 17-15。 消息样式

#snappedNotSupported {
    background-color: #3399aa;
    height: 100%;
    width: 100%;
    padding-top: 200px;
    text-align: center;
}

消息现在已经定义好了,但是默认情况下我用 CSS 隐藏了它。我们需要一种方法,在需要的时候轻松显示这个消息。将清单 17-16 中的功能添加到utilities.js中。

清单 17-16。 功能在截图时显示“屏幕不可用”信息

DisableInSnappedView: function () {
    var viewState = Windows.UI.ViewManagement.ApplicationView.value;
    var appViewState = Windows.UI.ViewManagement.ApplicationViewState;

    var snappedNotSupported = document.getElementById("snappedNotSupported");

    if (snappedNotSupported) {
        if (viewState === appViewState.snapped) {
            WinJS.Utilities.removeClass(snappedNotSupported, "hidden");
        } else {
            WinJS.Utilities.addClass(snappedNotSupported, "hidden");
        }
    }
},

这个新函数,DisableInSnappedView ,将决定 Clok 的当前视图状态。如果调用 Clok 时它处于快照视图中,将显示“不可用屏幕”消息。此时,在不支持快照视图的屏幕上调用DisableInSnappedView就很简单了。将清单 17-17 中突出显示的代码添加到directions.js中。

清单 17-17。 在不支持快照视图的屏幕上调用新功能

ready: function (element, options) {

    // SNIPPED

    Clok.Utilities.DisableInSnappedView();
},

updateLayout: function (element, viewState, lastViewState) {
    Clok.Utilities.DisableInSnappedView();
},

该屏幕调用了两次DisableInSnappedView函数。当屏幕第一次加载时,它在ready函数中被调用,并且每当在updateLayout函数中视图状态改变时,它被再次调用。在这两个地方调用它将确保显示消息,无论用户在已经处于 snapped 视图时试图导航到该屏幕,还是他或她首先导航到该屏幕,然后试图 snap Clok。

因为在快照视图中不支持时间表屏幕和时间表图表屏幕,所以清单 17-17 中的代码也应该添加到这些屏幕的 JavaScript 文件中。一旦你完成了这些,运行 Clok 并导航到时间表屏幕并抓取应用(见图 17-11 )。

9781430257790_Fig17-11.jpg

图 17-11 。在快照视图中,时间表屏幕不可用

image 注意当我在第二十三章中介绍准备您的应用在 Windows 商店中共享的步骤时,当前显示的logo.png文件将被替换。

Windows 的未来版本

在撰写本文时,Windows 下一版本的预览版已经发布。在 Windows 8 中,可以同时查看两个 Windows 应用商店应用,其中一个在预定义宽度的快照视图中。从 Windows 8.1 开始,可以同时查看两个以上的应用,每个视图的宽度可以由用户设置。因此,命名视图状态(如快照)将被弃用,并可能在 Windows 的未来版本中被删除。

对于 Windows 8.1,微软建议更改你的 CSS 媒体查询(见清单 17-18 )和 JavaScript(见清单 17-19 )来检查特定的宽度,并根据这些值更新界面。

清单 17-18 。CSS 媒体查询更改示例

/* replace media queries that check for named view states like this ... */
@media screen and (-ms-view-state: snapped) {
    /* SNIPPED */
}

/* ... with media queries that check for specific widths like this */
@media (max-width: 500px) {
    /* SNIPPED */
}

清单 17-19 。示例 JavaScript 更改

// replace JavaScript that checks for named view states like this ...
var viewState = Windows.UI.ViewManagement.ApplicationView.value;
if (viewState === Windows.UI.ViewManagement.ApplicationViewState.snapped) {
    // SNIPPED
}

// ... with JavaScript that checks for specific widths like this
var windowWidth = document.documentElement.offsetWidth;
if (windowWidth <= 500) {
    // SNIPPED
}

MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/dn263112.aspx)上描述了下一个版本的 Windows 将会做出的这些和其他一些改变。

结论

本章涵盖的主题——了解应用的激活状态和视图状态——可以总结为“做用户期望的事情”知道用户最后一次是如何与你的应用交互的将允许你适当地初始化应用。注意应用的视图状态,以及应用运行时视图状态可能如何变化,这将允许您设置屏幕格式以适应适当的空间。这些小细节是你的用户可能会忽略的,但是如果你没有做,他们会注意到的。

十八、外部库

正如你现在所知道的(我希望),这本书的目的是介绍如何用 HTML 和 JavaScript 构建 Windows Store 应用。这本书的每一章都集中在教你一个概念,允许你使用现有的 HTML 和 JavaScript 知识来构建原生的 Windows 应用。然而,这一章将有一个稍微不同的焦点:使用外部库。

外部库在可重用的包中提供功能,无论它们是 JavaScript 库还是 WinRT 组件。如果您的背景是 web 开发,您可能会首先想到无处不在的 jQuery 库。如果您的背景是与。NET 框架,您可能会想到诸如 log4net 之类的东西,这是一个用于执行日志记录操作的流行库。

当我第一次写这本书的大纲时,我本来打算给这一章起名叫第三方库,但是我很快意识到这并不能提供一个完整的画面。虽然将他人编写的应用库合并到您的应用中是很常见的,但是构建您自己的可重用组件并将其添加到您的应用中也是很常见的。

在这一章中,我将介绍一些你在评估第三方 JavaScript 库时必须考虑的事情。我还将提供一个简单的例子,用 C#创建一个 WinRT 组件供 Clok 使用,同时允许您在其他 Windows 应用商店应用甚至 Windows Phone 应用中重用该功能。

JavaScript 库

JavaScript 库并不缺乏。如果你是一名 web 开发人员,你可以找到一个 JavaScript 库来完成你想要的几乎任何任务,从通用库,如 jQuery,到专用库,如我在第十三章中讨论的 Flotr2 图形库。除了这些第三方库之外,您还可以构建自己的自定义 JavaScript 库,并在多个 Windows 应用商店应用以及您的网站上使用它。

许多 JavaScript 库可以在 Windows 应用商店应用中使用。如果你有一个喜欢的 JavaScript 库,你会很高兴知道它可能会工作。也就是说,有一些注意事项要记住。

安全问题

你可能认为让一个外部库访问用户的计算机是危险的,你是对的。当然,在编写软件时,无论是构建网站、桌面应用还是 Windows 应用商店应用,这都应该是一个问题。在决定将第三方库合并到您的应用中时应该小心,在 Windows 应用商店应用中可能比在网站上更小心,因为用户可能已经授予您的 Windows 应用商店应用对文件系统或相机的访问权限。

为了限制风险,应用中的页面在两种上下文之一中运行:本地上下文或 web 上下文。在 web 上下文中运行的页面,例如在iframe中托管的网页,对用户计算机的访问是有限的,并且不能访问 WinRT。应用中包含的页面,比如我们添加到 Clok 中的每个页面,都在本地上下文中运行。这些页面对用户的计算机有更大的访问权限。因此,在本地上下文中运行的任何脚本对它可以添加到页面的内容都有一些限制。

如果本地上下文中的一个脚本将 HTML 添加到页面中,该 HTML 将由window.toStaticHTML函数处理,以确定是否允许动态添加。如果它包含可能是恶意的代码,比如脚本或iframe,就会抛出异常。当使用某些属性或函数添加内容时,会发生这种情况。例如,试图设置innerHTMLouterHTML属性只有在内容被toStaticHTML成功处理后才会成功。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465388.aspx)上提供了不允许的 HTML 元素和属性的列表。

如果您要添加到应用中的 JavaScript 库使用了这些受限的属性或函数,您应该确保进行彻底的测试,以确保您的应用按预期工作。有关哪些属性和功能受到限制的更多信息,请访问 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx)。

image 注意请记住,您可能包含的 JavaScript 库本身并不一定是恶意的。这些限制是为了防止这些库动态地将恶意代码添加到通过XmlHttpRequest从互联网甚至从用户输入获得的页面中。

绕过安全限制

在一些合法的情况下,您可以拥有一些您信任并且知道是安全的内容,但是通过由toStaticHTML函数强制实现的安全性,这是不允许的。如果您正在编写自己的 JavaScript 库,或者可以修改第三方库,有一些方法可以绕过这些限制,尽管应该谨慎使用。这些限制是有原因的——保护你的用户。只有当您可以确定没有潜在的危险影响时,才应该使用这些方法,因为本节中描述的方法不受上一节中描述的过滤的影响。

在上一节中,我提到了innerHTMLouterHTML属性。虽然设置这些属性需要经过toStaticHTML的过滤,但是WinJS.Utilities.setInnerHTMLUnsafeWinJS.Utilities.setOuterHTMLUnsafe功能则不需要。如果您确信内容是安全的,您可以使用这些方法来设置这些属性。关于setInnerHTMLUnsafe功能和setOuterHTMLUnsafe功能的更多信息可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br211696.aspxhttp://msdn.microsoft.com/en-us/library/windows/apps/br211698.aspx)上获得。

此外,如果你必须调用一个通常被认为不安全的函数,比如动态添加对 DOM 的 JavaScript 引用,你可以把它包装在对MSApp.execUnsafeLocalFunction 的调用中(参见清单 18-1 )。

清单 18-1。 执行不安全的功能

MSApp.execUnsafeLocalFunction(function() {
    // something typically considered unsafe
});

与用于网站的 HTML 和 JavaScript 相比,用于 Windows 应用商店的 HTML 和 JavaScript 的其他差异和相似之处可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx)上找到。

同样,应该非常谨慎地使用这些方法,并且只有当您控制了将对页面进行的更改时才使用这些方法。

WinRT 组件

除了 JavaScript 库,您还可以将 Windows 运行时组件或 WinRT 组件整合到您的应用中。WinRT 组件 是一个可以从 Windows 应用商店应用中使用的 DLL。该组件可以用 C#、VB 或 C++编写,可以在用这些语言编写的 Windows 应用商店应用中使用,也可以在用 HTML 和 JavaScript 编写的 Windows 应用商店应用中使用。

许多第三方 WinRT 组件是可用的。例如,如果您想使用 SQLite 数据库而不是 IndexedDB,您可以在www.sqlite.org/download.html下载一个组件。想要在您的应用中添加电话呼叫支持吗?Twilio 有 WinRT 组件的源代码,可以在www.github.com/twilio/twilio-csharp获得。如果你的应用将利用 Windows Azure 移动服务,你可以从www.windowsazure.com/en-us/develop/mobile获得你需要的信息。

当然,许多其他 WinRT 组件也是可用的,但是在本章的剩余部分,我将介绍如何构建一个非常简单的 WinRT 组件,然后将它集成到 Clok 中。

建的原因 WinRT 组件

有几个原因可能导致您决定构建 WinRT 组件。可能您需要的一些功能在 JavaScript 语言本身中不可用,并且不是由 WinJS 或 WinRT 库提供的。构建 WinRT 组件的另一个常见原因是代码重用。您可以构建一个组件,并在多个 Windows 应用商店应用甚至 Windows Phone 应用中重用它。此外,您可能已经有了为另一个平台编写的大量代码,例如用 C++编写的 iOS 应用代码。

对于这一章,我创建了一个新的需求来添加一些 JavaScript 中没有的功能。在第十一章中,当定义Project类时,我将id属性的默认值设置为基于当前时间的值。对于 Clok 来说,这可能没问题,但是,正如我当时提到的,不能保证它是唯一的。我本可以在第十四章中引入 IndexedDB 时改变这种行为,因为 IndexedDB 允许你指定一个自动递增的标识符,但是我不想让ProjectTimeEntry类依赖于数据源的选择。此外,虽然本书没有涉及,但我可能会在某个时候决定 Clok 需要跨多个设备共享其数据。虽然在同一毫秒内产生两个物体,从而产生相同的id的可能性非常小,但这并非不可能。除了将所有存储和id生成转移到某个中央服务器,要求 Clok 始终连接到互联网,使用全球唯一标识符 GUID,因为id是最佳选择。不幸的是,JavaScript 没有创建 GUIDs 的方法。幸运的是,C#做到了,而且非常容易。

image 注意正如我提到的,WinRT 组件可以在 C#、VB 或 C++中创建。在这个例子中,我将使用 C#,但是如果您对其他语言更熟悉的话,也可以随意使用其中的一种。

创建 C# WinRT 组件

如果你做过的话。NET 开发,那么您可能熟悉如何在 Visual Studio 中创建新的类库项目。您会发现创建 WinRT 组件的过程几乎是相同的。以下步骤将引导您完成这一过程。

  1. Right-click the Clok solution in Visual Studio’s Solution Explorer. Select Add image New Project (see Figure 18-1).

    9781430257790_Fig18-01.jpg

    图 18-1 。向 Clok Visual Studio 解决方案添加新项目

  2. In the left pane of the New Project dialog (see Figure 18-2), select Visual C# image Windows Store.

    9781430257790_Fig18-02.jpg

    图 18-2 。Visual Studio 的 C# Windows 应用商店项目模板

  3. 选择 Windows 运行时组件项目模板。

  4. 给这个项目起个名字:ClokUtilities。

  5. 单击“确定”创建您的项目。

Visual Studio 为你创建项目的时候,还会在项目中添加一个名为Class1.cs 的 C#类文件(见图 18-3 )。在解决方案资源管理器中右键单击Class1.cs文件,并将其重命名为Guid.cs

9781430257790_Fig18-03.jpg

图 18-3 。自动创建的类文件(左),重命名为 Guid.cs(右))

这个类文件Guid.cs,是本书中我们将要修改的 ClokUtilities 项目中唯一的文件。但是,您可以使用这个项目添加任何其他您想用 C#构建的功能。组件不限于单个类。然而,我们现在的要求很简单。我将向您展示一些执行两个简单任务的 C#代码。首先,它提供了一种创建 GUID 并在 JavaScript 中使用它的方法。其次,它为我们提供了一种检查字符串以确定它是否是有效 GUID 的方法。用清单 18-2 中的代码替换Guid.cs的内容。

***清单 18-2。***Guid 类

public sealed class Guid
{
    public static string NewGuid()
    {
        return System.Guid.NewGuid().ToString();
    }

    public static bool IsGuid(string guidToTest)
    {
        if (string.IsNullOrEmpty(guidToTest))
        {
            return false;
        }

        System.Guid guid;
        return System.Guid.TryParse(guidToTest, out guid);
    }
}

现在,从 Visual Studio 的“生成”菜单中,选择“生成解决方案”选项。就这样。您刚刚创建了一个 WinRT 组件,尽管它非常简单。在进入下一节之前,我想指出一些关于这个类和 WinRT 组件的事情。首先,注意到Guid类是公共的和密封的。这是在 JavaScript 中使用 WinRT 类所必需的。

第二,虽然好用。在组件内部,公开的类型 必须是 WinRT 类型。这包括任何公共函数的返回类型、公共函数的任何参数的类型以及任何公共属性的类型。由于这个要求,我不能从NewGuid函数返回类型为System.Guid的值。相反,我必须首先将它转换成一个字符串,然后返回那个值。MSDN ( http://msdn.microsoft.com/en-us/library/br205768(v=vs.85).aspx)上有 WinRT 类型列表。

第三,当使用 JavaScript 中的 WinRT 类时,属性和函数的名称是大小写字母——也就是说,名称中的第一个字符是小写字母,后面的每个单词都以大写字母开头。另一方面,名称空间和类是 Pascal 大小写——每个单词,包括名称的第一个字符,都以大写字符开头。因此,当你在本章后面使用 JavaScript 中的Guid类时,你将调用ClokUtilities.Guid.newGuid,尽管我们在 C#中将其定义为ClokUtilities.Guid.NewGuid

因为这不是一本关于使用 C#创建 WinRT 组件的书,所以我就不赘述了。然而,你可以在http://msdn.microsoft.com/en-us/library/windows/apps/br230301.aspxhttp://msdn.microsoft.com/en-us/library/windows/apps/hh779077.aspx找到大量关于在 MSDN 上用 C#创建 WinRT 组件的附加信息。

更新时钟

在这一节中,我将带您完成利用您刚刚创建的Guid类所需的更改。我将介绍如何更新ProjectTimeEntry类的id属性以使用newGuid函数,以及使用isGuid函数测试有效的 GUIDs。

引用 Clok 中的 ClokUtilities 项目

在使用新的 WinRT 组件之前,必须首先从 Clok 引用 ClokUtilities 项目。

在解决方案资源管理器中,展开 Clok 项目。然后右击引用并选择添加引用。 . .从上下文菜单中(参见图 18-4 )。

9781430257790_Fig18-04.jpg

图 18-4 。打开参考管理器

在打开的参考管理器窗口中,从左窗格中选择解决方案image项目,并勾选 ClokUtilities 旁边的框(参见图 18-5 )。然后单击“确定”按钮添加引用并关闭“引用管理器”窗口。

9781430257790_Fig18-05.jpg

图 18-5 。在引用管理器中添加引用

添加了对组件的引用后,现在可以从 JavaScript 代码中访问上一节中创建的Guid类。要完成从使用数字到 GUIDs 的转换,需要对现有文件做一些小的改动。

更改数据类别

所需的大部分更改是对data文件夹中的类进行的:ProjectTimeEntry,Storage类。让我们从Project类开始。在data文件夹中,打开project.js并用清单 18-3 中突出显示的代码更新constructor函数。

清单 18-3。 变更项目施工方

function constructor() {
    this.id = ClokUtilities.Guid.newGuid();
    this.name = "";
    // SNIPPED
},

这会将id属性的默认值从基于当前时间的数字更改为从 ClokUtilities WinRT 组件生成的 GUID 的字符串表示形式。在project.js中的createFromDeserialized功能也需要类似的改变。用清单 18-4 中突出显示的代码更新createFromDeserialized函数。

清单 18-4。 改变项目工厂方法

createFromDeserialized: function (value) {
    var project = new Clok.Data.Project();

    project.id = (ClokUtilities.Guid.isGuid(value.id) && value.id) || project.id;
    project.name = value.name;

    // SNIPPED

    return project;
},

这种变化利用了isGuid函数来确保id的值始终是一个 GUID。

TimeEntry类的更改是类似的。清单 18-5 突出显示了timeEntry.js中的constructor函数需要做的两处修改,而清单 18-6 中突出显示的修改,看起来与清单 18-4 几乎相同,应该在projectId属性定义中进行。

清单 18-5。 更改 TimeEntry 构造函数

function constructor() {
    this.id = ClokUtilities.Guid.newGuid();
    this._projectId = "";
    this._dateWorked = (new Date()).removeTimePart();
    this.elapsedSeconds = 0;
    this.notes = "";
},

清单 18-6。 改变 TimeEntry 类中的 projectId 属性

projectId: {
    get: function () {
        return this._projectId;
    },
    set: function (value) {
        this._projectId = (ClokUtilities.Guid.isGuid(value) && value) || this._projectId;
    }
},

Storage类中唯一需要改变的是getSortedFilteredTimeEntriesAsync函数,它用于确定在时间表页面上显示哪些时间条目。用清单 18-7 中突出显示的代码行更新storage.js

清单 18-7。 存储类中唯一的变化

storage.timeEntries.getSortedFilteredTimeEntriesAsync = function (begin, end, projectId) {
    return new WinJS.Promise(function (complete, error) {
        setTimeout(function () {
            try {

                var filtered = this
                    .createFiltered(function (te) {
                        if (begin) {
                            if (te.dateWorked < begin) return false;
                        }

                        if (end) {
                            if (te.dateWorked >= end.addDays(1)) return false;
                        }

                        if (projectId && ClokUtilities.Guid.isGuid(projectId)) {
                            if (te.projectId !== projectId) return false;
                        }

                        if (!te.project || te.project.status !== data.ProjectStatuses.Active)
                            return false;

                        return true;
                    });

                var sorted = filtered.createSorted(storage.compareTimeEntries);

                complete(sorted);
            } catch (e) {
                error(e);
            }
        }.bind(this), 10);
    }.bind(this));
};
}

更改 Clok 仪表板屏幕

您必须对 Clok Dashboard 屏幕进行四项更改以支持 GUIDs。其中三个变化是完全一样的,所以我只展示一次。用清单 18-8 中突出显示的代码更新home.js中的editProjectButton_click函数。

清单 18-8。 清除转换为数字

editProjectButton_click: function (e) {
    var id = project.options[project.selectedIndex].value;
    nav.navigate("/pages/projects/detail.html", { id: id });
},

因为在 JavaScript 中我们将 GUIDs 视为字符串,所以这一更改是为了移除id属性到数字的转换。除了editProjectButton_click之外,在save功能和saveDashboardStateToSettings功能中也需要相同的变化,同样在home.js中也是如此。实际上,在getIndexOfProjectId函数中也需要同样的改变,除了验证一个值是一个 GUID,而不是验证它是一个数字。用清单 18-9 中突出显示的代码更新home.js中的getIndexOfProjectId函数。

清单 18-9。 在下拉列表中查找指定项目

getIndexOfProjectId: function (projectId) {
    var index = 0;

    for (var i = 0; i < project.options.length; i++) {
        if (ClokUtilities.Guid.isGuid(project.options[i].value)
                && project.options[i].value === projectId) {

            index = i;
            break;
        }
    }

    return index;
}

更改索引数据库辅助设置弹出按钮

除了上面描述的更改,您还必须对idbhelper.html中的addTestData函数进行更改。在您可以使用“IndexedDB 辅助程序设置”弹出按钮上的“添加测试数据”按钮之前,这是必需的。必要的更改只是用硬编码的 GUIDs 替换硬编码的数字,所以我在这里不举例说明这些更改。您可以在该书的产品页面(www.apress.com/9781430257790)的“源代码/下载”选项卡上找到本章的源代码示例中的更新文件。

调试 WinRT 组件

当调试使用您创建的 WinRT 组件的应用时,Visual Studio 不允许您同时调试 C#代码和 JavaScript 代码。不幸的是,你必须做出选择。幸运的是,在两者之间切换只需要点击几下。首先,在解决方案资源管理器中右击 Clok 项目并选择属性菜单项(参见图 18-6 )。

9781430257790_Fig18-06.jpg

图 18-6 。在解决方案资源管理器中选择属性菜单项

这将打开 Clok 属性页窗口。在左窗格中选择调试,然后将调试器类型更改为您希望调试的类型(参见图 18-7 )。选择“仅脚本”,用于调试 HTML/JavaScript 项目,如 Clok,或选择“仅托管”,用于调试用 C#编写的 WinRT 组件,如 ClokUtilities。

9781430257790_Fig18-07.jpg

图 18-7 。更改调试器类型

无论调试哪种类型的代码,都不能直接启动 WinRT 组件。因此,您必须确保 Clok 项目是在您开始调试时启动的项目。在开始调试之前,可以通过在解决方案资源管理器中右击 Clok 项目并选择“设为启动项目”来实现这一点。

升级期间保留用户数据

如果您现在运行 Clok,您将不会在 Clok 仪表板屏幕的下拉列表中看到任何项目。您也不会在时间表页面上看到任何以前的时间条目。这种变化风险很低,因为 Clok 还没有发布。然而,如果用户已经在使用 Clok,并且存储了项目和时间输入数据,那么如果这些数据全部丢失,他们会非常失望。

当然,如果您有用户,您应该关心在这样的更改期间保留用户的数据。当将来进行类似的更改时,您应该确保包含将应用中的所有现有数据修改为新格式的代码。有许多方法可以实现这一点,但是最合适的地方是在 IndexedDB 初始化代码中。在storage.js中,不是调用indexedDB.open("Clok", 1),而是调用indexedDB.open("Clok", 2),以表明您想要打开一个到数据库版本 2 的连接。因为第一次尝试打开数据库时,版本 2 并不存在,所以你必须更新onupgradeneeded函数来将数据从版本 1 迁移到版本 2(参见清单 18-10 )。

清单 18-10。 将数据迁移到新格式的不完整例子

request.onupgradeneeded = function (e) {
    var upgradedDb = e.target.result;
    if (e.oldVersion < 1) {
        // Version 1: the initial version of the database
        upgradedDb.createObjectStore("projects", { keyPath: "id", autoIncrement: false });
        upgradedDb.createObjectStore("timeEntries", { keyPath: "id", autoIncrement: false });
    }
    if (e.oldVersion < 2) {
        // Version 2: data updated to use GUIDs for id values
        // TODO - modify all projects and time entries
    }
};

但是我们还没有任何用户,所以现在,只需运行 Clok 并使用 IndexedDB 助手来删除所有当前数据并添加新的测试数据。一旦你这样做了,你应该不会注意到与第十七章结尾的版本有什么不同。所有的变化都在幕后。

结论

外部库是重用功能的好方法,不管是别人创建的还是你自己创建的。大量的 JavaScript 库几乎可用于任何目的,或者您可以重用为您的网站创建的 JavaScript 库。如果您需要 JavaScript 中没有的功能,或者如果您有最初为另一个平台编写的现有代码,WinRT 组件是补充您的项目的好方法。虽然您在本章中创建的 WinRT 组件非常简单,但是可以应用相同的技术来构建更复杂的功能,这些功能可以在所有 Windows 应用商店应用以及 Windows Phone 应用中使用。

十九、搜索和共享契约

借助 Windows 8,用户可以以全新的不同方式与您的应用进行交互。当然,他们可以启动您的应用,并利用您构建的用户界面。除此之外,您的应用可以实现某些功能,这些功能将允许与操作系统本身更紧密地集成。通过利用契约,您可以确保您的应用以一致的方式支持 Windows 的通用功能。

当你签署一份协议,做一些工作来换取报酬,这份文件描述了你和你的客户双方的责任和期望。同样,Windows 8 合约描述了您的应用与另一个应用之间的交互,或者您的应用与 Windows 本身之间的交互。Windows 8 包括许多您可以在应用中实现的契约。例如,通过实现文件保存选取器协定(请参阅本章后面的“文件选取器”一节),当用户从另一个应用中保存文件时,他们可以选择将文件保存在您的应用中,或者您可以实现联系人选取器扩展以向用户提供存储在您的应用中的联系人详细信息。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh464906.aspx)上列出了 Windows 应用商店应用可以实现的契约。

您已经实现了设置契约,允许 Clok 向设置窗格添加一些设置弹出按钮。在这一章中,我将带你实现搜索契约,为 Clok 添加对从 Windows Search charm 搜索项目的支持,以及实现共享契约,为在文档库之间共享文档添加支持。

搜索

对于 Windows 8,用户希望使用一个通用的搜索界面。不管他们在搜索什么,他们都会使用 Windows 搜索的魅力。Windows 8 中始终提供 Windows charms,包括搜索功能。有几种方法可以打开它们。

  • 键盘上的 Windows 徽标键+C
  • 从触摸屏的右边缘滑动
  • 将鼠标移动到屏幕的右上角

当符咒可见时,通过选择图 19-1 中所示的搜索符咒,系统范围的搜索界面可用。

9781430257790_Fig19-01.jpg

图 19-1 。Windows 搜索的魅力

image 用户也可以通过在键盘上键入 Windows Logo 键+Q 来直接激活 Windows 搜索界面。此外,您可以编写代码从应用内部激活该接口。

在 Windows 搜索界面打开的情况下,用户可以输入搜索词。默认情况下,搜索范围是当前活动的应用,但用户可以选择支持搜索的任何其他应用,以及切换到搜索文件或设置。在图 19-2 中,你可以看到我最近在 Channel 9 应用中搜索“WinJS”。只需点击一下鼠标,我就可以选择在 Clok 中搜索。

9781430257790_Fig19-02.jpg

图 19-2 。Windows 8 中的通用搜索界面

在本节的剩余部分,我将向您展示如何向 Clok 添加支持,以允许用户从 Windows 搜索界面搜索项目。方便的是,当您在应用中添加搜索契约时,许多繁琐的工作都会自动完成。

添加搜索契约

为了实现搜索契约并让 Clok 出现在用户可以搜索的应用列表中,必须完成几个步骤。

  • 搜索契约必须在包清单(package.appxmanifest)中声明。
  • 当应用激活的kindsearch时,一个activated事件处理器必须处理这种情况。
  • 必须创建搜索结果屏幕。
  • 必须实现搜索应用数据的功能,以便它可以显示在搜索结果屏幕上。

这些步骤可以按任何顺序进行,都可以手动完成。对于某些应用来说,这可能是在应用中添加搜索的最简单或最好的方式。但是,前三个步骤都可以通过向项目中添加一个新项来完成。在这一节中,我将带您了解如何将搜索契约项添加到 Clok,以便它出现在用户可以搜索的应用列表中。在接下来的部分中,我将介绍一些定制默认搜索契约实现的步骤,以便更好地适应 Clok。

搜索契约是从您用于添加页面控件的同一“添加新项目”对话框中添加的。事实上,正如您稍后将看到的,就像新的页面控件一样,当添加搜索契约时,会创建三个文件——一个 HTML 文件、一个 CSS 文件和一个 JavaScript 文件。你需要为这些新文件准备一个家。在pages文件夹中创建一个名为searchResults的文件夹。右键单击新的searchResults文件夹,选择添加新项目的选项。在打开的添加新项目对话框中,选择搜索契约项目类型并创建一个名为searchResults.html的新项目(参见图 19-3 )。

9781430257790_Fig19-03.jpg

图 19-3 。添加搜索契约

尽管这个过程添加了将用于显示搜索结果的 HTML、CSS 和 JavaScript 文件,但是 HTML 文件中实际上并没有引用 JavaScript 文件。这与创建页面控件时不同。相反,您必须手动添加对searchResults.jsdefault.html的引用。将清单 19-1 中突出显示的代码添加到default.html中。

清单 19-1。 引用搜索 default.html 契约脚本

<script src="/pages/searchResults/searchResults.js"></script>
<script src="/js/default.js"></script>

我们将在接下来的部分对searchResults.jsdefault.js进行一些修改。在此之前,查看项目清单中为您所做的更改是很重要的。打开package.appxmanifest并切换到申报选项卡(参见图 19-4 )。

9781430257790_Fig19-04.jpg

图 19-4 。package.appxmanifest 文件会自动更新

当您将搜索契约项添加到项目中时,Visual Studio 声明 Clok 通过将正确的元素添加到package.appxmanifest中来支持搜索。这个文件实际上是一个 XML 文件,Visual Studio 提供了一个很好的界面来更新这个文件。清单 19-2 展示了 Visual Studio 添加到清单中的 XML,允许 Clok 支持搜索。

清单 19-2。 自动对 package.appxmanifest 进行修改

<Extensions>
    <Extension Category="windows.search" />
</Extensions>

虽然在这一点上没有实际的搜索工作,但是清单的这一变化使得 Clok 出现在用户可以搜索的应用列表中(见图 19-5 )。

9781430257790_Fig19-05.jpg

图 19-5 。现在可以搜索 Clok 了

image 注意 Windows 会自动显示一个与您的应用打包在一起的图标文件。在第二十三章中,我将展示如何更新图标以匹配应用的风格。

向 Clok 数据模型添加搜索功能

一旦我们完成了这个搜索实现,用户将能够从 Windows 搜索界面按项目名称、客户名称或项目编号搜索项目。我们可以通过一些更智能的搜索功能使这种搜索变得非常复杂,但是对于本书来说,我们只需添加一个函数,该函数将返回其中一个可搜索字段包含用户输入的文本的任何项目。

目前,storage.js有一个基于项目状态过滤项目的功能。清单 19-3 定义了一个搜索函数,它将返回所有未被删除的项目,并且项目名称、客户名称或项目编号包含用户的搜索文本。将清单 19-3 中的searchProjects函数添加到storage.js中,就在getGroupedProjectsByStatus函数定义之前。

清单 19-3。 向 storage.js 添加搜索功能

storage.projects.searchProjects = function (queryText) {
    var filtered = this
        .createFiltered(function (p) {
            if (p.status == data.ProjectStatuses.Deleted) return false;

            if (!queryText) return false;

            if ((p.name.toUpperCase().indexOf(queryText.toUpperCase()) >= 0)
                    || (p.clientName.toUpperCase().indexOf(queryText.toUpperCase()) >= 0)
                    || (p.projectNumber.toUpperCase().indexOf(queryText.toUpperCase()) >= 0)) {

                return true;
            }

            return false;

        });

    return filtered.createSorted(storage.compareProjects);
};

除了这个函数,您还需要添加一个比较和排序项目的函数。您已经定义了一个函数来比较和排序项目组,所以将清单 19-4 中的函数添加到storage.js中,就在compareProjectGroups函数定义之前。该功能将按照客户和项目名称对项目进行排序。

清单 19-4。 为项目排序功能

compareProjects: function (left, right) {
    // first sort by client name...
    if (left.clientName !== right.clientName) {
        return (left.clientName > right.clientName) ? 1 : -1;
    }

    // then sort by project name...
    if (left.name !== right.name) {
        return (left.name > right.name) ? 1 : -1;
    }

    return 0;
},

对生成的搜索结果屏幕的更改

当您向 Clok 添加搜索协定时,Visual Studio 为搜索结果屏幕添加了 HTML 和 CSS 文件。它们非常接近在 Clok 中显示项目搜索结果所需的内容。唯一需要改变的是用于格式化每个结果的WinJS.Binding.Template。因为默认的Template没有指定属于Project对象的字段,所以用来自清单 19-5 的代码更新searchResults.html

清单 19-5。 修改模板

<div class="itemtemplate" data-win-control="WinJS.Binding.Template">
    <div data-class="listViewItem" data-win-bind="className: status">
        <h4 data-win-bind="innerHTML: name searchResults.markText"></h4>
        <h6>
            <span data-win-bind="innerHTML: projectNumber searchResults.markText"></span>
                (<span data-win-bind="innerText: status"></span>)
        </h6>
        <h6 data-win-bind="innerHTML: clientName searchResults.markText"></h6>
    </div>
</div>

除了对Template做一些小改动,还需要做一些相应的 CSS 改动。这个 CSS 非常类似于您在创建列出所有项目的项目屏幕时添加的内容。用清单 19-6 中突出显示的代码更新searchResults.css

清单 19-6。 更新了 CSS 规则

/* SNIPPED */
        .searchResults section[role=main] .resultslist .win-container {
            margin-bottom: 10px;
            margin-left: 23px;
            margin-right: 23px;
        }

        .searchResults section[role=main] .resultslist [data-class=listViewItem] {
            min-width: 250px;
            height: 75px;
            padding: 5px;
            overflow: hidden;
        }

            .searchResults section[role=main] .resultslist [data-class=listViewItem].active {
                background-color: #000046;
            }

            .searchResults section[role=main] .resultslist [data-class=listViewItem].inactive {
                background-color: #464646;
                color: #cccccc;
            }

            /* Define a style for both selected filters and text matching the query. */
            .searchResults section[role=main] .resultslist [data-class=listViewItem] mark {
                background: transparent;
                color: limegreen;
            }

@media screen and (-ms-view-state: snapped) {
/* SNIPPED */

对生成的 JavaScript 文件的更改

当您在本章前面的“添加新项”对话框中添加搜索协定时,Visual Studio 为您添加了大量代码。但是,仍有一些代码需要您自己添加。例如,searchResults.js有占位符,让您定义哪些过滤器可供用户利用,当用户单击一个项目时处理事件,当然,还可以确定屏幕上应该显示什么结果。

在这一节中,我将介绍这些以及您必须对searchResults.js进行的一些其他更改。让我们首先定义用户过滤搜索结果的选项。在第十一章的中,我们在项目屏幕中添加了过滤器,用于查看所有项目、仅活动项目或仅非活动项目。我们将在搜索结果屏幕中添加相同的选项。Visual Studio 在searchResults.js中生成的代码包含一个名为_generateFilters的函数。屏幕已经配置为使用该函数来显示我们添加的任何过滤器,并实现过滤行为。用清单 19-7 中突出显示的代码更新searchResults.js中的_generateFilters,添加一个活动过滤器和一个非活动过滤器。

清单 19-7。 为活动和非活动项目添加过滤器

_generateFilters: function () {
    this._filters = [];
    this._filters.push({
        results: null,
        text: "All",
        predicate: function (item) { return true; }
    });

    var statuses = Clok.Data.ProjectStatuses;
    this._filters.push({
        results: null,
        text: "Active",
        predicate: function (item) { return item.status === statuses.Active; }
    });
    this._filters.push({
        results: null,
        text: "Inactive",
        predicate: function (item) { return item.status === statuses.Inactive; }
    });
},

因为我们在清单 19-3 的中定义了searchProjects函数,所以找到匹配用户查询的项目就像调用该函数一样简单。用清单 19-8 中突出显示的代码更新searchResults.js中的_searchData函数。

清单 19-8。 检索搜索结果

_searchData: function (queryText) {
    var storage = Clok.Data.Storage;
    return storage.projects.searchProjects(queryText);
}

一旦用户看到该函数返回的结果列表,他或她可能会点击其中一个来查看该项目的项目细节屏幕。清单 19-9 包含了_itemInvoked函数的新定义,供您在searchResults.js中更新。

清单 19-9。 导航至项目详情屏幕

_itemInvoked: function (args) {
    args.detail.itemPromise.done(function itemInvoked(item) {
        WinJS.Navigation.navigate("/pages/projects/detail.html", { id: item.data.id });
    });
},

Visual Studio 添加的searchResults.js文件中提供的一个有用特性是WinJS.Binding.converter函数_markText。该功能将在结果中突出显示用户搜索词的任何出现。默认情况下,_markText使用区分大小写的匹配来确定要突出显示的内容。例如,如果我搜索“win”,结果中将返回 Northwind Traders 的项目和任何名为 Windows Store App 的项目。然而,只有“Northwind”中的“win”会被默认的_markText功能高亮显示,因为 Windows Store 应用以大写的 *W、*开头,而我搜索的“win”是小写的 w 。用清单 19-10 中的替换searchResults.js中该函数的定义,这样所有匹配都将被突出显示,无论大小写。

清单 19-10。 高亮匹配术语

_markText: function (text) {
    return text.replace(new RegExp(this._lastSearch, "i"), function (match, capture) {
        return "<mark>" + match + "</mark>";
    });
},

这个文件中需要的最后一个修改是更改页面标题。用清单 19-11 中高亮显示的代码行更新searchResults.js中的_initializeLayout函数。

清单 19-11。 改变页面标题

_initializeLayout: function (listView, viewState) {
    /// <param name="listView" value="WinJS.UI.ListView.prototype" />

    if (viewState === appViewState.snapped) {
        listView.layout = new ui.ListLayout();
        document.querySelector(".titlearea .pagetitle").textContent
            = '"' + this._lastSearch + '"';
        document.querySelector(".titlearea .pagesubtitle").textContent = "";
    } else {
        listView.layout = new ui.GridLayout();

        document.querySelector(".titlearea .pagetitle").textContent = "Clok";
        document.querySelector(".titlearea .pagesubtitle").textContent
            = "Results for "" + this._lastSearch + '"';
    }
},

对应用激活的更改

根据您的应用的需要,您可能只需做很少的修改就可以使用生成的代码,就像我在上一节中展示的那样。然而,在 Clok 的情况下,稍微偏离生成的代码会提供更好的体验。在第十七章中,我们定义了当用户点击开始屏幕上的磁贴启动 Clok 时会发生什么。当他或她从 Windows 搜索界面激活 Clok 时,也需要大部分相同的激活逻辑。

不管用户如何激活 Clok,我们仍然需要从 IndexedDB 中合并我们的数据模型类,初始化漫游设置,并将设置弹出按钮添加到设置窗格中。在searchResults.js中生成的代码包括一个用于WinJS.Application.onactivated事件的事件处理程序。因为激活过程几乎是一样的,我们将把它合并到已经存在于default.js中的事件处理程序中。首先,让我们去掉searchResults.js中多余的事件处理程序。从searchResults.js中的清单 19-12 中找到代码并删除它。

清单 19-12。 从 searchResults.js 中移除激活的处理程序

WinJS.Application.addEventListener("activated", function (args) {
    // SNIPPED
});

用清单 19-13 中的高亮代码修改default.jsonactivated事件的处理程序。这将允许我们现有的所有逻辑执行,无论用户从开始屏幕启动 Clok 还是通过执行搜索。

清单 19-13。 通过搜索检查激活

app.addEventListener("activated", function (args) {
    if ((args.detail.kind === activation.ActivationKind.launch)
            || (args.detail.kind === activation.ActivationKind.search)) {

        var extender = new Clok.SplashScreen.Extender(
            extendedSplash,
            args.detail.splashScreen,
            function (e) {
                args.setPromise(Clok.Data.Storage.initialize());
                simulateDelay(500);
                launchActivation(args);
            });
    }
});

虽然我们在整本书中添加的所有激活码都适用于任何一种激活类型,但在launch激活和search激活之间有一个区别。Clok 应该导航到不同的屏幕,这取决于它是如何被激活的。在激活launch的情况下,用户应该看到他或她之前所在的屏幕,或者 Clok 仪表板屏幕。然而,在search激活的情况下,有一些可能性。将清单 19-14 中突出显示的代码添加到default.js中。

清单 19-14。 激活后导航到正确的屏幕

args.setPromise(WinJS.UI.processAll().then(function () {
    configureClock();

    if (args.detail.kind === activation.ActivationKind.search) {
        var searchPageURI = "/pages/searchResults/searchResults.html";
        var execState = activation.ApplicationExecutionState;

        if (args.detail.queryText === "") {
            if ((args.detail.previousExecutionState === execState.closedByUser)
                    || (args.detail.previousExecutionState === execState.notRunning)) {
                return nav.navigate(Application.navigator.home);
            } else if ((args.detail.previousExecutionState === execState.suspended)
                    || (args.detail.previousExecutionState === execState.terminated)) {
                return nav.navigate(nav.location, nav.state);
            }
            else {
                return nav.navigate(searchPageURI, { queryText: args.detail.queryText });
            }
        } else {
            if (!nav.location) {
                nav.history.current = {
                    location: Application.navigator.home,
                    initialState: {}
                };
            }

            return nav.navigate(searchPageURI, { queryText: args.detail.queryText });
        }
    } else if (nav.location) {
        nav.history.current.initialPlaceholder = true;
        return nav.navigate(nav.location, nav.state);
    } else {
        return nav.navigate(Application.navigator.home);
    }
}));

有两种方法可以从 Windows 搜索界面激活 Clok 。

  • 用户可以输入搜索词,然后在应用列表中单击 Clok。
  • 用户可以单击应用列表中的 Clok,然后输入搜索词。

如果用户输入搜索词,然后选择 Clok,清单 19-14 中的代码会将他们的搜索词——属性——传递给搜索结果屏幕,显示所有结果。然而,如果用户激活了 Windows 搜索界面,并在输入任何搜索词之前点击 Clok,以将他们的搜索“预先限定”到 Clok,queryText的值将为空。在这种情况下,用户可能对他们在搜索时将看到的内容有不同的期望。因此,我们遵循类似于用户启动 Clok 时的模式。如果应用被暂停或终止,让用户返回到他们认为仍然活动的屏幕。如果应用之前没有运行,显示 Clok 仪表板屏幕。如果它正在运行,请导航到搜索结果屏幕,即使尚未显示任何结果。您可以决定在应用中以不同的方式处理这些情况。重要的是考虑用户在各种情况下期望发生什么。

现在运行 Clok 并在 Clok 中执行搜索。图 19-6 显示了我搜索“周期”的结果,所有匹配的项目都在结果中突出显示。

9781430257790_Fig19-06.jpg

图 19-6 。搜索结果

您可能已经意识到这个屏幕与列出所有项目的项目屏幕是多么相似。你可以把这两者合并成一个屏幕。这将消除一些多余的代码,如 CSS 和标记,以显示结果和过滤结果的功能。目前最大的区别是,默认情况下,项目屏幕显示 Clok 中的所有项目,而搜索结果屏幕不会显示任何结果,直到执行搜索。然而,有一个特别的原因需要考虑将它们作为单独的屏幕。使用专用的搜索结果屏幕,您可以修改逻辑,使其也包括对时间条目的搜索,甚至是对合并结果列表中的文档的搜索。如果将项目屏幕和搜索结果屏幕结合起来,这种行为就没有意义。同样,做你认为对你的用户最好的事情。要留心。

完成本节后,您会注意到将“添加新项”对话框中的搜索协定项添加到 Visual Studio 项目中实际上并不是一个必需的步骤。当您完成这一步时,Visual Studio 没有添加任何特别的东西。您可以手动完成每个步骤。事实上,如果我们需要对生成的代码进行更多的修改,或者如果我们想要将项目屏幕与搜索结果屏幕结合起来,那么手动完成这些工作可能会更简单。重申一下我在本章前面说过的,实现搜索契约包括几个步骤。

  • 搜索契约必须在包清单(package.appxmanifest)中声明。
  • 当应用激活的kindsearch时,一个activated事件处理器必须处理这种情况。
  • 必须创建搜索结果屏幕。
  • 必须实现搜索应用数据的功能,以便它可以显示在搜索结果屏幕上。

Visual Studio 可以为您生成部分或大部分代码,您也可以手动生成。

调试搜索激活

在测试应用的可选激活时,我应该指出一个有用的调试技巧。大部分时间我启动 Clok 都没有附加 Visual Studio 调试器。您可以通过键入 Ctrl+F5 来实现这一点,或者您可以转到 Debug image Start 而不进行调试。当我必须通过代码来解决问题或者在 Clok 运行时查看不同变量的值时,我用 Visual Studio 调试器启动 Clok。你可以通过键入 F5,进入调试image开始调试(见图 19-7 ,或者点击工具栏中的按钮来完成。

9781430257790_Fig19-07.jpg

图 19-7 。开始调试或不调试就开始

默认情况下,调试时,Visual Studio 将启动 Clok。大多数时候,这是我们所希望的。然而,在调试其他类型的激活的情况下,您实际上并不希望 Clok 最初运行,这样您就可以调试添加到onactivated事件处理程序中的各种代码分支。幸运的是,有一个简单的方法可以改变这一点。

在解决方案资源管理器中右击 Clok 项目,并从上下文菜单中选择属性。确保选择 Clok 项目,而不是顶层的同名 Clok 解决方案。在“Clok 属性页”窗口的左窗格中,选择“调试”。然后将启动应用的值更改为否(参见图 19-8 )。

9781430257790_Fig19-08.jpg

图 19-8 。更改调试选项

如果您现在调试(按 F5),Visual Studio 将切换到其调试模式,并等待 Clok 被激活,然后再执行其他操作。如果您现在使用 Windows 搜索界面来搜索项目,您将能够逐步完成激活处理程序。只需记住在调试与应用激活无关的其他 Clok 特性之前,将该属性设置回 Yes。

分享

Windows 8 的另一个新功能是在应用之间共享数据的能力。当一个应用支持共享时,用户可以使用图 19-9 中所示的共享符打开 Windows 共享界面,使数据对另一个应用可用。

9781430257790_Fig19-09.jpg

图 19-9 。窗户也有魅力

image 用户也可以直接激活分享界面,绕过 Share charm,在键盘上键入 Windows Logo 键+H。此外,我将在本章后面展示如何以编程方式激活共享界面。

开箱即用,您可以为您的应用添加对共享文本、HTML、URIs、图像和文件的支持。此外,您还可以创建自定义数据类型并共享它们。在这一节中,我将介绍将 Clok 配置为共享目标和共享源的步骤。

份额目标

在实现共享目标协定后,您的应用将向 Windows 表明自己是共享目标。共享目标是能够从另一个应用(共享源)接收共享数据的应用。当实现共享目标契约时,您指定您的应用能够接收什么类型的数据,并且当用户共享该类型的数据时,您的应用将在他或她可以选择的目标应用列表中。在这一节中,我将向您展示如何添加对与 Clok 共享文档的支持,以及如何将它们添加到项目的文档库中。

在我们开始在 Clok 中实现共享目标契约之前,你需要在你的电脑上安装一个可以共享文件的应用。您可能已经有一个或多个应用可以做到这一点,但是与其尝试确定哪些应用能够共享文档,我建议您查看名为“共享内容源应用示例”的示例项目您可以单独下载这个示例项目(http://code.msdn.microsoft.com/windowsapps/Sharing-Content-Source-App-d9bffd84),但是我建议从 MSDN 下载整个示例应用包(http://msdn.microsoft.com/en-US/windows/apps/br229516)。当您在 Visual Studio 中构建这个应用时,它将在您的开始屏幕上显示为 Share Source JS。

添加份额目标契约

与搜索协定类似,实现共享目标协定最简单的方法是在 Visual Studio 中将特定类型的项添加到项目中。在 Visual Studio 项目的pages文件夹中创建一个名为shareTarget的新文件夹。然后在shareTarget文件夹中添加一个名为shareTarget.html的新份额目标合约项(见图 19-10 )。

9781430257790_Fig19-10.jpg

图 19-10 。添加股份目标合约

当您添加这种类型的项时,Visual Studio 会对package.appxmanifest进行修改。它添加了所需的共享目标声明,因此 Clok 将显示在 Windows 共享界面的目标列表中。默认情况下,它指定您的项目可以接收共享文本和 URIs(参见图 19-11 )。

9781430257790_Fig19-11.jpg

图 19-11 。Visual Studio 添加的共享目标声明

我们不会在 Clok 中支持这些格式,所以把它们都删除。因为我们希望用户能够将文档共享到文档库中,所以我们必须指定支持哪些文件类型。如果我们只想接受 Microsoft Word 文档,我们可以添加一个新的受支持的文件类型并指定”。docx”作为文件类型。然而,在 Clok 的情况下,用户可能希望共享 Microsoft Word 文档、电子表格、一些模型或任何数量的其他类型的文件。为此,只需勾选“支持任何文件类型”复选框(见图 19-12 )。

9781430257790_Fig19-12.jpg

图 19-12 。更新 package.appxmanifest 文件

在 Clok 中,共享文件被简单地保存到文档库中,根本不用查看文件的内容。如果您的应用将使用共享文件的内容,您应该将文件类型限制为您的应用可以理解的类型。此外,正如任何时候应用接受来自用户的数据一样,在对文件做任何其他事情之前,您应该仔细分析内容以确保它们是适当的格式。很容易将文件的名称和扩展名从badThing.exe更改为niceThing.xml

更改生成的共享目标屏幕

除了对package.appxmanifest的更改,当您添加共享目标契约时,Visual Studio 创建了一个页面,当用户选择 Clok 作为共享目标时,该页面将显示给用户。Windows 会将shareTarget.html加载到一个类似设置弹出的滑动窗口中。必须对该页面进行定制,以便以对您的应用有意义的方式处理共享数据。

在向 Clok 中的项目文档库添加文档的情况下,我们将显示一些关于共享文件的细节,并向用户提供一个项目列表以供选择。与设置弹出按钮不同,此页面不是作为default.html的一部分加载的,因此您必须包含完成共享操作所需的任何脚本引用。用清单 19-15 中的代码更新shareTarget.html

清单 19-15。 更新 searchTarget.html 内容

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="ms-design-extensionType" content="ShareTarget" />
    <title>Share Target Contract</title>

    <link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <link href="/css/default.css" rel="stylesheet" />
    <link href="/css/themeroller.css" rel="stylesheet" />

    <script src="/js/extensions.js"></script>
    <script src="/js/utilities.js"></script>
    <script src="/js/navigator.js"></script>

    <script src="/data/project.js"></script>
    <script src="/data/timeEntry.js"></script>
    <script src="/data/storage.js"></script>

    <link href="shareTarget.css" rel="stylesheet" />
    <script src="shareTarget.js"></script>
</head>
<body>
    <!-- The content that will be loaded and displayed. -->
    <section aria-label="Main content" role="main">
        <header>
            <div>
                <img class="shared-thumbnail" src="#" alt="share metadata image" />
            </div>
            <div class="shared-metadata">
                <h2 class="shared-title win-type-ellipsis"></h2>
                <h4 class="shared-description"></h4>
                <ul id="fileNames"></ul>
            </div>
        </header>

        <div id="projectContainer">
            <label for="project">Select a project to add these documents to:</label><br />
            <select id="project">
                <option value="">Choose a project</option>
            </select>
        </div>

        <div class="sharecontrols">
            <div class="progressindicators">
                <progress></progress>
                <span>Sharing...</span>
            </div>
            <input class="submitbutton" type="button" value="Share" />
        </div>
    </section>
</body>
</html>

一些 CSS 的变化也是必要的。首先,因为在清单 19-15 中,你用projectContainer div替换了一个commentbox元素,在shareTarget.css中,用清单 19-17 中的选择器更新清单 19-16 中的 CSS 选择器。

清单 19-16。 找到这个 CSS 规则

section[role=main] .commentbox {
    -ms-grid-column-align: stretch;
    -ms-grid-column: 2;
    -ms-grid-row: 2;
    height: 25px;
    margin-top: 0px;
    width: calc(100% - 4px);
}

清单 19-17。 改变选择器

section[role=main] #projectContainer {
    -ms-grid-column-align: stretch;
    -ms-grid-column: 2;
    -ms-grid-row: 2;
    height: 25px;
    margin-top: 0px;
    width: calc(100% - 4px);
}

在清单 19-15 中,您添加了一个无序列表,其中包含用户与 Clok 共享的文件名列表。将清单 19-18 中的 CSS 规则添加到shareTarget.css中。

清单 19-18。 CSS 文件列表规则

section[role=main] header .shared-metadata #fileNames {
    overflow-y: scroll;
    height: 80px;
}

接收共享文件

当您将共享目标契约添加到 Clok 时,Visual Studio 生成的 JavaScript 非常简单。这是一个很好的起点,但还不够。在这一节中,我将带您了解shareTarget.js中所需的更改。将清单 19-19 中高亮显示的别名添加到shareTarget.js的顶部。

清单 19-19。 添加一些别名

var app = WinJS.Application;
var appData = Windows.Storage.ApplicationData.current;
var storage = Clok.Data.Storage;

var createOption = Windows.Storage.CreationCollisionOption;
var standardDataFormats = Windows.ApplicationModel.DataTransfer.StandardDataFormats;

var share;

当用户在 Windows Share 界面的目标列表中选择 Clok 时,Clok 会被shareTarget ActivationKind激活。因为我们正在构建的共享屏幕不是作为default.html的一部分托管的,所以您必须处理searchTarget.js中的onactivated事件。Visual Studio 生成的代码为此定义了一个处理程序,它包含大量代码。在许多情况下,将代码直接包含在onactivated事件处理程序中是完全可以接受的。但是,建议尽快完成此事件处理程序。我们将遵循本例中 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh758302.aspx)上建议的指导,将大部分逻辑移到一个新函数中,并引发一个自定义事件来执行该函数。在shareTarget.js中,用清单 19-20 中的代码替换onactivated事件处理程序。

清单 19-20。 从 Windows 搜索界面处理激活

app.onactivated = function (args) {
    if (args.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.shareTarget) {
        WinJS.Application.addEventListener("shareactivated", shareActivated, false);
        WinJS.Application.queueEvent({ type: "shareactivated", detail: args.detail });
    }
};

var shareActivated = function (args) {
    var thumbnail;

    document.querySelector(".submitbutton").disabled = true;
    document.querySelector(".submitbutton").onclick = onShareSubmit;

    bindListOfProjects();
    project.onchange = project_change;

    share = args.detail.shareOperation;

    document.querySelector(".shared-title").textContent = share.data.properties.title;
    document.querySelector(".shared-description").textContent
        = share.data.properties.description;

    thumbnail = share.data.properties.thumbnail;
    if (thumbnail) {
        // If the share data includes a thumbnail, display it.
        args.setPromise(thumbnail.openReadAsync().done(function displayThumbnail(stream) {
            document.querySelector(".shared-thumbnail").src
                = window.URL.createObjectURL(stream);
        }));
    } else {
        // If no thumbnail is present, expand the description  and
        // title elements to fill the unused space.
        document
            .querySelector("section[role=main] header")
            .style
            .setProperty("-ms-grid-columns", "0px 0px 1fr");

        document
            .querySelector(".shared-thumbnail")
            .style
            .visibility = "hidden";
    }

    if (share.data.contains(standardDataFormats.storageItems)) {
        share.data.getStorageItemsAsync().done(function (files) {
            if (files && files.length > 0) {
                var names = files.map(function (file) {
                    return "<li>" + file.name + "</li>";
                }).join("");
                fileNames.innerHTML = names;
            }
        });
    }
};

在这段代码中,共享屏幕的配置类似于您在ready函数中初始化之前页面的方式。全局变量share被设置为代表 Windows 将包含在激活中的shareOperation。这个变量是您将如何访问与您的应用共享的数据(在本例中是文件)。如果share变量指定了缩略图,则显示该缩略图;否则,允许标题和描述扩展到共享屏幕的整个宽度。最后,这个共享操作中包含的所有文件都被列出,以确保用户了解哪些文件将被复制到他或她选择的项目的文档库。

与 Clok 仪表板屏幕类似,共享屏幕将包括一个活动项目列表。用户将使用此列表来选择哪个项目的文档库将接收他或她共享的文件。添加从清单 19-21 到shareTarget.js的函数,以填充项目列表,并仅在项目被选中时启用共享按钮。

清单 19-21。 绑定项目列表

var bindListOfProjects = function () {
    storage.initialize().then(function () {
        project.options.length = 1; // remove all except first project

        var activeProjects = storage.projects.filter(function (p) {
            return p.status === Clok.Data.ProjectStatuses.Active;
        });

        activeProjects.forEach(function (item) {
            var option = document.createElement("option");
            option.text = item.name + " (" + item.projectNumber + ")";
            option.title = item.clientName;
            option.value = item.id;
            project.appendChild(option);
        });
    });
};

function project_change() {
    document.querySelector(".submitbutton").disabled
        = (project.options[project.selectedIndex].value === "");
}

为了让您走上正确的道路,生成的代码包括一个 Share 按钮的click事件的处理函数。它会显示进度指示器,这样用户就知道发生了什么,尽管在 Clok 的情况下,共享操作通常会很快完成,所以用户可能永远也看不到它们。用清单 19-22 中突出显示的代码更新shareTarget.js中的onShareSubmit函数。

清单 19-22。 修改按钮点击处理程序

function onShareSubmit() {
    document.querySelector(".progressindicators").style.visibility = "visible";
    document.querySelector(" #project ").disabled = true;
    document.querySelector(".submitbutton").disabled = true;

    share.reportStarted();
    addDocuments();
    share.reportCompleted();
}

除了显示进度指示器之外,生成的代码还被修改为禁用项目列表。它还禁用了“共享”按钮,以防止意外的重复提交。它最终调用reportCompleted函数,这将关闭 Clok 并让用户返回到他或她之前使用的应用。除了调用addDocuments(您稍后将定义的函数)将文件复制到文档库,我还调用了reportStarted。我在这里没有使用它,但是如果您的应用在接收共享数据时遇到任何错误,您也可以考虑调用reportError函数。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.datatransfer.sharetarget.shareoperation.aspx)上记录了ShareOperation类的这些和其他功能。

完成使 Clok 成为文件共享目标的任务的最后一步是实现addDocuments函数。该函数将确定哪个storageFolder对应于所选项目,并从share变量中复制所有的storageItem对象,有效地将它们添加到该项目的文档库中。添加从列表 19-23 到shareTarget.js的功能。

清单 19-23。 向库中添加文档

var addDocuments = function () {
    var projectId = project.options[project.selectedIndex].value;
    getProjectFolder(projectId).then(function (projFolder) {
        if (share.data.contains(standardDataFormats.storageItems)) {
            share.data.getStorageItemsAsync().done(function (files) {
                var copyPromises = files.map(function (item) {
                    return item.copyAsync(projFolder, item.name, createOption.replaceExisting);
                });

                WinJS.Promise.join(copyPromises);
            });
        }
    });
};

var getProjectFolder = function (projectId) {
    return appData.localFolder
        .createFolderAsync("projectDocs", createOption.openIfExists)
        .then(function (folder) {
            return folder.createFolderAsync(projectId.toString(), createOption.openIfExists)
        });
};

看到它的实际应用

至此,您已经成功地将 Clok 配置为文件共享目标。通常,我不会用一整节的时间来尝试一个新特性,但是在这种情况下,已经有足够多的内容要介绍了。在调试该功能之前,确保启动调试器时不会启动 Clok(回头参考图 19-8 )。

image 注意如果你不打算用附加的调试器测试这个特性,那么你可以跳过这一步,但是你必须构建并运行 Clok 一次,让它注册为一个共享目标。

在本章的前面,我提到了共享源代码 JS 示例项目。现在构建并运行该项目。这个项目是构建一个可以作为共享操作源的应用的例子,这个主题我将在下一节讨论。因此,它包含了许多测试 Clok 时不需要的特性。通过尝试不同的场景,比如与另一个应用共享文本,来熟悉这个应用。为了测试 Clok,你应该特别关注标题为“共享文件”的场景(见图 19-13 )。

9781430257790_Fig19-13.jpg

图 19-13 。选择目标应用

在图 19-13 中,你可以看到我使用了选择文件按钮来指定两个应该共享的文件:bicycle-data.xlsxmotorcycle-data.xlsx。使用“选择文件”按钮从您自己的计算机中选择一些文件与 Clok 共享。选择文件后,通过以下方法之一激活 Windows 共享界面:

  • 单击 Share Source JS 应用中的 Share 按钮,以编程方式打开 Windows 共享界面。
  • 点击之前在图 19-9 中显示的共享图标,打开 Windows 共享界面。
  • 使用键盘快捷键 Windows Logo 键+H 直接打开 Windows 共享界面,绕过 Windows charms。

当 Windows 共享界面处于活动状态时,您应该会看到 Clok 被列为可选的共享目标。你不会看到 Clok 作为其他场景的选项,比如那些共享文本或 HTML 内容的场景。一旦选择了 Clok,应用就会被激活。因为searchTarget.html被指定为搜索目标契约的起始页,所以您在过去几页中构建的页面将会打开,并且将会执行shareTarget.js中的激活码。你应该看到你选择的文件列表,以及可以接收这些文件到他们的文档库的活动项目列表(见图 19-14 )。

9781430257790_Fig19-14.jpg

图 19-14 。与克洛克共享文件

从列表中选择一个项目,然后单击“共享”按钮。共享屏幕将关闭,您将返回到共享源 JS 应用。如果您现在启动 Clok 并导航到所选项目的文档库,您的文件将被列出,就好像您是从 Clok 中直接添加的一样。

当使用 Share Source JS 应用的“Share files”场景时,文件被添加到我在上一节中提到的全局share变量中。这种情况平等对待所有文件,不区分文档、电子表格、图像或任何其他文件类型。另一方面,Share Source JS 应用的“共享图像”场景专门共享图像。因为共享数据是一个图像,所以该场景通过共享操作指定了共享图像的缩略图。尽管 Clok 不支持图像的共享目标契约,但共享源 JS 示例的“共享图像”场景以位图数据格式共享图像,并将其作为文件共享。在清单 19-20 中,我们指定如果缩略图存在,它将被显示。测试该场景,查看缩略图可用时如何显示(参见图 19-15 )。

9781430257790_Fig19-15.jpg

图 19-15 。用缩略图共享图像

共享源

在上一节中,您添加了对 Clok 的支持,以便从其他应用接收共享文件。在本节中,您将实现该操作的相反方面,通过向文档库添加对从 Clok 到其他应用共享文件的支持,使 Clok 成为一个共享源。

在上一节中,您使用了 Share Source JS 应用来与 Clok 共享文件。该应用的开发人员在每个屏幕上添加了一个按钮来启动共享过程。这是一个可选的步骤,因为用户总是可以使用 Windows charms 来实现这一点,但是这是一个很好的步骤,有两个原因。首先,点击一个已经可见的按钮通常比使用其他方法打开 Windows 共享界面更简单。第二,也是更重要的一点,在我看来,在屏幕上看到一个共享按钮让用户清楚地知道当前屏幕上的项目是可以共享的。我不止一次使用 Windows Store 应用,我认为它可以让我分享一些东西。然而,当我点击 Windows Share charm 时,出现一条消息告诉我不能(见图 19-16 )。

9781430257790_Fig19-16.jpg

图 19-16 。目前没有可共享的内容

向文档库添加共享按钮

因此,在进入共享文件的细节之前,让我们在 Document Library 屏幕上添加一个用户界面,在应用栏中添加一个共享按钮。因为我们将应用栏配置为每当用户在文档库中选择文件时自动出现,所以共享文档的能力将是显而易见的,并且只需点击一下就可以启动共享过程。将清单 19-24 中的代码添加到library.html中。

清单 19-24。 添加新 App 栏命令

<button
    data-win-control="WinJS.UI.AppBarCommand"
    data-win-options="{
        id:'shareDocumentsCommand',
        label:'Share',
        icon:'url(/img/Share-small-sprites.png)',
        section:'selection',
        tooltip:'Share',
        disabled: true}">
</button>

image 注意一如既往,本书附带的源代码包括一个完整的项目,包含本章使用的所有源代码,包括用于共享AppBarCommand图标的Share-small-sprites.png图像。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790)。

当用户点击这个按钮时,showShareUI功能会导致 Windows 共享界面打开。将清单 19-25 中的代码添加到library.js中。

清单 19-25。 以编程方式打开 Windows 共享界面

shareDocumentsCommand_click: function (e) {
    dataTransferManager.showShareUI();
},

在继续之前,请确保在ready函数中配置该事件处理程序并更新libraryListView_selectionChanged,以便在选择文件时启用共享应用栏按钮,类似于删除应用栏按钮。另外,将清单 19-26 中显示的dataTransferManager别名添加到library.js的顶部。

清单 19-26。 添加别名

var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager;

使用 DataTransferManager 类

对清单 19-25 中的调用将导致 Windows 共享界面打开。当它打开时,接下来发生的事情是 Windows 将从 Clok 请求应该共享的数据。这是通过引发一个DataTransferManager对象的datarequested事件来实现的。你要在library.js里处理这个事件。您还必须确保在用户离开该页面时停止处理该事件。因为您的应用可以从不同的屏幕共享不同类型的数据,所以您必须确保仅当应该共享的数据在范围内时才处理该事件。通过添加清单 19-27 到library.js中突出显示的代码来添加和删除datarequested事件的事件处理程序。

清单 19-27。 添加和删除事件处理程序

ready: function (element, options) {
    // SNIPPED

    var transferMgr = dataTransferManager.getForCurrentView();
    this.transferMgr_dataRequested_boundThis = this.transferMgr_dataRequested.bind(this);
    transferMgr.addEventListener("datarequested", this.transferMgr_dataRequested_boundThis);
},

unload: function () {
    var transferMgr = dataTransferManager.getForCurrentView();
    transferMgr.removeEventListener("datarequested", this.transferMgr_dataRequested_boundThis);
},

image 注意你可能想知道为什么我创建了名为transferMgr_dataRequested_boundThis的函数。一般来说,我更喜欢使用bind函数来表示函数中的this关键字引用声明该函数的this的相同值。为了确保在unload函数中删除相同的事件处理程序,我将transferMgr_dataRequested_boundThis定义为一个已经绑定了this关键字的函数。另一种流行的技术是定义一个全局变量,通常命名为that$this,并在整个代码中使用它来代替this关键字。您可能在许多其他地方看到过这种技术,如果您开发 JavaScript 已经有一段时间了,您可能对它很熟悉。如果是这样,您应该继续以这种方式编写代码。就像用 HTML 和 JavaScript 构建 Windows 应用商店应用时遇到的任何其他事情一样,除了提供 WinJS 和 WinRT 库之外,您编写的代码只是“普通的 HTML 和 JavaScript”如果您有自己喜欢的开发实践,您仍然可以使用它们。

当 Windows 从 Clok 请求共享数据时,最终调用的transferMgr_dataRequested函数,是这个特性的主力。将清单 19-28 中的代码添加到library.js中。

清单 19-28。 向 Windows 提供共享数据

transferMgr_dataRequested: function (e) {
    var request = e.request;
    var selectionCount = libraryListView.winControl.selection.count();

    if (selectionCount <= 0) {
        request.failWithDisplayText("Please select one or more documents and try again.");
        return;
    }

    libraryListView.winControl.selection.getItems()
        .then(function (selectedItems) {
            var project = storage.projects.getById(this.projectId);

            if (selectionCount === 1 && isImageType(selectedItems[0].data.fileType)) {
                // handle single image
                request.data.properties.title = "Image shared from Clok project";
                request.data.properties.description
                    = "From " + project.name + " (" + project.clientName + ")";

                var streamRef = Windows.Storage.Streams.RandomAccessStreamReference;
                var stream = streamRef.createFromFile(selectedItems[0].data);
                request.data.properties.thumbnail = stream;
                request.data.setBitmap(stream);
            } else {
                // handle non-images or multiple files
                request.data.properties.title = "File(s) shared from Clok project";
                request.data.properties.description
                    = selectionCount.toString() + " file(s) from "
                        + project.name + " (" + project.clientName + ")";
            }

            // share as files whether single image, non-images or multiple files
            var files = selectedItems.map(function (item) {
                return item.data;
            });

            request.data.setStorageItems(files);
        }.bind(this));
},

如果用户没有选择任何要共享的文件,可以使用failWithDisplayText功能向他或她显示消息或说明(参见图 19-17 )。

9781430257790_Fig19-17.jpg

图 19-17 。目前(仍然)没有可共享的内容

Clok 中的文档库可以包含任何类型的文件,包括图像文件,比如 JPG 或 PNG 文件。Windows 中的共享功能支持共享任何类型的文件,但也支持将图像作为数据流共享。如果您想要共享的图像不在磁盘上,这很有用,例如,可能是因为您从数据库中检索到了它。一些应用可能支持接收共享图像流,而其他应用可能支持接收文件;Clok 只支持以文件形式接收图像。

知道有些应用可能不支持这种或那种格式,当从文档库屏幕共享一个单一的图像时,清单 19-28 中的代码以两种格式共享图像。这将增加用户可以指定为共享目标的应用的数量,从而改善他们的体验。如果用户共享多个文件,或者不是图像的文件,那么我们只将数据作为文件共享。此外,如果只共享一个图像,我们指定该图像应该用作共享操作的缩略图。

看到它的实际应用

让 Clok 成为一个共享资源要简单得多。剩下唯一要做的事就是尝试一下。运行 Clok 并导航到包含一些文件的文档库。选择一个或多个文件,并通过以下方法之一共享它们(参见图 19-18 ):

  • 点按应用栏中的“共享”按钮。
  • 点击图 19-9 所示的分享符。
  • 使用键盘快捷键 Windows 徽标键+H,绕过 Windows charms。

9781430257790_Fig19-18.jpg

图 19-18 。共享文档库中的文件

image 注意如果你在用 Visual Studio 调试,别忘了再次更改项目属性,这样当你开始调试时 Clok 会自动启动。

选择目标应用。在我的测试中,我选择了邮件应用,正如你在图 19-19 中看到的,它有一个更大的共享屏幕,允许用户将文件作为附件发送给客户端。

9781430257790_Fig19-19.jpg

图 19-19 。将文件共享到邮件应用

其他类似于分享的概念

除了将您的应用配置为共享目标或共享源之外,还有其他方法允许用户将数据放入您的应用以及从您的应用中获取数据。支持这种类型的共享的两种常见方法(这里我指的是“共享”的一般定义,而不是 Windows 8 特定的定义)是文件选取器和复制粘贴。

文件拾取器

文件打开选择器契约和文件保存选择器契约分别类似于共享源和共享目标。当用户使用文件选择器打开或保存文件时,这些契约允许选择您的应用。例如,当向文档库添加文件时,您可以选择已声明文件打开选择器契约的其他应用,并直接从这些应用导入文件,如 SkyDrive 或 Photos 应用(参见图 19-20 ),即使它们当前不存在于您计算机的文件系统中。

9781430257790_Fig19-20.jpg

图 19-20 。文件打开选择器

同样,如果您将文件保存选择器契约添加到 Clok,您可以将邮件应用中的电子邮件附件直接保存到文档库中。我不会在本书中实现任何文件选择器,但是你可以考虑把它们作为作业添加到 Clok 中。更多关于文件拾取器契约的信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh465174.aspx)上找到。

复制并粘贴

复制粘贴已经成为我们生活的一部分很久了。我记得大约 30 年前,我在自己的第一台苹果电脑上使用了复制粘贴技术。Windows 8 以最令人期待的方式支持复制和粘贴,开箱即用:用户将能够自动从文本输入控件中复制和粘贴。此外,您可以以编程方式操作剪贴板。

一个有用的例子是添加支持,将时间表屏幕中的时间条目复制到制表符分隔的格式中,这样可以很容易地粘贴到 Microsoft Excel 中。事实上,虽然我不会在本书中涉及它,但是该特性已经在本书附带的源代码中实现了。(您可以在本书[ www.apress.com/9781430257790 ]的产品页面的源代码/下载选项卡上找到本章的代码示例。)Clok 中支持复制粘贴的另一种情况是将它添加到文档库中,以允许用户将文档从一个项目的文档库复制到另一个项目的文档库中。有关在应用中复制和粘贴数据的更多信息,请访问 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh758298.aspx)。

结论

Windows 8 做了大量工作来提高应用中完成常见任务的一致性。通过在应用之间搜索和共享数据的通用界面,用户将很快熟悉 Windows 应用商店应用支持这些功能的方式。尽管如果搜索是您的应用的一个重要特性,偏离通用的 Windows 搜索界面是完全可以接受的,甚至是推荐的,但是在大多数情况下,遵循已建立的约定更好——对用户更好,因为它是一致的,对您作为开发人员也更好,因为许多管道代码已经为您编写好了。

除了搜索和共享契约,Windows 8 还提供了许多契约和扩展,帮助您构建用户自动熟悉的应用。我鼓励你在 MSDN 上回顾它们。