二十九、使用传感器
Windows 8 支持一个传感器框架,您可以使用它来获取有关设备运行条件的信息。在本章中,我描述了最常遇到的传感器,从位置传感器开始,通过它您可以确定设备正在世界上的哪个地方使用。
即使设备中没有特殊的位置感应硬件,如 GPS 接收器,Windows 也会尝试确定设备位置。我在本章中描述的其他传感器确实需要特殊的硬件,如光和加速度传感器,尽管这种设备越来越常见,特别是在笔记本电脑和平板电脑中。
在本章中,我将向您展示如何读取来自不同传感器的数据,但您如何使用这些数据为用户带来好处则取决于具体的应用。因此,示例通常只是将传感器数据显示为文本。
当你阅读这一章的时候,你会遇到一些地方,我已经注意到,在我的测试硬件中,某些特性对传感器不起作用。这并不罕见,因为传感器及其设备驱动程序的质量有很大的可变性,因此需要仔细测试。
使用传感器时,注意不要根据你收到的数据对用户在做什么做出大胆的推断。例如,当使用光线水平传感器时,不要仅仅因为光线水平低就认为现在是夜间——这可能是因为设备被放在包中,或者只是因为传感器被遮挡。始终允许用户覆盖基于传感器数据对应用行为所做的更改,并在可能的情况下,在对当前条件做出假设之前,尝试从不同来源收集数据——例如,通过将光线水平与当前时间和位置相结合,您可以避免假设工作日是晚上。表 1 提供了本章的总结。
创建示例应用
我将创建一个示例应用,它将允许我演示我在本章中介绍的每个传感器。我将按照我在本书其他地方使用的模式来实现这一点,即创建一个使用导航条导航到内容页面的应用,每个页面包含一个主要功能。我创建了一个名为Sensors
的新 Visual Studio 项目,您可以在清单 1 中看到default.html
文件的内容。这个文件包含目标元素,我将在其中加载内容页面和导航栏,以及每个页面的命令。
清单 1。default.html 文件的内容
`
Select a page from the NavBar
HTML 文件包含了导航我在本章中使用的所有内容页面的按钮,但是我不会在每一节开始之前添加这些文件。您可以在清单 2 的示例中看到我定义的管理元素布局的样式,它显示了/css/default.css
文件的内容。
清单 2。/css/default.css 文件的内容
`body {display: -ms-flexbox; -ms-flex-direction: column;
-ms-flex-align: stretch; -ms-flex-pack: center; }
#contentTarget {display: -ms-flexbox; -ms-flex-direction: row;
-ms-flex-align: stretch; -ms-flex-pack: center;}
.container { border: medium solid white; margin: 10px; padding: 10px;}
.containerHeader {text-align: center;}
#buttonsContainer button {font-size: 20pt; margin: 20pt; display: block; width: 80%;}
*.imgElem {height: 500px;}
*.imgTitle { color: white; background-color: black;font-size: 30pt; padding-left: 10px;}
.messageItem {display: block; font-size: 20pt; width: 100%; margin-top: 10px;}
#messageContainer {width: 60%;}
.messageList {height: 80vh;}
.label { margin: 20px; width: 600px;}
.label span { display: inline-block; width: 250px; text-align: right;}
h1.warning { display: none; text-align: center;}`
最后,我已经定义了清单 3 中的代码,它显示了/js/default.js
文件的内容。在这一章中,我关注的是设备,所以我不担心管理不同的生命周期事件——因此,JavaScript 代码执行一个非常基本的应用初始化来设置内容页面的导航,我将把它添加到 Visual Studio 项目的pages
文件夹中。
清单 3。/js/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();
})();`
如果您启动应用并显示 NavBar,您将看到如图图 1 所示的布局——尽管单击 NavBar 命令栏会产生错误,因为我还没有添加激活时加载的文件。
***图 1。*示例 app 的基本布局
注意为了让这个例子尽可能简单,我编写了每个内容页面,就好像它是唯一将被显示的页面一样。这意味着在显示来自不同传感器的数据之前,您必须重新启动示例应用。
使用地理定位
地理定位对于应用开发来说是一个越来越重要的功能,因为它为根据用户所处的位置定制用户体验提供了基础。许多 Windows 8 设备将配备 GPS 硬件,但 Windows 也可以尝试通过结合从一系列替代来源获取的信息来确定没有 GPS 的设备的当前位置——例如,包括设备 IP 地址和附近无线网络的名称——这种技术可能会惊人地准确。在接下来的小节中,我将向您展示如何使用地理定位特性,以及如何使用 Visual Studio simulator 测试地理定位。
注意地理定位是一个功能的例子,您可以使用特定于 Windows 的 API 或通过 HTML5 API 来访问。我发现 Windows APIs 倾向于提供与 Windows 设备的功能更一致的细粒度功能,这是意料之中的,因为 HTML5 APIs 的用途非常广泛。我将在本章中使用 Windows 地理定位 API。
准备地理定位示例
为了演示地理定位特性,我在 Visual Studio 项目的pages
文件夹中添加了一个名为geolocation.html
的文件。您可以在清单 4 中看到这个文件的内容。
清单 4。geolocation.html 文件的内容
`
Messages
该文件定义了一个布局,在左侧面板中有三个按钮,可以激活不同的地理定位功能,在右侧有一个大面板,可以显示更新消息。我已经将这个例子的 JavaScript 放到了一个名为/pages/geolocation.js
的单独文件中,您可以在清单 5 中看到这个文件。目前,该文件包含一个处理程序函数,用于在点击button
元素时进行监听,以及在收到事件时执行的占位符函数——我将在接下来的小节中填充这些函数,向您展示地理定位功能的不同方面。我还添加了代码,让我可以轻松地在应用布局中显示消息。
清单 5。/pages/geolocation.js 文件的初始内容
`(function () {
var geo = Windows.Devices.Geolocation;
var messages = new WinJS.Binding.List();
function writeMessage(msg) {
messages.push({ message: msg });
}
function getFromCode(src, code) {
for (var propName in src) {
if (code == src[propName]) { return propName; }
}
}
WinJS.UI.Pages.define(“/pages/geolocation.html”, {
ready: function () {
messageList.winControl.itemDataSource = messages.dataSource;
WinJS.Utilities.query(“#buttonsContainer button”).listen(“click”,
function (e) {
switch (e.target.innerText) {
case “Get Location”:
getLocation();
break;
case “Start Tracking”:
startTracking();
break;
case “Stop Tracking”:
stopTracking();
break;
}
});
}
});
function getLocation() {
// …code will be added here…
}
function startTracking() {
// …code will be added here…
}
function stopTracking() {
// …code will be added here…
}
})();`
在清单中启用位置访问
必须在清单中启用对设备位置的访问,方法是打开package.appxmanifest
文件,导航到Capabilities
部分,并选中Location
选项,如图图 2 所示。
***图二。*启用清单中的位置访问
警告如果您试图在未启用清单中的
Location
功能的情况下访问设备位置,将会产生错误。
获取位置快照
获取设备当前位置最简单的方法就是拍快照,也就是说你向系统询问当前位置,系统提供,然后你就完事了。另一种方法是跟踪当前位置,这意味着当位置发生变化时,系统会为您的应用提供更新。我将在本章的后面向您展示位置跟踪是如何工作的,但是拍摄位置快照相对简单,让我介绍支持地理定位特性的对象,这些对象在Windows.Devices.Geolocation
名称空间中定义。
注意除非我另有说明,否则我在本节中引用的所有新对象都可以在
Windows.Devices.Geolocation
名称空间中找到,在示例中我将它别名化为变量geo
。
为了添加对拍摄位置快照的支持,我在/pages/geolocation.js
文件中实现了getLocation
函数,如清单 6 所示。
清单 6。实现 getLocation 函数来拍摄位置快照
`(function () {
var geo = Windows.Devices.Geolocation;
** var geoloc;**
var messages = new WinJS.Binding.List();
function writeMessage(msg) {
messages.push({ message: msg });
}
** function getStatus(code) {**
** for (var propName in geo.PositionStatus) {**
** if (code == geo.PositionStatus[propName]) { return propName; }**
** }**
** }**
WinJS.UI.Pages.define(“/pages/geolocation.html”, {
ready: function () {
messageList.winControl.itemDataSource = messages.dataSource;
WinJS.Utilities.query(“#buttonsContainer button”).listen(“click”,
function (e) {
switch (e.target.innerText) {
case “Get Location”:
getLocation();
break;
case “Start Tracking”:
startTracking();
break;
case “Stop Tracking”:
stopTracking();
break;
}
});
** geoloc = new geo.Geolocator();**
** writeMessage("Status: " + getStatus(geoloc.locationStatus));**
** geoloc.addEventListener(“statuschanged”, function (e) {**
** writeMessage("Status: " + getStatus(geoloc.locationStatus));**
** });**
}
});
** function getLocation() {**
** geoloc.desiredAccuracy = geo.PositionAccuracy.high;**
** geoloc.getGeopositionAsync().then(function (pos) {**
** writeMessage(“Snapshot - Lat: " + pos.coordinate.latitude**
** + " Lon: " + pos.coordinate.longitude**
** + " (” + pos.coordinate.timestamp.toTimeString() + “)”);**
** });**
** }**
function startTracking() { /* …code will go here… /}
function stopTracking() { / …code will go here… */}
})();`
虽然只有少量的添加,但是这段代码中有很多内容,所以我将在接下来的部分中一步一步地进行分解。
创建和配置地理定位器对象
获取位置的第一步是创建并配置一个Geolocator
对象。一个对象可以用于多次获取位置,所以我在加载内容页面时执行的ready
函数中创建了一个Geolocation
对象——这将允许我在整个示例中使用一个Geolocator
对象。当你创建一个新的Geolocator
对象时,没有使用构造函数参数,如清单 7 所示,这里我重复了来自geolocation.js
文件的语句。
清单 7。创建新的地理定位器对象
... geoloc = new geo.Geolocator(); ...
下一步是配置Geolocator
对象。拍摄位置快照时,只有一个设置,通过desiredAccuracy
属性访问。这个属性必须设置为由PositionAccuracy
枚举定义的值之一,我已经在表 2 中描述过了。
default
和high
值并没有指定位置应该被确定的精确程度。相反,它们指定设备应该如何获取位置。default
值将倾向于不太准确的信息,这些信息可以快速、轻松、免费地获得。high
值将获得设备可以产生的最精确的位置,并且它将使用它所拥有的所有资源来获得该精确度,即使这意味着消耗大量的电池电量或使用可能花费用户金钱的服务(例如来自蜂窝服务提供商的定位服务)。
提示请注意,您无法指定用于获取位置的技术,甚至无法找出设备获取位置的不同选项。你所能做的就是设置你想要的精确度,剩下的就交给 Windows 了。
关于为属性desiredAccurcy
使用哪个值的决定通常最好由用户来做,尤其是出于财务方面的考虑。发现一个写得很糟糕的应用耗尽了你所有的电池电量已经够糟糕了,但发现它在这样做的同时一直在耗费你的钱,这是无法忍受的。您可以看到我是如何在清单 8 的示例中指定高精度选项的,在这里我重复了来自geolocation.js
文件的语句。
清单 8。配置地理定位器对象的精确度
... geoloc.desiredAccuracy = **geo.PositionAccuracy.high**; ...
我不担心获取位置的成本,因为我将使用 Visual Studio 模拟器向应用提供位置数据。
监控地理位置状态
Geolocator
对象定义了statuschanged
事件,它提供了关于地理定位特性状态的通知。一个Geoloctor
对象的状态是通过locationStatus
属性获得的,并且将是由PositionStatus
枚举定义的值之一,我已经在表 3 中列出了这些值。
我通过读取locationStatus
属性的值并在应用布局右侧面板的ListView
控件中显示一条消息来处理statuschanged
事件。如果启动 app,会提示允许 app 访问位置数据,如图图 3 所示。
***图三。*请求位置访问
如果点击Block
按钮,Windows 会拒绝你的 app 访问位置数据,并且会触发statuschanged
事件,表示locationStatus
属性已经更改为disabled
,如图图 4 所示。
***图 4。*禁止访问位置数据的影响
如果您在应用中获得了disabled
值,那么您将知道您将无法访问位置数据。在这一点上,根据你的应用,你可能能够继续并向用户提供某种减少的功能,或者你可能需要显示一条消息,鼓励用户授予你的应用所需的访问权限。你应该而不是做的是忽略disabled
值,只是尝试读取位置数据——你不会得到任何数据,用户也不会明显看出缺乏位置访问是你的应用产生奇怪结果的原因。
要访问位置数据,激活Settings
图标,点击Permissions
链接,改变Location
拨动开关的位置。当你重新加载 app 时,你会看到状态显示为ready
,表示有位置数据可用,如图图 5 。
***图 5。*允许访问位置数据的效果
获取位置
点击应用布局中的Get Location
按钮,调用geolocation.js
文件中的getLocation
函数,就是这个函数生成位置的快照。在的清单 9 中,我重复了getLocation
的实现。
清单 9。getLocation 函数的实现
... function getLocation() { geoloc.desiredAccuracy = geo.PositionAccuracy.high;
geoloc.getGeopositionAsync().then(function (pos) { writeMessage("Snapshot - Lat: " + pos.coordinate.latitude + " Lon: " + pos.coordinate.longitude + " (" + pos.coordinate.timestamp.toTimeString() + ")"); }); } ...
一旦我配置了Geolocator
对象,我就调用getGeopositionAsync
方法。这个方法返回一个Promise
,当它被实现时,将产生当前位置的快照。快照作为一个Geoposition
对象传递给then
函数,它定义了我在表 4 中描述的两个属性。
CivicAddress
对象有点令人失望,因为 Windows 没有用地址的细节填充该对象。这个想法是 Windows 的第三方附加软件可以提供这项服务,但它们并没有被广泛使用,所以你不能指望在用户的设备上找到一个安装的。
超越获得位置
Windows 位置传感器非常善于给你设备的经度和纬度,但仅此而已,你甚至不能依赖于自动填充的CivicAddress
对象。如果您想超越获取基本坐标的范围,那么您将需要使用众多可用的地图 web 服务之一。我喜欢openstreetmap.org
,它有出色的反向地理编码服务(将坐标转化为街道地址)和精确的地图数据。最重要的是,这些服务可以免费使用,并且不需要您将 API 密钥嵌入到您的应用中(当密钥更改或过期时,这是一个管理难题)。
如果你比我更少受到 API 键的困扰,那么你可能会考虑 Bing 地图 AJAX 控件,它使获取地址和在 Windows 应用中显示地图变得非常容易。有使用限制,高请求量收费,但地图很好,有一些应用开发的代码示例。你可以在[
bingmapsportal.com](http://bingmapsportal.com)
找到更多细节。当然,谷歌也有一个类似的库,它的功能和 Bing 选项一样好,也需要 API 密钥和高使用率的费用。您可以在[
developers.google.com/maps](https://developers.google.com/maps)
获得 Google APIs 的详细信息。
我将把注意力集中在Geocoordinate
对象上,它是正确填充的,并定义了我在表 5 中描述的属性。
并非Geocoordinate
对象中的所有属性都将被填充——例如,这可能是因为设备没有能够提供高度细节的硬件,或者因为某些属性仅在位置被跟踪时可用,而不是拍摄快照。在示例中,当由getGeopositionAsync
方法返回的Promise
被满足时,我使用latitude
、longitude
和timestamp
属性在应用布局的右侧面板中显示一条消息。
测试位置快照
现在您已经看到了各种对象是如何组合在一起的,是时候测试示例应用拍摄位置数据快照的能力了。最简单的方法是使用 Visual Studio 模拟器,它能够模拟位置数据。启动 app 前,点击模拟器窗口的Set location
按钮,勾选Use simulated location
选项,输入纬度值38.89
和经度值-77.03
,如图图 6 (这里是白宫所在地)。
***图六。*输入模拟位置数据
点击Set Location
按钮应用模拟位置,然后启动示例应用。点击Get Location
按钮生成位置快照,并在应用布局中显示消息。您可以在图 7 中看到本次测试使用模拟数据的结果。
***图 7。*生成位置快照
不一定要用模拟数据。如果您没有在模拟器弹出窗口中选择Use simulated location
选项,那么应用将从 Windows 中读取位置数据。我使用了模拟数据,因为我想创建一个可以持续重复的结果,但我建议也使用真实数据——尤其是如果你的设备不支持 GPS。虽然可变,但非 GPS 位置数据的准确性可能相当惊人——仅使用无线网络名称,我的电脑就可以确定其位置在我家 200 英尺以内。
追踪位置
下一个向您展示的功能是在位置变化时跟踪位置的能力。如果您想监控设备的位置,您可以定期调用getGeopositionAsync
方法,但是这是一个很难实现的过程。您不希望过于频繁地拍摄位置快照,因为设备可能不会移动,而且您会不必要地消耗设备资源(可能还会消耗用户的钱)。如果您拍摄快照的频率太低,您将错过设备移动的时刻,并以部分数据磁道结束。
为了更容易跟踪位置,Geolocator
对象定义了positionchanged
事件,当设备的位置超出您定义的阈值时,就会触发该事件。您可以看到我是如何在清单 10 中的示例应用中添加位置跟踪支持的,它展示了我对startTracking
和stopTracking
函数的实现,以及一个编写显示位置信息的新函数。
清单 10。位置跟踪的实现
... **function startTracking() {** ** geoloc.movementThreshold = 100;** ** geoloc.addEventListener("positionchanged", displayLocation);** ** writeMessage("Tracking started");** **start.disabled = !(stop.disabled = false);**
`}
function stopTracking() {
** geoloc.removeEventListener(“positionchanged”, displayLocation);**
** writeMessage(“Tracking stopped”);**
** start.disabled = !(stop.disabled = true);**
}
function displayLocation(e) {
** writeMessage(“Track - Lat: " + e.position.coordinate.latitude**
** + " Lon: " + e.position.coordinate.longitude**
** + " (” + e.position.coordinate.timestamp.toTimeString() + “)”);**
}
…`
您可以通过设置Geolocator.movementThreshold
属性的值来设置移动的阈值。当设备移动的距离超过您指定的米数时,将触发positionchanged
事件。
提示一米大约是 3 英尺 3 英寸。可以指定一个低于设备确定其位置精度的移动阈值,在这种情况下,只要位置改变,就会触发
positionchanged
事件。
在startTracking
函数中,我将阈值设置为 100 米(大约 110 码),然后使用addEventListener
方法为positionchanged
事件设置一个事件监听器,指定使用displayLocation
函数(也在清单中定义)来处理该事件。为了停止跟踪,我简单地调用removeEventListener
函数来取消displayLocation
作为事件监听器的注册。
传递给处理函数的事件对象定义了一个返回一个Geoposition
对象的position
属性,我使用它的coordinate
属性来显示新位置的细节。
要测试位置跟踪,启动应用,通过导航栏导航到geolocation.html
页面,然后单击Start Tracking
按钮。现在点击 Visual Studio 模拟器的Set location
按钮,输入新的坐标,然后点击Set Location
按钮。每次设置新坐标时,只要新位置距离旧位置至少 100 米,就会在应用布局中显示一条新消息。在图 8 中可以看到追踪位置的效果。
***图 8。*使用 positionchanged 事件跟踪位置
使用光线传感器
如今,光传感器越来越常见,用于改变照亮设备屏幕的电量,以最大限度地减少电池消耗(屏幕通常是设备功耗的最大消耗者,在光线较暗的情况下调暗屏幕可以节省大量电力,并使屏幕使用起来不那么累)。对于一个应用来说,光传感器最常见的用途是试图找出设备何时在户外或用户何时入睡,这两者都可以与其他信息(如时间或位置)相关联,以改变应用的行为。几年前,我有一台 PDA,它利用光传感器提供不同寻常的选项,例如黎明警报和提醒我是否在室内呆了太长时间——虽然不总是成功,但玩起来很有趣。
注意与即使设备没有专用的位置硬件也能产生位置的位置传感器不同,光传感器(以及我在本章中描述的其他传感器)需要实际的硬件。硬件相当常见,为了测试示例项目,我使用了我在第六章的中提到的戴尔 Inspiron Duo。这对组合有一个触摸屏和一系列硬件传感器,这使它成为测试应用的理想选择。我不想听起来像是戴尔的广告(我不太喜欢戴尔),但这对组合非常便宜,尤其是二手的,我发现它对于在部署前测试应用非常有用,特别是在确保我的触摸屏交互有意义和感觉自然的时候。
为了演示光传感器,我在名为light.html
的项目的pages
文件夹中添加了一个文件,其内容可以在清单 11 中看到。
清单 11。light.html 文件的内容
`
function displaySensorReading(reading) {
level.innerText = reading;
var conditionText = “Unknown”;
if (reading > 10000) {
conditionText = “Outdoors”;
} else if (reading > 300) {
conditionText = “Office”;
} else if (reading > 50) {
conditionText: “Home”;
} else {
conditionText = “Dark”;
}
condition.innerText = conditionText;
}
No Light Sensor Installed
Light level: (None)
Condition: (None)
光线传感器由Windows.Devices.Sensors.LightSensor
对象表示,您通过getDefault
方法获得对传感器的引用,如下所示:
... var sensor = Windows.Devices.Sensors.LightSensor.getDefault(); ...
如果getDefault
方法返回的值是null
,那么当前设备不包含光传感器。在这个例子中,我检查了null
结果,如果传感器不存在,就显示一条消息。
拍摄光线水平的快照
一旦获得了传感器对象,就可以通过调用getCurrentReading
方法来获取传感器值的快照,如清单 12 所示。
清单 12。拍摄亮度快照
... displaySensorReading(**sensor.getCurrentReading()**.illuminanceInLux); ...
getCurrentReading
方法返回一个Windows.Devices.Sensors.LightSensorReading
对象,它定义了我在表 6 中描述的两个属性。
illuminanceInLux
属性返回以勒克斯为单位的亮度。维基百科对勒克斯单位有很好的描述,并有一个表格描述了一些有用的勒克斯范围。可以在[
en.wikipedia.org/wiki/Lux](http://en.wikipedia.org/wiki/Lux)
看到文章。我使用这些勒克斯范围的简化集来猜测设备的工作条件,包括室外、室内和办公室内——你可以在light.html
文件的displaySensorReading
函数中看到我的映射。
注意您会注意到对
getCurrentReading
方法的调用在清单 11 中被注释掉了,它显示了light.html
文件的内容。如果我调用getCurrentReading
方法来拍摄光照水平的快照,我发现跟踪光照水平(我将在下一节描述)不再有效。这可能只是我的戴尔 Duo 中的光线传感器的一个特征,但我无法使用任何其他传感器来确定。
跟踪光线水平
拍摄光线水平的快照可能很有用,但通常您会希望在光线水平变化时收到通知。LightSensor
对象定义了readingchanged
事件,当光线级别改变时触发该事件。传递给处理函数的事件定义了一个名为reading
的属性,该属性返回一个包含光线级别细节的LightSensorReading
对象。在清单 13 的中,你可以看到我是如何为这个事件添加一个处理函数的,以及我是如何通过调用我的displaySensorReading
函数来显示亮度级别(以及我对设备运行条件的猜测)的。
清单 13。通过 readingchanged 事件跟踪光线水平
... sensor.addEventListener("**readingchanged**", function (e) { displaySensorReading(e.reading.illuminanceInLux); }); ...
测试光传感器示例
你可以在图 9 的中看到light.html
页面的布局和我测试示例应用时的光线水平。
***图九。*使用光传感器
我建议在根据光线水平决定应用行为时要谨慎,因为光线水平和设备运行条件之间没有明确的相关性。在图中,你可以看到我的照明水平是 8907 勒克斯,我将其归类为在办公室。这是一个不错的猜测,但我是在黄昏前设备在我家一个有玻璃墙的房间里时拍的这张快照。我的观点是,相同的光照水平可以存在于一系列不同的系统中,在响应光传感器的读数时保持灵活是有好处的——例如,如果一个应用将我的网络设置更改为 8900 勒克斯的办公室配置,这可能会有所帮助,但某种覆盖是必不可少的,因为我今天正好在家工作。我在本章中描述的所有传感器都是如此——传感器数据可能很有用,它可以使你的应用更有帮助和更灵活,但你应该适应你对读数的反应方式,并在数据导致你做出错误推断时,总是为用户提供一个覆盖。
使用倾斜仪
测斜仪测量设备倾斜的角度。倾斜仪通常用在游戏中,以允许该设备被用作控制器。您很快就会看到,使用测斜仪与使用光传感器非常相似,因为大多数代表传感器的物体都有大致相似的设计。为了演示测斜仪的使用,我在示例项目的pages
文件夹中添加了一个名为tilt.html
的文件,其内容可以在清单 14 中看到。
清单 14。tilt.html 文件的内容
`
setInterval(function () {
displaySensorReading(sensor.getCurrentReading());
}, 100);
//sensor.addEventListener(“readingchanged”, function (e) {
// displaySensorReading(e.reading);
//});
}
}
});
function displaySensorReading(reading) {
pitch.innerText = reading.pitchDegrees.toFixed(1);
roll.innerText = reading.rollDegrees.toFixed(1);
yaw.innerText = reading.yawDegrees.toFixed(1);
box.style.transform = “rotate(” + (360 - reading.rollDegrees) + “deg)”;
}
No Inclinometer Installed
Pitch: (None)
Roll: (None)
Yaw: (None)
测斜仪由Windows.Devices.Sensors.Inclinometer
物体表示;就像光传感器一样,您必须调用getDefault
方法来获取一个您可以从中读取数据的对象,就像这样:
... var sensor = Windows.Devices.Sensors.Inclinometer.getDefault(); ...
如果getDefault
方法返回的值为 null,那么设备没有安装测斜仪。
使设备倾斜
您可以通过调用getCurrentReading
方法来获取当前设备倾斜的快照,该方法返回一个Windows.Devices.Sensors.InclinometerReading
对象,该对象定义了表 7 中显示的属性。
对象定义了一个事件,但是我不能让它在我的设备上触发。为了解决这个问题,我已经注释掉了事件处理代码,并用一个对setInterval
的调用来代替它,我用它来重复轮询传感器的倾斜值。作为显示读数过程的一部分,我对布局中的一个div
元素进行了变换,以便显示一个随着设备旋转而“自动调平”的正方形——这与我在第十八章中描述的变换类型相同。你可以在图 10 中看到该应用的布局(请记住,这张截图是在设备倾斜时拍摄的)。
***图十。*跟踪和显示设备倾斜
使用加速度计
加速度计测量加速度,通常与位置数据一起使用,以确定设备移动的方式和时间-例如,如果位置跟踪报告设备以每小时 6 英里的速度移动,而加速度计报告有规律的加速度脉冲,则与锻炼相关的应用可能会开始记录数据,以防用户忘记记录他们的日常跑步。加速度计也可以用于确定设备如何定向,因为当设备静止时,朝向地球的加速度将是 1g。
为了演示加速度计的使用,我在 Visual Studio 项目的pages
文件夹中添加了一个名为acceleration.html
的新文件。您可以在清单 15 中看到这个文件的内容。
清单 15。acceleration.html 文件的内容
`
function displaySensorReading(reading) {
x.innerText = reading.accelerationX.toFixed(2);
y.innerText = reading.accelerationY.toFixed(2);
z.innerText = reading.accelerationZ.toFixed(2);
}
No Accelerometer Installed
Accelerate X: (None)
Accelerate Y: (None)
Accelerate Z: (None)
你现在应该能认出这个模式了。加速度计设备由Windows.Devices.Sensors.Accelerometer
对象表示,您必须调用getDefault
方法来获取表示传感器的对象,如下所示:
... var sensor = Windows.Devices.Sensors.Accelerometer.getDefault(); ...
如果来自getDefault
方法的结果为空,则设备中没有加速度计硬件。
获得设备加速度
您可以通过调用返回一个Windows.Devices.Sensors.AccelerometerReading
对象的getCurrentReading
方法来获得加速度计测量的力的快照。这个对象定义了我在表 8 中描述的属性。
我在displaySensorReading
函数中读取这些属性的值,这产生了如图图 11 所示的布局和数据。
***图 11。*测量加速度
您可以通过监听由Accelerometer
对象发出的readingchanged
事件来跟踪设备加速度。传递给处理函数的事件对象的reading
属性返回一个AccelerometerReading
对象,您可以在acceleration.html
文件中看到我是如何处理这个事件的。
注
Accelerometer
也定义了shaken
事件。一些加速度计硬件可以检测到设备何时被快速摇动,这个手势将触发shaken
事件。我的测试设备中的加速度计硬件不支持摇动手势,通常解释设备方向的变化(我在第六章的中描述过)。依赖此事件时要小心,因为它可能不会被触发,并且要求用户摇动设备可能会导致意外的配置更改,正如我在戴尔 Duo 上的经历一样。
使用指南针
指南针允许您确定设备指向的方向。使用指南针需要磁力计硬件,它可以测量磁场的强度和方向。指南针在确定地图数据的方向以使其与现实世界相符时最有用——例如,我进行了大量的长距离行走和跑步,我的手持(非 Windows) GPS 设备使用其指南针来确保拓扑图与我所面对的方向相符,这使我更容易找到自己的位置。
为了演示指南针传感器,我在 Visual Studio 项目的pages
文件夹中添加了一个名为direction.html
的新文件,您可以在清单 16 中看到该文件的内容。
清单 16。direction.html 文件的内容
`
function displaySensorReading(reading) {
heading.innerText = reading.headingMagneticNorth;
cspan.style.transform = “rotate(”
+ (360 - reading.headingMagneticNorth) + “deg)”;
}
No Compass Installed
Heading: (None)
指南针的工作原理和我在本章中描述的其他传感器一样。指南针由Windows.Devices.Sensors.Compass
对象表示,您必须调用getDefault
方法来获取对象,以便读取传感器数据,如下所示:
... var sensor = Windows.Devices.Sensors.Compass.getDefault(); ...
如果getDefault
方法返回 null,那么设备没有指南针传感器硬件。
获取设备方向
您可以通过调用返回一个Windows.Devices.Sensors.CompassReading
对象的getCurrentReading
方法来获得设备朝向的快照。该对象定义了表 9 中所示的属性。
并非所有的指南针硬件包都能够产生磁航向和真北航向,例如,我的测试设备中的传感器只产生磁方位。在本例中,我将CompassReading
对象传递给displaySensorReading
函数,该函数显示数字标题并对div
元素进行旋转,以显示一个始终指向北方的箭头。您可以在图 12 的中看到布局和传感器数据。
***图 12。*显示指南针的数据
您可以通过监听readingchanged
事件来跟踪航向的变化,当指南针传感器报告的方向发生变化时,Compass
对象会触发该事件。我在示例中使用这个事件来保持示例布局最新。
总结
在本章中,我向您展示了您的应用如何利用 Windows 8 传感器框架来获取真实世界的数据。像所有高级功能一样,传感器数据需要谨慎使用,但您可以通过使用您收到的数据来创建灵活和创新的应用,以适应他们的环境。在本书的下一部分,我将向您展示如何准备您的应用并将其发布到 Windows 应用商店。
三十、创建要发布的应用
在本书的这一部分,我将向您展示如何在 Windows 应用商店中发布应用。我将从头开始,创建一个应用,从创建到提交给微软审查。在此过程中,我将向您展示如何应用驱动您的业务模式的功能,并确保应用符合 Windows 应用商店的政策。在这一章中,我将带您完成开始之前需要的步骤,然后创建一个应用,我将在接下来的章节中介绍它的发布过程。
决定你的应用
显然,应该从决定你的应用要做什么开始。对于这一章,我将创建一个照片浏览器应用。这不是任何真正的用户想要付费的东西(尤其是因为 Windows 8 中已经包含了这样的应用),但它是这本书这一部分的理想例子,因为功能简单且独立,让我专注于发布过程的不同部分。
决定你的商业模式
当创建一个应用时,首先要做的是决定你想要使用的商业模式。Windows 应用商店支持一系列不同的应用销售方式,包括免费赠送、试用版、收取固定费用、收取订阅费、应用内广告和销售应用内升级。
对于我的示例应用,我将提供一个免费的限时试用来吸引用户,然后向他们收取 5 美元的基本应用费用。但我的 Windows 8 财富将以应用内升级的形式出现,其中一些将获得永久许可,一些将在订阅的基础上出售。
注意我不打算演示应用内广告,因为它与 Windows 8 商店没有直接关系。微软有一个广告 SDK,可以很容易地在一个应用中包含广告,你可以从
[
advertising.microsoft.com/windowsadvertising/developer](http://advertising.microsoft.com/windowsadvertising/developer)
获得。如果你想使用另一家广告提供商,你需要仔细检查广告内容和获取广告的机制是否违反了应用认证要求(我在第三十三章中向你展示了如何测试)。
在表 30-1 中,我已经列出了我的应用的不同可购买功能、它们的价格以及它们的用途。该应用的免费试用将支持 30 天的所有功能。
让我再次强调,我不会真的卖这个应用,我只是需要一个工具来告诉你如何卖你的。因此,虽然我的应用及其升级既乏味又昂贵,但它们将允许我演示如何在一个应用中创建和组合一系列商业模式。
准备就绪
在开始创建我的应用之前,我需要做几件事情。首先是创建一个 Windows Store 开发者账户,允许你向商店发布应用,并从中获得任何收入。您可以以个人身份或代表公司创建一个帐户,Microsoft 会更改该帐户的年费(目前个人为 49 美元,公司为 99 美元)。您将需要一个 Microsoft 帐户来打开 Windows 应用商店开发人员帐户,但您在下载 Visual Studio 时应该已经有了一个。
提示微软将 Windows Store 账户作为其开发者产品的一部分,如 TechNet 和 MSDN。如果您已经购买了这些服务中的一项,您可能不必直接为帐户付费。
在 Visual Studio 的Store
菜单中选择Open Developer Account
,开始创建帐户的过程。获得帐户的过程需要填写一些表格并设置支付细节,这样你就可以从你的应用销售中获得利润。一旦你创建了一个账户,你会看到如图图 30-1 所示的仪表板。此仪表板提供了你的商店帐户的详细信息,包括你的应用和付款。
***图 30-1。*Windows Store 仪表盘
保留应用名称
开始开发应用之前,您可以在 Windows 应用商店中保留应用的名称。预约有效期为一年,在此期间,只有您可以发布该名称的应用。保留一个名字是一个明智的想法,这样你就可以继续创作艺术品、网站和营销宣传材料,而不用担心在你开发应用时别人会用这个名字。
你可以通过从 Visual Studio Store
菜单中选择Reserve App Name
项或者点击 Windows 应用商店仪表盘中的Submit an app
链接来保留名称(反正Reserve App Name
菜单会带你去那里)。仪表盘呈现了发布一个 app 所需的不同步骤,但我目前唯一关心的是第一步,也就是App name
步骤,如图图 30-2 所示。
***图 30-2。*显示应用发布流程第一步的 Windows 仪表盘
点击App name
项,输入您想要预订的姓名。对于我的 app,我保留了名称Simple Photo Album
,如图图 30-3 所示。
***图 30-3。*选择应用的名称
您保留的名称是应用将在商店中列出的名称,与您的 Visual Studio 项目的名称不同。我将在第三十三章中向您展示如何将 Visual Studio 项目与仪表板关联起来,但我甚至还没有创建我的 Visual Studio 项目)。
这意味着当你阅读这一章时,我对
Simple Photo Album
名称的保留可能已经失效,并且可能已经被其他人使用。
一旦您保留了您的应用名称,仪表板将更新发布过程的第一步以反映您的选择,如图 30-4 所示。
***图 30-4。*完成应用发布流程的第一步
这就是目前所需要的全部准备工作。保留名称后,我现在可以创建我的 Visual Studio 项目并开始构建我的应用。
创建 Visual Studio 项目
我已经使用Blank App
模板创建了一个新的 Visual Studio 项目。我将项目命名为PhotoApp
,只是为了演示您可以将项目的名称与应用在商店中的名称和用户名称分开。你可以在图 30-5 中看到成品 app 的外观。
***图 30-5。*示例 app 的布局
左侧面板包含一些按钮和ToggleSwitch
控件,允许用户选择显示哪些图像以及是否显示缩略图。右侧面板包含一个大的FlipView
控件,该控件始终可见,可用于浏览已加载的图像。在右边面板的底部是一个ListView
控件,显示可用图像的缩略图。缩略图的可见性和图像的选择将由用户购买的功能来控制。在本章的后面,我将向你展示应用布局的各个组成部分。
创建商业模式代码
如果你正在创建一个完全免费的应用,那么你可以通过应用我在本书中向你展示的技术来开始构建你的功能。你不必担心收款或启用功能或任何其他类型的与商店的互动。但是如果你打算对你的应用或者它的一些功能收费,你需要仔细考虑你的应用的结构。我发现最好的方法是从一开始就将一些与核心商业模式相关的功能植入应用,这样我就可以构建应用的功能,然后回来实施我的商业计划。
为此,我添加到PhotoApp
项目的第一个文件叫做store.js
,我把它放到了js
文件夹中。你可以在清单 30-1 中看到这个文件的内容。
清单 30-1 。/js/store.js 文件的内容
`(function() {
WinJS.Namespace.define(“ViewModel.Store”, {
events: WinJS.Utilities.eventMixin,
checkCapability: function(name) {
var available = true;
setImmediate(function () {
ViewModel.Store.events.dispatchEvent(“capabilitycheck”,
{ capability: name, enabled: available });
});
return available;
}
});
})();`
这个文件目前非常简单,但当我添加将我的应用集成到 Windows 商店的支持时,它将成为我在第三十一章中的主要关注点。我创建了一个名为ViewModel.Store
的新名称空间,并在其中添加了一个checkCapability
函数。应用的其他部分将调用这个函数来查看用户是否已经购买了执行某些操作的权利——在第三十二章中,我将实现代码来检查我的产品层和升级,但目前每个请求都返回true
,表明某个功能可用。这将允许我在实现我的业务模型代码之前构建出我的应用的功能。
当调用checkCapability
函数时,它会发出一个capabilitycheck
事件——我将在第三十二章中使用这个事件来响应用户试图使用他们尚未购买的功能。我已经使用WinJS.Utilties.eventMixin
实现了事件,这是一个有用的对象,您可以使用它向常规 JavaScript 对象添加事件支持。它定义了在 DOM 元素对象上可以找到的标准的addEventListener
和removeEventListener
方法,以及dispatchEvent
方法,该方法允许您向注册的侦听器发送任意类型的事件。使用eventMixin
对象比编写自己的事件处理代码更简单,也更不容易出错,通过ViewModel.Store.event
属性使对象可用,我提供了一个点,应用的其他部分可以在这里注册事件。
提示你不必创建一个
eventMixin
对象的新实例。一个实例在想要使用该对象的应用的任何部分之间共享,eventMixin
代码将不同类型事件的侦听器分开。你可以通过查看 Visual Studio 项目参考中的base.js
文件来了解微软是如何实现这个特性的。
创建视图模型状态
我的下一步是创建让我维护应用状态的代码,我已经通过添加一个名为viewmodel.js
的新文件到js
文件夹中完成了。您可以在清单 30-2 中看到该文件的内容。
清单 30-2 。viewmodel.js 文件的内容
`(function () {
WinJS.Namespace.define(“ViewModel”, {
State: WinJS.Binding.as({
pictureDataSource: new WinJS.Binding.List(),
fileTypes: false,
depth: false,
thumbnails: false,
}),
});
WinJS.Namespace.define(“Converters”, {
display: WinJS.Binding.converter(function(val) {
return val ? “block” : “none”;
})
});
})();`
名称空间ViewModel.State
包含了WinJS.Binding.List
对象,我将使用它作为数据源,通过 WinJS FlipView
和ListView
UI 控件来显示图片。我还定义了一组可观察的属性,我将使用它们来跟踪用户当前在应用中启用了哪些功能——这不同于用户是否许可了它们。我需要跟踪用户使用某个特性的权限以及它当前是否开启,而ViewModel.State
名称空间中的属性跟踪后者。
提示这种方法要求我在启用
ViewModel.State
名称空间中的相应属性之前,检查用户是否有权使用某个特性。这就是ViewModel.Store.checkCapability
函数的用途,当我向您展示/js/default.js
文件的内容时,您将很快看到我是如何处理它的。
定义布局
为了创建我在图 30-5 中展示的布局,我将清单 30-3 中显示的元素添加到default.html
文件中。这个应用不需要任何导航或内容页面,因为它需要这样一个简单的布局。
清单 30-3 。在 default.html 文件中定义示例应用的标记
`
**
**** <div id=“depth” data-win-control=“WinJS.UI.ToggleSwitch”**
** data-win-bind=“winControl.checked: State.depth”**
** data-win-options=“{title: ‘All Folders’, labelOn: ‘Yes’, labelOff: ‘No’}”>**
** **
** <div id=“thumbnails” data-win-control=“WinJS.UI.ToggleSwitch”**
** data-win-bind=“winControl.checked: State.thumbnails”**
** data-win-options=“{title: ‘Show Thumbnails’,labelOn: ‘Yes’, labelOff: ‘No’}”>**
** **
** Refresh**
** Buy/Upgrade**
**
** <div id=“flipView” data-win-control=“WinJS.UI.FlipView”**
** data-win-options=“{ itemTemplate: flipTemplate,**
** itemDataSource: ViewModel.State.pictureDataSource.dataSource}”>**
**
** <div id=“listView” data-win-control=“WinJS.UI.ListView”**
** data-win-bind=“style.display: State.thumbnails Converters.display”**
** data-win-options=“{ itemTemplate: listTemplate,**
** tapBehavior: invokeOnly,**
** itemDataSource: ViewModel.State.pictureDataSource.dataSource}”>**
** **
** ** `
标记可以分为三类。第一类是进入左侧面板的控制元素(由属性为buttonsContainer
的div
元素表示)。除了标准的 HTML button
元素,我还应用了WinJS.UI.ToggleSwitch
控件,我在第十一章的中描述过。您可以在图 30-6 中看到详细的控制按钮,并且您会注意到这些按钮代表了我将作为升级出售给用户的功能。
***图 30-6。*控制元素
我需要某种方式让用户在试用期间购买基本功能或升级到全套功能,这就是我添加Buy/Upgrade
按钮的原因。您可以看到我是如何将ToggleSwitch
控件的 checked 属性链接到ViewModel.State
名称空间中的可观察值的。
下一部分标记包含在div
元素中,该元素的id
属性是imageContainer
。我使用一个FlipView
控件作为主图像显示,一个ListView
控件显示缩略图。我在第十四章和第十五章中介绍了这些 WinJS UI 控件,我在这个应用中对它们的使用非常简单和标准。
标记的最后一部分是用于ListView
和FlipView
控件的模板。这些使用了我在第八章的中描述的WinJS.Binding.Template
功能。两个模板都有一个img
元素,对于用于FlipView
控件的模板,我使用一个div
元素来显示当前显示文件的名称。在图 30-7 中,您可以看到控件和模板是如何组合成应用布局的右侧面板的。
***图 30-7。*示例应用的右侧面板
定义 CSS
这个例子的 CSS 非常简单——我非常依赖 flex box 布局来创建一个适应不同屏幕分辨率、设备方向和布局的应用。你可以在清单 30-4 的文件中看到我定义的样式。
清单 30-4 。/css/default.css 文件的内容
`body {display: -ms-flexbox; -ms-flex-direction: row;}
div.container { border: medium solid white; padding: 20px; margin: 20px;
display: -ms-flexbox; -ms-flex-direction: column; -ms-flex-align: stretch;
-ms-flex-pack: center;}
div[data-win-control=‘WinJS.UI.ToggleSwitch’] { border: thin solid white;
padding: 20px; margin: 10px 0;}
#buttonContainer {-ms-flex-pack: start;}
#buttonContainer button { margin: 10px 0;}
#imageContainer {-ms-flex: 2;}
#flipView {-ms-flex: 2;}
#listView { -ms-flex: 1; max-height: 200px; min-height: 200px; border: thin solid white;
display: none;}
.flipItem {display: -ms-flexbox;-ms-flex-direction: column;-ms-flex-align: center;
-ms-flex-pack: center; width: 100%; height: 100%;}
.flipImg { position: relative; top: 0; left: 0; height: calc(100% - 70px); z-index: 15;}
.flipTitle { font-size: 30pt;}
.listItem img { width: 250px; height: 200px;}
@media print {
#buttonContainer, #listView { display: none;}
}
@media screen and (-ms-view-state: snapped) {
#buttonContainer { display: none;}
}
@media screen and (-ms-view-state: fullscreen-portrait) {
body { -ms-flex-direction: column-reverse;}
#buttonContainer { -ms-flex-direction: row; -ms-flex-pack: distribute;}
#buttonContainer button {display: none;}
}`
在打印时,以及当应用处于纵向和快照布局时,我使用媒体查询来更改应用布局。不同的布局与 Windows 应用商店集成没有直接关系,但为了完整起见,我在布局中添加了一些变化。
定义 JavaScript 代码
/js/default.js
文件包含将布局的不同部分联系在一起并显示图像的代码。你可以在清单 30-5 中看到这个文件的内容。
清单 30-5 。default.js 文件的内容
`(function () {
“use strict”;
WinJS.Binding.optimizeBindingReferences = true;
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var storage = Windows.Storage;
var search = storage.Search;
app.onactivated = function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
if (args.detail.previousExecutionState !==
activation.ApplicationExecutionState.suspended) {
refresh.addEventListener(“click”, function (e) {
loadFiles();
});
WinJS.Utilities.query(“#buttonContainer > div”).listen(“change”,
function (e) {
if (ViewModel.Store.checkCapability(e.target.id)) {
ViewModel.State[e.target.id] = e.target.winControl.checked;
if (e.target.id == “thumbnails”) {
listView.winControl.itemDataSource
= ViewModel.State.pictureDataSource.dataSource;
} else {
setImmediate(loadFiles);
}
} else {
e.target.winControl.checked = false;
}
});
listView.addEventListener(“iteminvoked”, function (e) {
flipView.winControl.currentPage = e.detail.itemIndex;
});
flipView.addEventListener(“pageselected”, function (e) {
var index = flipView.winControl.currentPage;
listView.winControl.ensureVisible(index);
});
}
args.setPromise(WinJS.UI.processAll().then(function() {
return WinJS.Binding.processAll(document.body, ViewModel)
.then(function () {
setupPrinting();
loadFiles();
});
}));
}
};
function setupPrinting() {
Windows.Graphics.Printing.PrintManager.getForCurrentView().onprinttaskrequested =
function (e) {
if (ViewModel.Store.checkCapability(“print”)
&& ViewModel.State.pictureDataSource.length > 0) {
var printTask = e.request.createPrintTask(“PrintAlbum”,
function (printEvent) {
printEvent.setSource(
MSApp.getHtmlPrintDocumentSource(document));
});
printTask.options.orientation
= Windows.Graphics.Printing.PrintOrientation.landscape;
};
};
}
function loadFiles() {
var options = new search.QueryOptions();
options.fileTypeFilter.push(“.png”);
if (ViewModel.State.fileTypes) {
options.fileTypeFilter.push(“.jpg”, “.jpeg”);
}
if (ViewModel.State.depth) {
options.folderDepth = search.FolderDepth.deep;
} else {
options.folderDepth = search.FolderDepth.shallow;
}
storage.KnownFolders.picturesLibrary.createFileQueryWithOptions(options)
.getFilesAsync().then(function (files) {
var list = ViewModel.State.pictureDataSource;
list.dataSource.beginEdits();
list.length = 0;
files.forEach(function (file) {
list.push({
image: URL.createObjectURL(file),
title: file.displayName
});
});
list.dataSource.endEdits();
})
};
app.start();
})();`
这个例子中需要注意的重要一点是,ViewModel.State
名称空间中的属性控制应用行为的方式。例如,在loadFiles
函数中,定位的文件类型和搜索文件的深度由ViewModel.State.fileTypes
和ViewModel.State.depth
属性驱动,如下所示:
... if (**ViewModel.State.fileTypes**) { options.fileTypeFilter.push(".jpg", ".jpeg"); } if (**ViewModel.State.depth**) { options.folderDepth = search.FolderDepth.deep; } else { options.folderDepth = search.FolderDepth.shallow; } ...
这些属性由应用布局中的ToggleSwitch
控件设置,但是只有在成功调用ViewModel.Store.checkCapability
函数后,这些值才会更改,如下所示:
... if (**ViewModel.Store.checkCapability(e.target.id)**) { ViewModel.State[e.target.id] = e.target.winControl.checked; if (e.target.id == "thumbnails") { listView.winControl.itemDataSource= ViewModel.State.pictureDataSource.dataSource; } else { setImmediate(loadFiles); } } else { e.target.winControl.checked = false; } ...
为了简单起见,我将每个ToggleSwitch
控件的id
设置为我想要检查的功能的名称,并在ViewModel.State
名称空间中使用相同的属性名称。实现业务模型代码可能会变得复杂,在你的应用组件和你销售的产品和升级之间实现尽可能多的通用性会让生活变得容易得多。
也就是说,在很大程度上,这些代码使用了我在本书中向您展示的技术。我不想详细讨论代码,因为我已经描述了各种特性是如何工作的。万一有什么东西吸引了你的注意,而你又找不到它,我在表 30-2 中列出了代码的关键特性,你可以在本书的什么地方找到它们。
更新清单
创建基本应用的最后一步是更新清单。为了访问Pictures
库中的文件,我需要打开package.appxmanifest
文件,导航到Capabilities
部分,勾选Pictures Library
选项,如图图 30-8 所示。
***图 30-8。*允许访问清单中的图片库
我还在images
文件夹中添加了一些新文件,用于磁贴和闪屏。我添加的所有文件都显示相同的图标,在透明背景上绘制成白色。你可以在图 30-9 中看到该图标,它显示在黑色背景上,以便在页面上可见。
***图 30-9。*用于示例应用的图标
我添加的文件被命名为tile<size>.png
,其中<size>
是以像素为单位的图像宽度。你可以在图 30-10 中看到我是如何将图像应用到应用中的,图中显示了货单的Application UI
部分以及我所做的更改。
***图 30-10。*更改应用使用的磁贴和闪屏图像
测试示例应用
本章剩下的工作就是测试示例应用。使用 Visual Studio Debug
菜单中的Start Debugging
项启动应用。默认情况下,ViewModel.State
命名空间中的属性设置为false
,这意味着您将看到基本的应用布局,如图图 30-11 所示。
***图 30-11。*基础 app 布局
当然,当你运行应用时,你会看到什么取决于你的Pictures Library
的内容。当应用首次启动时,它将只显示根Pictures
目录中的 PNG 文件,这将限制显示的内容。在基本模式下,您可以通过滑动FlipView
或点击其导航按钮来浏览图像。
通过启用ToggleSwitch
控件,您可以扩大显示图像的范围,以便包含 JPG 文件和Pictures Library
中更深层次的文件。当然,还会显示ListView
控件,显示可用图像的缩略图。
Refresh
按钮将清除数据源的内容,并从磁盘重新加载文件。此时Buy/Upgrade
按钮没有任何作用,但是我会在第三十二章中连接它,这样用户就可以进行购买。
总结
在本章中,我开始了创建和发布应用的过程,首先为我的应用保留名称,然后构建将提供给用户的基本功能。从一开始,我就添加了检查用户是否购买了应用关键功能的支持,我将在下一章实现这些检查背后的策略,我还将向您展示如何将您的应用集成到 Windows 应用商店中。
三十一、Windows 应用商店集成
在本章中,我将向您展示如何将您的应用与 Windows 应用商店集成,并实现您的应用商业模式。我将向您展示提供商店功能访问的对象,以及如何在您的应用发布到商店之前使用它们来模拟不同的许可场景。我将向您展示如何确定您的用户有权使用哪些功能,以及如何强制实施该模型来阻止在没有合适许可证的情况下使用应用。
您会发现,与 Windows 商店集成的技术相当简单,但是实施业务模型和模拟不同的场景可能相当复杂,需要大量的测试。
关于应用许可的一般建议
在我开始为我的示例应用实现和实施业务模型之前,我想提供一些一般性的建议。期望你的用户付费使用你的应用是完全合理和公平的,但你需要对你提供的功能的价值和你所处的世界持现实态度。
你必须接受你的应用会被广泛盗版。它会在发布后的几个小时内出现在意想不到的地方,任何许可执行和权限管理都会立即被破坏。用户(如果你有一个特别吸引人的应用,会有很多用户)会从你的辛勤工作中获益,而不用付给你一分钱。
如果这是你的第一个应用,你会经历一系列常见的情绪:震惊、沮丧、愤怒和不公平感。你可能会非常沮丧,以至于更新你的应用来添加更复杂的许可方案,或者要求定期检查一些额外的验证服务。这是你能做的最糟糕的事情。
每次你添加额外的权限管理和许可层,你就让你的应用更难使用。但你实际上并没有让复制变得更难,因为没有一个现存的方案能够抵挡住想要免费拷贝的人的关注。你唯一惩罚的人是你的合法用户,他们现在不得不千方百计让你的应用工作。在竞争激烈的市场中,你希望消除尽可能多的障碍来吸引用户使用你的应用——而你增加的每一个使用障碍都会让竞争对手的应用更有吸引力。
一个更明智的方法是接受人们总是会抄袭你的软件,并花些时间考虑为什么会这样。
你可能想知道是什么让我觉得我可以告诉你让所有那些罪犯敲诈你——但这是我花了很多时间思考的事情,因为它每天都影响着我。
书籍和应用有很多共同点
我的书在出版后几小时内就出现在文件共享网站上。这种情况已经发生了很多年,我开始明白,这并不是第一次出现的问题。
首先,从纯实践的角度来看,我没有办法阻止我的书被分享,即使我想这样做。第二,我开始意识到人们下载非法拷贝有各种原因,其中一些可能有利于我的长期销售数字。以下是我为自己的书考虑的几大类别:
- 收集者
- 快速修补程序
- 预算员
- 疼痛回避者
收藏者分享我的书是因为他们可以——他们喜欢收集大型图书馆的书籍、音乐、应用、游戏以及任何他们感兴趣的东西。对我来说,这些人并不代表销售的损失,因为他们从一开始就不会买一本,以后也不会。
速战速决者是那些有特定问题想要解决的人,他们会复制我的一本书,看看里面是否有解决方案。这些人也不代表销售失败,因为对他们来说,一个特定问题的解决方案并不代表 20-50 美元的价值。但是这些人确实代表了潜在的未来客户——他们可能记得他们发现我的书很有用,并为他们想更深入了解的主题购买了一本(或我的另一本著作)。
预算者是那些可能更喜欢买一本,但又买不起的人。书可能很贵,如果手头紧的话,书有多重要也没关系。这些人是也是潜在客户,因为他们现在可能破产了,但情况不会总是这样。当他们有更多的钱时,他们可能会开始买书,我希望他们在买书时对我的书有正面的感觉。
痛苦回避者是那些想要内容,但无法以适合他们的形式获得的读者。他们希望他们的内容以一种特定的方式或以一种特定的方式传递,而我和 Apress 不会给他们。所以他们转向文件共享,因为它以他们需要的方式给了他们所需要的东西。这些人是潜在的客户,无论是当我的作品以他们想要的方式出现时,还是当他们的需求发生变化时。
所以,总的来说,有一类人会抄袭我的书,因为他们可以也永远不会买一本,还有三类人今天会抄袭我的书,但将来可能会成为大客户。他们可能还会向其他人推荐我的书,这些人可能会付费购买一本(这种情况比你想象的更常见)。
收藏家对我来说是一个失败的事业,所以我不会因为他们而失眠。其他类型的文件复印机是我想培养的人,让他们体验我的内容,希望我将来能从他们身上赚些钱。我可以诚实地说,我从未把我的任何一本书放在文件共享网站上,但我很想这么做,因为我认为这有助于确保未来的收入。
同样重要的是,让我的书更难用并不会让任何一个复印机去买书。速战速决者会在别处找到解决方案,预算者不会有更多的闲钱,所以他们只会抄袭别人的书,而逃避痛苦者仍然无法以他们想要的方式获得内容。他们永远不会知道他们是否喜欢我的写作风格,因为他们永远也不会看到——我现在和将来都不会从他们那里得到任何销售。
关注重要的事情
我试着考虑他们想要什么,而不是试图惩罚复印机。需要快速修复吗?我在每章的开头添加了汇总表,以便更容易找到具体的解决方案。没有现金吗?我写了 Windows Revealed 的书,花几美元就能让你快速入门。需要一种特定格式的电子书?我与 Apress 合作,它提供了一系列不同的无 DRM 格式。
我对文件共享者的回应是努力让我的书更有吸引力、更有用,而不是更难使用。而且,如果我成功了,我会取悦我的付费读者,因为他们希望快速找到解决方案,获得一系列电子书格式,并获得新主题的廉价有效的快速入门书籍。
我没有浪费时间去烦恼,而是为我的书被如此广泛地分享而暗自自豪,并一直希望今天抄袭我的书的人明天会为这些书付高价。
我给你的建议是,对你的 Windows 应用商店应用采取类似的方法。想想为什么人们复制它们,并试图为那些未来可能购买的人增加价值,同时为现在已经购买的人增加价值。你不能停止文件复制,所以你可以很好地接受它,并认为它是真正的自由市场暴露。
我不希望人们抄袭我的书,但如果他们不打算买一本,我宁愿他们抄袭我的书,而不是竞争对手的书,对书来说是真的,对应用来说也是真的。
处理基本的商店场景
实现应用业务模型的过程有点奇怪,因为您创建了一系列代表不同许可场景的 XML 文件,然后在您的应用中实现处理它们的代码。这些文件被称为场景文件,代表当你的应用在商店中发布并被用户下载或购买时,你的应用可以访问的数据。在本节中,我将创建一个非常基本的场景文件来介绍该格式,向您展示如何访问该场景所代表的数据,并在应用中对其进行响应。
创建场景文件
我已经在项目中添加了一个名为store
的新文件夹,我将在其中放置场景文件。我的第一个场景描述了用户下载了应用,但没有购买任何应用内升级或订阅的情况。对于我的业务模型,这意味着我对表示三种场景感兴趣:
- 用户购买了基本应用功能的永久许可证。
- 用户下载了尚未过期的免费试用版。
- 用户下载了一个已经过期的免费试用版。
为了表示和测试这些场景,我在 store 文件夹中创建了一个名为initial.xml
的新文件,其内容可以在清单 1 中看到。
清单 1。/store/initial.xml 的内容
<?xml version="1.0" encoding="utf-16" ?> <CurrentApp> <ListingInformation> <App> <AppId>e4bbe35f-0509-4cca-a27a-4ec43bed783c</AppId> <LinkUri>http://apress.com</LinkUri> <CurrentMarket>en-US</CurrentMarket> <AgeRating>3</AgeRating> <MarketData xml:lang="en-us">
<Name>Simple Photo Album</Name> <Description>An app to display your photos</Description> <Price>4.99</Price> <CurrencySymbol>$</CurrencySymbol> <CurrencyCode>USD</CurrencyCode> </MarketData> </App> </ListingInformation> <LicenseInformation> <App> <IsActive>true</IsActive> <IsTrial>false</IsTrial> <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate> </App> </LicenseInformation> </CurrentApp>
这个文件有两个部分,包含在CurrentApp
元素中。ListingInformation
元素包含 Windows Store 上应用列表的详细信息,而LicenseInformation
元素包含用户获得该软件的许可证的详细信息。我将在接下来的小节中解释这两个元素。
注意 Windows 对场景文件的内容非常挑剔。确保
CurrentApp
元素关闭后没有内容,包括空行。如果应用在启动时崩溃,请尝试从 Visual Studio 重新加载应用,如果这不起作用,请仔细查看您的场景文件,看看您是否添加了 Windows 不期望的内容。
了解清单部分
场景文件的ListingInformation
部分包含了应用的描述,并为您提供了应用如何在商店中显示给用户的详细信息。我不太注意场景文件的这一部分,尽管当我为我想要提供的应用内升级和订阅创建定义时,我会在第三十二章中添加它。在表 1 中,我描述了商店整合和测试过程中的各种元素及其效果。
虽然你需要在一个场景文件中为这些元素创建值,但是ListingInformation
部分中的值取自你发布应用时提供的信息,我将在第三十三章中演示。使用什么值进行测试并不重要,它们是必需的,但这些值只是真实数据的占位符,您不必使用您计划为真实 Windows 应用商店部署提供的相同值。
注意【Windows 应用商店集成期间的一个常见问题是为
AppId
元素使用格式错误的值。我发现最简单的方法是从应用清单中获取值。打开清单,导航到Packaging
选项卡,从Package name
字段复制值。
了解许可部分
LicenseInformation
元素包含用户为应用及其升级获得的所有许可证的详细信息。对于我最初的例子,我只为应用本身定义了一个测试许可证,我已经在清单 2 中重复了。正是这一部分,我将改变创造不同的场景,我想迎合。
清单 2。来自 initial.xml 场景文件的许可信息
... <LicenseInformation> <App> <IsActive>true</IsActive> <IsTrial>false</IsTrial> <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate> </App> </LicenseInformation> ...
我在表 2 中描述了场景文件这一部分的元素。
对于我的初始场景,我已经将IsActive
元素设置为true
,将IsTrial
元素设置为false
——这种组合代表了用户已经购买了应用的基本功能的情况。
使用许可信息
现在我已经定义了场景文件,我可以在我的应用中使用它了。我希望尽可能将许可证的管理与应用中的其他代码分开,这样我就可以在一个地方改变商业模式。在 Windows 商店中销售应用和应用内升级的方式有很大的灵活性,并且您可能不会第一次就获得可选功能的定价和组合,因此在您将进行更改的基础上进行编码是有意义的,并且您希望尽可能简化该过程。
首先,我对/js/store.js
文件做了一些补充,如清单 3 所示。只有几行新的代码,但是有许多新的对象和技术要介绍,所以我将在接下来的小节中一步一步地分解这些新增内容。
清单 3。添加到/js/store.js 文件
`(function() {
** var storage = Windows.Storage;**
** var licensedCapabilities = {**
** basicApp: false,**
** }**
WinJS.Namespace.define(“ViewModel.Store”, {
events: WinJS.Utilities.eventMixin,
checkCapability: function (name) {
** var available = licensedCapabilities[name] != undefined**
** ? licensedCapabilities[name] : true;**
setImmediate(function () {
ViewModel.Store.events.dispatchEvent(“capabilitycheck”,
{ capability: name, enabled: available });
});
return available;
},
** currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,**
** loadLicenseData: function () {**
** var url = new Windows.Foundation.Uri(“ms-appx:///store/initial.xml”);**
** return storage.StorageFile.getFileFromApplicationUriAsync(url)**
** .then(function (file) {**
** return ViewModel.Store.currentApp.reloadSimulatorAsync(file);**
** });**
** },**
});
** ViewModel.Store.currentApp.licenseInformation.addEventListener(“licensechanged”,**
** function () {**
** var license = ViewModel.Store.currentApp.licenseInformation;**
** licensedCapabilities.basicApp = license.isActive;**
** });**
})();`
跟踪能力权利
我对store.js
文件做的第一个添加是定义一个名为licensedCapabilities
的对象,我将使用它来跟踪用户对应用中各个功能区域的权限——尽管最初我只跟踪应用的基本功能,如下所示:
... var licensedCapabilities = { basicApp: false, } ...
即使你的应用的功能与你要出售的应用内升级完全匹配,也要跟踪用户使用你的应用的功能的权利,这一点很重要,我设计的示例应用就是这种情况。
这有两个原因:首先,您可能需要稍后进行更改,并且您希望使您的代码过于依赖您最初定义的许可证。第二,你很少会得到一个完美的匹配,所以,作为一个例子,我打算提供一个名为The Works
(如第三十章中的所述)的升级,它将允许用户访问所有的应用功能,因此在这个升级的许可证和应用功能之间没有直接的映射——你可以在第三十二章中看到我如何处理这个轻微的不匹配。
添加了licensedCapabilities
对象后,我可以更新Windows.Store.checkCapabilities
方法,以便它开始反映用户的权限,如下所示:
... checkCapability: function (name) { ** var available = licensedCapabilities[name] != undefined ? licensedCapabilities[name]** ** : true;** setImmediate(function () {
ViewModel.Store.events.dispatchEvent("capabilitycheck", { capability: name, enabled: available }); }); return available; }, ...
我用粗体标出了关键语句。如果在licensedCapabilities
中有一个属性对应于被检查的能力,那么我使用licensedCapabilities
值来响应checkCapability
调用。
请注意默认值的不同。在licensedCapabilities
对象中,我将basicApp
属性的值设置为false
,这将默认拒绝用户访问该功能。然而,在checkCapability
方法中,如果在licensedCapabilities
对象中没有相应的值,我将返回true
。我这样做是为了让我定义的功能总是需要一个许可,但是如果我忘记定义一个功能,我不会通过禁用一个即使他们愿意付费也不能激活的功能来破坏用户的体验。
提示这可能会让你觉得奇怪的谨慎,但是我已经写了一些这样的 Windows Store 实现,它们变得非常复杂——如果你忘记连接一个应用功能,在慷慨方面犯错误会带来更好的体验。
获取当前应用的数据
用于管理 Windows 应用商店集成的对象位于Windows.ApplicationModel.Store
名称空间中。这个关键对象叫做CurrentApp
,它提供了对从 Windows 商店获得的许可证信息的访问,并定义了允许您启动应用和升级购买流程的方法。表 3 显示了CurrentApp
对象定义的方法和属性,我将在本章中使用它们来管理示例应用。
我将在这一章中使用关键方法和属性,我将在使用它们时引入来自Windows.ApplicationModel.Store
名称空间的其他对象。然而,CurrentApp
对象有一个问题——它只有在应用发布后用户从商店下载了应用时才有效。
出于集成和测试的目的,您必须使用CurrentAppSimulator
对象,它也包含在Windows.ApplicationModel.Store
名称空间中。CurrentAppSimulator
对象定义了CurrentApp
定义的所有方法和属性,但是作用于场景文件。您使用CurrentAppSimulator
对象,直到您准备好发布您的应用,此时您替换应用中的引用,以便您使用CurrentApp
对象。
我想尽可能简单地完成从集成到发布的过渡,所以我定义了一个对CurrentAppSimulator
类的引用,这样以后我只需要做一个修改。您可以看到我添加到ViewModel.Store
名称空间的引用,如下所示:
`…
WinJS.Namespace.define(“ViewModel.Store”, {
events: WinJS.Utilities.eventMixin,
checkCapability: function (name) {
// …code removed for brevity…
},
** currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,**
loadLicenseData: function () {
// …code removed for brevity…
},
});
…`
我会在任何需要使用CurrentApp/CurrentAppSimulator
功能的时候引用ViewModel.Store.currentApp
属性,当我准备好发布时只做一个改变(我会在第三十三章中演示)。
除了那些由CurrentApp
定义的方法之外,CurrentAppSimulator
对象还定义了一个额外的方法。这个方法叫做getFileFromApplicationUriAsync
,它将一个代表场景文件的StorageFile
对象作为它的参数。该方法在文件中加载 XML 元素,并使用它们来模拟许可场景,因此您可以在将应用发布到应用商店之前实现您的业务模型。
为了加载场景文件,我在ViewModel.Store
名称空间中定义了一个名为loadLicenseData
的函数,如下所示:
`…
WinJS.Namespace.define(“ViewModel.Store”, {
events: WinJS.Utilities.eventMixin,
checkCapability: function (name) {
// …code removed for brevity…
},
currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,
** loadLicenseData: function () {**
** var url = new Windows.Foundation.Uri(“ms-appx:///store/initial.xml”);
return storage.StorageFile.getFileFromApplicationUriAsync(url)**
** .then(function (file) {**
** return ViewModel.Store.currentApp.reloadSimulatorAsync(file);**
** });**
** },**
});
…`
我使用 URL 获得一个StorageFile
对象,然后调用reloadSimulatorAsync
方法从我的/store/initial.xml
文件中加载数据。
响应许可变更
我对/js/store.js
文件做的最后一个添加是为licensechanged
事件添加一个处理函数。当许可证信息发生变化时(例如当用户进行购买时)以及当使用CurrentAppSimulator
对象加载一个场景文件时,该事件被触发。下面是我定义的处理函数:
... ViewModel.Store.currentApp.**licenseInformation**.addEventListener("**licensechanged**", function () { var license = ViewModel.Store.currentApp.licenseInformation; licensedCapabilities.basicApp = license.isActive; } ); ...
CurrentApp.licenseInformation
属性返回一个LicenseInformation
对象。该对象定义了licensechanged
事件,我已经为其添加了处理程序。
当事件被触发时,我再次读取CurrentApp.licenseInformation
属性的值以获得LicenseInformation
对象,该对象定义了我在表 4 中描述的属性。
从表中可以看出,场景文件中的元素直接对应于由LicenseInformation
对象定义的属性,这使得创建和测试一系列不同的许可情况变得相对容易。在我的处理函数中,我将licenseCapabilities
对象中basicApp
属性的值设置为LicenseInformation.isActive
属性的值。这意味着如果用户拥有有效的许可证,对basicApp
功能的ViewModel.Store.checkCapability
方法的调用将返回true
。
加载场景数据
为了加载我的场景数据,我从/js/default.js
文件中添加了对loadLicenseData
方法的调用,如清单 4 所示。
清单 4。确保应用启动时加载许可信息
... app.onactivated = function (args) { if (args.detail.kind === activation.ActivationKind.launch) { if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.suspended) { // *...code removed for brevity...* } args.setPromise(WinJS.UI.processAll().then(function() { return WinJS.Binding.processAll(document.body, ViewModel) .then(function () { ** return ViewModel.Store.loadLicenseData().then(function () {** setupPrinting(); loadFiles(); ** });** }); })); } }; ...
我调用ViewModel.Store.loadLicenseData
方法作为Promise
对象链的一部分,这些对象将在应用启动时闪屏被移除之前完成。这可确保在向用户显示应用功能之前加载我的许可证数据。
实施许可政策
在这一点上,我已经建立了对 Windows Store 的支持,因此如果在场景文件中有应用本身的有效许可证,我的checkCapabilities
方法将返回true
,但是测试这个特性非常令人失望,因为我没有在应用的任何地方强制执行许可证。在这一部分,我将开始充实执行我的商业模式的不同方面的代码,通过只允许用户访问应用的基本功能,如果他们已经购买了许可证或如果他们正在使用免费试用。
触发能力检查
我需要做的第一件事是调用checkCapability
方法来查看用户是否有权使用basicApp
功能。我已经在/js/default.js
文件中这样做了,如清单 5 所示。
清单 5。检查用户是否有权使用基本应用功能
... app.onactivated = function (args) { if (args.detail.kind === activation.ActivationKind.launch) { if (args.detail.previousExecutionState !==
activation.ApplicationExecutionState.suspended) { // *...code removed for brevity...* } args.setPromise(WinJS.UI.processAll().then(function() { return WinJS.Binding.processAll(document.body, ViewModel) .then(function () { return ViewModel.Store.loadLicenseData().then(function () { setupPrinting(); loadFiles(); ** ViewModel.Store.checkCapability("basicApp");** }); }); })); } }; ...
您会注意到,虽然我调用了checkCapability
方法,但是我并没有对结果做任何事情。我依赖于事件capabilitycheck
事件,每当调用checkCapability
方法时就会触发该事件。我将在执行基本应用功能策略的代码中处理这个事件,我已经在一个名为/js/storeInteractions.js
的新文件中定义了它。我为storeInteractions.js
文件在default.html
文件中添加了一个新的script
元素,如清单 6 所示。
清单 6。向 default.html 文件添加新的脚本元素
`…
我定义了一个新文件,因为执行这种策略的代码往往冗长且重复,我希望将它与store.js
文件中的代码分开。您可以在清单 7 中看到storeInteractions.js
文件的内容。
清单 7。storeInteractions.js 文件的初始内容
(function () {
` var pops = Windows.UI.Popups;
ViewModel.Store.events.addEventListener(“capabilitycheck”, function (e) {
if (e.detail.capability == “basicApp”) {
if (ViewModel.Store.currentApp.licenseInformation.isTrial &&
e.detail.enabled) {
** // user has a trial period which has not expired**
} else if (!e.detail.enabled) {
** // user has a trial period which has expired**
}
}
});
function buyApp() {
** // code to purchase the app will go here**
** return WinJS.Promise.wrap(true);**
}
})();`
作为调用我添加到default.js
中的ViewModel.Store.checkCapability
方法的结果,将调用capabilitycheck
事件的处理函数。我对这个代码文件中的两个场景感兴趣。第一是用户正在使用尚未过期的 app 免费试用,第二是免费试用已经过期。您可以在清单中看到我是如何结合事件对象和LicenseInformation
对象的细节来处理这些场景的。
注意我对基本应用功能的第三个设想是,用户已经购买了许可证。在这种情况下,我什么都不做,因为我想远离我的付费客户。嗯,至少在他们尝试使用需要升级的功能之前是这样,但是我会在第三十二章回到这个话题。
在接下来的小节中,我将填充事件处理函数中当前包含注释的三个部分。
处理有效试用期
我想借此机会提醒用户,他们正在试用我的应用,让他们有机会在启动应用时购买许可证。您可以在清单 8 的中看到我是如何做到这一点的,其中我展示了我对storeInteractions.js
文件所做的添加。
清单 8。提示用户购买应用
`(function () {
var pops = Windows.UI.Popups;
ViewModel.Store.events.addEventListener(“capabilitycheck”, function (e) {
if (e.detail.capability == “basicApp”) {
if (ViewModel.Store.currentApp.licenseInformation.isTrial
&& e.detail.enabled) {
** var daysLeft = Math.ceil(
(ViewModel.Store.currentApp.licenseInformation.expirationDate**
** - new Date()) / (24 * 60 * 60 * 1000));**
** var md = new pops.MessageDialog(“You have " + daysLeft**
** + " days left in your free trial”);**
** md.title = “Free Trial”;**
** md.commands.append(new pops.UICommand(“OK”));**
** md.commands.append(new pops.UICommand(“Buy Now”));**
** md.defaultCommandIndex = 0;**
** md.showAsync().then(function (command) {**
** if (command.label == “Buy Now”) {**
** buyApp();**
** }**
** });**
} else if (!e.detail.enabled) {
// user has a trial period which has expired
}
}
});
function buyApp() {
// code to purchase the app will go here
return WinJS.Promise.wrap(true);
}
})();`
我计算出用户的试用期还剩多少天,并使用Windows.UI.Popups.MessageDialog
显示这些信息,我在第十三章的中对此进行了描述。MessageDialog
有一个OK
按钮可以关闭弹出窗口,还有一个Buy Now
按钮可以调用buyApp
功能(我很快就会实现)。
测试场景
我需要修改我的场景文件来测试这个新代码,创建一个用户有一个试用期的场景。在清单 9 的中,您可以看到我是如何修改/store/initial.xml
文件来创建我想要的环境的。
清单 9。更改 initial.xml 文件中的场景
... <LicenseInformation> <App> <IsActive>true</IsActive> <IsTrial>**true**</IsTrial> <ExpirationDate>**2012-09-30T00:00:00.00Z**</ExpirationDate> </App> </LicenseInformation> ...
我已经将IsTrial
元素的值更改为true
,并确保ExpirationDate
元素包含一个未来几天的日期。我在 2012 年 9 月 22 日写这一章,所以我在清单中指定的日期是未来 8 天。当您测试这个示例时,您将需要更改数据。
当你启动 app 时,你会看到一条消息,告诉你试用还剩多少天,如图图 1 所示。
***图 1。*向用户显示试用期还剩多少时间
点击OK
按钮关闭对话框,让用户继续使用应用。点击Buy Now
按钮调用我在storeInteractions.js
文件中定义的buyApp
函数,当我在下一节实现它时,它将启动购买过程。有了这个新功能,我可以提醒用户,他们只有一个试用期,让他们有机会尽早购买应用。
处理过期的试用期
我想阻止用户使用该应用时,试用期已过,只提供他们购买的机会。我使用另一个MessageDialog
来做这件事,如清单 10 中的所示。
清单 10。处理过期的试用期
`(function () {
var pops = Windows.UI.Popups;
ViewModel.Store.events.addEventListener(“capabilitycheck”, function (e) {
if (e.detail.capability == “basicApp”) {
if (ViewModel.Store.currentApp.licenseInformation.isTrial
&& e.detail.enabled) {
var daysLeft = Math.ceil(
(ViewModel.Store.currentApp.licenseInformation.expirationDate
- new Date()) / (24 * 60 * 60 * 1000));
var md = new pops.MessageDialog(“You have " + daysLeft
+ " days left in your free trial”);
md.title = “Free Trial”;
md.commands.append(new pops.UICommand(“OK”));
md.commands.append(new pops.UICommand(“Buy Now”));
md.defaultCommandIndex = 0;
md.showAsync().then(function (command) {
if (command.label == “Buy Now”) {
buyApp();
}
});
} else if (!e.detail.enabled) {
** var md = new pops.MessageDialog(“Your free trial has expired”);**
** md.commands.append(new pops.UICommand(“Buy Now”));**
** md.showAsync().then(function () {**
** buyApp().then(function (purchaseResult) {**
** if (!purchaseResult) {**
** ViewModel.Store.checkCapability(“basicApp”);**
** }**
** });**
** });**
}
}
});
function buyApp() {
// code to purchase the app will go here
return WinJS.Promise.wrap(true);
}
})();`
我向用户显示的对话框通知他们试用已经过期,并提供给他们一个Buy Now
按钮。当按钮被点击时,我调用buyApp
函数,它将负责启动购买过程。该函数返回一个Promise
对象,该对象在流程完成时实现,如果购买成功,则产生true
,如果没有购买,则产生false
(这可能有多种原因,包括用户取消交易、未能提供有效的支付形式,或者因为无法访问 Windows Store,没有连接的设备无法进行购买)。
提示在开发免费试用的应用时,我喜欢加上一个宽限期。几年前,我想在一次长途飞行中使用一个试用应用,却发现它已经过期,并且我的连接能力不足意味着我无法升级,尽管我愿意这样做。相反,我对开发人员决定不再购买应用的硬性规定感到恼火。因此,对于我自己的项目,我通常会添加一个按钮,将试用期延长几天,以便他们有机会进行购买。我只允许一次扩展,之后应用停止工作。
如果购买成功,我就关闭对话框,这样用户就可以使用这个应用了。如果购买没有完成,我调用ViewModel.Store.checkCapability
方法,再次评估许可证,导致相同的对话框显示,直到用户终止应用或能够完成购买。我使用了checkCapability
方法,以便触发capabilitycheck
事件,让我的应用的其他部分保持对正在发生的事情的了解。
测试场景
为了测试这个场景,我必须在/store/initial.xml
场景文件中指定一个过去的日期,如清单 11 所示。为了得到我想要的效果,我必须将isActive
元素设置为false
,并确保将isTrial
元素设置为true
。
清单 11。在 initial.xml 文件中创建过期的试用场景
... <LicenseInformation> <App> <IsActive>**false**</IsActive> <IsTrial>**true**</IsTrial> <ExpirationDate>**2011-09-30T00:00:00.00Z**</ExpirationDate> </App> </LicenseInformation> ...
当你启动应用时,你会看到如图图 2 所示的对话框。
***图二。*处理过期的试用期
此时,buyApp
函数返回的Promise
返回true
,表示从商店购买成功。点击Buy Now
按钮,对话框将被关闭,您可以使用该应用。
为了模拟一次失败的购买,修改buyApp
函数,使Promise
产生false
,如清单 12 所示。
清单 12。模拟一次失败的购买
... function buyApp() { // code to purchase the app will go here return WinJS.Promise.wrap(**false**); } ...
重启应用,你会看到同样的对话框。点击Buy Now
按钮将暂时关闭对话框,但它会在一会儿后重新出现,阻止应用被使用。
增加购买应用的支持
购买应用的大部分工作由 Windows 和 Windows 商店负责。我所要做的就是表明我想要启动这个过程,这是通过CurrentApp
对象来完成的。您可以看到我如何在清单 13 的storeInteractions.js
文件中实现了buyApp
函数,以使用CurrentApp
功能的这一方面。
清单 13。实现 buyApp 功能
... function buyApp() { ** var md = new pops.MessageDialog("placholder");** ** return ViewModel.Store.currentApp.requestAppPurchaseAsync(false).then(function () {** ** if (ViewModel.Store.currentApp.licenseInformation.isActive) {** ** md.title = "Success"** ** md.content = "Your purchase was succesful. Thank you.";** ** return md.showAsync().then(function () {** ** return true;** ** });** ** } else {** ** return false;** ** }** ** }, function () {** ** md.title = "Error"** ** md.content = "Your purchase could not be completed. Please try again.";** ** return md.showAsync().then(function () {** ** return false;** ** });** ** });** } ...
关键部分是对requestAppPurchaseAsync
方法的调用,它启动了购买过程。这个方法的参数是一个boolean
值,指示是否应该为交易生成收据——我已经指定了false
,这意味着不需要收据。
如果购买成功,requestAppPurchaseAsync
方法返回一个正常完成的WinJS.Promise
,否则调用错误函数(参见第九章,了解传递给Promise.then
方法的不同函数的解释)。
成功和错误与流程本身有关,因此您需要检查 success 函数中的许可证状态,以确保购买成功。我通过使用另一个MessageDialog
报告结果的来响应购买的结果。我的buyApp
函数返回一个Promise
,如果购买成功则返回true
,否则返回false
。
当使用CurrentAppSimulator
对象时,调用requestAppPurchaseAsync
方法会显示一个对话框,允许您模拟购买请求的不同结果。要看到这个对话框,启动应用并点击Buy Now
按钮。图 3 显示了对话框和你可以选择的选项来模拟结果。
***图三。*模拟 app 购买流程
S_OK
选项将模拟一次成功的购买,而其他值模拟不同种类的错误。在我的例子中,我没有区分不同种类的错误,并以同样的方式对待所有失败的购买。
测试失败的购买
如果您选择一个错误条件并点击Continue
按钮,您将看到如图图 4 所示的错误信息。点击Close
按钮关闭对话框,因为应用仍未获得许可,导致应用再次显示过期警告。
***图 31-4。*通知用户购买失败
测试一次成功的购买
测试成功购买稍微复杂一些,因为检查许可证信息是否正确更新很重要。首先,启动应用,在执行任何其他操作之前,在 Visual Studio JavaScript 控制台中输入以下语句:
console.log("Active: " + ViewModel.Store.currentApp.licenseInformation.isActive); console.log("Trial: " + ViewModel.Store.currentApp.licenseInformation.isTrial);
这些语句产生以下输出,表明该应用没有有效的许可证,并且是作为免费试用获得的,如下所示:
Active: false Trial: true
点击应用对话框上的Buy Now
按钮,通过从列表中选择S_OK
值并点击Continue
按钮来模拟一次成功的购买。您将看到如图图 5 所示的对话框,当您点击Close
按钮时,您将能够使用该应用。
***图 5。*确认成功购买应用
现在,在 JavaScript 控制台窗口中重新输入这些命令,您将看到许可信息已经更改,表明用户已经许可了应用,如下所示:
Active: true Trial: false
当用户获得许可证时,licensechanged
事件被自动触发,这意味着我在/js/store.js
文件中的处理函数将重新评估可用的许可证,并更新用于响应对ViewModel.Store.checkCapability
函数的调用的属性,确保应用状态与用户获得的许可证保持一致。
注意重要的是要理解场景文件不会改变——只会改变正在运行的应用中许可信息的状态。如果重启 app,会重新加载
/store/initial.xml
文件中描述的场景,重新创建用户没有许可证,免费试用期已过的情况。
总结
在本章中,我已经向您介绍了Windows.ApplicationModel.Store
名称空间,并使用它包含的一些对象来实现我的示例业务模型的一部分。我已经为我的应用添加了对强制试用期和从商店购买应用以获得永久许可证的支持,并使用场景文件测试了该功能。在下一章,我将向你展示如何销售和管理应用内升级。
三十二、销售升级
在这一章中,我将向您展示如何使用 Windows 应用商店从您的应用内向您的用户销售升级。我演示了如何在模拟器文件中创建描述升级的条目,如何获取有关已购买的升级的信息,以及如何启动 Windows Store 过程来购买升级。
在场景文件中定义产品
创建应用内升级的技术从场景文件中的定义开始。对于这一章,我在store
文件夹中创建了一个名为upgrades.xml
的新文件,其内容可以在清单 1 中看到。
清单 1。upgrades.xml 文件的内容
<?xml version="1.0" encoding="utf-16" ?> <CurrentApp> <ListingInformation> <App> <AppId>e4bbe35f-0509-4cca-a27a-4ec43bed783c</AppId> <LinkUri>http://apress.com</LinkUri> <CurrentMarket>en-US</CurrentMarket> <AgeRating>3</AgeRating> <MarketData xml:lang="en-us"> <Name>Simple Photo Album</Name> <Description>An app to display your photos</Description> <Price>4.99</Price> <CurrencySymbol>$</CurrencySymbol> <CurrencyCode>USD</CurrencyCode> </MarketData> </App> ** <Product ProductId="fileTypes">** ** <MarketData xml:lang="en-us">** ** <Name>JPG Files Upgrade</Name>** ** <Price>4.99</Price>** ** <CurrencySymbol>$</CurrencySymbol>** ** <CurrencyCode>USD</CurrencyCode>** ** </MarketData>**
** </Product>** </ListingInformation> <LicenseInformation> <App> <IsActive>true</IsActive> <IsTrial>false</IsTrial> <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate> </App> ** <Product ProductId="fileTypes">** ** <IsActive>true</IsActive>** ** </Product>** </LicenseInformation> </CurrentApp>
我只在 upgrades.xml 文件中添加了一个应用内升级。在 Windows Store 的说法中,应用内升级被称为产品,与定义基本功能的应用相对。要定义一个新产品,您必须在场景文件的ListingInformation
和LicenseInformation
部分添加一个元素,就像我在示例中所做的那样。
注意对于这个场景文件,我希望应用的基本功能可以在试用期尚未到期。您可以看到我是如何在
LicenseInformation.App
元素中做到这一点的,但是您必须更改ExpirationDate
元素中的日期,以指定一个未来的日期来获得正确的效果。
定义产品详细信息
您使用一个Product
元素定义产品的细节,并使用ProductId
属性指定产品的名称,如下所示:
... <Product **ProductId="fileTypes"**> ...
这是升级将被您的应用识别的名称,不会向用户显示。我已经指定了fileTypes
名称,这与我在应用中构建的功能一致。包含在Product
元素中的元素与应用在App
元素中的元素具有相同的含义,它们描述升级并指定其成本和货币。在本例中,我将fileTypes
产品描述为JPG Files Upgrade
,并将其价格设为 4.99 美元。
定义产品许可
您可以通过向 XML 文件的LicenseInformation
部分添加一个Product
元素来设置该场景的许可状态。您必须确保ProductId
属性的值与您用来描述升级的值相匹配,并且您必须包含一个IsActive
元素,该元素被设置为true
以指示用户拥有有效的许可证,否则为false
。
您还可以使用ExpirationDate
元素来表示您打算在订阅的基础上销售的产品。您指定的日期将是订阅结束的时间点(或者,如果您指定了过去的日期,则为已经结束的时间点)。
定义剩余产品
既然我已经向您展示了如何定义单个产品,我将为我的应用将支持的其他升级向场景文件添加条目。您可以在清单 2 的中看到相当长的附加内容。
清单 2。为剩余产品定义场景条目
<?xml version="1.0" encoding="utf-16" ?> <CurrentApp> <ListingInformation> <App> <AppId>e4bbe35f-0509-4cca-a27a-4ec43bed783c</AppId> <LinkUri>http://apress.com</LinkUri> <CurrentMarket>en-US</CurrentMarket> <AgeRating>3</AgeRating> <MarketData xml:lang="en-us"> <Name>Simple Photo Album</Name> <Description>An app to display your photos</Description> <Price>4.99</Price> <CurrencySymbol>$</CurrencySymbol> <CurrencyCode>USD</CurrencyCode> </MarketData> </App> <Product ProductId="fileTypes"> <MarketData xml:lang="en-us"> <Name>JPG Files Upgrade</Name> <Price>4.99</Price> <CurrencySymbol>$</CurrencySymbol> <CurrencyCode>USD</CurrencyCode> </MarketData> </Product> ** <Product ProductId="depth">** ** <MarketData xml:lang="en-us">** ** <Name>All Folders Upgrade</Name>** ** <Price>4.99</Price>** ** <CurrencySymbol>$</CurrencySymbol>** ** <CurrencyCode>USD</CurrencyCode>** ** </MarketData>** ** </Product>** ** <Product ProductId="thumbnails">** ** <MarketData xml:lang="en-us">** ** <Name>Thumbnails Upgrade</Name>** ** <Price>1.99</Price>** ** <CurrencySymbol>$</CurrencySymbol>** ** <CurrencyCode>USD</CurrencyCode>** ** </MarketData>** ** </Product>** ** <Product ProductId="theworks">** ** <MarketData xml:lang="en-us">** ** <Name>The Works Upgrade + Printing</Name>** ** <Price>9.99</Price>**
** <CurrencySymbol>$</CurrencySymbol>** ** <CurrencyCode>USD</CurrencyCode>** ** </MarketData>** ** </Product>** </ListingInformation> <LicenseInformation> <App> <IsActive>true</IsActive> <IsTrial>false</IsTrial> <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate> </App> <Product ProductId="fileTypes"> <IsActive>true</IsActive> </Product> ** <Product ProductId="thumbnails">** ** <IsActive>false</IsActive>** ** <ExpirationDate>2011-09-30T00:00:00.00Z</ExpirationDate>** ** </Product>** </LicenseInformation> </CurrentApp>
我的场景文件现在包含我销售的所有升级的列表信息和其中两个的许可信息。fileTypes
升级得到了正确的许可,但是我在订阅基础上出售的缩略图升级的许可已经过期。
提醒一下,我已经在表 1 中列出了升级及其产品 id。
从场景文件的LicenseInformation
部分省略产品的细节相当于它们不是该产品的许可证——您也可以通过添加一个Product
元素但将IsActive
元素设置为false
来实现这种效果。
切换到新的场景文件
为了使用我的新场景文件,我需要更新/js/store.js
文件中的代码,如清单 3 所示。通常,您将构建一个不同场景文件的库来支持全面的测试。对于我自己的项目,我发现在我有一套完整的测试来覆盖所有的许可证排列之前,我可能会有多达 20 个不同的场景文件。幸运的是,对于我相对简单的示例应用,我不需要那么多文件。
清单 3。更改 store.js 文件以加载新场景
... loadLicenseData: function () { var url = new Windows.Foundation.Uri("**ms-appx:///store/upgrades.xml**"); return storage.StorageFile.getFileFromApplicationUriAsync(url) .then(function (file) { return ViewModel.Store.currentApp.reloadSimulatorAsync(file); }); }, ...
提示我发现如果我得到了意想不到的结果,那通常是因为我忘记了加载正确的文件。
使用许可信息
我现在的目标是使用我定义的许可证信息来设置用户对应用中不同功能的权限。我通过CurrentApp
对象(或者开发过程中的CurrentAppSimulator
对象)来做这件事。您可以看到我对/js/store.js
文件所做的更改,以利用清单 4 中的新许可信息。
清单 4。使用/js/store.js 文件中的产品许可信息
`(function() {
var storage = Windows.Storage;
var licensedCapabilities = {
basicApp: false,
** fileTypes: false,**
** depth: false,**
** thumbnails: false,**
** print: false,**
}
WinJS.Namespace.define(“ViewModel.Store”, {
events: WinJS.Utilities.eventMixin,
checkCapability: function (name) {
var available = licensedCapabilities[name] != undefined
? licensedCapabilities[name] : true;
setImmediate(function () {
ViewModel.Store.events.dispatchEvent(“capabilitycheck”,
{ capability: name, enabled: available });
});
return available;
},
currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,
loadLicenseData: function () {
var url
= new Windows.Foundation.Uri(“ms-appx:///store/upgrades.xml”);
return storage.StorageFile.getFileFromApplicationUriAsync(url)
.then(function (file) {
return ViewModel.Store.currentApp.reloadSimulatorAsync(file);
});
},
});
ViewModel.Store.currentApp.licenseInformation.addEventListener(“licensechanged”,
function () {
var license = ViewModel.Store.currentApp.licenseInformation;
licensedCapabilities.basicApp = license.isActive;
** var products = license.productLicenses;**
** if (products.lookup(“theworks”).isActive) {**
** licensedCapabilities.fileTypes = true;**
** licensedCapabilities.depth = true;**
** licensedCapabilities.thumbnails = true;**
** licensedCapabilities.print = true;**
** } else {**
** licensedCapabilities.fileTypes = products.lookup("fileTypes").isActive;
** licensedCapabilities.depth = products.lookup(“depth”).isActive;
** licensedCapabilities.thumbnails = products.lookup(“thumbnails”).isActive;**
** }**
});
})();`
处理产品许可证
我在store.js
文件中做的另一个更改是扩展了licensechanged
事件的事件处理程序中的代码,这样它就可以处理升级产品的许可证并设置licensedCapabilities
事件中的属性。
LicenseInformation
对象定义了一个productLicenses
属性(我从CurrentApp.licenseInformation
属性中获得了LicenseInformation
对象)。由productLicenses
属性返回的对象允许您使用lookup
方法查找单个产品,如下所示:
... products.lookup("theworks") ...
lookup 方法的参数是场景文件中Product
元素的ProductId
属性值。在上面的片段中,我已经请求了名为theworks
的升级许可。
lookup 方法返回一个ProductLicense
对象,它包含所请求产品的许可状态的详细信息。ProductLicense
对象定义了表 2 中描述的属性。
lookup
方法的好处在于,即使在场景文件中没有相应的Product
元素,它也会返回一个ProductLicense
对象——属性isActive
将被设置为false
,表示没有值许可。这意味着我可以安全地查找名为theworks
的产品,并获得一个响应,我可以用它来决定应该启用哪些应用功能。我处理产品许可的方法是从查看用户是否购买了theworks
的许可开始。如果是,那么我启用由licensedCapabilities
对象定义的所有功能,如下所示:
... var products = license.productLicenses; if (products.lookup("theworks").isActive) { ** licensedCapabilities.fileTypes = true;** ** licensedCapabilities.depth = true;** ** licensedCapabilities.thumbnails = true;** ** licensedCapabilities.print = true;** } else { licensedCapabilities.fileTypes = products.lookup("fileTypes").isActive; licensedCapabilities.depth = products.lookup("depth").isActive; licensedCapabilities.thumbnails = products.lookup("thumbnails").isActive; } ...
如果用户没有获得作品的有效许可,那么我会依次查找其他产品,并设置相应功能的状态。这意味着,例如,只有当用户拥有theworks
时,才会启用print
功能,提供了一个我如何将应用功能与升级产品分离的简单演示。
测试许可证信息
您可以通过启动应用并在 Visual Studio JavaScript 控制台窗口中输入以下语句来测试许可证信息:
["fileTypes", "depth", "thumbnails", "print"].forEach(function(cap) { console.log(cap + ": " + ViewModel.Store.checkCapability(cap)); });
点击 Return,您应该会看到以下输出,它根据场景文件中的许可证信息指示用户有权使用哪些应用功能:
fileTypes: true depth: false thumbnails: false print: false
如您所料,fileTypes
功能被启用,所有其他功能被禁用。您可以在应用中看到这些信息。关闭告诉您试用期还剩多少天的对话框,尝试更改ToggleSwitch
控件的位置。您只能移动标有Show JPG
的选项,因为其他选项与用户无权使用的功能相关。类似地,如果您激活 Devices Charm,您将看到如图图 1 所示的消息,因为print
功能对用户不可用。
***图 1。*当打印功能未被许可时激活设备魅力
更正许可权利
既然我已经向您展示了如何检查单个产品的许可信息,我需要返回并纠正/js/store.js
文件中的代码。当应用处于试用期时,我希望用户能够使用所有的应用功能,就像他们购买了基本应用并订阅了theworks
升级一样。您可以在清单 5 的中看到我对store.js
所做的修改。
清单 5。允许用户在试用期内使用所有功能
`…
ViewModel.Store.currentApp.licenseInformation.addEventListener(“licensechanged”,
function () {
var license = ViewModel.Store.currentApp.licenseInformation;
licensedCapabilities.basicApp = license.isActive;
var products = license.productLicenses;
if (products.lookup(“theworks”).isActive
** || (license.isActive && license.isTrial)) {**
licensedCapabilities.fileTypes = true;
licensedCapabilities.depth = true;
licensedCapabilities.thumbnails = true;
licensedCapabilities.print = true;
} else {
licensedCapabilities.fileTypes = products.lookup(“fileTypes”).isActive;
licensedCapabilities.depth = products.lookup(“depth”).isActive;
licensedCapabilities.thumbnails = products.lookup(“thumbnails”).isActive;
}
});
…`
我已经使用了基本应用的许可证信息,在未到期的试用期和基本应用加theworks
升级的许可证之间建立了等价关系。
您会记得,upgrade.xml
场景文件指定应用处于试用期,因此如果您在更新了store.js
文件后重启应用,您应该可以访问所有功能。
销售升级产品
现在,我的应用强制执行产品许可证,我可以提示用户购买升级。如何做到这一点取决于你的应用的性质。我建议你仔细考虑这一点,因为以有益和礼貌的方式提示用户和不断向他们索要金钱是有区别的。
对于这一章,我将使用一个简单的方法,即当用户试图使用一个未经许可的特性时,提示用户进行升级。
提示我打算提示用户升级,因为我想把重点放在升级机制上,但我不会在真正的应用中这样做,因为这让用户很烦。当我第一次提示用户升级时,我通常会提供一个选项来禁用对同一特性的任何进一步提示。我建议你考虑类似的方法。你可以使用应用数据功能永久记录用户的偏好,我在第二十章的中描述过。
您将回忆起当用户试图使用一项功能时,Windows.Store.checkCapability
方法触发了capabilitycheck
事件。我在第三十一章的/js/storeInteractions.js
文件中对此事件做出了回应,以加强我对基本应用的商业模式,这样我就可以向用户出售应用并加强试用期。我将使用类似的技术来管理升级过程。在接下来的部分中,我将介绍我所做的更改和添加。
调度状态事件
我将从添加对从ViewModel.State
名称空间调度事件的支持开始。我销售升级的方法是提示用户响应应用布局的变化,我想确保当用户成功购买升级时,我正确地更新了应用布局的状态。这意味着我需要某种方式来表明ViewModel.State
名称空间中的数据已经更改,为此,我在/js/viewmodel.js
文件中添加了一些内容,如清单 6 所示。
清单 6。添加对从 ViewModel 发出事件的支持。状态名称空间
`(function () {
WinJS.Namespace.define(“ViewModel”, {
State: WinJS.Binding.as({
pictureDataSource: new WinJS.Binding.List(),
fileTypes: false,
depth: false,
thumbnails: false,
** events: WinJS.Utilities.eventMixin,**
** reloadState: function () {**
** ViewModel.State.events.dispatchEvent(“reloadstate”, {});**
** }**
}),
});
WinJS.Namespace.define(“Converters”, {
display: WinJS.Binding.converter(function(val) {
return val ? “block” : “none”;
})
});
})();`
名称空间中的单个属性是可以观察到的,但我需要某种方式来表明应用状态发生了根本变化,应用中的数据应该被刷新。为此,我添加了一个events
属性,并为其分配了WinJS.Utilities.eventMixin
对象和一个reloadState
函数,当被调用时,该函数会触发一个名为reloadstate
的事件。在接下来的小节中,您将看到我是如何使用该函数并响应事件的。
管理采购流程
当用户试图激活他们无权使用的功能时,我会启动升级购买流程。我通过处理capabilitycheck
事件来检测这种情况,扩展我在第三十一章中添加的代码来处理应用购买过程。在清单 7 中,您可以看到我对/js/storeInteractions.js
文件所做的修改,这些修改扩展了购买,包括了升级。
清单 7。增加销售应用内升级的支持
`(function () {
var pops = Windows.UI.Popups;
ViewModel.Store.events.addEventListener(“capabilitycheck”, function (e) {
if (e.detail.capability == “basicApp”) {
// …statements removed for brevity…
** } else if (e.detail.capability == “print” && !e.detail.enabled) {**
** var md = new pops.MessageDialog(“Printing is only available to subscribers”);**
** md.commands.append(new pops.UICommand(“Subscribe”));**
** md.commands.append(new pops.UICommand(“Cancel”));**
** md.showAsync().then(function (command) {**
** if (command.label != “Cancel”) {**
** buyUpgrade(“theworks”);
}**
** });**
** } else if (!e.detail.enabled) {**
** var md = new pops.MessageDialog("You need to buy an upgrade to use this “**
** + " feature or subscribe to unlock all features”);**
** md.commands.append(new pops.UICommand(“Upgrade”));**
** md.commands.append(new pops.UICommand(“Subscribe”));**
** md.commands.append(new pops.UICommand(“Cancel”));**
** md.showAsync().then(function (command) {**
** if (command.label != “Cancel”) {**
** var product = command.label**
** == “Upgrade” ? e.detail.capability : “theworks”;**
** buyUpgrade(product).then(function (upgradeResult) {**
** if (upgradeResult) {**
** var val = ViewModel.State[e.detail.capability];**
** if (val != undefined) {**
** ViewModel.State[e.detail.capability] = !val;**
** }**
** ViewModel.State.reloadState();**
** }**
** });**
** }**
** });**
** }**
});
function buyApp() {
// …statements removed for brevity…
}
** function buyUpgrade(product) {**
** var md = new pops.MessageDialog(“”);**
** return ViewModel.Store.currentApp.requestProductPurchaseAsync(product, false)**
** .then(function () {**
** if (ViewModel.Store.currentApp.licenseInformation.productLicenses**
** .lookup(product).isActive) {**
** md.title = “Success”**
** md.content = “Your upgrade was succesful. Thank you.”;**
** return md.showAsync().then(function () {**
** return true;**
** });**
** } else {**
** return false;**
** }**
** }, function () {**
** md.title = “Error”**
** md.content = “Your upgrade could not be completed. Please try again.”;**
** return md.showAsync().then(function () {**
** return false;**
** });**
** });
}**
}})();`
您将回忆起当调用ViewModel.Store.checkCapability
方法时会触发capabilitycheck
事件。如果要求的功能不是basicApp
,那么我知道它与我想出售的升级相关联。在接下来的章节中,我将解释如何销售不同类别的升级产品。
间接销售能力
我的应用包含了print
功能(我只将其作为我的theworks
产品的一部分出售),可以解锁应用中的所有内容。这是一个间接升级的例子,我销售的产品不只是激活用户现在想要的功能。我将print
功能的请求与其他类型的升级分开处理,如下所示:
... } else if (**e.detail.capability == "print" && !e.detail.enabled**) { var md = new pops.MessageDialog("Printing is only available to subscribers"); md.commands.append(new pops.UICommand("Subscribe")); md.commands.append(new pops.UICommand("Cancel")); md.showAsync().then(function (command) { if (command.label != "Cancel") { buyUpgrade("theworks"); } }); } ...
我向用户显示一条简单的消息,解释他们请求的功能只对订户可用,并给他们购买订阅的机会,如图图 2 所示。
***图二。*请求打印时提示用户订阅
Windows 8 没有为我提供一种方法来阻止在激活设备魅力时显示设备窗格,所以我显示我的消息,以便用户在关闭窗格时可以看到它。如果用户点击Subscribe
按钮,然后调用buyUpgrade
函数,这将启动升级过程(我将很快对此进行描述)。我以theworks
为参数调用buyUpgrade
函数,表示用户想要购买订阅产品。
直接销售能力
对于应用中的其他功能,我想让用户选择是只购买他们尝试使用的功能,还是订阅并解锁所有功能,我的做法如下:
... } else if (!e.detail.enabled) { var md = new pops.MessageDialog("You need to buy an upgrade to use this " + " feature or subscribe to unlock all features"); ** md.commands.append(new pops.UICommand("Upgrade"));** ** md.commands.append(new pops.UICommand("Subscribe"));** ** md.commands.append(new pops.UICommand("Cancel"));** ** md.showAsync().then(function (command) {** ** if (command.label != "Cancel") {** ** var product = command.label** ** == "Upgrade" ? e.detail.capability : "theworks";** buyUpgrade(product).then(function (upgradeResult) { if (upgradeResult) { var val = ViewModel.State[e.detail.capability]; if (val != undefined) { ViewModel.State[e.detail.capability] = !val; } ViewModel.State.reloadState(); } }); } }); } ...
我通过给MessageDialog
添加一个Subscribe
按钮并改变我传递给buyUpgrade
函数的参数来达到我想要的效果。你可以在图 3 中看到我呈现给用户的对话框。
***图三。*向用户出售升级和订阅
如果用户点击Upgrade
按钮,我就开始购买解锁该功能的产品。如果用户点击Subscribe
按钮,我就开始购买theworks
产品。
如果用户成功购买,那么我会尝试更新与该功能相关联的ViewModel.State
属性,如果有的话。这就完成了用户通过激活一个ToggleSwitch
开始的 UI 交互,意味着升级的结果是立竿见影的。用户可以许可的一些升级需要重新加载数据,所以我调用我在清单 6 的/js/viewmodel.js
文件中定义的ViewMode.State.reloadState
方法。
刷新应用状态
我需要执行的最后一步是确保应用状态反映了用户许可的功能。最简单的方法是从Pictures
库中重新加载文件,以便应用显示用户有权查看的所有图像。您可以在清单 8 中看到我如何刷新应用状态以响应reloadstate
事件,它显示了我对/js/default.js
文件中的onactivated
函数所做的更改。
清单 8。刷新应用状态
`…
args.setPromise(WinJS.UI.processAll().then(function() {
return WinJS.Binding.processAll(document.body, ViewModel)
.then(function () {
return ViewModel.Store.loadLicenseData().then(function () {
** ViewModel.State.events.addEventListener(“reloadstate”, function (e) {**
** loadFiles();**
** listView.winControl.itemDataSource**
** = ViewModel.State.pictureDataSource.dataSource;**
** });**
setupPrinting();
loadFiles();
ViewModel.Store.checkCapability(“basicApp”);
});
});
}));
…`
为了处理该事件,我调用了loadFiles
函数,该函数定位用户有权查看的文件。我还刷新了用于显示缩略图的ListView
控件的数据源,如下所示:
... listView.winControl.itemDataSource = ViewModel.State.pictureDataSource.dataSource; ...
ListView
控件的一个奇怪之处在于,如果它在被隐藏的时候被初始化,然后显示给用户,它就不能正确显示内容——你得到的只是一个看起来空空的控件。一个快速简单的修复方法是设置itemDataSource
属性,它触发更新并生成新的内容元素。我没有改变数据源——我只是将它再次分配给 control 属性,以便当用户购买查看缩略图的功能时,内容能够正确显示。
购买升级
buyUpgrade
函数负责发起购买过程并响应结果。currentApp
对象定义了requestProductPurchaseAsync
方法,该方法启动 Windows 商店升级过程(或者在使用CurrentAppSimulator
对象时的购买模拟)。该方法的参数是与购买相关的产品的ProductId
值和指示是否需要收据的boolean
值:
... ViewModel.Store.currentApp.**requestProductPurchaseAsync**(product, false) ...
我收到了应该购买的产品作为buyUpgrade
函数的参数,我指定了false
,表示我不想要收据。requestProductPurchaseAsync
方法返回一个Promise
,当购买过程完成时,它被实现。当购买成功时,执行成功处理函数,否则执行错误处理函数。我向用户显示确认购买结果的消息,如下所示:
... function buyUpgrade(product) { var md = new pops.MessageDialog(""); return ViewModel.Store.currentApp.requestProductPurchaseAsync(product, false) .then(function () { if (ViewModel.Store.currentApp.licenseInformation.productLicenses .lookup(product).isActive) { ** md.title = "Success"** ** md.content = "Your upgrade was succesful. Thank you.";** ** return md.showAsync().then(function () {** ** return true;** }); } else { ** return false;** } }, function () { ** md.title = "Error"** ** md.content = "Your upgrade could not be completed. Please try again.";** ** return md.showAsync().then(function () {** ** return false;** ** });** }); } ...
我的buyUpgrade
函数返回一个Promise
,当用户关闭消息对话框时该函数被满足,如果购买成功则产生true
,否则产生false
,允许我构建动作链,比如刷新应用状态。
测试场景
如果您启动应用并将Show Thumbnails
切换开关滑动到Yes
位置,系统会提示您升级或订阅。点击Upgrade
按钮,会出现模拟购买对话框,如图图 4 所示。
***图 4。*模拟购买升级
这是模拟应用购买的同一个对话框,它有相同的结果选择。该对话框显示所购买产品的名称,我在图中突出显示了该名称。
确保选择了S_OK
选项,并点击Continue
按钮模拟成功购买。当您关闭确认购买成功的对话框时,您会看到应用布局更新为显示缩略图,并且ToggleSwitch
已经移动到Yes
位置,如图图 5 所示。
***图 5。*购买缩略图功能许可证的影响
其他功能仍然未经许可,这意味着如果你滑动其他ToggleSwitch
控件或激活设备的魅力,你会再次得到提示。
测试订阅升级
重启应用,使许可信息重置为场景文件的内容,并再次滑动Show Thumbnails
切换开关。这一次,当出现提示时,点击Subscribe
按钮。您将再次看到购买模拟器对话框,但这次购买的产品被报告为theworks
,如图图 6 所示。
***图六。*模拟购买解锁多种能力的升级
选择S_OK
选项并点击Continue
按钮,模拟一次成功的购买。缩略图将像以前一样显示,但这一次应用的其他功能也已解锁——例如,如果您将All Folders
ToggleSwitch 滑动到Yes
位置,将使用深度查询来定位您的Pictures
库中的文件,如图图 7 所示。
***图 7。*购买适用于多种能力的升级的效果
创建应用内商店
我对示例应用的最后一个添加是连接Buy/Upgrade
按钮,以便为我想卖给用户的各种产品创建一个店面。到目前为止,我的所有销售提示都是由用户试图执行特定操作而触发的,但我也想让用户有机会在任何时候出于任何原因进行购买。在接下来的部分中,我将对应用进行一系列的添加,以便用户可以点击按钮,看到可用的选项,并进行购买。
注意这种添加需要相对大量的代码,但它对许多应用来说是一种重要的添加,需要我在本书中向你展示的许多技术——包括数据绑定、模板和
Flyout
控件——以及一些基本的 DOM 操作。这一部分的清单有点长,但是我建议努力完成它们。
增强视图模型。存储命名空间
我不想将我的商业模式的细节泄露到应用的主要功能中,所以我将对ViewModel.Store
名称空间进行一些添加,以表示产品并启动购买,而不直接公开Windows.ApplicationModel.Store
名称空间。您可以在清单 9 的中看到我对/js/store.js
文件所做的添加。
清单 9。扩展视图模型的功能。商店名称空间
`(function() {
var storage = Windows.Storage;
var licensedCapabilities = {
basicApp: false,
fileTypes: false,
depth: false,
thumbnails: false,
print: false,
}
WinJS.Namespace.define(“ViewModel.Store”, {
events: WinJS.Utilities.eventMixin,
checkCapability: function (name) {
// …statements omitted for brevity…
},
currentApp: Windows.ApplicationModel.Store.CurrentAppSimulator,
loadLicenseData: function () {
// …statements omitted for brevity…
},
** isBasicAppPurchased: function () {**
** var license = ViewModel.Store.currentApp.licenseInformation;
return license.isActive && !license.isTrial;**
** },**
** isFullyUpgraded: function() {**
** return ViewModel.Store.currentApp.licenseInformation.productLicenses**
** .lookup(“theworks”).isActive;**
** },**
** getProductInfo: function () {**
** var products = [**
** { id: “p1”, name: “Product 1”, price: “$4.99”, purchased: true },**
** { id: “p2”, name: “Product 2”, price: “$1.99”, purchased: false },**
** { id: “p3”, name: “Product 3”, price: “$10.99”, purchased: false },**
** { id: “p4”, name: “Product 4”, price: “$0.99”, purchased: false }];**
** return products;**
** },**
** requestAppPurchase: function() {**
** ViewModel.Store.events.dispatchEvent(“apppurchaserequested”);**
** },**
** requestUpgradePurchase: function (productId) {**
** ViewModel.Store.events.dispatchEvent(“productpurchaserequested”,**
** { product: productId });**
** }**
});
ViewModel.Store.currentApp.licenseInformation.addEventListener(“licensechanged”,
function () {
// …statements omitted for brevity…
});
})();`
我添加到ViewModel.Store
名称空间的函数分为三类:两个提供管理button
元素所需的信息,一个提供可用产品的目录,两个启动购买过程。我将在下面的章节中解释每一个。
为布局提供信息
与 Windows 商店(或任何其他商业平台,就此而言)集成的一个方面是应用可能处于的不同状态的数量,以及满足所有这些状态的需要。你可以在getStoreLabel
、isBasicAppPurchased
和isFullyUpgraded
函数中看到这一点的缩影,我使用所有这些函数来管理Buy/Upgrade
按钮在应用布局中的呈现。
如果用户是通过免费试用期的一部分,那么我希望按钮提供他们购买应用的机会。Windows Store 不允许用户购买应用内升级,直到购买了基本应用,因此我需要确保我为用户提供正确的交易,以便提供合理而有用的用户体验。
isBasicAppPurchased
允许我告诉什么时候我需要向用户出售基本应用,什么时候我需要出售升级。另一方面,当用户购买了theworks
升级时,我想禁用button
元素,因为没有什么可卖的了。为此,我创建了 isFullyUpgraded
函数,当不再有任何产品留给用户时,该函数返回true
。
提示请注意,我没有透露任何定义基本应用功能的细节,也没有透露必须购买哪些产品才能创建完全升级的条件。我热衷于在应用的不同部分之间保持强烈的分离感,而不是在
store.js
文件之外的任何地方建立商业模式的知识,以便我在未来可以更容易地进行调整。
提供产品信息
getProductInfo
功能提供了应用可用升级的详细信息。该函数返回一个对象数组,每个对象都包含表示商店产品id
(对应于场景文件中的条目)、应该向用户显示的name
、升级的price
以及产品是否已经是purchased
的属性,如下所示:
... getProductInfo: function () { var products = [ ** { id: "p1", name: "Product 1", price: "$4.99", purchased: true },** ** { id: "p2", name: "Product 2", price: "$1.99", purchased: false },** ** { id: "p3", name: "Product 3", price: "$10.99", purchased: false },** ** { id: "p4", name: "Product 4", price: "$0.99", purchased: false }];** return products; }, ...
我最初实现这个函数时使用了静态虚拟数据。当我使用 Windows Store 时,我经常这样做,因为这让我可以绝对确保我不会依赖于Windows.ApplicationModel.Store
名称空间中的对象或应用中销售的实际产品。同样,我这样做是为了使长期维护尽可能容易。一旦我让应用内商店前台功能正常工作并添加对生成真实数据的支持,我将返回到这个函数。
为购买提供支持
我添加到ViewModel.Store
名称空间的最后两个函数为应用的其他部分提供支持,以启动应用和升级购买。我在不久前创建的布局中使用这个特性向用户显示产品:
`…
requestAppPurchase: function() {
ViewModel.Store.events.dispatchEvent(“apppurchaserequested”);
},
requestUpgradePurchase: function (productId) {
ViewModel.Store.events.dispatchEvent(“productpurchaserequested”,
{ product: productId });
}
…`
storeInteractions.js
文件包含处理购买过程的代码,我不想将该功能与store.js
文件紧密耦合,所以我使用两个新事件发出购买请求:apppurchaserequested
和productpurchaserequested
。我将在下一节处理这些事件。
定义商场互动
为了理清后端流程,我需要处理我在 store.js 文件的storeInteractions.js
文件中创建的两个新事件,该文件已经包含了我需要卖给用户的所有代码。您可以在清单 10 中看到我对storeInteractions.js
文件所做的添加。
清单 10。响应 storeInteractions.js 文件中的新购买事件
`(function () {
var pops = Windows.UI.Popups;
ViewModel.Store.events.addEventListener(“capabilitycheck”, function (e) {
// …statements omitted for brevity…
});
** ViewModel.Store.events.addEventListener(“apppurchaserequested”, function () {**
** buyApp().then(function (result) {**
** if (result) {**
** ViewModel.State.reloadState();**
** }**
** });**
** });**
** ViewModel.Store.events.addEventListener(“productpurchaserequested”, function (e) {**
** buyUpgrade(e.detail.product).then(function (result) {**
** if (result) {**
** ViewModel.State.reloadState();**
** }**
** });**
** });**
function buyApp() {
// …statements omitted for brevity…
}
function buyUpgrade(product) {
// …statements omitted for brevity…
}
})();`
收到事件后,我调用现有的buyApp
或buyUpgrade
函数,并在成功购买后调用ViewModel.State.reloadState
方法,以确保应用布局反映新获得的功能。
定义标记和样式
我将使用一个WinJS.UI.Flyout
控件向用户展示应用内商店,我在第十二章中对此进行了描述。商店将包含由ViewModel.Store.getProductInfo
方法返回的每个产品的详细信息,我将使用一个WinJS.Binding.Template
对象生成我需要的 HTML 元素,我在第八章的中对此进行了描述。您可以看到我对清单 11 中的default.html
文件所做的修改,添加了Flyout
和模板。
清单 11。将弹出按钮和模板添加到 default.html 文件
`…
**
** **
** **
**
** <div id=“storeFlyout” data-win-control=“WinJS.UI.Flyout”**
** data-win-options=“{placement: ‘right’}”>**
**
**
**
**
**
**
**
** Cancel**
**
** **
...`
这个新标记非常简单。我将为每个产品生成productTemplate
模板中的元素,并将它们添加到Flyout
中的productContainer
元素中(以及我将在代码中生成的一些补充元素)。我向名为/css/store.css
的项目添加了一个新的样式表,以包含我用于Flyout
和模板元素的样式,您可以在清单 12 中看到这个新文件的内容。
清单 12。定义弹出菜单和模板元素的样式
.title {font-size: 20pt;text-align: center;} #productContainer {display: -ms-grid;} .pname, .pprice, .pbuy, .purchased {margin: 10px;} .pname {-ms-grid-column: 1;} .pprice {-ms-grid-column: 2; text-align: right;} .pbuy, .purchased { text-align: center; -ms-grid-column: 3;} #cancelContainer {text-align: center}
我依靠 CSS 网格布局来定位元素;一部分网格信息是通过样式表应用的,另一部分是当我从default.js
文件中的模板生成元素时在代码中应用的,我将在下一节中描述。我向default.html
文件的head
部分添加了一个脚本元素,以便在应用中包含新文件,如清单 13 中的所示。
清单 13。将 store.css 文件的脚本元素添加到 default.html 文件
`…
** **
编写代码
我已经为我的应用内商店做好了所有的基础准备,剩下的就是将代码添加到/js/default.js
文件中,将各个部分组合在一起。您可以在清单 14 中看到我添加的内容。
清单 14。实现应用商店所需的 default.js 文件的附加内容
`(function () {
“use strict”;
WinJS.Binding.optimizeBindingReferences = true;
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var storage = Windows.Storage;
var search = storage.Search;
app.onactivated = function (args) {
if (args.detail.kind === activation.ActivationKind.launch) {
if (args.detail.previousExecutionState !==
activation.ApplicationExecutionState.suspended) {
// …statements removed for brevity…
}
args.setPromise(WinJS.UI.processAll().then(function() {
return WinJS.Binding.processAll(document.body, ViewModel)
.then(function () {
return ViewModel.Store.loadLicenseData().then(function () {
ViewModel.State.events.addEventListener(“reloadstate”,
function (e) {
loadFiles();
listView.winControl.itemDataSource
= ViewModel.State.pictureDataSource.dataSource;
** configureUpgradeButton();**
});
** upgrade.addEventListener(“click”, function () {**
** if (ViewModel.Store.isBasicAppPurchased()) {**
** showStoreFront();**
** } else {**
** ViewModel.Store.requestAppPurchase();**
** }**
** });**
setupPrinting();
loadFiles();
** configureUpgradeButton();**
ViewModel.Store.checkCapability(“basicApp”);
});
});
}));
}
};
** function configureUpgradeButton() {**
** if (ViewModel.Store.isFullyUpgraded()) {**
** upgrade.disabled = “true”**
** } else if (ViewModel.Store.isBasicAppPurchased()) {**
** upgrade.innerText = “Upgrade”;**
** } else {**
** upgrade.innerText = “Purchase”;**
** }**
** }
function showStoreFront() {**
** var products = ViewModel.Store.getProductInfo();**
** var rowNum = 2;**
** WinJS.Utilities.empty(productContainer);**
** products.forEach(function (product) {**
** productTemplate.winControl.render(product).then(function (newDiv) {**
** if (!product.purchased) {**
** var button = document.createElement(“button”);**
** button.innerText = “Buy”;**
** button.setAttribute(“data-product”, product.id);**
** WinJS.Utilities.addClass(button, “pbuy”);**
** newDiv.appendChild(button);**
** } else {**
** var div = document.createElement(“div”);**
** div.innerText = “Purchased”;**
** WinJS.Utilities.addClass(div, “purchased”);**
** newDiv.appendChild(div);**
** }**
** while (newDiv.children.length > 0) {**
** var celem = newDiv.children[0];**
** celem.style.msGridRow = rowNum;**
** productContainer.appendChild(celem);**
** }**
** });**
** rowNum++;**
** });**
** WinJS.Utilities.query(“button.pbuy”, productContainer).listen(“click”,**
** function(e) {**
** var productId = e.target.getAttribute(“data-product”);**
** ViewModel.Store.requestUpgradePurchase(productId);**
** });**
** WinJS.Utilities.query(“#cancelContainer button”).listen(“click”, function () {**
** storeFlyout.winControl.hide();**
** });**
** storeFlyout.winControl.show(upgrade);**
** }**
function setupPrinting() {
// …statements removed for brevity…
}
function loadFiles() {
// …statements removed for brevity…
};
app.start();
})();`
这里有很多新代码,但都很简单。为了便于理解,我将把它分成两个部分。
管理按钮元素
我的第一个任务是确保用户点击显示商店的button
元素被正确显示,这是我通过添加configureUpgradeButton
函数完成的,如下所示:
... function configureUpgradeButton() { if (**ViewModel.Store.isFullyUpgraded**()) { upgrade.disabled = "true" } else if (**ViewModel.Store.isBasicAppPurchased**()) { upgrade.innerText = "Upgrade"; } else { upgrade.innerText = "Purchase"; } } ...
您可以看到我是如何使用在ViewModel.Store
名称空间中添加的方法来处理按钮的。如果应用已经完全升级,没有什么可卖的,那么我禁用按钮。如果用户已经购买了应用,那么我将按钮中的文本设置为Upgrade
,如果用户还没有购买基本功能,我将文本设置为Purchase
。当我测试我的应用商店功能时,你可以在本章的后面看到这些不同的状态。
我还使用了isBasicAppPurchased
方法来判断当用户点击按钮时该做什么。如果用户还没有购买基础 app,那么我调用ViewModel.Store.requestAppPurchase
方法,这将导致 app 购买过程开始,如下所示:
... upgrade.addEventListener("click", function () { if (**ViewModel.Store.isBasicAppPurchased()**) { showStoreFront(); } else { ** ViewModel.Store.requestAppPurchase();** } }); ...
如果用户已经让购买了这个应用,那么我调用showStoreFront
函数,我将在下一节描述这个函数。
推广弹出型按钮
showStoreFront
函数负责填充Flyout
控件并对其进行配置,以便用户可以开始升级的购买过程。这个函数的代码很冗长,因为我用一个button
来补充从模板生成的元素,以发起对产品的购买,或者用一个div
元素来表示已经购买了升级。如果您忽略函数的这一部分,代码的其余部分将变得更容易理解,如下所示:
... function showStoreFront() { var products = ViewModel.Store.getProductInfo();
` var rowNum = 2;
WinJS.Utilities.empty(productContainer);
products.forEach(function (product) {
productTemplate.winControl.render(product).then(function (newDiv) {
// …statements to supplement template elements omitted…
});
rowNum++;
});
WinJS.Utilities.query(“button.pbuy”, productContainer).listen(“click”,
function(e) {
var productId = e.target.getAttribute(“data-product”);
ViewModel.Store.requestUpgradePurchase(productId);
});
WinJS.Utilities.query(“#cancelContainer button”).listen(“click”, function () {
storeFlyout.winControl.hide();
});
storeFlyout.winControl.show(upgrade);
}
…`
我为每个产品的Flyout
添加元素,并为启动升级过程的按钮的click
事件设置一个处理程序。一旦我填充并配置了Flyout
控件,我就调用show
方法将它显示给用户。当我测试新功能时,您可以在下一部分看到商店是如何出现的。
测试应用商店功能
我已经准备好测试我为应用商店添加的新功能,尽管你会注意到我仍然在使用我的静态虚拟产品数据。只有当我对应用内商店的工作方式感到满意时,我才会转向真实数据。我需要从配置我的场景文件开始。我仍然在使用我在本章前面创建的upgrades.xml
文件,在清单 15 中,你可以看到LicenseInformation.App
部分的初始设置,这是我将为这些测试更改的部分。
清单 15。基本应用购买测试的许可证配置
... <LicenseInformation> <App> ** <IsActive>true</IsActive>** ** <IsTrial>true</IsTrial>** ** <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate>** </App> <Product ProductId="fileTypes"> <IsActive>true</IsActive> </Product> <Product ProductId="thumbnails"> <IsActive>false</IsActive> <ExpirationDate>2011-09-30T00:00:00.00Z</ExpirationDate>
</Product> </LicenseInformation> </CurrentApp> ...
我想模拟一个还没有到期的试用期,所以我将isActive
和isTrial
元素设置为true
,并在ExpirationDate
元素中指定一个日期,对我来说是将来的某一天。你必须使用不同的日期来获得正确的效果。
测试基本应用购买
启动该应用,在您取消关于试用期还剩多少天的提醒后,您会看到布局中的按钮标记为Purchase
。如果您点击此按钮,您将看到购买模拟器对话框,其中将显示正在购买基本应用。
***图 8。*模拟购买基本应用功能
确保选择了S_OK
选项并点击Continue
按钮,模拟一次成功的购买。当您关闭确认您购买的消息时,您会看到按钮标签已更改为Upgrade
。
测试升级采购
不用重启 app,再次点击按钮就会看到应用内商店,如图图 9 所示。我使用了一个非常基本的布局,但是您可以看到我是如何显示虚拟产品数据来为用户提供升级的。
单击其中一个Buy
按钮,您将看到购买模拟器对话框,尽管是针对一个在场景文件中不存在的产品。
***图九。*显示虚拟产品数据的应用内商店
添加真实产品数据
现在我知道我的应用内商店工作了,我可以添加真正的产品数据,这是通过修改/js/store.js
文件中的代码来完成的。您可以在清单 16 中看到我所做的修改。
清单 16。使用真实产品数据
`…
getProductInfo: function () {
** return ViewModel.Store.currentApp.loadListingInformationAsync()**
** .then(function (info) {**
** var products = [];**
** var cursor = info.productListings.first();**
** do {**
** var prodInfo = cursor.current;**
** products.push({**
** id: prodInfo.value.productId,**
** name: prodInfo.value.name,**
** price: prodInfo.value.formattedPrice,**
** purchased: ViewModel.Store.currentApp.licenseInformation.productLicenses**
** .lookup(prodInfo.value.productId).isActive**
** });**
** } while (cursor.moveNext());**
** return products;**
** });**
},
…`
CurrentApp
和CurrentAppSimjulator
对象定义了loadListingInformationAsync
。该方法返回一个Promise
,完成后会产生一个Windows.ApplicationModel.Store.ListingInformation
对象,其中包含 Windows 应用商店中关于您的应用及其升级的列表信息——该信息对应于场景文件的ListingInformation
部分。ListingInformation
对象定义了我在表 3 中描述的属性。
我对应用内商店感兴趣的是productListings
属性,因为它返回了一个ProductListing
对象的列表,每个对象描述了我的应用的一个升级。ProductListing
对象定义了我在表 4 中描述的属性。
属性返回的对象将对象呈现为一个列表,这就是为什么我使用了一个 ?? 循环。对于每个产品列表,我创建一个具有我的应用内商店Flyout
所需属性的对象,并查找每个产品的许可信息,以查看是否已经购买。
我修改过的getProductInfo
方法的结果是一个Promise
,当它被实现时,产生一个描述性对象的数组;这意味着我需要更新default.js
文件中的showStoreFront
函数来期待Promise
,如清单 17 所示。
清单 17。修改 Default.js 文件中的 showStoreFront 函数
function showStoreFront() { ** ViewModel.Store.getProductInfo().then(function (products) {** var rowNum = 2; WinJS.Utilities.empty(productContainer); products.forEach(function (product) { productTemplate.winControl.render(product).then(function (newDiv) { if (!product.purchased) { var button = document.createElement("button"); button.innerText = "Buy";
`button.setAttribute(“data-product”, product.id);
WinJS.Utilities.addClass(button, “pbuy”);
newDiv.appendChild(button);
} else {
var div = document.createElement(“div”);
div.innerText = “Purchased”;
WinJS.Utilities.addClass(div, “purchased”);
newDiv.appendChild(div);
}
while (newDiv.children.length > 0) {
var celem = newDiv.children[0];
celem.style.msGridRow = rowNum;
productContainer.appendChild(celem);
}
});
rowNum++;
});
WinJS.Utilities.query(“button.pbuy”, productContainer).listen(“click”,
function (e) {
var productId = e.target.getAttribute(“data-product”);
ViewModel.Store.requestUpgradePurchase(productId);
});
WinJS.Utilities.query(“#cancelContainer button”).listen(“click”, function () {
storeFlyout.winControl.hide();
});
storeFlyout.winControl.show(upgrade); });
}`
通过这一更改,我的应用内商店功能将显示真实的产品数据,这些数据在使用CurrentAppSimulator
对象时从场景文件中获得,在使用CurrentApp
对象时从 Windows 商店数据中获得。
用真实数据测试应用内商店
最终测试是检查真实产品数据是否正确显示,以及当用户购买theworks
升级时布局中的按钮是否被禁用。首先更新upgrades.xml
场景文件,这样应用启动时就有了基本应用的许可证。将IsActive
元素设置为true
,将IsTrial
元素设置为false
,如清单 18 所示。
清单 18。更新最终测试的场景文件
... <LicenseInformation> <App> ** <IsActive>true</IsActive>** ** <IsTrial>false</IsTrial>** <ExpirationDate>2012-09-30T00:00:00.00Z</ExpirationDate> </App> <Product ProductId="fileTypes"> <IsActive>true</IsActive>
</Product> <Product ProductId="thumbnails"> <IsActive>false</IsActive> <ExpirationDate>2011-09-30T00:00:00.00Z</ExpirationDate> </Product> </LicenseInformation> ...
启动应用,点击标有Upgrade
的按钮;你会看到应用商店显示真实的产品数据,如图 10 所示。
***图 10。*显示真实产品数据的应用内商店
您可以看到,JPG Files Upgrade
显示为 purchased,这与场景文件的LicenseInformation
部分中的数据相匹配。点击The Works Upgrade
的Buy
按钮,您将看到购买模拟器对话框。如果您模拟一次成功的购买并关闭确认对话框,您将会看到Upgrade
按钮现在已被禁用,表示没有进一步的升级可用。
总结
在本章中,我向您展示了如何利用 Windows 应用商店支持销售应用内升级。我演示了如何销售单个和多个应用功能的升级,以及如何确定用户购买了哪些功能。我还演示了如何创建一个应用商店,它允许用户随时购买升级。在下一章,也是本书的最后一章,我将向你展示如何准备并发布你的应用到 Windows 应用商店。