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

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

先决条件

示例应用

本快速入门介绍如何向应用添加推送通知支持。 请参阅本快速入门中的示例代码,该示例代码位于 GitHub 上的示例应用上下文中。

API 参考

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

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

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

步骤 1:创建 AAD 应用注册

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

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

  1. 提供应用名称。

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

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

  4. 记下应用程序(客户端)ID,因为这是将在激活注册和访问令牌请求期间使用的 Azure AppId

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

    重要

    AAD App Registration Tenant 记下应用程序(客户端) ID目录(租户) ID

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

    Screenshot showing the Managed application in local directory option on the Essentials page

    Screenshot showing the Object ID field

    注意

    获取对象 ID 需要服务主体,如果没有与应用关联的服务主体,请按照以下文章之一中的步骤在Azure 门户或使用命令行创建一个:

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

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

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

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

AAD App Secret

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

重要

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

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

重要

Windows 推送通知服务(WNS)现已与 Azure 门户集成。 新的注册体验现已推出预览版。 如果是打包的应用(包括打包到外部位置),则可以使用此流来映射应用的包系列名称(PFN)及其 Azure AppId。

如果应用是打包的 Win32 应用,则通过电子邮件向 Win_App_SDK_Push@microsoft.com 发送主题行“Windows 应用 SDK 推送通知请求”和正文“Azure 订阅:[Azure 订阅 ID]”来请求访问新的 Azure 门户预览体验。 请求每周完成一次。 完成映射请求后,你将收到通知。

配置应用以接收推送通知

步骤 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 ChannelURI 并将其注册到 WNS 服务器

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

注意

WNS Channel URI 在 30 天后过期。

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

PushNotificationManager 将尝试创建 Channel URI,自动重试时间不超过 15 分钟。 创建事件处理程序以等待调用完成。 调用完成后,如果成功,则将 URI 注册到 WNS 服务器。

// 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 TenantId 和 Azure AppId 的信息,请参阅获取用于登录的租户和应用 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-Typetext/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);

资源