快速入门:在 Windows 应用 SDK 中推送通知

在本快速入门中,你将创建一个桌面 Windows 应用程序,该应用程序使用 Windows 应用 SDK 发送和接收推送通知。

先决条件

示例应用

本快速入门介绍如何向应用添加推送通知支持。 在 GitHub上的示例应用中查看本快速入门的示例代码,以了解其实际使用场景。

API 参考

有关推送通知的 API 参考文档,请参阅 Microsoft.Windows.PushNotifications 命名空间

在 Azure Active Directory 中配置应用的标识(AAD)

Windows 应用 SDK 中的推送通知使用 Azure Active Directory(AAD)中的标识。 请求 WNS 通道 URI 和请求访问令牌时,需要 Azure 凭据才能发送推送通知。 注意:我们在合作伙伴中心使用 Windows 应用 SDK 推送 Microsoft通知 支持。

步骤 1:创建 AAD 应用注册

登录到 Azure 帐户并创建新的 AAD 应用注册 资源。 选择“新注册”

步骤 2:提供名称并选择多租户选项

  1. 提供应用名称。

  2. 推送通知需要多租户选项,因此请选择该选项。

    1. 有关租户的详细信息,请参阅谁可以登录到你的应用?
  3. 选择注册

  4. 请记下您的 应用程序(客户端)ID,因为这是您在进行激活注册和请求访问令牌时会用到的 Azure 应用 ID

  5. 请记下你的 目录(租户)ID,因为这是你请求访问令牌时将使用的 Azure 租户ID

    重要

    AAD 应用注册租户 记下您的 应用程序(客户端)ID目录(租户)ID

  6. 请记下 对象标识符,因为这是在请求通道时将使用的 Azure ObjectId。 请注意,这不是 “概要” 页上列出的对象 ID。 相反,若要查找正确的对象 ID,请在 Essentials 页的本地目录字段中单击托管应用程序中的应用名称:

    屏幕截图,显示“概要”页上的“本地目录”选项中的托管应用程序

    显示“对象 ID”字段的屏幕截图

    注释

    如果您的应用未关联对象 ID,则获取对象 ID 需要 服务主体。请按照以下任一文章中的步骤,通过 Azure 门户或命令行创建一个:

    使用门户创建可访问资源的 Azure AD 应用程序和服务主体

    通过 Azure PowerShell 使用证书创建服务主体

步骤 3:为应用注册创建机密

在请求用于发送推送通知的访问令牌时,你的密钥将与 Azure AppId/ClientId 一起使用。

AAD 应用机密

导航到 证书和机密 ,然后选择 “新建客户端密码”。

重要

请在创建机密后确保将其复制并存储在安全位置,例如 Azure Key Vault。 创建后,该内容只能查看一次。

步骤 4:将应用的程序包系列名称映射到其 Azure 应用 ID

如果您的应用已打包(包括与外部来源一起打包),您可以使用此流程来映射应用的程序包系列名称(PFN)及其 Azure 应用 ID。

如果您的应用是打包的 Win32 应用,请发送主题为“Windows 应用 SDK 推送通知映射请求”的电子邮件至 Win_App_SDK_Push@microsoft.com ,邮件正文为“PFN:[您的 PFN]”、AppId:[您的 APPId]、ObjectId:[您的 ObjectId],以创建包系列名称(PFN)映射请求。 映射请求每周都会完成。 映射请求完成后,将收到通知。

将应用配置为接收推送通知

步骤 1:添加命名空间声明

为 Windows 应用 SDK 推送通知添加命名空间 Microsoft.Windows.PushNotifications

#include <winrt/Microsoft.Windows.PushNotifications.h>

using namespace winrt::Microsoft::Windows::PushNotifications;

步骤 2:将 COM 激活器添加到应用的清单

重要

如果应用已解压缩(即运行时缺少包标识),请跳到 步骤 3:注册并响应应用启动时的推送通知

如果你的应用程序已打包(包括与外部位置一起打包),请打开 Package.appxmanifest。 在元素中添加 <Application> 以下内容。 将IdExecutableDisplayName值替换为特定于你的应用的值。

<!--Packaged apps only-->
<!--package.appxmanifest-->

