HTML5 和 JavaScript 的 Windows8 开发高级教程(八)
二十三、集成文件服务
在这一章中,我将基于第二十二章中的技术向你展示如何向用户公开文件操作。我将向您展示如何使用文件选择器来请求用户选择文件和文件夹,如何缓存位置以便您的应用保留对它们的访问,以及如何使用文件系统作为 WinJS 数据驱动 UI 控件(如FlipView和ListView)的数据源。表 23-1 对本章进行了总结。
创建示例应用
对于这一章,我已经创建了一个名为FileServices的示例应用,它遵循单页内容模型并使用WinJS.Navigation名称空间,由应用导航栏上的按钮驱动。本章中的例子不容易放入一个布局中,所以这种方法将让我在同一个应用中向你展示多个内容页面。您可以在清单 23-1 的中看到我对default.html文件所做的修改。
清单 23-1 。来自文件服务项目的 default.html 文件
`
` ` FileServices **Select a page from the NavBar
** **** <div id="navbar" data-win-control="WinJS.UI.AppBar"** ** data-win-options="{placement:'top'}">** ** <button data-win-control="WinJS.UI.AppBarCommand"** ** data-win-options="{id:'displayFile', label:'Display File',** ** icon:'\u0031', section:'selection'}">** ** ** ** <button data-win-control="WinJS.UI.AppBarCommand"** ** data-win-options="{id:'pickers', label:'Pickers',** ** icon:'\u0032', section:'selection'}">** ** ** ** <button data-win-control="WinJS.UI.AppBarCommand"** ** data-win-options="{id:'access', label:'Access Cache',** ** icon:'\u0033', section:'selection'}">** ** ** ** <button data-win-control="WinJS.UI.AppBarCommand"** ** data-win-options="{id:'dataSources', label:'Data Sources',** ** icon:'\u0034', section:'selection'}">** ** ** ** **
`其id为contentTarget的div元素将成为其他页面内容的目标,这些内容将被导入到文档中以响应单击导航条按钮。你可以在default.html文件中看到 NavBar 命令,我将在本章中添加它们相关的文件。初始内容是一条提示用户去导航条的消息,如图图 23-1 所示。
***图 23-1。*示例应用的布局
您可以在清单 23-2 的中看到/css/default.css文件的内容。该文件包含示例应用中使用的常见样式,我将在示例的内容页面中添加特定于元素的内容。
清单 23-2 。css/default.css 文件的内容
`body {display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: center; -ms-flex-pack: center; }
#contentTarget { display: -ms-flexbox; -ms-flex-direction: row; -ms-flex-align: center; -ms-flex-pack: center; text-align: center;}
.container {border: medium solid white; margin: 10px; padding: 10px;} .container button {font-size: 25pt; margin: 10px; display: block; width: 300px;}
*.imgElem {height: 500px;} *.imgTitle { color: white; background-color: black;font-size: 30pt; padding-left: 10px;}`
CSS 中没有新的技术或特性——样式和属性仅用于展示示例中的元素。对于这个项目来说,/js/default.js文件的内容非常简单,只包含设置和导航代码——所有有趣的特性都在我在本章每一节添加的单独文件中。您可以在清单 23-3 中看到default.js文件的内容。
清单 23-3 。default.js 文件的内容
(function () {
` var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
WinJS.Navigation.addEventListener("navigating", function (e) { WinJS.UI.Animation.exitPage(contentTarget.children).then(function () { WinJS.Utilities.empty(contentTarget); WinJS.UI.Pages.render(e.detail.location, contentTarget, WinJS.Navigation.state) .then(function () { return WinJS.UI.Animation.enterPage(contentTarget.children) }); }); });
app.onactivated = function (args) { if (args.detail.previousExecutionState != activation.ApplicationExecutionState.suspended) { args.setPromise(WinJS.UI.processAll().then(function() { navbar.addEventListener("click", function (e) { var navTarget = "pages/" + e.target.winControl.id + ".html"; WinJS.Navigation.navigate(navTarget); navbar.winControl.hide(); }); })); } };
app.start(); })();`
声明文件定位能力
让基本示例工作的最后一步是启用对Pictures库的访问。为此,我打开package.appxmanifest文件,切换到Capabilities选项卡并检查Pictures Library能力,如图图 23-2 所示。
***图 23-2。*启用对图片库的访问
我将在本章的后面返回到清单来声明额外的功能,但是对Pictures库的访问已经足够开始了。
显示图像文件
对于本章的第一个例子,我将从一些非常简单的事情开始:在应用布局中显示一个图像文件。这是一个很好的切入点,因为它让我在第二十二章中向您展示的文件系统功能的基础上进行构建,并演示它们如何与 JavaScript Metro 应用中的 HTML 布局相关联。
注意除非我另有说明,否则我在本章中使用的对象都在
Windows.Storage名称空间中。
对于这一节,我已经在项目中添加了一个名为pages/displayFile.html的文件。这是一个包含标记、CSS 和 JavaScript 的一体化文件,你可以在清单 23-4 中看到内容。
清单 23-4 。displayFile.html 文件的内容
`
#imgElem { height: 500px; } ` `这个内容的简单布局由一个用于显示文件的img元素和一个用于显示文件名的div元素组成。如果你启动应用,通过点击Display File命令使用导航条导航到这个文件,你会看到类似于图 23-3 的东西。
提示如果您的图片库中没有文件,您会看到一条
No Files Found消息。只需重启应用,点击Copy Sample Files按钮,然后选择Display File导航条命令来补救这种情况。
***图 23-3。*显示在图片库中找到的第一个文件
我说您将看到类似于下图的内容,因为本例中的代码对Pictures库中的文件进行了深度查询,并显示了第一个文件的内容。(我在第二十二章中描述了文件查询。)对于我的机器,找到的文件是我在前一章中使用的一组示例图像中的astor.jpg文件,但是它可能是您机器上的一个完全不同的图像。
提示如果
Pictures库中的第一个文件不是浏览器可以显示的图像,您会看到一条Not an image file消息。Windows 并没有对Pictures库强制执行只显示图像的策略,所以我在显示文件之前检查了文件类型。
displayFile.html文件中的script元素代码应该很熟悉。我在由KnownFolders.picturesLibrary属性返回的StorageFolder上调用getFilesAsync方法,指定 CommonFileQuery.orderByName值来排序文件并设置文件夹深度。该示例的关键部分是以下语句:
imgElem.src = **URL.createObjectURL**(files[0]);
由getFilesAsync方法返回的Promise产生一个代表图片库中文件的StorageFile对象数组。问题是我在布局中使用的img元素不知道如何显示StorageFile对象的内容,因为它们是一个 Windows 概念,不是 HTML 的一部分。
URL.createObjectURL方法通过充当文件和我的 HTML 标记之间的桥梁解决了我的问题。URL对象是 W3C 文件 API 规范的一部分,它是 HTML5 的附属标准草案。它是由浏览器实现的,这就是为什么我能够在不使用名称空间的情况下调用该方法。
createObjectURL方法获取一个文件并返回一个 URL,该 URL 可用于在 HTML 元素中访问该文件,这就是为什么我将示例中的createObjectURL调用的结果赋给了我的img元素的src属性。如果您使用 Visual Studio DOM Explorer窗口查看示例应用中的 HTML,您会看到img元素最终看起来像这样:
<img id="imgElem" src="blob:9A3CB2C5-9526-49E3-A8C3-6F0FFC3DCD66"></img>
提示
createObjectURL方法返回的 URL 特定于创建它的系统——您不能与其他设备共享该 URL。
使用文件和文件夹选择器
应用只能自由访问由KnownFolders对象定义的文件系统位置,这些位置在应用清单的功能部分中声明。为了访问其他位置,您需要用户的明确许可,这是使用文件和文件夹选择器获得的。使用选择器,您可以要求用户指定要打开或保存的文件,或者选择一个文件夹。
为了演示拣选器的使用,我在 Visual Studio 项目中添加了一个名为pages/pickers.html的新文件,其内容如清单 23-5 所示。当点击导航栏中的Pickers命令时,会显示该内容。首先,这个文件只包含演示其中一个选择器的代码,稍后我会为其他选择器添加代码。
清单 23-5 。pages/pickers.html 文件的初始内容
`
#pickerImgElem {display: none}这个例子包含三个按钮,我将用它们来激活拣选器。还有一个img和div元素,我将使用它们来显示选定的文件。此文件的代码演示了文件打开选择器的使用,它允许用户选择一个或多个文件。因为这是我第一次向您展示一个选择器,所以我将介绍它是如何呈现给用户的,然后解释代码是如何工作的。
举个例子
当您第一次选择导航栏上的命令时,您会看到一个非常基本的布局。img元素被隐藏,仅显示按钮和一条消息,如图 23-4 中的所示。
***图 23-4。*pickers.html 文件的初始布局
点击Open File Picker按钮显示拾取器,如图图 23-5 所示。选取器填充屏幕,允许用户在文件系统中导航,并显示当前文件夹中文件的缩略图。
***图 23-5。*文件打开选择器
导航到图中所示的flowers文件夹,选择其中一幅图像(该图像将被检查)。激活Open按钮—点击它选择文件。文件拾取器消失,选中的图像显示在应用布局中,如图图 23-6 所示。
***图 23-6。*显示用文件拾取器选择的图像
理解代码
现在您已经看到了文件选择器是如何呈现给用户的,我将解释示例中的代码是如何创建您所看到的行为的。当您需要用户选择一个或多个文件时,您可以使用位于Windows.Storage.Pickers名称空间中的FileOpenPicker对象。要使用选取器,您需要创建对象的新实例,然后为它定义的属性设置值。您可以在表 23-1 中看到FileOpenPicker属性列表。
我已经重复了创建和配置 23-清单 23-6 中的FileOpenPicker的语句。您可以看到我已经将png和jpg扩展添加到了fileTypeFilter数组中,并且我没有为commitTextButton属性设置值,而是依赖于默认值Open。
清单 23-6 。创建和配置拾取器
... var openPicker = Windows.Storage.Pickers.FileOpenPicker(); openPicker.fileTypeFilter.push(".png", ".jpg"); openPicker.suggestedStartLocation = pickers.PickerLocationId.picturesLibrary; openPicker.viewMode = pickers.PickerViewMode.thumbnail; ...
您使用suggestedStartLocation属性来指示应该在选择器中显示的初始文件夹。这只是一个建议,选取器可能会显示另一个值-如果之前使用过选取器并且它记住了最后一个位置,或者因为您指定的位置不可用,则可能会发生这种情况。你从Windows.Storage.Pickers.PickerLocationId对象中设置初始位置的值,我已经在表 23-2 中列出。在这个例子中,我使用了picturesLibrary值,这样提货人最初打开了Pictures位置。
使用来自Windows.Storage.Pickers.PickerViewMode对象的值设置viewMode属性,它允许您指定选取器如何显示文件和文件夹。有两个定义的值,我已经在表 23-3 中给出。
我在示例中使用了thumbnail值,这在您希望处理图像文件时非常理想,我的示例应用就是这样。用户不能在选取器中更改视图,因此您必须确保为您希望处理的内容选取一个合理的值。
挑选文件
FileOpenPicker对象定义了两个方法,当您准备好显示选择器并让用户做出选择时,您可以调用这两个方法。这些方法在表 23-4 中描述。
在这个例子中,我使用了pickSingleFileAsync方法,它允许用户选择单个文件。该方法返回一个Promise,当用户做出选择时,它被满足,并通过then方法产生一个StorageFile对象。(提醒一下,你可以在第九章中阅读所有关于Promise对象和then方法的内容。)如果用户没有选择就取消了选取器,那么传递给then方法函数的对象将是null。在清单 23-7 中,我重复了示例中显示选取器和处理用户选择的代码。
清单 23-7 。显示选取器并处理用户的选择
... openPicker.**pickSingleFileAsync()**.then(function (**pickedFile**) { if (pickedFile != null) { loadedFile = pickedFile; pickerImgElem.style.display = "block"; **pickerImgElem.src = URL.createObjectURL(pickedFile);** pickerTitleElem.innerText = pickedFile.displayName; save.disabled = false; } else { pickerImgElem.style.display = "none"; pickerTitleElem.innerText = "No file selected"; } }); ...
为了显示选中的文件,我使用了URL.createObjectURL方法,并将结果赋给布局中img元素的src属性。使用pickMultipleFilesAsync方法是类似的,除了当用户做出选择时,then方法被传递一个StorageFile对象的数组。
提示在这个例子中,我限制了用户可以选择的文件类型,这意味着我可以安全地在一个
img元素中显示文件的内容。在这一章的后面,我将向你展示如何处理不是图像的文件。
使用文件保存选择器
现在,您已经看到了其中一个拣选器是如何工作的,我可以介绍其他的拣选器,而不必进入相同的细节层次。清单 23-8 显示了我添加到 pickers.html 文件中的内容,当点击Save File Picker按钮时,它会做出响应。这段代码使用了Windows.Storage.Pickers.FileSavePicker对象,它允许用户选择一个位置并保存文件。
清单 23-8 。用文件保存选择器保存文件
`...
**...`
要测试这段代码,请启动应用,单击Open File Picker按钮,然后选择一个图像文件。当显示图像文件时,Save File Picker按钮将被激活,允许您将加载的图像文件保存到新位置。当您点击Save File Picker按钮时,将显示拾取器,如图图 23-7 所示。
***图 23-7。*用拾取器保存文件
除了用户能够为将要保存的文件指定名称和类型之外,保存选取器的外观类似于打开选取器。我已经将选取器的初始位置设置为Documents库,这就是为什么会列出这么多不同的文件夹。您可以通过创建一个新的FileSavePicker对象来创建一个新的选取器,并通过该对象的属性来配置它。我已经在表 23-5 中列出了房产,其中部分房产与FileOpenPicker共有。
在示例中,我使用了属性来约束用户的选择,以便他们可以选择文件的位置和名称,但只能选择用打开的选择器加载的文件类型。这是因为我不想进入文件类型转换的世界,而是想演示一下选择器是如何工作的。
挑选文件
当您准备好向用户显示选取器时,调用pickSaveFileAsync方法。这个方法返回一个Promise,它将一个代表用户选择的StorageFile对象传递给then方法函数。如果用户点击Cancel按钮,则null被传递到then功能。
选取器只向用户请求一个位置——应用有责任对它做些什么。例如,我将先前加载的文件复制到所选的位置。我已经重复了清单 23-9 中的代码。
清单 23-9 。处理用户选择的位置
... savePicker.pickSaveFileAsync().then(function (saveFile) { if (saveFile) { loadedFile.copyAndReplaceAsync(saveFile).then(function () { pickerImgElem.style.display = "none"; pickerTitleElem.innerText = "Saved: " + saveFile.name; }); } }); ...
使用文件夹选择器
第三个选择器允许用户选择一个文件夹。到目前为止,您已经理解了配置和使用选择器的模式,所以我在这个例子中添加了一项新技术,只是为了让事情变得更有趣。
到目前为止,我假设用户只想处理图像文件。这是一个方便的快捷方式,因为它与应用布局中的img元素配合得很好。当然,现实是大多数应用需要处理不同类型的文件,所以在这个例子中,我向你展示了如何显示你可能遇到的任何文件的缩略图。清单 23-10 显示了pickers.html文件的附加内容,用于在点击文件夹选择器按钮时使用文件夹选择器(并处理缩略图)。
注意当然,显示缩略图并不等同于阅读文件内容。正如我在前面的例子中演示的那样,处理图像文件的内容很容易,因为您可以依赖 IE10 对使用 HTML
img元素显示图像的内置支持。在第二十二章中,我向你展示了如何读取文件内容,并提到了对处理二进制内容的支持,这可能是你所需要的,取决于你的应用所操作的文件类型。
清单 23-10 。对 pickers.html 文件的补充,增加了对文件夹选择器的支持
`...
...`
使用文件夹选择器的技术与前面的示例类似。你创建一个Windows.Storage.Pickers.FolderPicker对象并通过它的属性配置它,我已经在表 23-6 中列出了。
在这个例子中,我将初始位置设置为Pictures库,并使用一个星号(*字符)来指定应该向用户显示所有类型的文件。
提示如果
fileTypeFilter数组没有包含至少一项,那么FolderPicker对象将抛出一个异常,因此你必须列出你想要显示的文件类型或者使用一个星号。
pickSingleFolderAsync方法显示选取器并允许用户选择一个文件夹。你可以在图 23-8 中看到拾取器是如何出现在用户面前的,它显示了包含本书前一章手稿的文件夹的内容。
***图 23-8。*使用文件夹选择器选择文件夹
FolderPicker看起来很像FileOpenPicker,但是用户不能选择单个文件,按钮文本清楚地表明正在选择一个文件夹。pickSingleFolderAsync方法返回一个Promise,当用户选择一个文件夹时,这个值就会被满足。用户的选择作为一个StorageFolder对象传递给then方法函数(我在第二十二章中介绍过)。
使用缩略图图像
我重复了前一个例子中处理清单 23-11 中的StorageFolder的代码。当用户选择一个文件夹时,我调用getFilesAsync方法来获取文件夹中的文件。
清单 23-11 。使用缩略图
... folderPicker.pickSingleFolderAsync().then(function(selectedFolder) { if (selectedFolder != null) { selectedFolder.getFilesAsync().then(function (files) { ** files[0].getThumbnailAsync(storage.FileProperties.ThumbnailMode.singleItem,** ** 500)** .then(function (thumb) { pickerImgElem.style.display = "block"; pickerImgElem.src = URL.createObjectURL(thumb); pickerTitleElem.innerText = files[0].displayName; }); }); } }); ...
我获取文件夹中的第一个StorageFile对象并调用getThumbnailAsync方法。此方法生成一个图像,可用于直观地引用文件。对于图像文件,缩略图将是文件内容,而对于其他文件,缩略图通常是系统用来打开文件类型的默认应用的应用图标。
getThumbnailAsync方法有两个参数。第一个参数是来自Windows.Storage.FileProperties.ThumbnailMode对象的一个值,它指定了将要生成的缩略图的种类。我已经在表 23-7 中列出并描述了ThumbnailMode值。
在示例中,我选择了用户选择的文件夹中的第一个文件,并在创建缩略图时使用了singleItem值,这意味着我将收到一个具有文件原始纵横比的大图像(这在显示图像文件时很重要)。第二个参数是您希望最长边的缩略图的大小—我已经指定了500,这意味着我的缩略图的最长边将是 500 像素。
当getThumbnailAsync方法返回的Promise被满足时,我使用URL.createObjectURL方法为缩略图创建一个 URL,如下所示:
... pickerImgElem.src = URL.createObjectURL(thumb); ...
createObjectURL方法接受一系列不同的对象类型,包括由getThumbnailAsync方法产生的Windows.Storage.FileProperties.StorageItemThumbnail对象。
为了演示如何为非图像文件生成缩略图,我在我的Music库中选择了包含手稿文件的文件夹。使用文件夹选取器选择文件夹是一个两阶段的过程。
首先,导航到想要选择的文件夹后,点击Choose this folder按钮。这做了一个临时的选择,但是执行了一个微妙的 UI 更新,文件夹显示在屏幕的底部边缘,按钮的文本变成了OK,如图图 23-9 所示。
***图 23-9。*临时挑选文件夹
你可以在图 23-10 中看到拾取一个非图像文件的效果。我选择了包含手稿章节文本的 Microsoft Word 文件,该文件使用与 Word 文件相关的缩略图显示。
***图 23-10。*Word 文件的缩略图
缓存位置访问
正如您多次看到的那样,Metro 应用在文件系统方面受到严格的访问限制。您的应用可以自由访问由KnownFolders对象定义的位置,但前提是您想要访问的每个位置都在应用清单中声明为一项功能。如果您需要处理不同位置的文件,那么您需要使用选择器来获得用户授予的显式访问权限。
如果您的应用允许用户创建一些内容,然后将其保存到单个文件中,这种模式就很好——在这种情况下,使用选择器是非常合理的,因为每个文件的打开或保存位置可能会有所不同。
但是许多 Metro 应用将需要持久访问用户选择的位置,为了管理这一点,您必须使用Windows.Storage.AccessCache名称空间中的对象。首先,我向示例 Visual Studio 项目添加了一个名为access.html的新文件,您可以在示例应用中使用Access Cache NavBar 命令来访问该文件。你可以在清单 23-12 中看到内容。
清单 23-12 。access.html 文件的初始内容
`
` ` #accessImgElem {display: none;} ` `这个例子背后的想法是展示两个相关的文件操作。布局中有两个button元素,为了测试这个例子,启动应用,在导航栏上选择适当的 common,然后单击Pick Folder按钮。该应用将显示文件夹选择器,以便您可以选择一个位置。选择任何不在Pictures库中的文件夹(因为应用已经在清单中声明了对Pictures的访问,所以已经可以访问那个位置)。
选择文件夹后,点击Load File按钮。该应用显示所选文件夹中第一个文件的名称——但它还没有显示缩略图,如图图 23-11 所示。
***图 23-11。*使用示例应用选择文件夹
现在,为了看看这个例子有什么不同,从 Visual Studio Debug菜单中选择Restart来重新启动应用。(重要的是重启,而不是刷新app。)
点击Load File按钮(不是Pick Folder按钮),你会看到缩略图和先前选择的文件夹中第一个文件的名称被显示出来,如图图 23-12 所示。我在文档库中选择了一个文件夹,应用通常无法访问该文件夹。
***图 23-12。*使用缓存文件位置
这个例子有两点很重要。第一个是所选位置被持久存储,第二个是访问该位置的许可也是持久的。这个应用不需要返回给用户并显示选取器来再次获取位置(以及访问它的权限)。
使用访问缓存
这个过程有两个部分——缓存对位置的访问和检索缓存的数据。我重复了清单 23-13 中处理缓存部分的例子中的关键语句。
清单 23-13 。缓存对文件系统位置的访问
... folderPicker.pickSingleFolderAsync().then(function (folder) { if (folder != null) { ** var token = access.StorageApplicationPermissions.futureAccessList.add(folder);** ** storage.ApplicationData.current.localSettings.values["folder"] = token;** accessTitleElem.innerText = "Selected: " + folder.displayName; } **});** **...**
名称空间Windows.Storage.AccessCache(在例子中我将其别名为access)包含了StorageApplicationPermissions对象。该对象定义了两个属性,如表 23-8 所述。
在这个例子中,我使用了futureAccessList属性,它返回一个Windows.Storage.AccessCache.StorageItemAccessList对象。我通过调用add方法存储我的位置,传入我希望在将来再次使用其位置的StorageFile或StorageFolder对象。
add方法返回了一个我需要注意的字符串令牌——我使用了我在第二十章的中描述的应用设置特性来持久地存储选择的文件夹作为folder设置。StorageItemAccessList对象定义了我在表 23-9 中列出的方法和属性。
当需要我显示文件的缩略图和名称时,我检索存储在应用设置中的令牌,并将其传递给getFolderAsync方法。这个方法返回一个Promise,当它被满足时,产生一个对应于缓存位置的StorageFolder对象。我重复了清单 23-14 中获取令牌和检索位置的例子中的语句。
清单 23-14 。从访问缓存中检索位置
... var token = storage.ApplicationData.current.localSettings.values["folder"]; var folder = **access.StorageApplicationPermissions.futureAccessList.getFolderAsync(token)** .then(function (folder) { // *...statements that process StorageFolder omitted for brevity...* }); ...
通过使用访问缓存,我能够保留用户授予我的访问位置的权限,这样我就不必在每次应用启动时都使用选择器,我只需获得一次我需要的位置,然后就可以继续使用它们。
当然,有几个考虑因素。首先,也是最重要的,我不能滥用用户的信任,在我的应用被授权访问的位置上执行意想不到的操作。根据经验,我继续使用缓存位置来执行非破坏性操作,比如读取文件、监视文件夹的更改或创建新文件。如果我需要对文件系统执行任何类型的更改,包括重命名、移动和(尤其是)删除文件,我会提示用户获得明确的许可。
提示用户不仅给了他们说不的机会,还意味着他们清楚地知道是我的应用做了一系列的改变,避免了当他们试图找到已经被归档到不同地方或者更糟的是已经被删除的文件时令人讨厌的意外。
使用访问缓存时的第二个考虑是确保缓存中的位置不超过 1,000 个。一千个位置听起来很多,但缓存的条目会很快增加,特别是如果你的应用经常使用,并且操作单个文件而不是文件夹。有两种方法可以处理这个问题——您可以手动管理futureAccessList.entries数组的内容,并确保它不超过maximumItemsAllowed值。每个条目由一个AccessListEntry项表示,其token属性通过getFileAsync和getFolderAsync方法返回访问缓存位置所需的令牌。
作为替代,您可以使用StorageApplicationPermissions.mostRecentlyUsedList。这就像futureList一样,但是它只包含 25 个最近使用的位置。当您添加第 26 个项目时,最少使用的项目会被自动删除,使您无需直接管理内容。
使用文件数据源
在第十四章、第十五章和第十六章中,我解释了如何通过数据驱动的 WinJS UI 控件FlipView、ListView和SemanticZoom使用数据源。在那些章节中,我使用了WinJS.Binding.List对象作为数据源,即使是在我演示如何显示图像的时候。
更好的方法是创建由文件系统查询直接驱动的数据源,允许您对用户存储在设备上的文件进行操作。为了演示这一点,我将pages/dataSources.html 文件添加到示例 Visual Studio 项目中,其内容可以在清单 23-15 中看到。
清单 23-15 。dataSources.html 文件的内容
`
#flip {background-color: black; width: 500px; height: 500px;}Converters.img.supportedForProcessing = true; Converters.general.supportedForProcessing = true;
WinJS.UI.Pages.define("/pages/dataSources.html", { ready: function () { storage.KnownFolders.picturesLibrary.getFolderAsync("flowers") .then(function (folder) { var options = new search.QueryOptions(); options.fileTypeFilter = [".png"]; options.folderDepth = search.FolderDepth.deep;
var query = folder.createFileQueryWithOptions(options);
flip.winControl.itemDataSource
= new WinJS.UI.StorageDataSource(query, {
mode: storage.FileProperties.ThumbnailMode.picturesView,
requestedThumbnailSize: 400,
thumbnailOptions:
storage.FileProperties.ThumbnailOptions.resizeThumbnail,
synchronous: false
}); });
}
});
这个例子在Pictures库中查询 PNG 图像文件,生成一个数据源,然后与FlipView控件一起使用,这样用户就可以浏览他们的图像。你可以在图 23-13 中看到应用的布局,你可以使用Data Sources导航条命令将其加载到示例应用中。
图 23-13。【dataSources.html 页面的布局
布局很简单,但是在这个简短的例子中有惊人的数量,所以我将详细检查代码。
创建数据源
简单的部分是创建数据源本身,这是使用WinJS.UI.StorageDataSource对象完成的。你首先创建一个QueryOptions对象,我在第二十二章中介绍过,然后把它传递给StorageFolder.createFileQueryWithOptions方法。这将返回一个StorageFolderQueryResult对象,您可以将它作为参数传递给StorageDataSource构造函数。我重复了清单 23-16 中设置StorageDataSource对象的例子中的语句。
清单 23-16 。创建存储数据源对象
`... var options = new search.QueryOptions(); options.fileTypeFilter = [".png"]; options.folderDepth = search.FolderDepth.deep;
var query = folder.createFileQueryWithOptions(options); flip.winControl.itemDataSource = new WinJS.UI.StorageDataSource(query, { mode: storage.FileProperties.ThumbnailMode.picturesView, requestedThumbnailSize: 400, thumbnailOptions: storage.FileProperties.ThumbnailOptions.resizeThumbnail, synchronous: false }); ...`
在这个例子中,我创建了一个QueryOptions对象,它执行深层文件夹查询,并将其匹配限制在 PNG 文件。我将StorageDataSource对象赋给了FlipView控件的itemDataSource属性,这意味着通过QueryOptions匹配的文件将由 UI 控件显示。
StorageDataSource构造函数有两个参数:QueryOptions对象和一个具有四个特定属性的对象。这些属性在表 23-10 中描述。
对于这个例子,我使用了ThumbnailMode.picturesView值来获得一个宽高比的缩略图,并指定大小为 400 像素。我使用了在表 23-11 中描述的ThumbnailOptions.resizeThumbnail值,并将synchronous属性设置为false,这意味着我需要满足那些缩略图尚未被加载的数据源项目——我将很快解释如何做到这一点。
生成模板数据
创建数据源只是这个过程的一部分——我还需要使用 WinJS 数据绑定特性来填充由FlipView控件使用的模板。这并没有想象中那么简单,因为文件系统查询返回的对象不能用新的属性来扩展,这正是WinJS.Binding.converter方法试图使数据对象的属性可观察到的事情。
相反,我必须使用一个开放的值转换器,这让我可以更松散地映射函数。我在第八章中描述了这种技术,处理不能扩展的对象是这种技术有用的一种情况。首先,我将data-win-bind属性添加到 HTML 模板的元素中,如清单 23-17 中的所示,这里我重复了示例中的模板元素。
清单 23-17 。向 HTML 模板元素添加 data-win-bind 属性
`...
为了支持这个模板,我定义了两个开放的转换器,我在清单 23-18 中重复了这两个转换器。
清单 23-18 。开放数据转换器以支持 HTML 模板
WinJS.Namespace.define("Converters", { img: function (src, srcprop, dest, destprop) { if (src.thumbnail == null) { src.addEventListener("thumbnailupdated", function (e) { dest[destprop] = URL.createObjectURL(src.thumbnail); }); } else { dest[destprop] = URL.createObjectURL(src.thumbnail); } }, general: function (src, srcprop, dest, destprop) { dest[destprop] = src[srcprop]; } }); Converters.img.supportedForProcessing = true; Converters.general.supportedForProcessing = true;
最简单的转换器叫做Converters.general,它只是将指定属性的值设置为指定的数据对象值。第二个称为Converters.img,需要稍微多解释一下。对于不能使用文件系统对象作为数据绑定值的来源这一问题,这是一个简单的解决方案。general转换器简单地使用源属性的值来设置目标属性的值,不进行转换或格式化,并充当Windows.Storage和WinJS.Binding名称空间之间的桥梁。
Converters.img转换器处理两个问题。首先,它使用URL.createObjectURL方法创建引用文件缩略图的 URL,这样我就可以在 HTML img元素中使用它们。
第二个问题是 Windows 只在需要的时候才生成文件缩略图。这意味着,如果您正在浏览数据源中的文件(在本例中,这是用户将使用FlipView控件进行的操作),那么您将会遇到缩略图尚未加载的数据对象。
使用StorageDataSource对象时,数据源将是一个Windows.Storage.BulkAccess.FileInformation对象。Windows.Storage.BulkAccess名称空间提供了可以用来执行大规模高效文件系统操作的对象,这通常对创建数据源提供者很有用,但通常对常规应用开发没什么用处(这就是为什么在本节之外我不详细介绍这个名称空间)。
缩略图可通过FileInformation.thumbnail属性获得,但如果 Windows 尚未生成并缓存合适的图像,这将返回null。为了解决这个问题,我监听了thumbnailupdated事件,当 Windows 生成缩略图时,FileInformation对象将发出该事件。结果是 HTML 元素将被更新,即使 Windows 需要一点时间来生成所需的缩略图。所有这些组合在一起提供了一个数据源,可以用于数据驱动的 WinJS UI 控件,它建立在我在《??》第二十二章中介绍的对象和技术之上。
总结
在这一章中,我已经向你展示了一些方法,你可以将文件系统的底层支持集成到你的应用中。我向您展示了如何创建引用图像文件内容的 URL,如何向用户呈现打开文件、保存文件和文件夹选择器,以及如何缓存对位置的访问,以便您可以向用户提供服务,而不必不断地纠缠他们让您访问文件系统位置。同时,我还向您展示了如何生成缩略图,以及如何创建查询文件系统的数据源,并以 WinJS 数据驱动 UI 控件可以使用的方式呈现结果。在下一章,我将向您展示如何实现与文件系统相关的 Metro 契约。
二十四、文件激活和选取器契约
在这一章中,我将向你展示如何实现允许 Windows 应用将文件相关功能集成到 Windows 中的三个契约。我将向您展示如何使用文件激活契约注册一个应用来处理特定类型的文件,以及如何使用保存选取器和打开选取器契约向其他应用提供存储服务。像所有契约一样,实现这些契约是可选的,但如果你的应用以任何方式处理文件,你应该仔细查看它们,看看它们是否提供了集成,使用户使用你的应用更加简单和容易。表 24-1 提供了本章的总结。
创建示例应用
本章的示例应用将提供一些基本功能,我将通过实现文件契约来增强这些功能。我要建立一个简单的相册应用,它的初始化身将让用户从文件系统中选择图像文件添加到相册中。每个图像的缩略图将显示在一个ListView UI 控件中(我在第十五章中描述过)。我将使用Windows.Storage.AppCache名称空间中的对象缓存用户选择的文件位置(我在第二十三章中描述过),这将使用户的文件选择持久化。
我使用Blank App模板创建了一个名为PhotoAlbum的新 Visual Studio 项目。您可以在清单 24-1 中看到default.html文件的内容。
清单 24-1 。来自相册 app 的 default.html 文件
`
` ` PhotoAlbum ** ** ** ** ****
** `该应用将使用标准内容导航模型,通过使用WinJS.Navigation名称空间将内容页面引入应用布局。与我使用WinJS.Navigation的其他例子不同,内容转换将由应用触发,以响应文件契约。这个default.html文件还包含了一个WinJS.Binding.Template的元素,我将用它来显示整个应用中图像文件的缩略图。模板显示图像和包含文件名的标签。
内容文件可以在pages文件夹中找到,我只从一页内容开始,名为pages/albumView.html,我将在应用启动时加载它。这将提供基本的相册特性,你可以在清单 24-2 中看到这个文件的内容。
清单 24-2 。albumView.html 文件的内容
`
除了显示图像缩略图的ListView控件之外,该文件还包含两个按钮。Open按钮显示文件打开选择器(在第二十三章的中描述),以便用户可以在应用中打开图像文件。Clear按钮从ListView中移除图像并清除位置缓存,将应用重置为初始状态。你可以在清单 24-3 中看到我为这些元素定义的样式,它显示了css/default.css文件的内容。CSS 中没有特殊的功能或特定于应用的技术。
清单 24-3 。default.css 文件的内容
`#contentTarget { width: 100%; height: 100%; display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: center; -ms-flex-pack: center;}
#listView { border: medium solid white; margin: 10px; height: 80%; width: 80%; padding: 20px;}
#buttonContainer button { font-size: 20pt; width: 100px;}
.imgContainer { border: thin solid white; padding: 2px;}
.listTitle { font-size: 18pt; max-width: 180px; text-overflow: ellipsis; display: block; white-space: nowrap; margin: 0 0 5px 5px; height: 35px;}
.listImg {height: 200px; width: 300px;} .title { font-size: 30pt;}`
定义 JavaScript
我想让这个项目中的default.js文件尽可能简单,因为它将是本章中变化最大的文件,我不想重复列出作为基本的非契约功能一部分的代码。为此,我创建了几个 JavaScript 文件来执行应用的基本设置,并提供管理相册所需的功能。这些文件中的第一个,/js/setup.js,如清单 24-4 所示。
清单 24-4 。setup.js 文件的内容
`(function () {
WinJS.Namespace.define("ViewModel", { fileList: new WinJS.Binding.List() });
WinJS.Navigation.addEventListener("navigating", function (e) { WinJS.UI.Animation.exitPage(contentTarget.children).then(function () { WinJS.Utilities.empty(contentTarget); WinJS.UI.Pages.render(e.detail.location, contentTarget, WinJS.Navigation.state) .then(function() { WinJS.Binding.processAll(document.body, ViewModel); }).then(function () { return WinJS.UI.Animation.enterPage(contentTarget.children) }); }); }); })();`
这个文件创建了ViewModel.fileList对象,我将它用作ListView控件的数据源,这样我就可以显示图像缩略图。我还为navigating事件设置了事件处理程序,它将内容加载到主页中。我提到的函数在/js/app.js文件中,其内容如清单 24-5 所示。
清单 24-5 。app.js 文件的内容
`(function () {
var storage = Windows.Storage; var access = storage.AccessCache; var cache = access.StorageApplicationPermissions.futureAccessList; var pickers = storage.Pickers;
WinJS.Namespace.define("App", {
loadFilesFromCache: function () {
return new WinJS.Promise(function (fDone, fErr, fProg) {
if (cache.entries.length > 0) {
ViewModel.fileList.length = 0;
var index = cache.entries.length - 1;
(function processEntry() {
cache.getFileAsync(cache.entries[index].token)
.then(function (file) {
App.processFile(file, false);
if (--index != -1) {
processEntry();
} else {
fDone();
}
});
})();
} else {
fDone();
} });
},
processFile: function (file, addToCache) { ViewModel.fileList.unshift({ img: URL.createObjectURL(file), title: file.displayName, file: file }); if (addToCache !== false) { cache.add(file); } },
pickFiles: function () { var picker = pickers.FileOpenPicker(); picker.fileTypeFilter.replaceAll([".jpg", ".png"]); picker.pickMultipleFilesAsync().then(function (files) { if (files != null) { files.forEach(function (file) { App.processFile(file); }); } }); },
clearCache: function () { cache.clear(); ViewModel.fileList.length = 0; } });
})();`
我不打算详细介绍这段代码,因为它建立在我在前面的章节中描述和演示的功能之上,我只是将它作为演示文件契约的基础。
也就是说,这个清单中的一些技术是我在前面章节中展示的技术的微小变化,值得指出。首先,我在FileOpenPicker对象上使用了pickMultipleFilesAsync方法,这样用户可以一次选择多个文件。这不是一个复杂的特性,与使用pickSingleFileAsync的唯一区别是,方法返回的Promise通过then方法产生一个StorageFile对象的数组(而不是单个StorageFile对象)。
第二个变化是我不存储由futureAccessList.add方法返回的令牌。相反,我处理的是entries数组中的项目,使用token属性从getFileAsync方法中获取我需要的字符串来获取StorageFile对象。(我反向枚举缓存位置,以便最近添加的图像显示在ListView显示屏的顶部。)最后一个文件是js/default.js,如清单 24-6 所示。
清单 24-6 。default.js 文件的初始内容
`(function () {
var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation; var appstate = activation.ApplicationExecutionState; var storage = Windows.Storage;
app.onactivated = function (args) {
if (args.detail.previousExecutionState != appstate.suspended) { args.setPromise(WinJS.UI.processAll().then(function () { if (ViewModel.fileList.length == 0) { App.loadFilesFromCache(); }
switch (args.detail.kind) { default: WinJS.Navigation.navigate("/pages/albumView.html"); break; } })); } }; app.start(); })();`
我尽可能保持简单,依靠我在viewmodel.js文件中创建的ViewModel和App名称空间中的对象来实现相册的基本功能,我将在下一节中演示。switch语句可能看起来有点奇怪,因为它只有一个default块,但是我将在本章中为不同类型的激活事件添加处理程序。
测试示例应用
为了测试这个例子,启动应用并点击Open按钮。导航到一个有一些图像文件的文件夹(我使用了我在前面章节中创建的Pictures/flowers文件夹),用选择器选择一个或多个图像。点击选择器中的Open按钮打开文件,你会看到缩略图显示在应用布局中,如图图 24-1 所示。
***图 24-1。*将图像载入示例应用
从 Visual Studio Debug菜单中选择Stop Debugging停止应用,然后再次启动应用。由于文件位置缓存在futureAccessList对象中,你会看到你之前加载的图像再次显示。
对于示例应用来说,这是一个很长的设置过程,但是它为我实现文件契约提供了一个很好的基础,现在我可以用最少的额外代码来实现它。
添加图像
我在这个示例应用中使用了许多图像,所有这些图像都可以在 Visual Studio 项目的 images 文件夹中找到。前两个图像被称为jpgLogo.png和pngLogo.png,我将在实现文件激活契约时使用它们。你可以在图 24-2 中看到这些图像。这两个文件显示相同的图像,但背景不同。
***图 24-2。*将在文件激活契约中使用的图像
我还将 Visual Studio 在images文件夹中创建的所有默认图像替换为具有相同尺寸的图像,但显示的图标与图中的图像相同。
对于logo.png、slashscreen.png和storelogo.png文件,我添加到项目中的图像在透明背景上显示一个白色图标,这意味着它们不可能在白色页面上显示——但是如果你想象一下图 24-2 中的一个图像没有彩色背景,你就会明白了。
我已经删除了smalllogo.png文件,用一个名为small.png的 30×30 像素文件代替,它以白色显示相同的图标,但背景为黑色,看起来和图 24-2 中左边的图像一样。然后我更新了应用清单,为Small logo字段指定了 small.png 文件,如图 24-3 中的所示。
***图 24-3。*更改小 logo 文件
这个名称的改变很重要,因为它解决了一个奇怪的错误——我将在本章后面实现文件激活契约时解释这个问题。
提示你可以在本书附带的源代码下载中找到所有这些图像文件,从
apress.com开始可以免费获得。
创建助手应用
我在本章中实现的一些契约为其他应用提供服务,我需要一个助手应用来演示这些功能。这个应用叫做FileHelper,它非常简单,使用我在第二十三章的中介绍的文件拾取器来加载和保存一个图像文件。您可以在清单 24-7 中看到FileHelper项目的default.html文件的内容。
清单 24-7 。file helper default.html 文件的内容
`
FileHelper
这个应用的布局是围绕两个button元素和一个img元素构建的。Open按钮使用文件打开选择器加载单个图像文件,该文件使用img元素显示。你可以在清单 24-8 中看到我用来样式化这些元素的 CSS,它显示了css/default.css文件的内容。
清单 24-8 。FileHelper 项目中的 default.css 文件
body, div.container {display: -ms-flexbox;-ms-flex-direction: row; -ms-flex-align: center; -ms-flex-pack: center; } div.container {margin: 10px; height: 80%; -ms-flex-direction: column;} button {font-size: 25pt; width: 200px; margin: 10px;} #thumbnail {width: 600px; border: medium white solid;}
这个项目中唯一的另一个文件是default.js,在这个文件中,我通过显示选择器来响应按钮点击,以便用户可以加载和保存图像文件。您可以在清单 24-9 中看到default.js文件的内容。
清单 24-9 。FileHelper 项目中 default.js 文件的内容
`(function () { "use strict";
var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation; var storage = Windows.Storage; var pickers = Windows.Storage.Pickers;
var pickedFile = null;
app.onactivated = function (args) { if (args.detail.previousExecutionState != activation.ApplicationExecutionState.suspended) {
args.setPromise(WinJS.UI.processAll().then(function() {
WinJS.Utilities.query('button').listen("click", function (e) {
if (this.id == "open") {
var openPicker = new pickers.FileOpenPicker();
openPicker.fileTypeFilter.replaceAll([".png", ".jpg"]);
openPicker.pickSingleFileAsync().then(function (file) {
pickedFile = file;
save.disabled = false;
thumbnail.src = URL.createObjectURL(file);
});
} else { var savePicker = new pickers.FileSavePicker();
savePicker.defaultFileExtension = pickedFile.fileType;
savePicker.fileTypeChoices.insert(pickedFile.displayType,
[pickedFile.fileType]);
savePicker.suggestedFileName = "New Image File";
savePicker.pickSaveFileAsync().then(function (saveFile) {
if (saveFile) {
pickedFile.copyAndReplaceAsync(saveFile);
}
});
}
});
}));
}
};
app.start();
})();`
在这个清单中没有新的技术,我使用了拣选器,就像我在第二十三章中所做的一样。助手应用的存在只是为了帮助我在PhotoAlbum应用中演示文件契约的实现。你可以在图 24-4 中看到助手应用,它显示了我在前面章节中使用的一个样本图像。
***图 24-4。*文件助手应用
实现文件激活契约
文件激活契约允许您声明您的应用愿意并且能够处理某种类型的文件。为了理解我的意思,打开桌面,使用文件浏览器找到一个 PNG 或 JPG 文件——前几章中的花卉图片是理想的。右键单击该文件并选择Open with菜单,您将看到一个应用列表,包括桌面和 Windows 应用商店应用,可以打开该文件。你可以在图 24-5 中看到在我的台式电脑上打开 PNG 文件的应用。
***图 24-5。*能够在我的系统上打开 PNG 文件的应用
如图所示,我可以使用 Microsoft Office、Paint、Paint.NET、Photos 应用、Snagit 编辑器(我用于截图)和 Windows Photo Viewer 打开一个 PNG 文件。在这一部分,我将向你展示如何将你的应用添加到列表中,并演示当你的应用被选中打开一个文件时,你如何回应。
声明文件类型关联
您可以在应用清单中声明想要支持的文件类型。从Solution Explorer打开package.appxmanifest文件,切换到Declarations选项卡。
从Declarations列表中选择File Type Associations,点击Add按钮。Visual Studio 将为您提供一个表单,用于填写文件关联的详细信息。对于这个例子,我需要两个文件关联—一个用于 PNG 文件,一个用于 JPG 文件。表 24-2 描述了需要填充的字段的含义,并提供了每个关联的值。使用First Form列中的值填充完第一个表单后,单击Add按钮创建第二个关联。使用Second Form列中的值填写表单,然后键入Control+S保存对清单的更改。当你完成时,你会在声明列表中看到两个条目,如图图 24-6 所示。
提示通过点击
Add New按钮,您可以在同一个声明中支持多个文件扩展名,这将在表单中添加一个新的Supported File Type部分。我使用了两个声明,因为我希望每种文件类型有不同的图像和描述性文本。
***图 24-6。*向应用清单添加文件类型关联声明
当您使用 Visual Studio 启动应用时,应用包会安装到设备上,这包括创建 Windows 文件关联。尽管我只实现了契约的一部分,但您已经可以看到文件关联的效果:启动应用,然后导航到桌面(最简单的方法是键入Control+D)。打开Explorer并找到任何 PNG 或 JPG 文件。如果右击文件并选择Open with菜单,你会看到PhotoAlbum app 已经添加到列表中,如图图 24-7 所示。(显示的名称取自清单的Application UI部分的Display name字段——我更改了这个值,在单词Photo和Album之间添加了一个空格。)
***图 24-7。*浏览器中与文件类型相关联的应用
注意当我创建示例应用时,我特意更改了在
Small logo清单字段中指定的文件名。有一个奇怪的错误,如果你不改变文件名,Windows 将显示你的项目images文件夹中的logo.png文件,它通常有一个透明的背景,以便它可以在开始屏幕上使用(这个主题我将在第十九章中深入讨论)。透明背景会阻止应用在Open with列表中正常显示。要避免此问题,请确保更改 30 x 30 像素文件的名称,以便使用该文件,为用户呈现具有纯色背景的图像。
处理文件激活事件
最后一步是当用户选择应用打开文件时做出响应,这是使用一个activated事件发出的信号,该事件的kind属性被设置为ActivationKind.file。你可以看到我对清单 24-10 中的default.js文件中的onactivated函数所做的修改,以支持这种事件。
清单 24-10 。响应文件激活事件
`(function () {
var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation; var appstate = activation.ApplicationExecutionState; var storage = Windows.Storage;
app.onactivated = function (args) { if (args.detail.previousExecutionState != appstate.suspended) { args.setPromise(WinJS.UI.processAll().then(function () {
var promise = ViewModel.fileList.length == 0 ? App.loadFilesFromCache() : WinJS.Promise.wrap(false);
promise.then(function () {
switch (args.detail.kind) {
** case activation.ActivationKind.file:**
** args.detail.files.forEach(function (file) { App.processFile(file);**
** });**
default:
WinJS.Navigation.navigate("/pages/albumView.html");
break;
}
});
return promise; })); } }; app.start(); })();`
粗体语句响应文件激活事件。事件对象的detail.files属性包含一个StorageFile对象的数组,每个对象都由用户选择由应用打开。在这个清单中,我使用forEach方法来枚举数组的内容,并为每个StorageFile调用App.processFile方法(我在app.js文件中定义了该方法),其效果是将缩略图添加到ListView控件并缓存文件位置,以便应用持久工作。
注意注意,处理文件激活事件的
case块一直到default块。这意味着albumView.html文件将用于为file事件以及常规发布事件提供内容。你可以在第十九章中了解更多关于发布活动的信息。
要测试事件的处理,启动应用,导航到桌面,右键单击 PNG 或 JPG 文件。选择Open with菜单项,点击列表中的Photo Album。您打开的文件的缩略图将被添加到布局中的ListView,与文件的displayName值一起显示。
提示如果没有得到正确的结果,那么右击开始屏幕上的
Photo Album应用图标,选择Uninstall,然后从 Visual Studio 再次启动应用。当应用重新启动时,Windows 并不总是会选择更改,卸载应用包可以解决这个问题。
将应用设为默认处理程序
我还想描述文件激活处理程序的另一个方面。为此,您需要将应用设置为文件类型的默认处理程序。从 Windows 桌面上,使用文件资源管理器找到并右键单击一个 PNG 文件,选择Open with
Choose default program菜单项。你会看到一个弹出窗口,如图图 24-8 所示。(该应用显示有我在清单的应用 UI 部分中定义的徽标。我使用了与文件关联相同的图标,但是背景是透明的。)选择Photo Album项,使示例应用成为默认处理程序,然后对 JPG 文件重复这个过程。
***图 24-8。*让应用成为 PNG 文件的默认处理程序
这不会改变应用的行为方式,但它确实意味着 Windows 将使用我在本章前面添加到应用的图像,这些图像被指定为文件关联声明的一部分,作为向用户显示 PNG 和 JPG 文件时的文件图标。你可以在图 24-9 的中看到这是如何出现的。
***图 24-9。*Windows 资源管理器中使用的文件关联声明中的文件图标
因为我正在处理图像文件,Windows 会尽可能显示文件内容,但图标的使用范围更广,包括在开始屏幕上搜索文件时,以及搜索其他文件类型时。该图显示了文件资源管理器的List视图。
实施 App-to-App 提货契约
应用到应用的挑选契约允许其他应用通过你的应用加载和保存文件,而不是本地文件系统。如果你的应用提供的某种价值超出了用户以常规方式存储文件所能获得的价值,这将非常有用——一个很好的例子是支持 Dropbox 或 SkyDrive 风格的文件复制或提供对存储在远程位置的文件的访问的应用。
我需要一些更简单的东西来演示文件选择器契约,所以我的示例应用将文件存储在本地应用数据文件夹中。这不会给用户增加任何价值,但这意味着我可以专注于契约,而不会有太多的分心和转移。
在接下来的章节中,我将向您展示如何实现 app-to-app 提货契约:保存提货契约和开放提货契约。
实施保存提货人契约
保存选择器契约允许其他应用将文件保存到你的应用,就好像它是一个文件系统位置,就像一个文件夹一样。与大多数契约一样,您必须在应用清单中进行声明,并处理特定类型的激活事件。这份契约与您目前看到的略有不同,因为您还需要准备应用的布局,以便应用可以在选取器中显示内容。我将在接下来的章节中解释它是如何工作的。
宣布支持该契约
第一步是声明应用实现清单中的协定。这告诉 Windows 您的应用应该作为一个可以保存文件的位置呈现给用户。从 Visual Studio 的Solution Explorer窗口打开package.appxmanifest文件,点击Declaration部分的标签。
从Available Declarations列表中选择File Save Picker,点击Add按钮。如果你希望你的应用能够处理任何类型的文件(这在 Dropbox/SkyDrive 场景中是有意义的),那么选择Supports any file type选项。PhotoAlbum示例应用将只支持 JPG 和 PNG 文件,因此在File type文本框中输入.png,单击Add New按钮获得另一个框,并在第二个File type文本框中输入.jpg。键入Control+S保存更改。您的清单应该看起来像图 24-10 中的清单。
***图 24-10。*将文件保存提货人契约声明添加到应用清单
测试保存选择器声明
仅仅进行清单声明就将应用定义为一个保存位置,尽管我还没有实现作为契约一部分的激活事件。现在看看这是如何工作的,将更容易理解应用为实现这个契约而必须做的其余工作。
测试 save picker 契约声明需要使用我在本章开始时创建的FileHelper应用以及PhotoAlbum本身,并且需要几个步骤。
首先启动PhotoAlbum app。该应用不需要运行到保存位置,但 Visual Studio 会在启动时安装该应用,这将向 Windows 注册清单声明中定义的文件类型。点击Open按钮,选择一些图像,这样应用中就会有一些内容。
其次,启动我在本章开始时创建的FileHelper应用,点击Open按钮,选择一个图像文件——最好是你刚才没有选择的文件。点击Save按钮。该应用将显示文件保存选择器。如果你点击Files链接旁边的箭头,你会看到一个保存目的地列表,包括Photo Album,如图图 24-11 所示。
***图 24-11。*在选取器中显示为保存位置的应用
如果从列表中选择Photo Album,你会看到类似于图 24-12 所示的布局。这是文件选择器中显示的PhotoAlbum应用布局的奇怪组合。
完成实现契约所需的工作是双重的:我需要更新应用,以便在选择器中显示更有用的布局,并支持从FileHelper应用保存文件。在下一节中,我将向您展示如何做到这两点。
***图 24-12。*保存选择器中显示的应用布局
处理被激活的事件
当用户选择应用作为保存位置时,Windows 会发送一个activated事件。事件的detail.kind属性被设置为ActivationKind.fileSavePicker,这是更改应用布局的提示,以便它适合选取器并为用户提供一些有意义的内容。你可以在清单 24-11 中的应用中看到我是如何响应事件的,它显示了我对/js/default.js文件所做的更改。
清单 24-11 。响应 default.html 保存提货人契约激活事件
`(function () {
var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation; var appstate = activation.ApplicationExecutionState; var storage = Windows.Storage;
app.onactivated = function (args) {
if (args.detail.previousExecutionState != appstate.suspended) { args.setPromise(WinJS.UI.processAll().then(function () {
if (ViewModel.fileList.length == 0) { App.loadFilesFromCache(); }
switch (args.detail.kind) { ** case activation.ActivationKind.fileSavePicker:** ** var pickerUI = args.detail.fileSavePickerUI;m,** ** WinJS.Navigation.navigate("/pages/savePickerView.html",** ** pickerUI);** ** break;** case activation.ActivationKind.file: args.detail.files.forEach(function (file) { App.processFile(file); }); default: WinJS.Navigation.navigate("/pages/albumView.html"); break; } })); } }; app.start(); })();`
除了这些更改之外,我还向项目添加了一个名为pages/savePickerView.html的新文件,当我获得文件保存选择器契约的激活事件时,我会显示这个文件。
注意,当我调用WinJS.Navigation.navigate方法时,我从激活事件中传递了detail.fileSavePickerUI属性的值。这个对象让我在用户保存文件时做出响应,通过将它传递给navigate方法,我将能够处理savePickerView.html文件中的对象,如清单 24-12 所示。
注意要明确的是,我能够像这样传递对象是因为我在
js/setup.js文件中定义的函数将WinJS.Navigation.state属性的值传递给了WinJS.UI.Pages.render方法。你可以在第七章的中了解更多关于WinJS.Navigation.state属性和WinJS.UI.Pages.render方法的知识。
清单 24-12 。pages/savePickerView.html 文件的内容
`
#saveListView { width: 75%; height: 275px;}这个文件有两个部分。该标记是自包含的,并向用户提供有用的消息和单行图像缩略图,以显示相册中已有的内容。当收到 activated事件时,我使用这个标记作为应用布局,数据绑定和ListView控件向用户提供内容。你可以在图 24-13 中看到结果。(我通过重启PhotoAlbum应用,切换到FileHelper应用,点击Save按钮,这样我就可以从文件位置列表中选择PhotoAlbum了。)
***图 24-13。*向用户呈现适合提货人的内容
在这种情况下,您可以使用任何您喜欢的布局,只要它适合文件选取器的信箱区。你展示的任何东西都应该是有用的——这通常意味着给用户一些已经可用的指示。在我看来,对用户的价值优先于严格准确的内容视图。在图中,你会看到我已经决定显示相册中的图片,而不是只关注那些作为保存位置存储在应用中的图片。
配置选取器
在选择器中向用户呈现内容只是任务的一部分。我还必须配置选择器本身,并在用户单击保存按钮时做出响应。在清单 24-13 的中,我重复了来自savePickerView.html文件的代码来完成这两项工作。
清单 24-13 。savePickerView.html 文件中处理拣选器的代码
... ready: function (element, pickerUI) { pickerUI.title = "Save to Photo Album"; pickerUI.addEventListener("targetfilerequested", function (e) { var deferral = e.request.getDeferral(); storage.ApplicationData.current.localFolder .createFileAsync(pickerUI.fileName, storage.CreationCollisionOption.replaceExisting) .then(function (file) { e.request.targetFile = file; App.processFile(file); deferral.complete(); }); }); } ...
我在我的ready函数中接收的pickerUI变量是来自激活事件的detail.fileSavePickerUI属性的值。该属性返回一个Windows.Storage.Pickers.Provider.FileSavePickerUI对象,用于配置呈现给用户的选择器。FileSavePickerUI对象定义了表 24-3 中描述的属性。
在示例中,我使用了title属性来指定字符串Save to Photo Album,这可以在图 24-13 中看到。当您提供的存储存在某种层次结构时,此属性非常有用,因为您可以使用它来指示文件的保存位置。FileSavePickerUI对象还定义了两个事件,其中一个我在例子中使用了。您可以在表 24-4 中查看这些事件的详细信息。
我对这一章感兴趣的是targetfilerequested事件,因为它向应用发出用户想要保存文件的信号。事件的处理程序被传递了一个Windows.Storage.Pickers.Provider.targetFileRequestedEventArgs对象。这个对象只定义了一个名为request的属性,这是完成契约所需要的。请求属性返回一个Windows.Storage.Pickers.Provider.TargetFileRequest对象,该对象定义了表 24-5 中显示的属性。
一个简单的操作涉及到很多对象。当您接收到targetfilerequested事件时,您调用request.getDeferral方法来告诉 Windows 在您执行异步操作时等待。
然后,创建或获取将被传递给另一个应用的StorageFile对象,以便它可以写入其内容。在示例中,我在代表本地应用数据文件夹的StorageFolder对象上调用了createFileAsync方法(如第二十章所述)。对于文件名,我读的是FileSavePickerUI.fileName属性。将StorageFile赋给request.targetfile属性,然后调用之前调用getDeferral时获得的对象的 complete 方法——这告诉 Windows 您已经完成了,现在可以将StorageFile传递给想要保存数据的应用。
更新数据
完成契约的实现还需要一个步骤,这涉及到保持你的应用布局是最新的。
如果当用户选择它作为存储位置时,您的应用没有运行,则应用会启动并发送文件激活事件。一旦保存操作完成,应用就会终止。
但是,如果您的应用在用户选择它作为保存位置时正在运行,则会创建应用的第二个实例,具有完全独立的全局名称空间和变量。一旦保存操作完成,第二个实例就被终止,但是它留下了一个问题:您的应用的保持运行的实例如何发现新保存的文件?
你不能依靠视图模型来解决这个问题,因为视图模型是一个全局变量,应用的每个实例都有自己的副本。您必须使用某种共享存储来解决这个问题,通过它您可以发现文件。对于我的应用,这意味着我必须监控本地应用数据文件夹,并加载我在那里发现的任何新文件。你可以看到我是如何使用我在第二十二章和清单 24-14 中展示的文件夹监控技术做到这一点的,其中显示了我在PhotoAlbum应用中对default.js文件所做的添加。
清单 24-14 。监控本地应用数据文件夹的变化
`(function () {
var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation; var appstate = activation.ApplicationExecutionState; var storage = Windows.Storage;
** var query = storage.ApplicationData.current.localFolder** ** .createFolderQuery();** ** query.addEventListener("contentschanged", function () {** ** App.loadFilesFromCache();** ** });** ** query.getFoldersAsync();**
app.onactivated = function (args) {
if (args.detail.previousExecutionState != appstate.suspended) { args.setPromise(WinJS.UI.processAll().then(function () {
if (ViewModel.fileList.length == 0) { App.loadFilesFromCache(); }
switch (args.detail.kind) {
case activation.ActivationKind.fileSavePicker:
var pickerUI = args.detail.fileSavePickerUI;
WinJS.Navigation.navigate("/pages/savePickerView.html", pickerUI);
break;
case activation.ActivationKind.file:
args.detail.files.forEach(function (file) {
App.processFile(file);
});
default:
WinJS.Navigation.navigate("/pages/albumView.html");
break;
}
}));
}
};
app.start();
})();`
每当我接收到contentschanged事件时,我就调用App.loadFilesFromCache函数,该函数在/js/app.js文件中定义(在本章前面显示过)。有了这个功能,你可以将文件从FileHelper应用保存到PhotoAlbum应用,并立即看到它们出现。对于这个简单的应用来说,重新加载所有文件比找出新内容更容易。有了这个附加功能,我有了一个很好的保存选择器契约的实现,并且可以从其他应用接收和存储文件。
现在,当您从FileHelper应用保存文件时,您可以切换到PhotoAlbum应用,并看到您保存的图像显示在应用布局中。你可以在图 24-14 中看到这个效果,它显示了一个图像添加到应用布局中。
***图 24-14。*通过保存选取器契约将图像保存到示例应用
实施开放提货人契约
既然您已经看到了保存选取器契约,那么它的补充——开放选取器契约——通过比较就很容易理解了。与所有契约一样,首先向应用清单添加一个声明。从 Visual Studio 的Solution Explorer窗口打开package.appxmanifest文件,点击Declarations部分的标签。
从Available Declarations列表中选择File Open Picker,点击Add按钮。示例中我打算支持 JPG 和 PNG 文件,所以在现有的File type文本框中输入.jpg,点击Add New按钮,在新创建的File type文本框中输入.png。键入Control+S保存更改。清单应该类似于图 24-15 中所示的清单。
***图 24-15。*增加文件打开提货人契约声明
处理激活事件
当用户选择应用作为打开文件的位置时,Windows 会发送激活事件。激活事件的detail.kind属性被设置为ActivationKind.fileOpenPicker,就像处理保存选取器事件一样,这是改变应用布局的提示,以便它适合选取器并为用户提供一些有意义的内容。在清单 24-15 中,您可以看到我是如何响应PhotoAlbum应用中的激活事件的,它显示了我对default.html文件中的switch语句所做的更改。
清单 24-15 。响应 default.html 文件中的打开选取器激活事件
... switch (args.detail.kind) { case activation.ActivationKind.fileOpenPicker: ** var pickerUI = args.detail.fileOpenPickerUI;** ** WinJS.Navigation.navigate("/pages/openPickerView.html",** ** pickerUI);** break; case activation.ActivationKind.fileSavePicker: var pickerUI = args.detail.fileSavePickerUI; WinJS.Navigation.navigate("/pages/savePickerView.html", pickerUI); break; case activation.ActivationKind.file: args.detail.files.forEach(function (file) { App.processFile(file); }); default: WinJS.Navigation.navigate("/pages/albumView.html"); break; } ...
该契约的工作方式与保存选取器契约非常相似。该应用的一个新实例被启动并发送激活事件,布局被嵌入显示给用户的文件选取器中。
激活事件的detail.fileOpenPickerUI属性返回我需要管理文件打开过程的对象,所以我将它传递给 navigate 方法,请求用我添加到项目中的/pages/openPickerView.html文件填充布局,其内容可以在清单 24-16 中看到。
清单 24-16 。/pages/openPickerView.html 文件的内容
`
#openListView { width: 75%; height: 275px;}var previousSelection = [];
openListView.winControl.selectionMode = (pickerUI.selectionMode == provider.FileSelectionMode.single) ? WinJS.UI.SelectionMode.single : WinJS.UI.SelectionMode.multi;
openListView.addEventListener("selectionchanged", function (e) {
previousSelection.forEach(function (id) { pickerUI.removeFile(id); }); previousSelection.length = 0;
var newSelection = openListView.winControl.selection.getItems()
.then(function (items) {
items.forEach(function (item) {
pickerUI.addFile(item.data.file.path,
item.data.file);
previousSelection.push(item.data.file.path);
}); });
});
}
});
该文件定义的布局非常类似于我用于保存选择器契约的布局——一个WinJS.UI.ListView UI 控件显示相册中当前的图像。用户将选择文件来挑选它们。
要看到这种布局,您需要经历一系列特定的事件。首先,重启PhotoAlbum应用——open picker 应用不需要运行才能工作,但您需要重启应用,以便 Visual Studio 通知 Windows 您的应用已声明支持 open picker 合约。
现在切换到FileHelper应用,点击Open按钮显示一个打开的拾取器。点击位置标题旁边的箭头,你会看到PhotoAlbum被列为文件来源,如图图 24-16 所示。
***图 24-16。*显示为要打开的文件源的示例应用
如果选择列表中的PhotoAlbum项,将触发激活事件,显示新的布局,如图图 24-17 所示。
***图 24-17。*使用开放选取器契约打开文件
选择其中一个文件并点击Open按钮,您将看到您选择的图像显示在FileHelper应用中。
从PhotoAlbum应用的角度来看,激活事件的detail.fileOpenPickerUI属性返回的对象是一个Windows.Storage.Pickers.Provider.FileOpenPickerUI对象,它的工作方式与保存选取器契约的相应对象略有不同。为了演示它是如何工作的,我将分解对象并依次讨论它的属性、方法和事件。
由FileOpenPickerUI对象定义的属性显示在清单 24-6 中,它们用于获取显示给用户的拣选器信息,并设置一些基本的配置选项。
selectionMode属性对你呈现给用户的布局影响最大,因为它表明试图打开文件的应用是接受一个文件还是多个文件。该属性返回由Windows.Storage.Pickers.Provider.FileSelectionMode对象定义的值之一,我已经在表 24-7 中列出了这些值。
确保您在打开的文件选择器中呈现给用户的布局遵循FileSelectionMode值是很重要的,否则您会让用户选择更多可以打开的文件,或者在他们应该可以选择多个文件时将他们限制为一个,从而使用户感到困惑。
我在示例中使用了selectionMode属性来改变ListView控件的selectionMode属性。即使属性名称相同,定义值的对象却不同,因此我必须从一个对象映射到另一个对象,如下所示:
... openListView.winControl.selectionMode = (pickerUI.selectionMode == provider.FileSelectionMode.single) ? WinJS.UI.SelectionMode.single : WinJS.UI.SelectionMode.multi; ...
当用户选择或取消选择项目时,ListView控件触发selectionchanged事件,我使用由FileOpenPickerUI定义的方法来响应,以反映用户选择的文件。这些方法在表 24-8 中描述。
当调用addFile方法时,传递一个代表用户选择的StorageFile和一个代表文件的惟一 ID。然后,您可以使用此 ID 来检查该文件是否已经是选择的一部分,或者将其从选择中删除。没有方法来枚举选择器中的文件,这是一个问题,因为selectionchanged事件并没有指出选择中发生了什么变化。这意味着我必须在每次用户改变ListView选择时清除选择的文件,并为每个选择的项目添加新的条目,以确保我没有在选择器中留下任何已被取消选择的文件。
除了调用addFile方法之外,不需要任何显式操作。你传递给addFile的StorageFile对象被交给为用户打开文件的应用,所以你唯一的义务就是确保StorageFile对象与用户的选择相对应。通过监听FileOpenPickerUI定义的事件,你可以更深入地了解挑选过程,我已经在表 24-9 中描述了这些事件。
总结
在这一章中,我向你展示了如何实现三个关键契约:文件激活、保存选择器和打开选择器。这些契约允许您将应用集成到 Windows 中,以便您可以代表用户处理文件,并为其他应用提供存储服务。在下一章中,我将向您展示共享契约,这是 Windows 8 的一项关键功能。
二十五、共享契约
共享契约允许用户在应用之间共享数据项。这是 Windows 应用的关键功能之一,它允许用户使用互不了解且仅共享相同数据格式的应用创建临时工作流。
需要两个应用参与共享契约。作为共享源的应用拥有用户想要共享的一项或多项数据。Windows 向用户提供了一个应用列表,称为共享目标,能够处理这些数据,用户选择他们想要从共享源接收项目的应用。在这一章中,我将向您展示如何在契约中创建两个参与者,并演示如何简化重复的操作,使用户的共享更简单。表 25-1 对本章进行了总结。
创建示例应用
我将从创建一个共享源开始,这是一个提供数据供另一个应用使用的应用。我创建了一个简单的助手应用,名为ShareHelper。我将介绍不支持共享的基本应用,然后展示如何实现共享源代码功能。清单 25-1 显示了来自ShareHelper应用的default.html文件。
清单 25-1 。来自 ShareHelper 应用的 default.html 文件
`
ShareHelper **这个应用的布局由一个大的ListView控件组成,使用了我在最近的其他例子中使用的相同类型的项目模板。这里的想法是向用户提供一组图像,他们可以从中进行选择,然后使用共享契约与PhotoAlbum应用共享这些图像,然后将共享的图像添加到相册中。
有了这样一个简单的布局,ShareHelper应用所需的 CSS 主要集中在模板中的元素上,正如你在清单 25-2 中看到的,它显示了css/default.css文件的内容。
清单 25-2 。来自 ShareHelper 应用的 css/default.css 文件
`body { display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-pack: center;}
#listView { width: 100%; height: 100%; padding: 10px;}
.imgContainer { border: thin solid white; padding: 2px; }
.listTitle { font-size: 18pt; max-width: 180px;
text-overflow: ellipsis; display: block; white-space: nowrap;
margin: 0 0 5px 5px; height: 35px;} .listImg {width: 300px; height: 200px;} `
创建助手应用的下一步是加载一些图像并填充ListView控件。你可以在清单 25-3 中看到我是如何做的,它显示了js/default.js文件。我对Pictures库进行了深度查询,并显示了我在那里找到的所有图像文件。
清单 25-3 。ShareHelper 应用中 default.js 文件的内容
`(function () {
var app = WinJS.Application; var activation = Windows.ApplicationModel.Activation; var appstate = activation.ApplicationExecutionState; var storage = Windows.Storage;
app.onactivated = function (args) { if (args.detail.previousExecutionState != appstate.suspended) { args.setPromise(WinJS.UI.processAll().then(function () { var list = new WinJS.Binding.List(); listView.winControl.itemDataSource = list.dataSource;
storage.KnownFolders.picturesLibrary .getFilesAsync(storage.Search.CommonFileQuery.orderByName) .then(function (files) { files.forEach(function (file) { list.unshift({ img: URL.createObjectURL(file), title: file.displayName, file: file }); }); }); })); } }; app.start(); })();`
这是一个基本的应用,我以简洁的名义做了一些假设,其中最重要的是我假设Pictures库中的所有文件都是图像文件。
创建助手应用的最后一步是在清单中声明需要访问Pictures库。打开package.appxmanifest文件,导航到Capabilities部分,勾选Pictures Library选项,如图图 25-1 所示。
***图 25-1。*声明访问图片库
如果你启动应用,你会看到你的Pictures文件夹中的图片显示在ListView中,如图图 25-2 所示。
***图 25-1。*share helper app 的基本功能
创建共享源
尽管共享源是共享契约的关键部分,但不会对应用清单进行任何更改。相反,当应用启动时,一切都在 JavaScript 中处理。为了演示这一点,我对清单 25-4 中显示的ShareHelper应用中的/js/default.js文件进行了修改。
***清单 25-4。*在 ShareHelper 应用中创建共享源
`(function () {
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var appstate = activation.ApplicationExecutionState;
var storage = Windows.Storage;
var share = Windows.ApplicationModel.DataTransfer; app.onactivated = function (args) {
if (args.detail.previousExecutionState != appstate.suspended) {
args.setPromise(WinJS.UI.processAll().then(function () {
var list = new WinJS.Binding.List();
listView.winControl.itemDataSource = list.dataSource;
storage.KnownFolders.picturesLibrary .getFilesAsync(storage.Search.CommonFileQuery.orderByName) .then(function (files) { files.forEach(function (file) { list.unshift({ img: URL.createObjectURL(file), title: file.displayName, file: file }); }); });
** share.DataTransferManager.getForCurrentView()** ** .addEventListener("datarequested", function (e) {**
** var deferral = e.request.getDeferral();**
** listView.winControl.selection.getItems().then(function (items) {** ** if (items.length > 0) {** ** var datapackage = e.request.data;**
** var files = [];** ** items.forEach(function (item) {** ** files.push(item.data.file);** ** });** ** datapackage.setStorageItems(files);**
** datapackage.setUri(new Windows.Foundation.Uri(** ** "apress.com"));**
** datapackage.properties.title = "Share Images";** ** datapackage.properties.description** ** = "Images from the Pictures Library";** ** datapackage.properties.applicationName = "ShareHelper";** ** } else {** ** e.request.failWithDisplayText(** ** "Select the images you want to share and try again");** ** }** ** });** ** deferral.complete();** ** });** })); } }; app.start(); })();`
我在清单中突出显示的代码做了两件事:它将应用注册为共享数据源,并在用户激活 Share Charm 时做出响应。我将在接下来的部分中解释每个活动。
注册为共享源
告诉 Windows 您的应用是共享数据的来源所需的技术不需要任何清单声明,也不是通过激活事件来完成的。相反,您必须处理位于Windows.ApplicationModel.DataTransfer名称空间中的DataTransferManager对象。(在示例中,我将这个名称空间别名为share。)对象DataTransferManager定义了我在表 25-2 中描述的事件。
注意在本章中,我引入的所有新对象都在
Windows.ApplicationModel.DataTransfer名称空间中,除非我另有说明。
要为这些事件注册一个处理函数,必须对由DataTransferManager.getForCurrentView方法返回的对象调用addEventListener方法。(如果你是一名. NET 程序员,这是另一个其结构更有意义的对象,但是只要你记得调用getForCurrentView,它在 JavaScript 中工作得非常好。)
对事件做出反应
我对本例中的datarequested事件感兴趣,该事件在用户激活 Windows Share Charm 时触发,提示我准备要共享的数据。传递给datarequested事件处理程序的对象有点奇怪,因为request属性,而不是detail属性,包含服务于共享操作所需的信息。
request属性返回一个DataRequest对象,用于创建一个数据包,该数据包将与另一个应用共享。我总结了表 25-3 中DataRequest对象定义的方法和属性。
当您接收到datarequested事件时,您需要做的第一件事是调用getDeferral方法,如果您将执行任何异步方法调用的话。如果你不调用getDeferral,那么当你的处理函数执行完成时,Windows 会认为你没有提供任何数据。
getDeferral方法返回一个定义了complete方法的DataRequestDeferral对象。这是DataRequestDeferral定义的唯一方法,当您创建了数据包并准备好呈现给用户时,您可以调用它。
对于我的示例应用,我已经定义了一些代码来创建事件处理程序的概要,如清单 25-5 中的所示。
清单 25-5 。处理 datarequested 对象
`... share.DataTransferManager.getForCurrentView().addEventListener("datarequested", function (e) {
** var deferral = e.request.getDeferral();**
listView.winControl.selection.getItems().then(function (items) { if (items.length > 0) { // ...statements to prepare shared data go here... } else { ** e.request.failWithDisplayText(** ** "Select the images you want to share and try again");** } }); ** deferral.complete();** }); ...`
我调用getDeferral方法是因为我需要使用异步getItems方法从ListView控件中获取选择,如果用户没有选择任何要共享的图像,我调用failWithDisplayText方法。如果用户在没有选择ListView控件中的任何图像的情况下激活了 Share Charm,他们将会看到传递给failWithDisplayText方法的消息。这是将应用设置为共享数据提供者的基本模式,将数据的准备和打包工作留给自己。我将在下一节向您展示如何做到这一点。
打包共享数据
如果您的应用有可以共享的数据,那么您必须填充从DataRequest.data属性获得的DataPackage对象,以便可以定位共享目标并传递您的数据。通过读取传递给datarequested事件处理函数的对象的request属性来获得DataRequest对象。
DataPackage非常灵活,可以用来共享各种数据。我在表 25-4 中总结了向DataPackage添加数据的最有用的方法。
您可以使用表中所示的方法将数据添加到将与其他应用共享的包中。您的包可以包含不同类型的数据,但是如果您两次调用相同的方法,则第二次传递的数据将替换包中已有的数据。
提示你不必事先指定你的数据包的内容——你可以在
datarequested事件到来的那一刻决定。这意味着你可以基于你的应用的状态共享不同种类的数据——例如,当用户在ListView控件中选择单个图像时,我可能会共享图像文件,如果选择了多个文件,我可能会共享文件名列表,如果用户导航到应用的不同部分,我可能会完全共享其他内容。共享契约的灵活性很大一部分来自于能够根据用户正在执行的任务选择共享的内容。
你可以在清单 25-6 中看到我如何使用setStorageItems方法将文件添加到数据包中,对应于用户在ListView中选择的图像。我还使用了setUri方法为用户提供一个链接,他们可以通过这个链接获得更多信息——在我的例子中,我使用了 URL apress.com作为占位符。
清单 25-6 。打包数据以供共享
`... share.DataTransferManager.getForCurrentView() .addEventListener("datarequested", function (e) {
var deferral = e.request.getDeferral();
listView.winControl.selection.getItems().then(function (items) { if (items.length > 0) { var datapackage = e.request.data;
** var files = [];**
** items.forEach(function (item) {**
** files.push(item.data.file);**
** }); datapackage.setStorageItems(files);**
** datapackage.setUri(new Windows.Foundation.Uri("apress.com"));**
datapackage.properties.title = "Share Images"; datapackage.properties.description = "Images from the Pictures Library"; datapackage.properties.applicationName = "ShareHelper";
} else { e.request.failWithDisplayText( "Select the images you want to share and try again"); } }); deferral.complete(); }); ...`
注我在这个数据包上使用
setUri方法的方式上故意引入了一个问题。在这一章的后面,我会回来解释这个问题以及为什么会经常遇到这个问题——所以,在你阅读完这一章的其余部分之前,不要使用setUri方法。
描述数据
DataPackage对象定义了一个名为properties,的属性,该属性返回一个DataPackagePropertySet对象(由于术语属性出现得如此频繁,本节中的对象和成员名称可能有点令人困惑)。
您使用DataPackagePropertySet来提供关于共享操作及其数据的附加信息,我已经在表 25-5 中描述了该对象定义的属性。
这些属性不是为您设置的,尽管其中一些属性——如applicationListUri和fileTypes——看起来应该设置。另一方面,您不需要为这些属性中的任何一个提供值,所以您只需要处理那些有助于用户理解共享操作的属性。
在我的例子中,我已经为其中三个属性设置了值,如清单 25-7 所示。当我在本章稍后将PhotoAlbum应用设为共享目标时,我将使用这些值来显示共享操作的细节,在下一节中您可以看到 Windows 是如何使用它们的。
清单 25-7 。用属性描述数据包
... datapackage.properties.title = "Share Images"; datapackage.properties.description = "Images from the Pictures Library"; datapackage.properties.applicationName = "ShareHelper"; ...
提示您也可以使用
insert方法向数据包添加自定义属性。在本章后面的添加自定义属性部分,我将向您展示一个这样的例子。
测试共享源代码应用
为了测试数据共享,使用 Visual Studio Debug菜单中的Start Debugging项启动ShareHelper应用,并在ListView中选择一个或多个图像。
打开魅力条,选择分享魅力(可以直接用Win + H激活分享魅力)。您将看到共享弹出按钮,其中包含可以处理由ShareHelper应用准备的数据包的应用列表,如图图 25-3 所示。
***图 25-3。*挑选应用的哪些数据将与哪些数据共享
我已经展开了共享弹出按钮的一部分,以便您可以看到我为数据包指定的属性的效果,以及它们是如何显示给用户来描述数据的。
图中有三个 app 可以从ShareHelper app 接收数据:Mail、People和SkyDrive。如果你在系统上安装了其他应用,你可能会看到其他条目,但请注意,列出的所有应用都是 Windows 应用商店应用,共享契约仅适用于应用,不包括传统的 Windows 桌面程序。
了解常见的共享问题
Windows 根据创建共享数据包时调用的set <XXX> 方法选择处理共享数据的应用。我调用了setStorageItems和setUri,所以 Windows 正在寻找可以处理文件或 URL 的应用。
关键词是或——应用只需要声明支持包中任何一种类型的数据,就可以成为合适的共享目标。应用可以自由地从共享包中获取它们支持的数据,而忽略其他任何东西。
在内置 Windows 应用的情况下,Mail应用(一个电子邮件客户端)将使用文件作为新电子邮件消息的附件,People应用(一个社交媒体工具)将与我的社交媒体联系人共享 URL,SkyDrive应用将图像文件保存到云存储中。
这是我在清单 25-6 中制造的问题。我使用setUri向包中添加了一个 URL,它向用户提供了一些额外的信息,但与用户试图共享的内容没有直接关系。然而,如果用户选择People或Mail应用作为分享目标,那么图像文件——真正的内容——将被丢弃,链接将被给予比它应有的更重要的对待。用户的联系人将被发送一个 URL,而没有用户试图共享的图像所提供的任何上下文——这对用户和消息的接收者来说是一个非常混乱的结果,可悲的是,这是共享数据时常见的错误。
如果您从“共享”弹出菜单中选择Mail应用,您就可以发现问题。Mail应用将显示在一个小的弹出菜单中,允许你通过选择收件人和添加一些文本来完成电子邮件,如图图 25-4 所示。
提示您需要在本地机器上执行这个测试,因为
Mail应用无法在 Visual Studio 模拟器中正常启动。
***图 25-4。*与邮件应用分享数据
用户试图分享的图片文件被忽略了,更糟糕的是,邮件应用找到了网址,找到了其中包含的图片,并以 FlipView 的形式呈现给用户。这只是简单的混淆,因为用户选择了图像文件,但现在提供了完全不同的图像选择,这些图像是从我的示例应用悄悄添加到数据包的 URL 获得的。
回避问题
为了避免这个问题,确保添加到包中的每种类型的数据都是独立的,并且对用户有价值,这一点很重要。我的偏好是,仅当多种类型的数据等效时,才将它们添加到包中,例如,如果我正在共享本章的文本,我可能会添加 RTF 格式的手稿、纯文本等效内容和 SkyDrive 存储上内容的 URL。
在解决如何共享数据的问题时,我的基本想法是问自己,用户期望会发生什么?如果您不能立即将数据包中的项目与用户的合理预期相关联,您应该重新查看您的数据包内容。
如果您确实需要向数据包添加补充信息以支持用户选择的数据,您应该定义一个自定义属性。自定义属性不用于选择合适的共享目标应用,任何不知道其重要性的应用都可以忽略它们。您可以通过使用DataPackagePropertySet.insert方法向数据包添加一个自定义属性,传递您想要赋予该属性的名称及其值。在清单 25-8 的中,您可以看到我是如何用一个名为referenceURL的自定义属性替换了setUri方法的。
清单 25-8 。使用插入方法添加自定义属性
`... var datapackage = e.request.data;
var files = []; items.forEach(function (item) { files.push(item.data.file); }); datapackage.setStorageItems(files);
// This statement is now commented out **//**datapackage.setUri(new Windows.Foundation.Uri("apress.com"));
datapackage.properties.title = "Share Images"; datapackage.properties.description = "Images from the Pictures Library"; datapackage.properties.applicationName = "ShareHelper"; datapackage.properties.insert("referenceURL", "apress.com"); ...`
如果你重启ShareHelper应用,在ListView中选择一些图像,并再次激活共享魔咒,你将在目标列表中只看到Mail和SkyDrive应用——这是因为People应用不能处理共享数据包中的文件,因此不会被 Windows 选为数据的目标。如果您选择Mail应用,您会看到用户选择的图像文件已经作为附件添加到消息中,如图图 25-5 所示。
***图 25-5。*用邮件应用分享文件
创建共享目标
在这一部分,我将通过把我在第二十四章的中创建的PhotoAlbum应用变成一个共享目标并允许它接收包含文件的数据包来展示共享契约的另一面。
提醒一下,PhotoAlbum应用的基本功能允许用户选择要在简单相册中显示的图像文件,该相册显示为在WinJS.UI.ListView控件中显示的一系列缩略图。
在前一章中,我在此基础上实现了文件激活、保存选取器和打开选取器合约,展示了应用集成到操作系统的不同方式——这是我将在本章分享合约中继续讨论的主题。提醒一下,图 25-6 显示了PhotoAlbum显示图像时的样子。
***图 25-6。*相册示例 app
我不打算重新列出示例应用的代码和标记,因为你可以在第二十四章中找到它们,或者从Apress.com下载该项目作为源代码包的一部分。我知道不得不前后翻页到另一章会令人沮丧,但另一种选择是花 10 页列出你已经看过的代码,我宁愿用这些空间向你展示新的契约和特性。
更新清单
与共享源不同,共享目标必须在清单中声明它们的功能。这是有意义的,因为共享源需要自由地共享用户正在处理的任何数据,而共享目标将有预先确定的方式来处理一组数据类型。
对于这一章,我将增强PhotoAlbum应用,使其能够处理包含.jpg和.png文件的数据包。为此,从 Visual Studio 的Solution Explorer窗口打开package.appxmanifest文件,并导航到Declarations部分。从Available Declarations列表中选择Share Target并点击Add按钮。与其他契约一样,显示附加细节的表格,如图图 25-7 所示。显示红色警告是因为我还没有填充表单。
***图 25-7。*将份额目标申报添加到清单中
声明数据格式
你可以用两种方式之一来表达你对共享目标的支持。第一种是使用数据格式,这可以通过单击清单表单的Data formats部分中的Add New按钮来完成。当你点击这个按钮时,你会看到一个Data format字段,你可以在其中输入你的应用支持的数据格式。
Data format字段接受与DataPackage对象中的方法相对应的值,我在本章前面已经描述过了。表 25-6 提供了共享源可以使用的DataPackage对象中的方法和相应的Data format值之间的快速参考,共享目标必须在清单中声明这些值才能接收那种包。
提示请记住,如果正在共享的数据包包含至少一种您声明的类型,Windows 会将您的应用包括在呈现给用户的共享目标列表中。您不需要声明
Data format值的精确组合来接收包含多种类型的包。同样,你不应该声明支持任何你的应用不能使用的数据类型。
如果您想支持多种类型的数据,比如文件和 URL,再次点击Add New创建额外的Data format字段,并从表格中输入适当的值。完成后,键入Control+S保存清单更改。
声明文件类型支持
您还可以通过使用清单的Supported file types section指定单个文件类型来声明对共享目标联系人的支持。这允许您支持包含您的应用可以处理的文件类型的数据包,而不是任何文件(这是StorageItems数据格式的效果)。
区别很重要。例如,Mail应用想要处理文件,但它并不关心它们是什么,因为每个文件都适合作为电子邮件的附件。我需要对PhotoAlbum应用更有选择性,因为我只支持图像文件——例如,我无法在相册应用中使用 Excel 电子表格,因此我需要确保我的应用只是 PNG 和 JPG 文件的共享目标。
提示我将向您展示清单声明的两个部分是如何分别工作的,但是您可以在同一个声明中使用这两个部分来支持数据类型和文件类型的混合。您必须为此联系人指定至少一种文件类型或数据格式,否则您可以随意组合。
要为PhotoAlbum应用设置清单声明,请单击Add New按钮创建一个新的File type字段,并输入.png文件扩展名(包括句点)。再次点击Add New按钮,在File type字段输入.jpg。清单应该类似于图 25-8 中所示的清单。当您添加了第一种文件类型后,红色警告标记将会消失,告诉您清单声明是有效的。
***图 25-1。*在相册清单中增加对特定文件类型的支持
如果数据包包含至少一个与您指定的类型相匹配的文件,Windows 会将您的应用添加到共享目标列表中。这意味着您可能会收到包含一些您无法处理的文件的包——您有责任在包中找到您可以处理的文件,并忽略其余的文件。(你可以在本章后面看到我是如何做到这一点的。)
响应激活事件
当您的应用被选为数据包的共享目标时,Windows 会使用activated事件通知您。激活事件的detail.kind属性设置为ActivationKind.shareTarget。在清单 25-9 中,您可以看到我在PhotoAlbum项目中对/js/default.js文件的添加,以响应这一事件。
清单 25-9 。响应被激活的事件
... switch (args.detail.kind) { ** case activation.ActivationKind.shareTarget:** ** WinJS.Navigation.navigate("/pages/shareTargetView.html",** ** args.detail.shareOperation);** ** break;** // *... statements for other activation types removed for brevity...* default: WinJS.Navigation.navigate("/pages/albumView.html"); break; } ...
args.detail.shareOperation属性返回一个Windows.ApplicationModel.DataTransfer.ShareTarget.ShareOperation对象,该对象提供对数据包的访问,并提供一些方法,通过这些方法,我可以在处理数据包时向 Windows 发送进度信号。我将ShareOperation对象传递给WinJS.Navigation.navigate方法,以便它可以在我创建的用于处理shareTarget激活事件的内容文件中使用,该文件名为shareTargetView.html,我将它添加到了PhotoAlbum Visual Studio 项目的pages文件夹中。在接下来的小节中,我将向您展示这个文件的内容。
注意需要注意的重要一点是,你的应用的一个新实例被启动来处理
shareTarget事件。这意味着如果您的应用已经在运行,那么将会创建第二个实例。您将需要提供某种协调,以便用户启动的应用实例反映 Windows 启动的实例所做的更新,以处理共享数据包。对于这个例子,我将把用户共享的文件复制到本地应用数据文件夹中,PhotoAlbum应用将监视这个文件夹的变化。
处理共享操作
在清单 25-10 中,您可以看到shareTargetView.html文件的内容,我创建这个文件是为了给PhotoAlbum应用添加对共享目标契约的支持。script元素中的两个关键函数是占位符,我将在本节稍后完成。
清单 25-10 。shareTargetView.html 文件的最初内容
`
body {background-color: #303030;} #shareListView { width: 90%; height: 275px; border: medium solid white; margin: 20px 10px;} .addButton { font-size: 18pt; margin: 10px; width: 175px} .titleSmall { font-size: 20pt;}** function copySelectedFiles(files) {** ** // ...this function will return a Promise that is fulfilled when** ** // ...all of the shared image files have been copied** ** }**
WinJS.UI.Pages.define("/pages/shareTargetView.html", { ready: function (element, shareOperation) {
processPackage(shareOperation.data).then(function (list) { if (list.length == 0) { shareOperation.reportError("No images files were shared"); return; }
shareOperation.reportStarted();
shareListView.winControl.itemDataSource = list.dataSource;
WinJS.Utilities.query("button.addButton").listen("click", function (e) {
if (this.id == "addAll") {
shareListView.winControl.selection.selectAll();
}
var filesToProcess = [];
shareListView.winControl.selection.getItems() .then(function (items) {
items.forEach(function (item) {
filesToProcess.push(item.data.file);
});
});
copySelectedFiles(filesToProcess).then(function () { shareOperation.reportDataRetrieved(); shareOperation.reportCompleted(); });; }); }); } });
即使有不完整的函数,这个文件中仍然有一些重要的事情在进行。对我来说,解释它们的最好方式是向你展示完成后的页面是什么样子,然后再返回。图 25-9 显示了 Windows 用来处理共享数据包的shareTargetView.html文件。
注意此时您将无法重新创建这个图,因为 default.js 文件中的关键函数尚未实现。
***图 25-9。*相册 app 收到分享数据包
Windows 在 650 像素的窗格中显示共享目标应用(其中 645 像素可供应用使用)。对于我的示例应用,我使用的布局包含一些标题信息(我将使用共享数据包中的属性填充)、一个ListView(我将使用数据包中的StorageFile对象填充)和两个button元素。
这些按钮允许用户进一步细化他们对包中文件的选择。点击Add All按钮会将数据包中的所有文件复制到本地 app data 文件夹中,并添加到相册中。点击Add Selected按钮将仅对用户在ListView中选择的图像进行操作。
我喜欢为用户提供进一步过滤数据包内容的选项,因为我对数据来自的应用一无所知。这在分享过程中创造了一个额外的步骤,这是一件坏事,但许多应用似乎不会让用户在分享前微调他们的选择,这更糟糕。我通过支持快速链接特性来证明向流程中添加额外步骤是正确的,我将在本章的后面对此进行描述。
报告进度
ShareOperation对象定义了许多方法,当你处理数据包时,这些方法用来通知窗口,如表 25-7 中所述。
你可以看到我是如何在清单 25-11 中的shareTargetView.html页面的ready函数中使用这些方法的,在这里我重复了代码并突出显示了关键语句。
清单 25-11 。处理共享包时使用 ShareOperation 方法
`... ready: function (element, shareOperation) {
processPackage(shareOperation.data).then(function (list) { if (list.length == 0) { ** shareOperation.reportError("No images files were shared");** return; }
shareListView.winControl.itemDataSource = list.dataSource; WinJS.Utilities.query("button.addButton").listen("click", function (e) { ** shareOperation.reportStarted();** if (this.id == "addAll") { ** shareListView.winControl.selection.selectAll();** } var filesToProcess = []; shareListView.winControl.selection.getItems().then(function (items) { items.forEach(function (item) { filesToProcess.push(item.data.file); }); });
copySelectedFiles(filesToProcess).then(function () { ** shareOperation.reportDataRetrieved();** ** shareOperation.reportCompleted();** });; }); }); } ...`
只有当用户不再需要与你的应用交互时,你才应该调用reportStarted方法——这是因为 Windows 可能会关闭你的应用,并允许共享操作在后台继续,从而允许用户继续使用共享源应用。
对于我的例子来说,这意味着我不能调用reportStarted,直到用户点击其中一个按钮,表明他们想要导入哪些文件。我一解析完包中的内容就调用reportStarted方法,并在将内容复制到本地 app data 文件夹后调用reportDataRetrieved和reportCompleted方法。
如果我从processPackage函数得到的WinJS.Binding.List对象不包含任何我可以操作的文件,我就调用reportError方法。在理想情况下,我不需要这样做,因为 Windows 已经将包的内容与清单声明中的文件类型进行了匹配,但我将此检查添加到我的共享目标应用中,以处理编写得不好的应用。一些共享源应用错误地设置了数据包中fileTypes属性的值(如本章前面所述)——该值会覆盖真正正在使用的文件类型,这会导致 Windows 发送不包含任何有用文件的我的应用包。
注意错误呈现给用户的方式有点奇怪。调用
reportError方法时,共享目标 app 立即关闭。然后向用户显示一条通知消息,告诉他们出现了一个问题。只有当他们点击通知时,他们才能看到您传递给reportError方法的消息。
处理数据包
ShareOperation.data属性返回一个DataPackageView对象,它是数据包的只读版本。您使用这个对象来了解发送给您的包,并获取其中包含的数据。表 25-8 显示了DataPackageView对象定义的方法。
从包中检索数据的方法都是异步的,并返回一个WinJS.Promise对象,该对象在完成时产生适当类型的数据。
contains方法让您检查包是否包含给定的数据类型。你可以将表 25-6 中的一个字符串值传递给这个方法,或者使用StandardDataFormats对象中的一个值,我已经在表 25-9 中列出了。
你可以在清单 25-12 中看到我是如何使用contains和getStorageItemsAsync方法的,它展示了我是如何在shareTargetView.html文件中实现processPackage函数的。
清单 25-12 。完成 processPackage 方法
... function processPackage(data) { if (**data.contains(share.StandardDataFormats.storageItems)**) { return **data.getStorageItemsAsync()**.then(function (files) { var fileList = new WinJS.Binding.List(); files.forEach(function (file) { if (file.fileType == ".jpg" || file.fileType == ".png") { fileList.unshift({ img: URL.createObjectURL(file), title: file.displayName, file: file }); } }); appName.innerText = data.properties.applicationName; shareTitle.innerText = data.properties.title; var refLink = data.properties["referenceURL"] infoAnchor.innerText = infoAnchor.href = refLink == null ? "N/A" : refLink; return fileList; }); }; } ...
我首先使用contains方法来确保数据包中有文件或文件夹——这是我在示例应用中支持的唯一一种数据,否则没有必要进一步处理数据包。
我调用getStorageItemsAsync方法并检查传递给then方法的每个对象的fileType属性,这允许我过滤掉错误类型的文件夹和文件。我为我找到的每个图像文件添加一个对象到一个WinJS.Binding.List中,该对象的属性意味着我可以使用 default.html 文件中的 HTML 模板在ListView UI 控件中显示它(这是我在第二十四章的中用于所有PhotoAlbum示例的相同模板)。
DataPackageView.properties属性返回一个对象,您可以使用该对象获取由共享源应用添加到数据包中的属性。我读取了applicationName和 title属性的值,并检查了ShareHelper应用添加到包中的自定义属性是否存在。我使用这些值来设置布局中一些 HTML 元素的内容。
提示在告诉 Windows 你已经完成了共享操作之前,你必须小心确保你的异步方法调用已经完成。在这个例子中,我通过从
processPackage函数返回一个Promise来完成这个任务,只有当我接收并处理了来自getStorageItemsAsync方法的结果时,这个任务才会完成。
复制数据
剩下的工作就是将数据从共享包复制到本地应用数据文件夹,我通过实现copySelectedFiles函数来完成,如清单 25-13 所示。
清单 25-13 。实现 copySelectedFiles 函数
... **function copySelectedFiles(files) {** ** var promises = []**; ** files.forEach(function (file) {;** ** var promise = localFolder.createFileAsync(file.name,** ** storage.CreationCollisionOption.replaceExisting)** ** .then(function (newfile) {** ** return file.copyAndReplaceAsync(newfile).then(function () {** ** App.processFile(newfile);** ** });** ** });** ** promises.push(promise);** ** });** ** return WinJS.Promise.join(promises)** **}** ...
这个函数中没有新的技术——你可以在第二十二章的中了解基本的文件操作,并在第九章的中了解如何使用Promise.join方法。completed copySelectedFiles函数将用户从数据包中选择的所有文件复制到本地 app data 文件夹中,并返回一个Promise,只有当所有复制操作都完成时,该函数才会完成(我这样做是为了确保在我知道已经完成了数据包的内容之前不会调用ShareOperation方法)。
我复制文件而不是使用我在数据包中收到的位置有两个原因。首先,如果有另一个示例应用正在运行,我可以更容易地确保相册中的新内容得到反映。这是我在第二十四章中实现应用到应用选取器契约时遇到的相同问题,我已经用相同的方式解决了它——通过将我正在处理的文件复制到一个位置,该位置由PhotoAlbum应用监控新文件。我承认这是一个小技巧,但是 Windows 同时创建你的应用的两个实例的情况很少,尽管我可能会尝试,但我无法找到更好的方法在它们之间进行通信。我复制文件的第二个原因是,当共享操作结束时,我不知道共享源应用打算如何处理它们。我需要制作一个副本,以确保用户可以使用这些图像。
测试共享目标实现情况
共享目标契约的实施已经完成,现在您可以与PhotoAlbum应用共享图像文件。你已经知道如何用ShareHelper应用来做这件事,但是分享契约的一个好处是你可以从任何应用接收兼容的数据包。为此,转到桌面并使用File Explorer找到一个在Pictures库之外的图像文件。
右键单击该文件,从弹出菜单中选择Open with,并从列表中选择Photos(这是 Windows 8 附带的默认图像查看器应用,从桌面选择它意味着您不必担心图像文件类型的默认应用,如果您遵循第二十四章中的中的示例,它很可能是PhotoAlbum应用)。
激活分享魔咒,从目标应用列表中选择Photo Album。你会看到来自shareTargetView.html文件的布局,如图图 25-10 所示。点击Add All按钮,启动PhotoAlbum应用——你会看到你分享的图片显示出来。
***图 25-10。*分享来自 Windows Photos 应用的图片文件
在我继续之前,还有最后一点需要注意。我让您在Pictures库之外定位一个图像文件的原因是,我想演示打包StorageFile对象的方式将访问这些文件的隐含权限转移到目标应用。一切都按照它应该的方式运行——例如,您不必担心确保目标应用已被授予读取包含该文件的文件夹的权限。
当然,有了这种隐含的许可,就隐含了对您的信任,即您不会对数据做一些意想不到的事情。您应该确保在没有获得用户明确许可的情况下,不要删除或修改原始文件。
创建快速链接
快速链接是一个预先配置的动作,允许用户使用他们之前做出的细节或决定来执行简化的共享动作。在我的PhotoAlbum应用中,我让用户选择他们想要复制和导入的数据包中的图像。每次用户与PhotoAlbum共享文件时,强迫用户做出相同的决定是重复和令人讨厌的,尤其是因为他们很有可能已经花时间在 share source 应用中选择了他们想要的图像。
为了简化我的应用,我将创建一个快速链接,允许用户导入所有文件,而无需与我的应用布局进行任何交互。使用快速链接有两个阶段——创建它们和接收它们——我将在接下来的小节中向您展示这两个阶段。
创建快速链接
您可以通过向ShareOperation.reportCompleted方法传递一个QuickLink对象来创建一个快速链接,这个对象可以在Windows.ApplicationModel.DataTransfer.ShareTarget名称空间中找到。配置QuickLink对象,使其包含应用重复用户刚刚执行的共享操作所需的所有信息。QuickLink对象定义了表 25-10 中所示的属性。
你可以在清单 25-14 中看到我如何为PhotoAlbum应用创建了一个QuickLink,它显示了我对shareTargetView.html文件中的ready函数所做的更改。
清单 25-14 。在共享操作结束时创建快速链接
`... WinJS.Utilities.query("button.addButton").listen("click", function (e) {
if (this.id == "addAll") {
shareListView.winControl.selection.selectAll();
}
var filesToProcess = [];
shareListView.winControl.selection.getItems()
.then(function (items) {
items.forEach(function (item) {
filesToProcess.push(item.data.file);
});
}); copySelectedFiles(filesToProcess).then(function () {
shareOperation.reportDataRetrieved();
if (e.target.id == "addAll") { ** var qlink = new share.ShareTarget.QuickLink();** ** qlink.id = "all";** ** qlink.supportedFileTypes.replaceAll([".png", ".jpg"]);** ** qlink.title = "Add all files";** ** qlink.thumbnail = storage.Streams.RandomAccessStreamReference.** ** createFromUri(Windows.Foundation.Uri("ms-appx:img/logo.png"));** ** shareOperation.reportCompleted(qlink);**
} else { shareOperation.reportCompleted(); } }); }); ...`
我想创建一个快速链接,只有当用户从数据包中选择了所有的图像,因为没有办法,我可以在未来有效地重复选择单个图像。其他种类的应用可以明智地提供一系列快速链接——例如,如果你正在编写一个电子邮件应用,你可能会创建QuickLink对象,以便用户可以快速发送电子邮件给以前的收件人。
您可以看到这些变化的效果,但这需要一段时间。启动PhotoAlbum应用(确保运行最新版本)。然后启动ShareHelper应用,使用ListView控制键选择图像,激活Share Charm,从列表中选择PhotoAlbum。点击Add All按钮。
保持在ShareHelper应用内,再次激活分享图标,你会在分享弹出菜单上看到一个新项目,如图图 25-11 所示。这是一个快速链接,在本例中,它允许用户在一个步骤中添加他们选择共享的所有文件。我还没有在PhotoAlbum应用中添加代码来处理快速链接——我将在下一节中做这件事——但是图中显示了快速链接是如何呈现给用户的。
***图 25-11。*分享弹出菜单中增加了一个快速链接
简单回顾一下,快速链接用于允许用户重复经常执行的共享操作。这就是为什么我让你激活两次分享魔咒:第一次定义了操作,第二次显示为快速链接。
选择快速链接目前只是加载标准的共享目标布局,但在下一节中,我将向您展示如何识别快速链接何时被选择,以便您可以简化共享操作。
接收快速链接
当你的应用被激活时,你可以通过读取ShareOperation.quickLinkId属性来确定用户是否选择了你的一个快速链接。该属性返回的值是您分配给QuickLink.id属性的值,允许您确定用户想要重复哪个共享操作。我的示例应用只有一个快速链接 id——all——你可以在清单 25-15 的中看到我是如何响应用户选择它的。
清单 25-15 。检测用户何时选择了快速链接
`... ready: function (element, shareOperation) {
processPackage(shareOperation.data).then(function (list) { if (list.length == 0) { shareOperation.reportError("No images files were shared"); return; ** } else if (shareOperation.quickLinkId == "all") {** ** shareOperation.reportStarted();** ** var files = [];** ** list.forEach(function (listItem) {** ** files.push(listItem.file);** ** });** ** copySelectedFiles(files).then(function () {** ** shareOperation.reportDataRetrieved();** ** shareOperation.reportCompleted();** ** });** ** } else {**
shareOperation.reportStarted();
shareListView.winControl.itemDataSource = list.dataSource; WinJS.Utilities.query("button.addButton").listen("click", function (e) { // ...statements removed for brevity... }); } }); } ...`
当用户选择快速链接时,我处理所有文件,不提示用户输入任何内容。结果是一个简化的共享操作,甚至不向用户呈现界面——文件只是无缝地添加到相册中。
总结
在这一章中,我向你展示了如何实现共享契约的两个部分,让你能够让数据从一个应用平稳地流向另一个应用。共享源应用负责打包数据并使其可供 Windows 使用,Windows 充当一个代理来查找可以处理共享数据的合适的共享目标应用。共享是关键的 Windows 交互之一,我鼓励你将它添加到你的应用中,并以支持最广泛的数据格式和文件类型的方式进行。你给用户分享你的应用的机会越多,你的应用就越能深入他们的工作流程。
在下一章,我将展示 Windows 8 支持的其他一些契约。