將零售示範 (RDX) 功能新增至您的應用程式
在您的 Windows 應用程式中加入零售示範模式,讓在銷售場所試用電腦和裝置的客戶可以直接開始試用。
當客戶在零售商店時,他們期望能夠試用電腦和裝置試用機。 他們經常透過零售示範體驗 (RDX) 花大量時間熟悉應用程式。
您可以設定應用程式,以在一般或零售模式中提供不同的體驗。 例如,如果您的 app 以設定流程開始,則您可以在零售模式略過此流程,並使用範例資料和預設的設定預先填入應用程式,就可以直接進入。
從客戶的觀點來看,只有一個應用程式。 為了協助客戶區分這兩種模式,我們建議您在應用程式處於零售模式時,可在標題列或適當位置以醒目方式顯示「零售」一詞。
除了 Microsoft Store 對應用程式的需求之外,RDX 感知應用程式也必須與 RDX 設定、清除和更新流程相容,以確保客戶在零售商店有一致的正面體驗。
設計原則
展現最佳狀態: 使用零售示範體驗來展示您的應用程式為何讓人驚艷。 這可能是客戶第一次看到您的應用程式,所以請向他們顯示最佳作品!
快速展示: 客戶可能沒有耐心等待,即使用者越快能體驗到您應用程式的實際價值,越好。
確保故事簡單明瞭: 零售示範體驗是推廣應用程式價值的電梯簡報。
專注於體驗: 讓使用者有時間消化您的內容。 雖然讓他們快速進入最佳部分很重要,但設計適當的暫停可以有助於他們充分享受體驗。
技術需求
由於 RDX 感知應用程式旨在向零售客戶展示您應用程式的最佳功能,因此必須符合技術需求,並遵守 Microsoft Store 針對所有零售示範體驗應用程式所規定的隱私權法規。
這可用來做為檢查清單,協助您準備驗證流程,並讓測試流程中更清楚明白。 請注意,這些需求必須予以維護,而不只是針對驗證流程,而是針對零售示範體驗應用程式的整個生命週期,只要您的應用程式持續在零售示範裝置上執行都必須這麼做。
重要需求
不符合這些重要需求的 RDX 感知應用程式會儘快從所有零售示範裝置中撤除。
請勿要求個人識別資訊 (PII): 這包括登入資訊、Microsoft 帳戶資訊或聯絡人詳細資料。
無錯誤體驗: 您的應用程式不出錯誤地執行。 此外,不應向使用零售示範裝置的客戶顯示任何錯誤快顯或通知。 錯誤會對應用程式本身、您的品牌、裝置的品牌、裝置的製造商品牌和 Microsoft 品牌產生負面影響。
付費應用程式必須具有試用版模式: 您的應用程式必須是免費或包含試用版模式。 客戶不會想要為零售商店的體驗付錢。
高優先順序需求
不符合這些高優先順序需求的 RDX 感知應用程式必須立即調查以修正。 如果找不到立即修正程式,則可能會從所有零售示範裝置中移除此應用程式。
令人難忘的離線體驗: 您的應用程式需要示範絕佳的離線體驗,因為在零售地點中大約有 50% 的裝置都是離線。 這是為了確保與您的應用程式互動的客戶仍然能夠獲得富有意義的積極體驗。
更新的內容體驗: 在線上時,您的應用程式絕對不應該提示更新。 如果需要更新,應該以無訊息方式執行更新。
無匿名通訊: 由於使用零售示範裝置的客戶是匿名使用者,因此他們不該能從裝置傳送訊息或共用內容。
使用清除流程提供一致的體驗: 當每位客戶使用零售示範裝置時,都應該有相同的體驗。 您的應用程式應該使用清除流程,在每次使用之後返回相同的預設狀態。 我們不希望下一個客戶看到上一個客戶留下的任何資訊。 這包括計分板、成就和解除鎖定。
年齡適當的內容: 所有應用程式內容都必須獲得指定的青少年或較低分級類別。 若要深入瞭解,請參閱為應用程式取得 IARC 和 ESRB 評等。
中優先順序需求
Windows Retail Store 小組可以直接與開發人員連絡,以安排如何修正這些問題的討論。
能夠在各種裝置上順利執行: 應用程式必須在所有裝置上正常執行,包括配備低端規格的裝置。 如果應用程式安裝在不符合最低規格的裝置上,應用程式必須清楚告知使用者有關此情況。 必須先告知最低裝置需求,好讓應用程式一律能以高效能執行。
符合零售商店應用程式大小要求: 應用程式必須小於 800MB。 如果您的 RDX 感知應用程式不符合大小需求,請直接連絡 Windows Retail Store 小組以進一步討論。
RetailInfo API:為示範模式準備好您的程式碼
IsDemoModeEnabled
RetailInfo 公用程式類別中的 IsDemoModeEnabled 屬性,是 Windows 10 和 Windows 11 SDK 中的Windows.System.Profile 命名空間,做為布林指標使用來指定應用程式要在那個程式碼路徑上執行:一般模式或零售模式。
using Windows.Storage;
StorageFolder folder = ApplicationData.Current.LocalFolder;
if (Windows.System.Profile.RetailInfo.IsDemoModeEnabled)
{
// Use the demo specific directory
folder = await folder.GetFolderAsync("demo");
}
StorageFile file = await folder.GetFileAsync("hello.txt");
// Now read from file
using namespace Windows::Storage;
StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
if (Windows::System::Profile::RetailInfo::IsDemoModeEnabled)
{
// Use the demo specific directory
create_task(localFolder->GetFolderAsync("demo").then([this](StorageFolder^ demoFolder)
{
return demoFolder->GetFileAsync("hello.txt");
}).then([this](task<StorageFile^> fileTask)
{
StorageFile^ file = fileTask.get();
});
// Do something with file
}
else
{
create_task(localFolder->GetFileAsync("hello.txt").then([this](StorageFile^ file)
{
// Do something with file
});
}
if (Windows.System.Profile.retailInfo.isDemoModeEnabled) {
console.log("Retail mode is enabled.");
} else {
Console.log("Retail mode is not enabled.");
}
RetailInfo.Properties
當 IsDemoModeEnabled 傳回 true,您可以使用 RetailInfo.Properties 查詢關於裝置的屬性集,以建立更客製化的零售示範體驗。 這些屬性包含 ManufacturerName、Screensize、Memory 等。
using Windows.UI.Xaml.Controls;
using Windows.System.Profile
TextBlock priceText = new TextBlock();
priceText.Text = RetailInfo.Properties[KnownRetailInfo.Price];
// Assume infoPanel is a StackPanel declared in XAML
this.infoPanel.Children.Add(priceText);
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::System::Profile;
TextBlock ^manufacturerText = ref new TextBlock();
manufacturerText.set_Text(RetailInfo::Properties[KnownRetailInfoProperties::Price]);
// Assume infoPanel is a StackPanel declared in XAML
this->infoPanel->Children->Add(manufacturerText);
var pro = Windows.System.Profile;
console.log(pro.retailInfo.properties[pro.KnownRetailInfoProperties.price);
IDL
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// WindowsRuntimeAPISet
import "oaidl.idl";
import "inspectable.idl";
import "Windows.Foundation.idl";
#include <sdkddkver.h>
namespace Windows.System.Profile
{
runtimeclass RetailInfo;
runtimeclass KnownRetailInfoProperties;
[version(NTDDI_WINTHRESHOLD), uuid(0712C6B8-8B92-4F2A-8499-031F1798D6EF), exclusiveto(RetailInfo)]
[version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
interface IRetailInfoStatics : IInspectable
{
[propget] HRESULT IsDemoModeEnabled([out, retval] boolean *value);
[propget] HRESULT Properties([out, retval, hasvariant] Windows.Foundation.Collections.IMapView<HSTRING, IInspectable *> **value);
}
[version(NTDDI_WINTHRESHOLD), uuid(50BA207B-33C4-4A5C-AD8A-CD39F0A9C2E9), exclusiveto(KnownRetailInfoProperties)]
[version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
interface IKnownRetailInfoPropertiesStatics : IInspectable
{
[propget] HRESULT RetailAccessCode([out, retval] HSTRING *value);
[propget] HRESULT ManufacturerName([out, retval] HSTRING *value);
[propget] HRESULT ModelName([out, retval] HSTRING *value);
[propget] HRESULT DisplayModelName([out, retval] HSTRING *value);
[propget] HRESULT Price([out, retval] HSTRING *value);
[propget] HRESULT IsFeatured([out, retval] HSTRING *value);
[propget] HRESULT FormFactor([out, retval] HSTRING *value);
[propget] HRESULT ScreenSize([out, retval] HSTRING *value);
[propget] HRESULT Weight([out, retval] HSTRING *value);
[propget] HRESULT DisplayDescription([out, retval] HSTRING *value);
[propget] HRESULT BatteryLifeDescription([out, retval] HSTRING *value);
[propget] HRESULT ProcessorDescription([out, retval] HSTRING *value);
[propget] HRESULT Memory([out, retval] HSTRING *value);
[propget] HRESULT StorageDescription([out, retval] HSTRING *value);
[propget] HRESULT GraphicsDescription([out, retval] HSTRING *value);
[propget] HRESULT FrontCameraDescription([out, retval] HSTRING *value);
[propget] HRESULT RearCameraDescription([out, retval] HSTRING *value);
[propget] HRESULT HasNfc([out, retval] HSTRING *value);
[propget] HRESULT HasSdSlot([out, retval] HSTRING *value);
[propget] HRESULT HasOpticalDrive([out, retval] HSTRING *value);
[propget] HRESULT IsOfficeInstalled([out, retval] HSTRING *value);
[propget] HRESULT WindowsVersion([out, retval] HSTRING *value);
}
[version(NTDDI_WINTHRESHOLD), static(IRetailInfoStatics, NTDDI_WINTHRESHOLD)]
[version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone), static(IRetailInfoStatics, NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
[threading(both)]
[marshaling_behavior(agile)]
runtimeclass RetailInfo
{
}
[version(NTDDI_WINTHRESHOLD), static(IKnownRetailInfoPropertiesStatics, NTDDI_WINTHRESHOLD)]
[version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone), static(IKnownRetailInfoPropertiesStatics, NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
[threading(both)]
[marshaling_behavior(agile)]
runtimeclass KnownRetailInfoProperties
{
}
}
清除程序
會在消費者停止與裝置互動的兩分鐘後開始清除。 零售示範會播放,而 Windows 會開始重設聯絡人、相片和其他應用程式中的任何範例資料。 視裝置而定,這可能需要 1-5 分鐘的時間,才能將所有項目完全重設為一般情況。 這可確保零售商店中的每個客戶都可以隨意使用任何裝置,並在與裝置互動時具有相同的體驗。
步驟 1:清除
- 所有 Win32 和商店應用程式都會關閉
- 在已知資料夾如:[圖片]、[影片]、[音樂]、[文件]、[儲存的圖片]、[相簿]、[桌面] 和 [下載] 資料夾中的所有檔案都會遭到刪除。
- 非結構化和結構化漫遊狀態會遭到刪除
- 結構化本機狀態會遭到刪除
步驟 2:設定
- 若為離線裝置:資料夾維持空白
- 若為線上裝置:零售示範資產可以從 Microsoft Store 推送至裝置
跨使用者工作階段儲存資料
若要跨使用者工作階段儲存資料,您可以將資訊儲存在 ApplicationData.Current.TemporaryFolder 中,因為預設清除流程不會自動刪除此資料夾中的資料。 請注意,在清除流程期間,會刪除使用 LocalState 儲存的資訊。
自訂清除流程
若要自訂清除流程,請將 Microsoft-RetailDemo-Cleanup
應用程式服務實作到您的應用程式。
需要自訂清除邏輯的案例包括執行大量設定、下載和快取資料,或不想刪除 LocalState 資料。
步驟 1:在應用程式資訊清單中宣告 Microsoft-RetailDemo-Cleanup 服務。
<Applications>
<Extensions>
<uap:Extension Category="windows.appService" EntryPoint="MyCompany.MyApp.RDXCustomCleanupTask">
<uap:AppService Name="Microsoft-RetailDemo-Cleanup" />
</uap:Extension>
</Extensions>
</Application>
</Applications>
步驟 2:使用下列範例範本,在 AppdataCleanup 案例函式下實作自訂清除邏輯。
using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;
using Windows.Storage;
namespace MyCompany.MyApp
{
public sealed class RDXCustomCleanupTask : IBackgroundTask
{
BackgroundTaskCancellationReason _cancelReason = BackgroundTaskCancellationReason.Abort;
BackgroundTaskDeferral _deferral = null;
IBackgroundTaskInstance _taskInstance = null;
AppServiceConnection _appServiceConnection = null;
const string MessageCommand = "Command";
public void Run(IBackgroundTaskInstance taskInstance)
{
// Get the deferral object from the task instance, and take a reference to the taskInstance;
_deferral = taskInstance.GetDeferral();
_taskInstance = taskInstance;
_taskInstance.Canceled += new BackgroundTaskCanceledEventHandler(OnCanceled);
AppServiceTriggerDetails appService = _taskInstance.TriggerDetails as AppServiceTriggerDetails;
if ((appService != null) && (appService.Name == "Microsoft-RetailDemo-Cleanup"))
{
_appServiceConnection = appService.AppServiceConnection;
_appServiceConnection.RequestReceived += _appServiceConnection_RequestReceived;
_appServiceConnection.ServiceClosed += _appServiceConnection_ServiceClosed;
}
else
{
_deferral.Complete();
}
}
void _appServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
}
async void _appServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
//Get a deferral because we will be calling async code
AppServiceDeferral requestDeferral = args.GetDeferral();
string command = null;
var returnData = new ValueSet();
try
{
ValueSet message = args.Request.Message;
if (message.ContainsKey(MessageCommand))
{
command = message[MessageCommand] as string;
}
if (command != null)
{
switch (command)
{
case "AppdataCleanup":
{
// Do custom clean up logic here
break;
}
}
}
}
catch (Exception e)
{
}
finally
{
requestDeferral.Complete();
// Also release the task deferral since we only process one request per instance.
_deferral.Complete();
}
}
private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
_cancelReason = reason;
}
}
}