<Package
  ...
  xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
  ...
  <Applications>
    <Application>
      ...
      <Extensions>

        <!--Register COM activator-->    
        <com:Extension Category="windows.comServer">
          <com:ComServer>
              <com:ExeServer Executable="SampleApp\SampleApp.exe" DisplayName="SampleApp" Arguments="----WindowsAppRuntimePushServer:">
                <com:Class Id="[Your app's Azure AppId]" DisplayName="Windows App SDK Push" />
            </com:ExeServer>
          </com:ComServer>
        </com:Extension>
    
      </Extensions>
    </Application>
  </Applications>
 </Package>    

步骤 3:在应用启动时注册和响应推送通知

更新应用 main() 的方法以添加以下内容:

  1. 通过调用 PushNotificationManager::Default().Register()注册应用以接收推送通知。
  2. 通过调用 AppInstance::GetCurrent().GetActivatedEventArgs()来检查激活请求的源。 如果激活是由推送通知触发的,请根据通知的消息内容进行响应。

重要

必须在调用 AppInstance.GetCurrent.GetActivatedEventArgs之前,调用 PushNotificationManager::Default().Register

以下示例来自 GitHub 上找到的示例打包应用。

// cpp-console.cpp
#include "pch.h"
#include <iostream>
#include <winrt/Microsoft.Windows.PushNotifications.h>
#include <winrt/Microsoft.Windows.AppLifecycle.h>
#include <winrt/Windows.Foundation.h>
#include <wil/result.h>
#include <wil/cppwinrt.h>


using namespace winrt;
using namespace Windows::Foundation;

using namespace winrt::Microsoft::Windows::PushNotifications;
using namespace winrt::Microsoft::Windows::AppLifecycle;

winrt::guid remoteId{ "7edfab6c-25ae-4678-b406-d1848f97919a" }; // Replace this with your own Azure ObjectId



void SubscribeForegroundEventHandler()
{
    winrt::event_token token{ PushNotificationManager::Default().PushReceived([](auto const&, PushNotificationReceivedEventArgs const& args)
    {
        auto payload{ args.Payload() };

        std::string payloadString(payload.begin(), payload.end());
        std::cout << "\nPush notification content received in the FOREGROUND: " << payloadString << std::endl;
    }) };
}

int main()
{
    // Setup an event handler, so we can receive notifications in the foreground while the app is running.
    SubscribeForegroundEventHandler();

    PushNotificationManager::Default().Register();

    auto args{ AppInstance::GetCurrent().GetActivatedEventArgs() };
    switch (args.Kind())
    {
        // When it is launched normally (by the users, or from the debugger), the sample requests a WNS Channel URI and
        // displays it, then waits for notifications. This user can take a copy of the WNS Channel URI and use it to send
        // notifications to the sample
        case ExtendedActivationKind::Launch:
        {
            // Checks to see if push notifications are supported. Certain self-contained apps may not support push notifications by design
            if (PushNotificationManager::IsSupported())
            {
                // Request a WNS Channel URI which can be passed off to an external app to send notifications to.
                // The WNS Channel URI uniquely identifies this app for this user and device.
                PushNotificationChannel channel{ RequestChannel() };
                if (!channel)
                {
                    std::cout << "\nThere was an error obtaining the WNS Channel URI" << std::endl;
    
                    if (remoteId == winrt::guid { "00000000-0000-0000-0000-000000000000" })
                    {
                        std::cout << "\nThe ObjectID has not been set. Refer to the readme file accompanying this sample\nfor the instructions on how to obtain and setup an ObjectID" << std::endl;
                    }
                }
    
                std::cout << "\nPress 'Enter' at any time to exit App." << std::endl;
                std::cin.ignore();
            }
            else
            {
                // App implements its own custom socket here to receive messages from the cloud since Push APIs are unsupported.
            }
        }
        break;

        // When it is activated from a push notification, the sample only displays the notification.
        // It doesn’t register for foreground activation of perform any other actions
        // because background activation is meant to let app perform only small tasks in order to preserve battery life.
        case ExtendedActivationKind::Push:
        {
            PushNotificationReceivedEventArgs pushArgs{ args.Data().as<PushNotificationReceivedEventArgs>() };

            // Call GetDeferral to ensure that code runs in low power
            auto deferral{ pushArgs.GetDeferral() };

            auto payload{ pushArgs.Payload() } ;

            // Do stuff to process the raw notification payload
            std::string payloadString(payload.begin(), payload.end());
            std::cout << "\nPush notification content received in the BACKGROUND: " << payloadString.c_str() << std::endl;
            std::cout << "\nPress 'Enter' to exit the App." << std::endl;

            // Call Complete on the deferral when finished processing the payload.
            // This removes the override that kept the app running even when the system was in a low power mode.
            deferral.Complete();
            std::cin.ignore();
        }
        break;

        default:
            std::cout << "\nUnexpected activation type" << std::endl;
            std::cout << "\nPress 'Enter' to exit the App." << std::endl;
            std::cin.ignore();
            break;
    }

    // We do not call PushNotificationManager::UnregisterActivator
    // because then we wouldn't be able to receive background activations, once the app has closed.
    // Call UnregisterActivator once you don't want to receive push notifications anymore.
}

步骤 4:请求 WNS 通道 URI 并将其注册到 WNS 服务器

WNS 通道 URI 是用于发送推送通知的 HTTP 终结点。 每个客户端必须请求通道 URI 并将其注册到 WNS 服务器以接收推送通知。

注释

WNS 通道 URI 在 30 天后过期。

auto channelOperation{ PushNotificationManager::Default().CreateChannelAsync(winrt::guid("[Your app's Azure ObjectID]")) };

PushNotificationManager 将尝试创建一个通道URI,并且在15分钟内自动重试。 创建事件处理程序以等待调用完成。 调用完成后,如果成功,请向 WNS 服务器注册 URI。

// cpp-console.cpp

winrt::Windows::Foundation::IAsyncOperation<PushNotificationChannel> RequestChannelAsync()
{
    // To obtain an AAD RemoteIdentifier for your app,
    // follow the instructions on https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app
    auto channelOperation = PushNotificationManager::Default().CreateChannelAsync(remoteId);

    // Setup the inprogress event handler
    channelOperation.Progress(
        [](auto&& sender, auto&& args)
        {
            if (args.status == PushNotificationChannelStatus::InProgress)
            {
                // This is basically a noop since it isn't really an error state
                std::cout << "Channel request is in progress." << std::endl << std::endl;
            }
            else if (args.status == PushNotificationChannelStatus::InProgressRetry)
            {
                LOG_HR_MSG(
                    args.extendedError,
                    "The channel request is in back-off retry mode because of a retryable error! Expect delays in acquiring it. RetryCount = %d",
                    args.retryCount);
            }
        });

    auto result = co_await channelOperation;

    if (result.Status() == PushNotificationChannelStatus::CompletedSuccess)
    {
        auto channelUri = result.Channel().Uri();

        std::cout << "channelUri: " << winrt::to_string(channelUri.ToString()) << std::endl << std::endl;

        auto channelExpiry = result.Channel().ExpirationTime();

        // Caller's responsibility to keep the channel alive
        co_return result.Channel();
    }
    else if (result.Status() == PushNotificationChannelStatus::CompletedFailure)
    {
        LOG_HR_MSG(result.ExtendedError(), "We hit a critical non-retryable error with channel request!");
        co_return nullptr;
    }
    else
    {
        LOG_HR_MSG(result.ExtendedError(), "Some other failure occurred.");
        co_return nullptr;
    }

};

PushNotificationChannel RequestChannel()
{
    auto task = RequestChannelAsync();
    if (task.wait_for(std::chrono::seconds(300)) != AsyncStatus::Completed)
    {
        task.Cancel();
        return nullptr;
    }

    auto result = task.GetResults();
    return result;
}

步骤 5:生成并安装应用

使用 Visual Studio 生成和安装应用。 右键单击解决方案资源管理器中的解决方案文件,然后选择 部署。 Visual Studio 将生成应用并将其安装在计算机上。 可以通过“开始”菜单或 Visual Studio 调试器启动应用来运行应用。

向应用发送推送通知

此时,所有配置都已完成,WNS 服务器可以将推送通知发送到客户端应用。 在以下步骤中,请参阅 推送通知服务器请求和响应标头 以获取更多详细信息。

步骤 1:请求访问令牌

若要发送推送通知,WNS 服务器首先需要请求访问令牌。 使用 Azure TenantId、Azure AppId 和机密发送 HTTP POST 请求。 有关获取 Azure 租户 ID 和应用程序 ID 的信息,请参阅 获取用于登录的租户 ID 和应用程序 ID 值

HTTP 示例请求:

POST /{tenantID}/oauth2/v2.0/token Http/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 160

grant_type=client_credentials&client_id=<Azure_App_Registration_AppId_Here>&client_secret=<Azure_App_Registration_Secret_Here>&scope=https://wns.windows.com/.default/

C# 示例请求:

//Sample C# Access token request
var client = new RestClient("https://login.microsoftonline.com/{tenantID}/oauth2/v2.0");
var request = new RestRequest("/token", Method.Post);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("grant_type", "client_credentials");
request.AddParameter("client_id", "[Your app's Azure AppId]");
request.AddParameter("client_secret", "[Your app's secret]");
request.AddParameter("scope", "https://wns.windows.com/.default");
RestResponse response = await client.ExecutePostAsync(request);
Console.WriteLine(response.Content);

如果请求成功,将收到一个响应,其中包含 access_token 字段中的令牌。

{
    "token_type":"Bearer",
    "expires_in":"86399",
    "ext_expires_in":"86399",
    "expires_on":"1653771789",
    "not_before":"1653685089",
    "access_token":"[your access token]"
}

步骤 2. 发送原始通知

创建一个 HTTP POST 请求,其中包含在上一步中获取的访问令牌和要发送的推送通知的内容。 推送通知的内容将传送到应用。

POST /?token=[The token query string parameter from your channel URL. E.g. AwYAAABa5cJ3...] HTTP/1.1
Host: dm3p.notify.windows.com
Content-Type: application/octet-stream
X-WNS-Type: wns/raw
Authorization: Bearer [your access token]
Content-Length: 46

{ Sync: "Hello from the Contoso App Service" }
var client = new RestClient("[Your channel URL. E.g. https://wns2-by3p.notify.windows.com/?token=AwYAAABa5cJ3...]");
var request = new RestRequest();
request.Method = Method.Post; 
request.AddHeader("Content-Type", "application/octet-stream");
request.AddHeader("X-WNS-Type", "wns/raw");
request.AddHeader("Authorization", "Bearer [your access token]");
request.AddBody("Notification body");
RestResponse response = await client.ExecutePostAsync(request);");

步骤 3:发送云源应用通知

如果只想发送原始通知,请忽略此步骤。 若要发送云源应用通知(也称为推送 Toast 通知),请先遵循 快速入门:Windows 应用 SDK 中的应用通知。 应用通知可以推送(从云发送)或在本地发送。 发送基于云的应用通知类似于在 步骤 2中发送原始通知,但与此不同的是,X-WNS-Type 标头需要 toastContent-Type 需要 text/xml,并且内容包含应用通知 XML 有效负载。 有关如何构造 XML 有效负载的详细信息,请参阅 通知 XML 架构

创建一个 HTTP POST 请求,其中包含访问令牌和要发送的云源应用通知的内容。 推送通知的内容将传送到应用。

POST /?token=AwYAAAB%2fQAhYEiAESPobjHzQcwGCTjHu%2f%2fP3CCNDcyfyvgbK5xD3kztniW%2bjba1b3aSSun58SA326GMxuzZooJYwtpgzL9AusPDES2alyQ8CHvW94cO5VuxxLDVzrSzdO1ZVgm%2bNSB9BAzOASvHqkMHQhsDy HTTP/1.1
Host: dm3p.notify.windows.com
Content-Type: text/xml
X-WNS-Type: wns/toast
Authorization: Bearer [your access token]
Content-Length: 180

<toast><visual><binding template="ToastGeneric"><text>Example cloud toast notification</text><text>This is an example cloud notification using XML</text></binding></visual></toast>
var client = new RestClient("https://dm3p.notify.windows.com/?token=AwYAAAB%2fQAhYEiAESPobjHzQcwGCTjHu%2f%2fP3CCNDcyfyvgbK5xD3kztniW%2bjba1b3aSSun58SA326GMxuzZooJYwtpgzL9AusPDES2alyQ8CHvW94cO5VuxxLDVzrSzdO1ZVgm%2bNSB9BAzOASvHqkMHQhsDy");
client.Timeout = -1;

var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "text/xml");
request.AddHeader("X-WNS-Type", "wns/toast");
request.AddHeader("Authorization", "Bearer <AccessToken>");
request.AddParameter("text/xml", "<toast><visual><binding template=\"ToastGeneric\"><text>Example cloud toast notification</text><text>This is an example cloud notification using XML</text></binding></visual></toast>",  ParameterType.RequestBody);
Console.WriteLine(response.Content);

资源