教程:使用 Azure 通知中心通过后端服务向 Xamarin.Forms 应用发送推送通知

下载示例 下载示例

本教程使用 Azure 通知中心将通知推送到面向 AndroidiOSXamarin.Forms 应用程序。

ASP.NET Core Web API 后端用于使用最新和最佳安装方法处理客户端的设备注册。 该服务还将以跨平台方式发送推送通知。

这些操作使用 用于后端操作的通知中心 SDK 进行处理。 有关总体方法的更多详细信息,请参阅 从应用后端注册 文档。

本教程将指导你完成以下步骤:

先决条件

若要继续操作,需要:

对于 Android,必须具有:

  • 开发人员解锁的物理设备或仿真器 (运行 API 26 及更高版本,) 安装 Google Play Services

对于 iOS,必须具有:

注意

iOS 模拟器不支持远程通知,因此在 iOS 上浏览此示例时需要物理设备。 但是,无需在 AndroidiOS 上运行应用即可完成本教程。

可以按照此第一原则示例中的步骤进行操作,但之前没有经验。 但是,通过熟悉以下方面,你将受益匪浅。

重要

提供的步骤特定于Visual Studio for Mac。 可以使用 Visual Studio 2019 继续操作,但可能需要协调一些差异。 例如,用户界面和工作流、模板名称、环境配置等的说明。

设置推送通知服务和 Azure 通知中心

在本部分中,将 设置 Firebase Cloud Messaging (FCM) Apple Push Notification Services (APNS) 。 然后,创建并配置通知中心以使用这些服务。

创建 Firebase 项目并启用适用于 Android 的 Firebase Cloud Messaging

  1. 登录到 Firebase 控制台。 创建一个新的 Firebase 项目,输入 PushDemo 作为 项目名称

    注意

    将为你生成唯一的名称。 默认情况下,它由你提供的名称的小写变体以及由短划线分隔的生成数字组成。 如果需要,可以更改此设置,前提是它仍然全局唯一。

  2. 创建项目后,选择“ 将 Firebase 添加到 Android 应用”。

    将 Firebase 添加到 Android 应用

  3. “将 Firebase 添加到 Android 应用 ”页上,执行以下步骤。

    1. 对于 Android 包名称,请输入包的名称。 例如: com.<organization_identifier>.<package_name>

      指定包名称

    2. 选择“ 注册应用”。

    3. 选择 “下载google-services.json”。 然后将文件保存到本地文件夹中供稍后使用,然后选择“ 下一步”。

      下载google-services.json

    4. 选择“ 下一步”。

    5. 选择“ 继续控制台”

      注意

      如果由于验证安装检查未启用“继续控制台”按钮,请选择“跳过此步骤”。

  4. 在 Firebase 控制台中,选择项目的齿轮。 然后选择“项目设置”。

    选择“项目设置”

    注意

    如果尚未下载 google-services.json 文件,可以在此页面上下载它。

  5. 切换到顶部的“ 云消息 ”选项卡。 复制并保存 服务器密钥 供以后使用。 使用此值配置通知中心。

    复制服务器密钥

注册 iOS 应用以获取推送通知

若要向 iOS 应用发送推送通知,请将应用程序注册到 Apple,并注册推送通知。

  1. 如果尚未注册应用,请浏览到 Apple 开发人员中心的 iOS 预配门户 。 使用 Apple ID 登录到门户,导航到 “证书”、“标识符 & 配置文件”,然后选择“ 标识符”。 单击 + 以注册新应用。

    iOS 预配门户应用 ID 页

  2. “注册新标识符” 屏幕上,选择“ 应用 ID ”单选按钮。 然后选择“ 继续”。

    iOS 预配门户注册新 ID 页

  3. 更新新应用的以下三个值,然后选择“ 继续”:

    • 说明:键入应用的描述性名称。

    • 捆绑 ID:输入 com.organization_identifier<>格式的捆绑 ID。<product_name>,如应用分发指南中所述。 在以下屏幕截图中 mobcat ,该值用作组织标识符, PushDemo 值用作产品名称。

      iOS 预配门户注册应用 ID 页

    • 推送通知:选中“功能”部分中的“推送通知”选项。

      用于注册新应用 ID 的表单

      此操作将生成应用 ID 并请求你确认信息。 选择“ 继续”,然后选择“ 注册 ”以确认新的应用 ID。

      确认新的应用 ID

      选择“ 注册”后,可以在“ 证书、标识符 & 配置文件 ”页中看到新的应用 ID 作为行项。

  4. “证书、标识符 & 配置文件 ”页 的“标识符”下,找到创建的“应用 ID”行项。 然后选择其行以显示 “编辑应用 ID 配置” 屏幕。

为通知中心创建证书

需要证书才能使通知中心能够与 Apple Push Notification Services (APNS) 配合使用,并且可以通过以下两种方式之一提供:

  1. 创建可直接上传到通知中心的 p12 推送证书 , (原始方法)

  2. 创建可用于基于令牌的身份验证的 p8 证书 , (较新的建议方法)

新方法具有许多优点,如 基于令牌的 (HTTP/2) APNS 身份验证中所述。 所需步骤更少,但针对特定方案也强制要求执行。 但是,已为这两种方法提供了步骤,因为两种方法都适用于本教程。

选项 1:创建可直接上传到通知中心的 p12 推送证书
  1. 在 Mac 上,运行密钥链访问工具。 可以从“ 实用工具” 文件夹或 Launchpad 上的 “其他 ”文件夹打开它。

  2. 选择“ 密钥链访问”,展开“ 证书助手”,然后选择“ 从证书颁发机构请求证书”。

    使用密钥链访问请求新证书

    注意

    默认情况下,Keychain Access 会选择列表中的第一项。 如果你在 “证书 ”类别中,而 Apple Worldwide Developer Relations 证书颁发机构 不是列表中的第一项,则这可能是个问题。 在生成 CSR (证书签名请求) 之前,请确保具有非密钥项或选择了 Apple Worldwide Developer Relations 证书颁发机构 密钥。

  3. 选择“用户Email地址”,输入“公用名”值,确保指定“保存到磁盘”,然后选择“继续”。 将 CA Email 地址留空,因为不需要。

    所需的证书信息

  4. 在“另存为中输入证书签名请求 (CSR) 文件的名称,在“位置”中选择位置,然后选择“保存”。

    选择证书的文件名

    此操作将 CSR 文件 保存在所选位置。 默认位置为“桌面”。 请记住为文件选择的位置。

  5. 返回到 iOS 预配门户中的“证书、标识符 & 配置文件”页,向下滚动到选中的“推送通知”选项,然后选择“配置”以创建证书。

    “编辑应用 ID”页

  6. 此时会显示 Apple 推送通知服务 TLS/SSL 证书 窗口。 选择“开发 TLS/SSL 证书”部分下的“创建证书”按钮。

    “为应用 ID 创建证书”按钮

    将显示 “创建新证书” 屏幕。

    注意

    本教程使用开发证书。 注册生产证书时使用相同的过程。 只需确保在发送通知时使用相同的证书类型。

  7. 选择“ 选择文件”,浏览到保存 CSR 文件的位置,然后双击证书名称以加载它。 然后选择“ 继续”。

  8. 门户创建证书后,选择“ 下载 ”按钮。 保存证书,并记住保存证书的位置。

    生成的证书下载页

    证书将下载并保存到“ 下载” 文件夹中的计算机。

    在“下载”文件夹中找到证书文件

    注意

    默认情况下,下载的开发证书名为 aps_development.cer

  9. 双击下载的推送证书 aps_development.cer。 此操作在密钥链中安装新证书,如下图所示:

    显示新证书的密钥链访问证书列表

    注意

    尽管证书中的名称可能不同,但名称将以 Apple Development iOS Push Services 作为前缀,并具有与之关联的相应捆绑标识符。

  10. 在“密钥链访问 ”中,控制 + 单击 在“证书”类别中创建的新推送 证书 。 选择“ 导出”,为文件命名,选择 p12 格式,然后选择“ 保存”。

    将证书导出为 p12 格式

    可以选择使用密码保护证书,但密码是可选的。 如果要绕过密码创建,请单击“ 确定 ”。 记下导出的 p12 证书的文件名和位置。 它们用于启用 APN 身份验证。

    注意

    p12 文件名和位置可能与本教程中所示不同。

选项 2:创建可用于基于令牌的身份验证的 p8 证书
  1. 请记下以下详细信息:

    • 应用 ID 前缀 (团队 ID)
    • 捆绑包 ID
  2. 返回到 “证书”“标识符 & 配置文件”中,单击“ 密钥”。

    注意

    如果已为 APNS 配置了密钥,则可以重复使用在创建后立即下载的 p8 证书。 如果是这样,可以忽略步骤 35

  3. +单击 (按钮或“创建密钥”按钮) 创建新密钥。

  4. 提供合适的“密钥名称”值,然后检查 Apple 推送通知服务 (APNS) 选项,然后单击“继续”,然后单击“注册”,然后在下一个屏幕上单击“注册”。

  5. 单击“ 下载 ”,然后将前缀为 AuthKey_) 的 p8文件 (移动到 安全的本地目录,然后单击“ 完成”。

    注意

    请确保将 p8 文件保存在安全位置 (并保存备份) 。 下载密钥后,无法重新下载该密钥,因为删除了服务器副本。

  6. “密钥”上,单击 (创建的密钥或现有密钥(如果选择改用) )。

  7. 记下 “密钥 ID” 值。

  8. 在所选的合适应用程序中打开 p8 证书,例如Visual Studio Code。 记下 -----BEGIN 私钥----------END PRIVATE KEY-----) 之间的密钥 (值。

    -----BEGIN 私钥-----
    <key_value>
    -----发送私钥-----

    注意

    这是稍后将用于配置通知中心的令牌值

在这些步骤结束时,你应具有以下信息,供稍后在 使用 APNS 信息配置通知中心中使用:

  • 团队 ID (请参阅步骤 1)
  • 捆绑 ID (请参阅步骤 1)
  • 密钥 ID (请参阅步骤 7)
  • 步骤 8) 中获取的令牌值 (p8 密钥值

为应用创建预配配置文件

  1. 返回到 iOS 预配门户,选择“ 证书”、“标识符 & 配置文件”,从左侧菜单中选择“ 配置文件+ ,然后选择创建新的配置文件。 将显示 “注册新预配配置文件” 屏幕。

  2. 选择“开发”下的“iOS 应用开发”作为预配配置文件类型,然后选择“继续”。

    预配配置文件列表

  3. 接下来,从“应用 ID”下拉列表中选择创建 的应用 ID ,然后选择“ 继续”。

    选择应用 ID

  4. “选择证书” 窗口中,选择用于代码签名的开发证书,然后选择“ 继续”。

    注意

    此证书不是在 上一步中创建的推送证书。 这是你的开发证书。 如果不存在,则必须创建它,因为这是本教程的 先决条件 。 开发人员证书可以在 Apple 开发人员门户中通过 Xcode 或在 Visual Studio 中创建。

  5. 返回到 “证书、标识符 & 配置文件 ”页,从左侧菜单中选择“ 配置文件 ”,然后选择 + 创建新的配置文件。 将显示 “注册新预配配置文件” 屏幕。

  6. “选择证书” 窗口中,选择创建的开发证书。 然后选择“ 继续”。

  7. 接下来,选择要用于测试的设备,然后选择“ 继续”。

  8. 最后,在“预配配置文件名称”中选择 配置文件的名称,然后选择“ 生成”。

    选择预配配置文件名称

  9. 创建新的预配配置文件后,选择“ 下载”。 记住保存它的位置。

  10. 浏览到预配配置文件的位置,然后双击它以将其安装在开发计算机上。

创建通知中心

在本部分中,将创建通知中心并使用 APNS 配置身份验证。 可以使用 p12 推送证书或基于令牌的身份验证。 如果要使用已创建的通知中心,可以跳到步骤 5。

  1. 登录到 Azure

  2. 单击“ 创建资源”,搜索并选择“ 通知中心”,然后单击“ 创建”。

  3. 更新以下字段,然后单击“ 创建”:

    基本详细信息

    订阅:从下拉列表中选择目标订阅
    资源组: 创建新的 资源组 (或选择现有的资源组)

    命名空间详细信息

    通知中心命名空间: 输入 通知中心 命名空间的全局唯一名称

    注意

    确保为此字段选择了 “新建” 选项。

    通知中心详细信息

    通知中心:输入通知中心的名称
    位置: 从下拉列表中选择合适的位置
    定价层: 保留默认的 “免费” 选项

    注意

    除非已达到免费层上中心的最大数目。

  4. 预配 通知中心 后,导航到该资源。

  5. 导航到新的 通知中心

  6. “管理) ”下的列表中选择 (访问策略

  7. 记下 “策略名称” 值及其对应的 “连接字符串” 值。

使用 APNS 信息配置通知中心

“通知服务”下,选择“ Apple ”,然后根据之前在 “为通知中心创建证书 ”部分选择的方法执行相应的步骤。

注意

仅当你想要向从应用商店购买应用的用户发送推送通知时,才使用应用程序模式的生产。

选项 1:使用 .p12 推送证书

  1. 选择“ 证书”。

  2. 选择文件图标。

  3. 选择之前导出的 .p12 文件,然后选择“ 打开”。

  4. 如有必要,请指定正确的密码。

  5. 选择 “沙盒 模式”。

  6. 选择“保存”。

选项 2:使用基于令牌的身份验证

  1. 选择“ 令牌”。

  2. 输入之前获取的以下值:

    • 密钥 ID
    • 捆绑 ID
    • 团队 ID
    • 令 牌
  3. 选择 “沙盒”。

  4. 选择“保存”。

使用 FCM 信息配置通知中心

  1. 在左侧菜单中的“设置”部分选择 Google (GCM/FCM)
  2. 输入从 Google Firebase 控制台中记录的服务器密钥
  3. 在工具栏上选择“ 保存 ”。

创建 ASP.NET Core Web API 后端应用程序

在本部分中,将创建 ASP.NET Core Web API 后端来处理设备注册和向 Xamarin.Forms 移动应用发送通知。

创建 Web 项目

  1. Visual Studio 中,选择“ 文件>新建解决方案”。

  2. 选择“.NET Core>应用>ASP.NET Core>API>下一步”。

  3. “配置新 ASP.NET Core Web API”对话框中,选择“.NET Core 3.1的目标框架”。

  4. “项目名称”输入 PushDemoApi,然后选择“创建”。

  5. (Command + Enter) 开始调试以测试模板化应用。

    注意

    模板化应用配置为使用 WeatherForecastController 作为 launchUrl。 此属性在 “属性>”launchSettings.json中设置。

    如果系统提示出现“ 发现开发证书无效” 消息:

    1. 单击“ ”同意运行“dotnet dev-certs https”工具来解决此问题。 然后,“dotnet dev-certs https”工具提示输入证书的密码和密钥链的密码。

    2. 当系统提示“安装并信任新证书”时,单击“”,然后输入密钥链的密码。

  6. 展开 “控制器” 文件夹,然后删除 WeatherForecastController.cs

  7. 删除 WeatherForecast.cs

  8. 使用 机密管理器工具设置本地配置值。 将机密与解决方案分离可确保它们最终不会在源代码管理中结束。 打开 终端 ,然后转到项目文件的 目录并运行以下命令:

    dotnet user-secrets init
    dotnet user-secrets set "NotificationHub:Name" <value>
    dotnet user-secrets set "NotificationHub:ConnectionString" <value>
    

    将占位符值替换为你自己的通知中心名称和连接字符串值。 你在 创建通知中心 部分记下了这些内容。 否则,可以在 Azure 中查找它们。

    NotificationHub:Name
    请参阅概述顶部的“概要”摘要中的“名称”。

    NotificationHub:ConnectionString
    请参阅访问策略中的 DefaultFullSharedAccessSignature

    注意

    对于生产方案,可以查看 Azure KeyVault 等选项,以安全地存储连接字符串。 为简单起见,机密将添加到Azure 应用服务应用程序设置中。

使用 API 密钥 (可选) 对客户端进行身份验证

API 密钥不如令牌安全,但足以满足本教程的目的。 可以通过 ASP.NET 中间件轻松配置 API 密钥。

  1. API 密钥 添加到本地配置值。

    dotnet user-secrets set "Authentication:ApiKey" <value>
    

    注意

    应将占位符值替换为自己的值,并记下它。

  2. 控制 + 单击PushDemoApi 项目,从“添加”菜单中选择“新建文件夹,然后单击“使用身份验证添加”作为文件夹名称

  3. 控制 + 单击“身份验证”文件夹,然后从“添加”菜单中选择“新建文件...”

  4. 选择“常规>空类”,输入“名称ApiKeyAuthOptions.cs”,然后单击“新建”添加以下实现。

    using Microsoft.AspNetCore.Authentication;
    
    namespace PushDemoApi.Authentication
    {
        public class ApiKeyAuthOptions : AuthenticationSchemeOptions
        {
            public const string DefaultScheme = "ApiKey";
            public string Scheme => DefaultScheme;
            public string ApiKey { get; set; }
        }
    }
    
  5. 将另一个空类添加到名为 ApiKeyAuthHandler.csAuthentication 文件夹,然后添加以下实现。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Text.Encodings.Web;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    
    namespace PushDemoApi.Authentication
    {
        public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
        {
            const string ApiKeyIdentifier = "apikey";
    
            public ApiKeyAuthHandler(
                IOptionsMonitor<ApiKeyAuthOptions> options,
                ILoggerFactory logger,
                UrlEncoder encoder,
                ISystemClock clock)
                : base(options, logger, encoder, clock) {}
    
            protected override Task<AuthenticateResult> HandleAuthenticateAsync()
            {
                string key = string.Empty;
    
                if (Request.Headers[ApiKeyIdentifier].Any())
                {
                    key = Request.Headers[ApiKeyIdentifier].FirstOrDefault();
                }
                else if (Request.Query.ContainsKey(ApiKeyIdentifier))
                {
                    if (Request.Query.TryGetValue(ApiKeyIdentifier, out var queryKey))
                        key = queryKey;
                }
    
                if (string.IsNullOrWhiteSpace(key))
                    return Task.FromResult(AuthenticateResult.Fail("No api key provided"));
    
                if (!string.Equals(key, Options.ApiKey, StringComparison.Ordinal))
                    return Task.FromResult(AuthenticateResult.Fail("Invalid api key."));
    
                var identities = new List<ClaimsIdentity> {
                    new ClaimsIdentity("ApiKeyIdentity")
                };
    
                var ticket = new AuthenticationTicket(
                    new ClaimsPrincipal(identities), Options.Scheme);
    
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
        }
    }
    

    注意

    身份验证处理程序是实现方案行为的类型,在本例中为自定义 API 密钥方案。

  6. 将另一个空类添加到名为 ApiKeyAuthenticationBuilderExtensions.cs的 Authentication 文件夹中,然后添加以下实现。

    using System;
    using Microsoft.AspNetCore.Authentication;
    
    namespace PushDemoApi.Authentication
    {
        public static class AuthenticationBuilderExtensions
        {
            public static AuthenticationBuilder AddApiKeyAuth(
                this AuthenticationBuilder builder,
                Action<ApiKeyAuthOptions> configureOptions)
            {
                return builder
                    .AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>(
                        ApiKeyAuthOptions.DefaultScheme,
                        configureOptions);
            }
        }
    }
    

    注意

    此扩展方法简化了 Startup.cs 中的中间件配置代码,使其更具可读性,并且通常更易于遵循。

  7. Startup.cs 中,更新 ConfigureServices 方法,以在对服务的调用下配置 API 密钥身份验证 。AddControllers 方法。

    using PushDemoApi.Authentication;
    using PushDemoApi.Models;
    using PushDemoApi.Services;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = ApiKeyAuthOptions.DefaultScheme;
            options.DefaultChallengeScheme = ApiKeyAuthOptions.DefaultScheme;
        }).AddApiKeyAuth(Configuration.GetSection("Authentication").Bind);
    }
    
  8. Startup.cs中,更新 Configure 方法以在应用的 IApplicationBuilder 上调用 UseAuthenticationUseAuthorization 扩展方法。 确保在 UseRouting 之后和应用之前调用这些方法 。UseEndpoints

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseRouting();
    
        app.UseAuthentication();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    

    注意

    调用 UseAuthentication 会注册中间件,该中间件使用以前从 ConfigureServices) 注册的身份验证方案 (。 在依赖于用户进行身份验证的任何中间件之前,必须调用它。

添加依赖项并配置服务

ASP.NET Core支持依赖项注入 (DI) 软件设计模式,这是一种在类及其依赖项之间实现控制 (IoC) 的反转的技术。

将通知中心和 通知中心 SDK 用于后端操作 会封装在服务中。 该服务已注册并通过合适的抽象提供。

  1. 控制 + 单击“依赖项”文件夹,然后选择“管理 NuGet 包...”

  2. 搜索 Microsoft.Azure.NotificationHubs 并确保已选中它。

  3. 单击“添加包”,然后在系统提示接受许可条款时单击“接受”。

  4. 控制 + 单击PushDemoApi 项目,从“添加”菜单中选择“新建文件夹”,然后单击“使用模型添加”作为文件夹名称

  5. 控制 + 单击“模型”文件夹,然后从“添加”菜单中选择“新建文件...”

  6. 选择“常规>空类”,输入“名称PushTemplates.cs,然后单击“新建”添加以下实现。

    namespace PushDemoApi.Models
    {
        public class PushTemplates
        {
            public class Generic
            {
                public const string Android = "{ \"notification\": { \"title\" : \"PushDemo\", \"body\" : \"$(alertMessage)\"}, \"data\" : { \"action\" : \"$(alertAction)\" } }";
                public const string iOS = "{ \"aps\" : {\"alert\" : \"$(alertMessage)\"}, \"action\" : \"$(alertAction)\" }";
            }
    
            public class Silent
            {
                public const string Android = "{ \"data\" : {\"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\"} }";
                public const string iOS = "{ \"aps\" : {\"content-available\" : 1, \"apns-priority\": 5, \"sound\" : \"\", \"badge\" : 0}, \"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\" }";
            }
        }
    }
    

    注意

    此类包含此方案所需的泛型和无提示通知的标记化通知有效负载。 有效负载在 安装 外部定义,以允许试验,而无需通过服务更新现有安装。 以这种方式处理对安装的更改不在本教程中。 对于生产环境,请考虑 使用自定义模板

  7. 将另一个空类添加到名为 DeviceInstallation.csModels 文件夹,然后添加以下实现。

    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace PushDemoApi.Models
    {
        public class DeviceInstallation
        {
            [Required]
            public string InstallationId { get; set; }
    
            [Required]
            public string Platform { get; set; }
    
            [Required]
            public string PushChannel { get; set; }
    
            public IList<string> Tags { get; set; } = Array.Empty<string>();
        }
    }
    
  8. 将另一个空类添加到名为 NotificationRequest.csModels 文件夹,然后添加以下实现。

    using System;
    
    namespace PushDemoApi.Models
    {
        public class NotificationRequest
        {
            public string Text { get; set; }
            public string Action { get; set; }
            public string[] Tags { get; set; } = Array.Empty<string>();
            public bool Silent { get; set; }
        }
    }
    
  9. 将另一个空类添加到名为 NotificationHubOptions.csModels 文件夹,然后添加以下实现。

    using System.ComponentModel.DataAnnotations;
    
    namespace PushDemoApi.Models
    {
        public class NotificationHubOptions
        {
            [Required]
            public string Name { get; set; }
    
            [Required]
            public string ConnectionString { get; set; }
        }
    }
    
  10. 将名为“服务”的新文件夹添加到 PushDemoApi 项目。

  11. 空接口添加到名为 INotificationService.cs的 Services 文件夹,然后添加以下实现。

    using System.Threading;
    using System.Threading.Tasks;
    using PushDemoApi.Models;
    
    namespace PushDemoApi.Services
    {
        public interface INotificationService
        {
            Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation deviceInstallation, CancellationToken token);
            Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken token);
            Task<bool> RequestNotificationAsync(NotificationRequest notificationRequest, CancellationToken token);
        }
    }
    
  12. 空类添加到名为 NotificationHubsService.cs的 Services 文件夹,然后添加以下代码来实现 INotificationService 接口:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.NotificationHubs;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using PushDemoApi.Models;
    
    namespace PushDemoApi.Services
    {
        public class NotificationHubService : INotificationService
        {
            readonly NotificationHubClient _hub;
            readonly Dictionary<string, NotificationPlatform> _installationPlatform;
            readonly ILogger<NotificationHubService> _logger;
    
            public NotificationHubService(IOptions<NotificationHubOptions> options, ILogger<NotificationHubService> logger)
            {
                _logger = logger;
                _hub = NotificationHubClient.CreateClientFromConnectionString(
                    options.Value.ConnectionString,
                    options.Value.Name);
    
                _installationPlatform = new Dictionary<string, NotificationPlatform>
                {
                    { nameof(NotificationPlatform.Apns).ToLower(), NotificationPlatform.Apns },
                    { nameof(NotificationPlatform.Fcm).ToLower(), NotificationPlatform.Fcm }
                };
            }
    
            public async Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation deviceInstallation, CancellationToken token)
            {
                if (string.IsNullOrWhiteSpace(deviceInstallation?.InstallationId) ||
                    string.IsNullOrWhiteSpace(deviceInstallation?.Platform) ||
                    string.IsNullOrWhiteSpace(deviceInstallation?.PushChannel))
                    return false;
    
                var installation = new Installation()
                {
                    InstallationId = deviceInstallation.InstallationId,
                    PushChannel = deviceInstallation.PushChannel,
                    Tags = deviceInstallation.Tags
                };
    
                if (_installationPlatform.TryGetValue(deviceInstallation.Platform, out var platform))
                    installation.Platform = platform;
                else
                    return false;
    
                try
                {
                    await _hub.CreateOrUpdateInstallationAsync(installation, token);
                }
                catch
                {
                    return false;
                }
    
                return true;
            }
    
            public async Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken token)
            {
                if (string.IsNullOrWhiteSpace(installationId))
                    return false;
    
                try
                {
                    await _hub.DeleteInstallationAsync(installationId, token);
                }
                catch
                {
                    return false;
                }
    
                return true;
            }
    
            public async Task<bool> RequestNotificationAsync(NotificationRequest notificationRequest, CancellationToken token)
            {
                if ((notificationRequest.Silent &&
                    string.IsNullOrWhiteSpace(notificationRequest?.Action)) ||
                    (!notificationRequest.Silent &&
                    (string.IsNullOrWhiteSpace(notificationRequest?.Text)) ||
                    string.IsNullOrWhiteSpace(notificationRequest?.Action)))
                    return false;
    
                var androidPushTemplate = notificationRequest.Silent ?
                    PushTemplates.Silent.Android :
                    PushTemplates.Generic.Android;
    
                var iOSPushTemplate = notificationRequest.Silent ?
                    PushTemplates.Silent.iOS :
                    PushTemplates.Generic.iOS;
    
                var androidPayload = PrepareNotificationPayload(
                    androidPushTemplate,
                    notificationRequest.Text,
                    notificationRequest.Action);
    
                var iOSPayload = PrepareNotificationPayload(
                    iOSPushTemplate,
                    notificationRequest.Text,
                    notificationRequest.Action);
    
                try
                {
                    if (notificationRequest.Tags.Length == 0)
                    {
                        // This will broadcast to all users registered in the notification hub
                        await SendPlatformNotificationsAsync(androidPayload, iOSPayload, token);
                    }
                    else if (notificationRequest.Tags.Length <= 20)
                    {
                        await SendPlatformNotificationsAsync(androidPayload, iOSPayload, notificationRequest.Tags, token);
                    }
                    else
                    {
                        var notificationTasks = notificationRequest.Tags
                            .Select((value, index) => (value, index))
                            .GroupBy(g => g.index / 20, i => i.value)
                            .Select(tags => SendPlatformNotificationsAsync(androidPayload, iOSPayload, tags, token));
    
                        await Task.WhenAll(notificationTasks);
                    }
    
                    return true;
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "Unexpected error sending notification");
                    return false;
                }
            }
    
            string PrepareNotificationPayload(string template, string text, string action) => template
                .Replace("$(alertMessage)", text, StringComparison.InvariantCulture)
                .Replace("$(alertAction)", action, StringComparison.InvariantCulture);
    
            Task SendPlatformNotificationsAsync(string androidPayload, string iOSPayload, CancellationToken token)
            {
                var sendTasks = new Task[]
                {
                    _hub.SendFcmNativeNotificationAsync(androidPayload, token),
                    _hub.SendAppleNativeNotificationAsync(iOSPayload, token)
                };
    
                return Task.WhenAll(sendTasks);
            }
    
            Task SendPlatformNotificationsAsync(string androidPayload, string iOSPayload, IEnumerable<string> tags, CancellationToken token)
            {
                var sendTasks = new Task[]
                {
                    _hub.SendFcmNativeNotificationAsync(androidPayload, tags, token),
                    _hub.SendAppleNativeNotificationAsync(iOSPayload, tags, token)
                };
    
                return Task.WhenAll(sendTasks);
            }
        }
    }
    

    注意

    提供给 SendTemplateNotificationAsync 的标记表达式限制为 20 个标记。 对于大多数运算符,它限制为 6,但表达式仅包含 (||在本例中) 。 如果请求中有 20 个以上的标记,则必须将其拆分为多个请求。 有关更多详细信息,请参阅 路由和标记表达式 文档。

  13. Startup.cs 中,更新 ConfigureServices 方法,将 NotificationHubsService 添加为 INotificationService 的单一实例实现。

    
    using PushDemoApi.Models;
    using PushDemoApi.Services;
    
    public void ConfigureServices(IServiceCollection services)
    {
        ...
    
        services.AddSingleton<INotificationService, NotificationHubService>();
    
        services.AddOptions<NotificationHubOptions>()
            .Configure(Configuration.GetSection("NotificationHub").Bind)
            .ValidateDataAnnotations();
    }
    

创建通知 API

  1. 控制 + 单击“控制器”文件夹,然后从“添加”菜单中选择“新建文件...”

  2. 选择“ASP.NET Core>Web API 控制器类”,输入“通知”“控制器”,输入“名称”,然后单击“新建”。

    注意

    如果要关注 Visual Studio 2019,请选择 具有读/写操作的 API 控制器 模板。

  3. 将以下命名空间添加到文件顶部。

    using System.ComponentModel.DataAnnotations;
    using System.Net;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using PushDemoApi.Models;
    using PushDemoApi.Services;
    
  4. 更新模板化控制器,使其派生自 ControllerBase 并使用 ApiController 属性进行修饰。

    [ApiController]
    [Route("api/[controller]")]
    public class NotificationsController : ControllerBase
    {
        // Templated methods here
    }
    

    注意

    Controller 基类提供对视图的支持,但在这种情况下不需要这样做,因此可以改用 ControllerBase。 如果要关注 Visual Studio 2019,则可以跳过此步骤。

  5. 如果选择完成“使用 API 密钥对客户端进行身份验证”部分,还应使用 Authorize 属性修饰 NotificationsController

    [Authorize]
    
  6. 更新构造函数以接受 已注册的 INotificationService 实例作为参数,并将其分配给只读成员。

    readonly INotificationService _notificationService;
    
    public NotificationsController(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }
    
  7. “属性”文件夹) launchSettings.json (中,将 launchUrlweatherforecast 更改为 api/notifications,以匹配 RegistrationsControllerRoute 属性中指定的 URL。

  8. 开始调试 (Command + Enter) 以验证应用是否使用新的 NotificationsController 并返回 “401 未授权” 状态。

    注意

    Visual Studio 可能不会在浏览器中自动启动应用。 此时,你将使用 Postman 测试 API。

  9. 在新的 Postman 选项卡上,将请求设置为 GET。 输入以下地址,将占位符 <applicationUrl> 替换为属性>launchSettings.json中的 https applicationUrl

    <applicationUrl>/api/notifications
    

    注意

    默认配置文件的 applicationUrl 应为“https://localhost:5001”。 如果在 Windows) 上使用 Visual Studio 2019 中默认 (IIS,则应改用 iisSettings 项中指定的 applicationUrl。 如果地址不正确,你将收到 404 响应。

  10. 如果选择完成 “使用 API 密钥对客户端进行身份验证” 部分,请确保将请求标头配置为包含 apikey 值。

    apikey <your_api_key>
  11. 单击“ 发送 ”按钮。

    注意

    应会收到包含某些 JSON 内容的“200 正常”状态。

    如果收到 SSL 证书验证警告,则可以在“设置”中关闭请求 SSL 证书验证 Postman 设置。

  12. NotificationsController.cs 中的模板化类方法替换为以下代码。

    [HttpPut]
    [Route("installations")]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
    public async Task<IActionResult> UpdateInstallation(
        [Required]DeviceInstallation deviceInstallation)
    {
        var success = await _notificationService
            .CreateOrUpdateInstallationAsync(deviceInstallation, HttpContext.RequestAborted);
    
        if (!success)
            return new UnprocessableEntityResult();
    
        return new OkResult();
    }
    
    [HttpDelete()]
    [Route("installations/{installationId}")]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
    public async Task<ActionResult> DeleteInstallation(
        [Required][FromRoute]string installationId)
    {
        var success = await _notificationService
            .DeleteInstallationByIdAsync(installationId, CancellationToken.None);
    
        if (!success)
            return new UnprocessableEntityResult();
    
        return new OkResult();
    }
    
    [HttpPost]
    [Route("requests")]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
    public async Task<IActionResult> RequestPush(
        [Required]NotificationRequest notificationRequest)
    {
        if ((notificationRequest.Silent &&
            string.IsNullOrWhiteSpace(notificationRequest?.Action)) ||
            (!notificationRequest.Silent &&
            string.IsNullOrWhiteSpace(notificationRequest?.Text)))
            return new BadRequestResult();
    
        var success = await _notificationService
            .RequestNotificationAsync(notificationRequest, HttpContext.RequestAborted);
    
        if (!success)
            return new UnprocessableEntityResult();
    
        return new OkResult();
    }
    

创建 API 应用

现在,在 Azure 应用服务 中创建 API 应用,用于托管后端服务。

  1. 登录 Azure 门户

  2. 单击“ 创建资源”,搜索并选择“ API 应用”,然后单击“ 创建”。

  3. 更新以下字段,然后单击“ 创建”。

    应用名称:
    输入 API 应用的全局唯一名称

    订阅:
    选择在其中创建通知中心的同一目标 订阅

    资源组:
    选择创建通知中心的同一 资源组

    App 服务计划/位置:
    创建新的App 服务计划

    注意

    从默认选项更改为包含 SSL 支持的计划。 否则,在使用移动应用时,需要采取适当的步骤,以防止 http 请求被阻止。

    Application Insights:
    保留建议选项 (将使用该名称创建新资源) 或选取现有资源。

  4. 预配 API 应用 后,导航到该资源。

  5. 记下“概述”顶部的“概要”摘要中的 URL 属性。 此 URL 是 后端终结点 ,稍后将在本教程中使用。

    注意

    URL 使用前面指定的 API 应用名称,格式 https://<app_name>.azurewebsites.net为 。

  6. 从“设置) ”下 (列表中选择“配置”。

  7. 对于以下每个设置,请单击“ 新建应用程序设置 ”以输入 “名称” 和“ ”,然后单击“ 确定”。

    名称
    Authentication:ApiKey <api_key_value>
    NotificationHub:Name <hub_name_value>
    NotificationHub:ConnectionString <hub_connection_string_value>

    注意

    这些设置与之前在用户设置中定义的设置相同。 你应该能够复制这些内容。 仅当选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 Authentication:ApiKey 设置。 对于生产方案,可以查看 Azure KeyVault 等选项。 为了简单起见,已添加这些设置作为应用程序设置在本例中。

  8. 添加所有应用程序设置后,单击“ 保存”,然后单击 “继续”。

发布后端服务

接下来,将应用部署到 API 应用,使其可从所有设备访问。

注意

以下步骤特定于Visual Studio for Mac。 如果你在 Windows 上关注 Visual Studio 2019 ,发布流将有所不同。 请参阅发布到 Windows 上的Azure 应用服务

  1. 将配置从 “调试” 更改为 “发布 ”(如果尚未这样做)。

  2. 控制 + 单击PushDemoApi 项目,然后从“发布”菜单中选择“发布到 Azure...”

  3. 如果系统提示,请按照身份验证流进行操作。 使用在上一 个创建 API 应用 部分中使用的帐户。

  4. 从列表中选择之前创建的Azure 应用服务 API 应用作为发布目标,然后单击“发布”。

完成向导后,它会将应用发布到 Azure,然后打开该应用。 请记下 URL (如果尚未这样做)。 此 URL 是本教程稍后使用的 后端终结点

验证已发布的 API

  1. Postman 中打开一个新选项卡,将请求设置为 PUT ,然后在下面输入地址。 将 占位符替换为在上一 次发布后端服务 部分中记下的基址。

    https://<app_name>.azurewebsites.net/api/notifications/installations
    

    注意

    基址的格式应为 https://<app_name>.azurewebsites.net/

  2. 如果选择完成 “使用 API 密钥对客户端进行身份验证” 部分,请确保将请求标头配置为包含 apikey 值。

    apikey <your_api_key>
  3. 选择“正文的原始选项,然后从格式选项列表中选择“JSON”,然后包括一些占位符 JSON 内容:

    {}
    
  4. 单击“Send”。

    注意

    应从服务收到 422 UnprocessableEntity 状态。

  5. 再次执行步骤 1-4,但这次指定请求终结点以验证你收到 400 错误请求 响应。

    https://<app_name>.azurewebsites.net/api/notifications/requests
    

注意

目前无法使用有效的请求数据测试 API,因为这需要来自客户端移动应用的特定于平台的信息。

创建跨平台 Xamarin.Forms 应用程序

在本部分中,将生成一个 Xamarin.Forms 移动应用程序,以跨平台的方式实现推送通知。

它使你能够通过创建的后端服务从通知中心注册和取消注册。

指定操作且应用位于前台时,将显示警报。 否则,通知将显示在通知中心。

注意

通常会在应用程序生命周期 (的适当时间点执行注册 (和取消注册) 操作,或者作为首次运行体验的一部分,在不使用显式用户注册/取消注册输入的情况下) 。 但是,此示例需要显式用户输入,以便更轻松地浏览和测试此功能。

创建 Xamarin.Forms 解决方案

  1. Visual Studio 中,使用空白窗体应用作为模板并输入 PushDemo 作为项目名称,创建新的 Xamarin.Forms 解决方案。

    注意

    “配置空白表单应用 ”对话框中,确保 “组织标识符 ”与之前使用的值匹配,并且已选中 AndroidiOS 目标。

  2. 控制 + 单击PushDemo 解决方案,然后选择 “更新 NuGet 包”。

  3. 控制 + 单击PushDemo 解决方案,然后选择“管理 NuGet 包”。

  4. 搜索 Newtonsoft.Json 并确保它已选中。

  5. 单击“ 添加包”,然后在系统提示接受许可条款时单击“ 接受 ”。

  6. 在每个目标平台上生成并运行应用, (输入命令 + ) 测试 模板化应用在设备上运行 () 。

实现跨平台组件

  1. 控制 + 单击PushDemo 项目,从“添加”菜单中选择“新建文件夹”,然后单击“使用模型添加”作为文件夹名称

  2. 控制 + 单击“模型”文件夹,然后从“添加”菜单中选择“新建文件...”

  3. 选择“ 常规>空类”,输入 “DeviceInstallation.cs”,然后添加以下实现。

    using System.Collections.Generic;
    using Newtonsoft.Json;
    
    namespace PushDemo.Models
    {
        public class DeviceInstallation
        {
            [JsonProperty("installationId")]
            public string InstallationId { get; set; }
    
            [JsonProperty("platform")]
            public string Platform { get; set; }
    
            [JsonProperty("pushChannel")]
            public string PushChannel { get; set; }
    
            [JsonProperty("tags")]
            public List<string> Tags { get; set; } = new List<string>();
        }
    }
    
  4. 使用以下实现将空枚举添加到名为 PushDemoAction.csModels 文件夹。

    namespace PushDemo.Models
    {
        public enum PushDemoAction
        {
            ActionA,
            ActionB
        }
    }
    
  5. 将名为 Services 的新文件夹添加到 PushDemo 项目,然后将空添加到名为 ServiceContainer.cs 的文件夹,并使用以下实现。

    using System;
    using System.Collections.Generic;
    
    namespace PushDemo.Services
    {
       public static class ServiceContainer
       {
           static readonly Dictionary<Type, Lazy<object>> services
               = new Dictionary<Type, Lazy<object>>();
    
           public static void Register<T>(Func<T> function)
               => services[typeof(T)] = new Lazy<object>(() => function());
    
           public static T Resolve<T>()
               => (T)Resolve(typeof(T));
    
           public static object Resolve(Type type)
           {
               {
                   if (services.TryGetValue(type, out var service))
                       return service.Value;
    
                   throw new KeyNotFoundException($"Service not found for type '{type}'");
               }
           }
       }
    }
    

    注意

    这是 XamCAT 存储库中 ServiceContainer 类的剪裁版本。 它将用作轻型 IoC (控制反转) 容器。

  6. 将空接口添加到名为 IDeviceInstallationService.cs的 Services 文件夹,然后添加以下代码。

    using PushDemo.Models;
    
    namespace PushDemo.Services
    {
        public interface IDeviceInstallationService
        {
            string Token { get; set; }
            bool NotificationsSupported { get; }
            string GetDeviceId();
            DeviceInstallation GetDeviceInstallation(params string[] tags);
        }
    }
    

    注意

    此接口稍后将由每个目标实现和启动,以提供后端服务所需的特定于平台的功能和 DeviceInstallation 信息。

  7. 将另一个空接口添加到名为 INotificationRegistrationService.cs的 Services 文件夹,然后添加以下代码。

    using System.Threading.Tasks;
    
    namespace PushDemo.Services
    {
        public interface INotificationRegistrationService
        {
            Task DeregisterDeviceAsync();
            Task RegisterDeviceAsync(params string[] tags);
            Task RefreshRegistrationAsync();
        }
    }
    

    注意

    这将处理客户端与后端服务之间的交互。

  8. 将另一个空接口添加到名为 INotificationActionService.cs的 Services 文件夹,然后添加以下代码。

    namespace PushDemo.Services
    {
        public interface INotificationActionService
        {
            void TriggerAction(string action);
        }
    }
    

    注意

    这用作集中处理通知操作的简单机制。

  9. 使用以下实现将空接口添加到名为 IPushDemoNotificationActionService.csServices 文件夹,该文件夹派生自 INotificationActionService

    using System;
    using PushDemo.Models;
    
    namespace PushDemo.Services
    {
        public interface IPushDemoNotificationActionService : INotificationActionService
        {
            event EventHandler<PushDemoAction> ActionTriggered;
        }
    }
    

    注意

    此类型特定于 PushDemo 应用程序,并使用 PushDemoAction 枚举来标识以强类型方式触发的操作。

  10. 将空添加到名为 NotificationRegistrationService.cs使用以下代码实现 INotificationRegistrationService的 Services 文件夹。

    using System;
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using Newtonsoft.Json;
    using PushDemo.Models;
    using Xamarin.Essentials;
    
    namespace PushDemo.Services
    {
        public class NotificationRegistrationService : INotificationRegistrationService
        {
            const string RequestUrl = "api/notifications/installations";
            const string CachedDeviceTokenKey = "cached_device_token";
            const string CachedTagsKey = "cached_tags";
    
            string _baseApiUrl;
            HttpClient _client;
            IDeviceInstallationService _deviceInstallationService;
    
            public NotificationRegistrationService(string baseApiUri, string apiKey)
            {
                _client = new HttpClient();
                _client.DefaultRequestHeaders.Add("Accept", "application/json");
                _client.DefaultRequestHeaders.Add("apikey", apiKey);
    
                _baseApiUrl = baseApiUri;
            }
    
            IDeviceInstallationService DeviceInstallationService
                => _deviceInstallationService ??
                    (_deviceInstallationService = ServiceContainer.Resolve<IDeviceInstallationService>());
    
            public async Task DeregisterDeviceAsync()
            {
                var cachedToken = await SecureStorage.GetAsync(CachedDeviceTokenKey)
                    .ConfigureAwait(false);
    
                if (cachedToken == null)
                    return;
    
                var deviceId = DeviceInstallationService?.GetDeviceId();
    
                if (string.IsNullOrWhiteSpace(deviceId))
                    throw new Exception("Unable to resolve an ID for the device.");
    
                await SendAsync(HttpMethod.Delete, $"{RequestUrl}/{deviceId}")
                    .ConfigureAwait(false);
    
                SecureStorage.Remove(CachedDeviceTokenKey);
                SecureStorage.Remove(CachedTagsKey);
            }
    
            public async Task RegisterDeviceAsync(params string[] tags)
            {
                var deviceInstallation = DeviceInstallationService?.GetDeviceInstallation(tags);
    
                await SendAsync<DeviceInstallation>(HttpMethod.Put, RequestUrl, deviceInstallation)
                    .ConfigureAwait(false);
    
                await SecureStorage.SetAsync(CachedDeviceTokenKey, deviceInstallation.PushChannel)
                    .ConfigureAwait(false);
    
                await SecureStorage.SetAsync(CachedTagsKey, JsonConvert.SerializeObject(tags));
            }
    
            public async Task RefreshRegistrationAsync()
            {
                var cachedToken = await SecureStorage.GetAsync(CachedDeviceTokenKey)
                    .ConfigureAwait(false);
    
                var serializedTags = await SecureStorage.GetAsync(CachedTagsKey)
                    .ConfigureAwait(false);
    
                if (string.IsNullOrWhiteSpace(cachedToken) ||
                    string.IsNullOrWhiteSpace(serializedTags) ||
                    string.IsNullOrWhiteSpace(DeviceInstallationService.Token) ||
                    cachedToken == DeviceInstallationService.Token)
                    return;
    
                var tags = JsonConvert.DeserializeObject<string[]>(serializedTags);
    
                await RegisterDeviceAsync(tags);
            }
    
            async Task SendAsync<T>(HttpMethod requestType, string requestUri, T obj)
            {
                string serializedContent = null;
    
                await Task.Run(() => serializedContent = JsonConvert.SerializeObject(obj))
                    .ConfigureAwait(false);
    
                await SendAsync(requestType, requestUri, serializedContent);
            }
    
            async Task SendAsync(
                HttpMethod requestType,
                string requestUri,
                string jsonRequest = null)
            {
                var request = new HttpRequestMessage(requestType, new Uri($"{_baseApiUrl}{requestUri}"));
    
                if (jsonRequest != null)
                    request.Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
    
                var response = await _client.SendAsync(request).ConfigureAwait(false);
    
                response.EnsureSuccessStatusCode();
            }
        }
    }
    

    注意

    仅当选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 apiKey 参数。

  11. 使用以下代码实现 IPushDemoNotificationActionServicePushDemoNotificationActionService.cs,将名为“空类”添加到 Services 文件夹。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using PushDemo.Models;
    
    namespace PushDemo.Services
    {
        public class PushDemoNotificationActionService : IPushDemoNotificationActionService
        {
            readonly Dictionary<string, PushDemoAction> _actionMappings = new Dictionary<string, PushDemoAction>
            {
                { "action_a", PushDemoAction.ActionA },
                { "action_b", PushDemoAction.ActionB }
            };
    
            public event EventHandler<PushDemoAction> ActionTriggered = delegate { };
    
            public void TriggerAction(string action)
            {
                if (!_actionMappings.TryGetValue(action, out var pushDemoAction))
                    return;
    
                List<Exception> exceptions = new List<Exception>();
    
                foreach (var handler in ActionTriggered?.GetInvocationList())
                {
                    try
                    {
                        handler.DynamicInvoke(this, pushDemoAction);
                    }
                    catch (Exception ex)
                    {
                        exceptions.Add(ex);
                    }
                }
    
                if (exceptions.Any())
                    throw new AggregateException(exceptions);
            }
        }
    }
    
  12. 使用以下实现将空类添加到名为 Config.csPushDemo 项目。

    namespace PushDemo
    {
        public static partial class Config
        {
            public static string ApiKey = "API_KEY";
            public static string BackendServiceEndpoint = "BACKEND_SERVICE_ENDPOINT";
        }
    }
    

    注意

    这用作将机密排除在源代码管理中的简单方法。 可以将这些值替换为自动生成的一部分,也可以使用本地分部类替代它们。 将在下一步中执行此操作。

    仅当选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 ApiKey 字段。

  13. 将另一个 空类 添加到 PushDemo 项目,这次调用 Config.local_secrets.cs 使用以下实现。

    namespace PushDemo
    {
        public static partial class Config
        {
            static Config()
            {
                ApiKey = "<your_api_key>";
                BackendServiceEndpoint = "<your_api_app_url>";
            }
        }
    }
    

    注意

    将占位符值替换为自己的值。 生成后端服务时,应该已记下这些内容。 API 应用 URL 应为 https://<api_app_name>.azurewebsites.net/。 请记得将 添加到 *.local_secrets.* gitignore 文件以避免提交此文件。

    仅当选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 ApiKey 字段。

  14. 使用以下实现将空类添加到名为 Bootstrap.csPushDemo 项目。

    using System;
    using PushDemo.Services;
    
    namespace PushDemo
    {
        public static class Bootstrap
        {
            public static void Begin(Func<IDeviceInstallationService> deviceInstallationService)
            {
                ServiceContainer.Register(deviceInstallationService);
    
                ServiceContainer.Register<IPushDemoNotificationActionService>(()
                    => new PushDemoNotificationActionService());
    
                ServiceContainer.Register<INotificationRegistrationService>(()
                    => new NotificationRegistrationService(
                        Config.BackendServiceEndpoint,
                        Config.ApiKey));
            }
        }
    }
    

    注意

    当应用启动时,每个平台都将调用 Begin 方法,并传入 IDeviceInstallationService 的特定于平台的实现。

    仅当选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 NotificationRegistrationServiceapiKey 构造函数参数。

实现跨平台 UI

  1. PushDemo 项目中,打开 MainPage.xaml ,并将 StackLayout 控件替换为以下内容。

    <StackLayout VerticalOptions="EndAndExpand"  
                 HorizontalOptions="FillAndExpand"
                 Padding="20,40">
        <Button x:Name="RegisterButton"
                Text="Register"
                Clicked="RegisterButtonClicked" />
        <Button x:Name="DeregisterButton"
                Text="Deregister"
                Clicked="DeregisterButtonClicked" />
    </StackLayout>
    
  2. 现在 ,MainPage.xaml.cs添加 只读 支持字段,以存储对 INotificationRegistrationService 实现的引用。

    readonly INotificationRegistrationService _notificationRegistrationService;
    
  3. MainPage 构造函数中,使用 ServiceContainer 解析 INotificationRegistrationService 实现,并将其分配给 notificationRegistrationService 支持字段。

    public MainPage()
    {
        InitializeComponent();
    
        _notificationRegistrationService =
            ServiceContainer.Resolve<INotificationRegistrationService>();
    }
    
  4. 为调用相应/Register Deregister 方法的 RegisterButtonDeregisterButton 按钮实现 Clicked 事件的事件处理程序。

    void RegisterButtonClicked(object sender, EventArgs e)
        => _notificationRegistrationService.RegisterDeviceAsync().ContinueWith((task)
            => { ShowAlert(task.IsFaulted ?
                    task.Exception.Message :
                    $"Device registered"); });
    
    void DeregisterButtonClicked(object sender, EventArgs e)
        => _notificationRegistrationService.DeregisterDeviceAsync().ContinueWith((task)
            => { ShowAlert(task.IsFaulted ?
                    task.Exception.Message :
                    $"Device deregistered"); });
    
    void ShowAlert(string message)
        => MainThread.BeginInvokeOnMainThread(()
            => DisplayAlert("PushDemo", message, "OK").ContinueWith((task)
                => { if (task.IsFaulted) throw task.Exception; }));
    
  5. 现在 ,在 App.xaml.cs 中,请确保引用以下命名空间。

    using PushDemo.Models;
    using PushDemo.Services;
    using Xamarin.Essentials;
    using Xamarin.Forms;
    
  6. 实现 IPushDemoNotificationActionServiceActionTriggered 事件的事件处理程序。

    void NotificationActionTriggered(object sender, PushDemoAction e)
        => ShowActionAlert(e);
    
    void ShowActionAlert(PushDemoAction action)
        => MainThread.BeginInvokeOnMainThread(()
            => MainPage?.DisplayAlert("PushDemo", $"{action} action received", "OK")
                .ContinueWith((task) => { if (task.IsFaulted) throw task.Exception; }));
    
  7. 应用构造函数中,使用 ServiceContainer 解析 IPushNotificationActionService 实现,并订阅 IPushDemoNotificationActionServiceActionTriggered 事件。

    public App()
    {
        InitializeComponent();
    
        ServiceContainer.Resolve<IPushDemoNotificationActionService>()
            .ActionTriggered += NotificationActionTriggered;
    
        MainPage = new MainPage();
    }
    

    注意

    这只是为了演示推送通知操作的接收和传播。 通常,这些操作将以无提示方式进行处理,例如导航到特定视图或刷新某些数据,而不是通过根 页面(在本例中为 MainPage )显示警报。

为推送通知配置本机 Android 项目

验证包名称和权限

  1. PushDemo.Android 中,从“生成”部分打开“项目选项”,然后打开“Android 应用程序”。

  2. 检查 包名称 是否与 Firebase 控制台PushDemo 项目中使用的值匹配。 包名称的格式com.<organization>.pushdemo为 。

  3. 最低 Android 版本 设置为 Android 8.0 (API 级别 26) 并将 目标 Android 版本 设置为最新 API 级别

    注意

    本教程仅支持运行 API 级别 26 及更高级别的 设备,但你可以将其扩展为支持运行旧版本的设备。

  4. 确保在“所需权限”下启用 INTERNETREAD_PHONE_STATE权限

  5. 单击“ 确定”

添加 Xamarin Google Play Services 基础和 Xamarin.Firebase.Messaging 包

  1. PushDemo.Android 中,控件 + 单击“包”文件夹,然后选择“管理 NuGet 包...”

  2. 搜索 Xamarin.GooglePlayServices.Base (而不是 地下室) ,并确保已选中。

  3. 搜索 Xamarin.Firebase.Messaging 并确保已选中它。

  4. 单击“添加包”,然后在系统提示接受许可条款时单击“接受”。

添加 Google Services JSON 文件

  1. 控制 + 单击该项目PushDemo.Android,然后从“添加”菜单中选择“现有文件...”

  2. 选择之前在 Firebase 控制台中设置 PushDemo 项目时下载的google-services.json文件,然后单击“打开”。

  3. 出现提示时,选择 “将文件复制到目录”。

  4. 控制 + 单击 项目中 的google-services.json 文件 PushDemo.Android ,并确保 将 GoogleServicesJson 设置为 生成操作

处理 Android 的推送通知

  1. 控制 + 单击PushDemo.Android项目,从“添加”菜单中选择“新建文件夹”,然后单击“使用服务添加”作为文件夹名称

  2. 控制 + 单击“服务”文件夹,然后从“添加”菜单中选择“新建文件...”

  3. 选择“常规>空类”,输入“名称DeviceInstallationService.cs,然后单击“新建添加以下实现”。

    using System;
    using Android.App;
    using Android.Gms.Common;
    using PushDemo.Models;
    using PushDemo.Services;
    using static Android.Provider.Settings;
    
    namespace PushDemo.Droid.Services
    {
        public class DeviceInstallationService : IDeviceInstallationService
        {
            public string Token { get; set; }
    
            public bool NotificationsSupported
                => GoogleApiAvailability.Instance
                    .IsGooglePlayServicesAvailable(Application.Context) == ConnectionResult.Success;
    
            public string GetDeviceId()
                => Secure.GetString(Application.Context.ContentResolver, Secure.AndroidId);
    
            public DeviceInstallation GetDeviceInstallation(params string[] tags)
            {
                if (!NotificationsSupported)
                    throw new Exception(GetPlayServicesError());
    
                if (string.IsNullOrWhiteSpace(Token))
                    throw new Exception("Unable to resolve token for FCM");
    
                var installation = new DeviceInstallation
                {
                    InstallationId = GetDeviceId(),
                    Platform = "fcm",
                    PushChannel = Token
                };
    
                installation.Tags.AddRange(tags);
    
                return installation;
            }
    
            string GetPlayServicesError()
            {
                int resultCode = GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(Application.Context);
    
                if (resultCode != ConnectionResult.Success)
                    return GoogleApiAvailability.Instance.IsUserResolvableError(resultCode) ?
                               GoogleApiAvailability.Instance.GetErrorString(resultCode) :
                               "This device is not supported";
    
                return "An error occurred preventing the use of push notifications";
            }
        }
    }
    

    注意

    此类使用 Secure.AndroidId) 作为通知中心注册有效负载的一部分提供唯一 ID (。

  4. 将另一个空类添加到名为 PushNotificationFirebaseMessagingService.cs的 Services 文件夹,然后添加以下实现。

    using Android.App;
    using Android.Content;
    using Firebase.Messaging;
    using PushDemo.Services;
    
    namespace PushDemo.Droid.Services
    {
        [Service]
        [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
        public class PushNotificationFirebaseMessagingService : FirebaseMessagingService
        {
            IPushDemoNotificationActionService _notificationActionService;
            INotificationRegistrationService _notificationRegistrationService;
            IDeviceInstallationService _deviceInstallationService;
    
            IPushDemoNotificationActionService NotificationActionService
                => _notificationActionService ??
                    (_notificationActionService =
                    ServiceContainer.Resolve<IPushDemoNotificationActionService>());
    
            INotificationRegistrationService NotificationRegistrationService
                => _notificationRegistrationService ??
                    (_notificationRegistrationService =
                    ServiceContainer.Resolve<INotificationRegistrationService>());
    
            IDeviceInstallationService DeviceInstallationService
                => _deviceInstallationService ??
                    (_deviceInstallationService =
                    ServiceContainer.Resolve<IDeviceInstallationService>());
    
            public override void OnNewToken(string token)
            {
                DeviceInstallationService.Token = token;
    
                NotificationRegistrationService.RefreshRegistrationAsync()
                    .ContinueWith((task) => { if (task.IsFaulted) throw task.Exception; });
            }
    
            public override void OnMessageReceived(RemoteMessage message)
            {
                if(message.Data.TryGetValue("action", out var messageAction))
                    NotificationActionService.TriggerAction(messageAction);
            }
        }
    }
    
  5. MainActivity.cs 中,确保以下命名空间已添加到文件的顶部。

    using System;
    using Android.App;
    using Android.Content;
    using Android.Content.PM;
    using Android.OS;
    using Android.Runtime;
    using Firebase.Iid;
    using PushDemo.Droid.Services;
    using PushDemo.Services;
    
  6. MainActivity.cs中,将 LaunchMode 设置为 SingleTop ,以便在打开时不会再次创建 MainActivity

    [Activity(
        Label = "PushDemo",
        LaunchMode = LaunchMode.SingleTop,
        Icon = "@mipmap/icon",
        Theme = "@style/MainTheme",
        MainLauncher = true,
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    
  7. 添加专用属性和相应的支持字段,以存储对 IPushNotificationActionServiceIDeviceInstallationService 实现的引用。

    IPushDemoNotificationActionService _notificationActionService;
    IDeviceInstallationService _deviceInstallationService;
    
    IPushDemoNotificationActionService NotificationActionService
        => _notificationActionService ??
            (_notificationActionService =
            ServiceContainer.Resolve<IPushDemoNotificationActionService>());
    
    IDeviceInstallationService DeviceInstallationService
        => _deviceInstallationService ??
            (_deviceInstallationService =
            ServiceContainer.Resolve<IDeviceInstallationService>());
    
  8. 实现 IOnSuccessListener 接口以检索和存储 Firebase 令牌。

    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity, Android.Gms.Tasks.IOnSuccessListener
    {
        ...
    
        public void OnSuccess(Java.Lang.Object result)
            => DeviceInstallationService.Token =
                result.Class.GetMethod("getToken").Invoke(result).ToString();
    }
    
  9. 添加名为 ProcessNotificationActions 的新方法,该方法将检查给定意向是否具有名为 action 的额外值。 使用 IPushDemoNotificationActionService 实现有条件地触发该操作。

    void ProcessNotificationActions(Intent intent)
    {
        try
        {
            if (intent?.HasExtra("action") == true)
            {
                var action = intent.GetStringExtra("action");
    
                if (!string.IsNullOrEmpty(action))
                    NotificationActionService.TriggerAction(action);
            }
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.Message);
        }
    }
    
  10. 重写 OnNewIntent 方法以调用 ProcessNotificationActions 方法。

    protected override void OnNewIntent(Intent intent)
    {
        base.OnNewIntent(intent);
        ProcessNotificationActions(intent);
    }
    

    注意

    由于活动的LaunchMode 设置为 SingleTop,因此意向将通过 OnNewIntent 方法而不是 OnCreate 方法发送到现有 Activity 实例,因此必须在 OnCreateOnNewIntent 方法中处理传入意向。

  11. 更新 OnCreate 方法,以在调用 base.OnCreate 传入 IDeviceInstallationService 的平台特定实现后立即调用Bootstrap.Begin

    Bootstrap.Begin(() => new DeviceInstallationService());
    
  12. 在同一方法中,有条件地调用 FirebaseApp 实例上的 GetInstanceId,紧接在 调用 Bootstrap.Begin之后,将 MainActivity 添加为 IOnSuccessListener

    if (DeviceInstallationService.NotificationsSupported)
    {
        FirebaseInstanceId.GetInstance(Firebase.FirebaseApp.Instance)
            .GetInstanceId()
            .AddOnSuccessListener(this);
    }
    
  13. 仍在 OnCreate 中,在调用 传入当前意向后立即调用 LoadApplicationProcessNotificationActions

    ...
    
    LoadApplication(new App());
    
    ProcessNotificationActions(Intent);
    

注意

每次运行应用时,都必须重新注册应用,并从调试会话中停止应用,才能继续接收推送通知。

为推送通知配置本机 iOS 项目

配置 Info.plist 和 Entitlements.plist

  1. 确保已在 Visual Studio> 首选项中登录到 Apple 开发人员帐户...>出版>Apple 开发人员帐户以及相应的证书预配配置文件已下载。 在前面的步骤中,应该已创建这些资产。

  2. PushDemo.iOS 中,打开 Info.plist 并确保 BundleIdentifierApple 开发人员门户中用于相应预配配置文件的值匹配。 BundleIdentifier 的格式com.<organization>.PushDemo为 。

  3. 在同一文件中,将 “最低系统版本 ”设置为 13.0

    注意

    本教程仅支持运行 iOS 13.0 及更高版本的 设备,但你可以将其扩展为支持运行较旧版本的设备。

  4. 打开 PushDemo.iOS的项目选项 (双击项目) 。

  5. “项目选项”“生成 > iOS 捆绑签名”下,确保“ 团队”下选择了“开发人员帐户”。 然后,确保选中“自动管理签名”,并自动选择签名证书和预配配置文件。

    注意

    如果尚未自动选择 签名证书预配配置文件 ,请选择 “手动预配”,然后单击“ 捆绑签名选项”。 确保为“签名标识”选择团队,并为“调试”“发布”配置选择“特定于 PushDemo 的预配配置文件”,确保在这两种情况下都为平台选择 iPhone

  6. PushDemo.iOS 中,打开 Entitlements.plist,并确保在“权利”选项卡中查看时选中“启用推送通知”。然后,在“”选项卡中查看时,请确保将“APS 环境”设置设置为“开发”。

处理 iOS 的推送通知

  1. 控制 + 单击PushDemo.iOS 项目,从“添加”菜单中选择“新建文件夹”,然后单击“使用服务作为文件夹名称添加。

  2. 控制 + 单击“服务”文件夹,然后从“添加”菜单中选择“新建文件...”

  3. 选择“常规>空类”,输入“名称DeviceInstallationService.cs,然后单击“新建添加以下实现”。

    using System;
    using PushDemo.Models;
    using PushDemo.Services;
    using UIKit;
    
    namespace PushDemo.iOS.Services
    {
        public class DeviceInstallationService : IDeviceInstallationService
        {
            const int SupportedVersionMajor = 13;
            const int SupportedVersionMinor = 0;
    
            public string Token { get; set; }
    
            public bool NotificationsSupported
                => UIDevice.CurrentDevice.CheckSystemVersion(SupportedVersionMajor, SupportedVersionMinor);
    
            public string GetDeviceId()
                => UIDevice.CurrentDevice.IdentifierForVendor.ToString();
    
            public DeviceInstallation GetDeviceInstallation(params string[] tags)
            {
                if (!NotificationsSupported)
                    throw new Exception(GetNotificationsSupportError());
    
                if (string.IsNullOrWhiteSpace(Token))
                    throw new Exception("Unable to resolve token for APNS");
    
                var installation = new DeviceInstallation
                {
                    InstallationId = GetDeviceId(),
                    Platform = "apns",
                    PushChannel = Token
                };
    
                installation.Tags.AddRange(tags);
    
                return installation;
            }
    
            string GetNotificationsSupportError()
            {
                if (!NotificationsSupported)
                    return $"This app only supports notifications on iOS {SupportedVersionMajor}.{SupportedVersionMinor} and above. You are running {UIDevice.CurrentDevice.SystemVersion}.";
    
                if (Token == null)
                    return $"This app can support notifications but you must enable this in your settings.";
    
    
                return "An error occurred preventing the use of push notifications";
            }
        }
    }
    

    注意

    此类使用 UIDevice.IdentifierForVendor 值) 和通知中心注册有效负载提供唯一 ID (。

  4. 将名为 Extensions 的新文件夹添加到 PushDemo.iOS 项目,然后通过以下实现将空类添加到名为 NSDataExtensions.cs 的文件夹。

    using System.Text;
    using Foundation;
    
    namespace PushDemo.iOS.Extensions
    {
        internal static class NSDataExtensions
        {
            internal static string ToHexString(this NSData data)
            {
                var bytes = data.ToArray();
    
                if (bytes == null)
                    return null;
    
                StringBuilder sb = new StringBuilder(bytes.Length * 2);
    
                foreach (byte b in bytes)
                    sb.AppendFormat("{0:x2}", b);
    
                return sb.ToString().ToUpperInvariant();
            }
        }
    }
    
  5. AppDelegate.cs 中,确保以下命名空间已添加到文件的顶部。

    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using Foundation;
    using PushDemo.iOS.Extensions;
    using PushDemo.iOS.Services;
    using PushDemo.Services;
    using UIKit;
    using UserNotifications;
    using Xamarin.Essentials;
    
  6. 添加私有属性及其各自的支持字段,以存储对 IPushDemoNotificationActionServiceINotificationRegistrationServiceIDeviceInstallationService 实现的引用。

    IPushDemoNotificationActionService _notificationActionService;
    INotificationRegistrationService _notificationRegistrationService;
    IDeviceInstallationService _deviceInstallationService;
    
    IPushDemoNotificationActionService NotificationActionService
        => _notificationActionService ??
            (_notificationActionService =
            ServiceContainer.Resolve<IPushDemoNotificationActionService>());
    
    INotificationRegistrationService NotificationRegistrationService
        => _notificationRegistrationService ??
            (_notificationRegistrationService =
            ServiceContainer.Resolve<INotificationRegistrationService>());
    
    IDeviceInstallationService DeviceInstallationService
        => _deviceInstallationService ??
            (_deviceInstallationService =
            ServiceContainer.Resolve<IDeviceInstallationService>());
    
  7. 添加 RegisterForRemoteNotifications 方法以注册用户通知设置,然后使用 APNS 注册远程通知。

    void RegisterForRemoteNotifications()
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            var pushSettings = UIUserNotificationSettings.GetSettingsForTypes(
                UIUserNotificationType.Alert |
                UIUserNotificationType.Badge |
                UIUserNotificationType.Sound,
                new NSSet());
    
            UIApplication.SharedApplication.RegisterUserNotificationSettings(pushSettings);
            UIApplication.SharedApplication.RegisterForRemoteNotifications();
        });
    }
    
  8. 添加 CompleteRegistrationAsync 方法以设置 IDeviceInstallationService.Token 属性值。 刷新注册并缓存设备令牌(如果自上次存储以来已更新)。

    Task CompleteRegistrationAsync(NSData deviceToken)
    {
        DeviceInstallationService.Token = deviceToken.ToHexString();
        return NotificationRegistrationService.RefreshRegistrationAsync();
    }
    
  9. 添加 ProcessNotificationActions 方法,用于处理 NSDictionary 通知数据并有条件地调用 NotificationActionService.TriggerAction

    void ProcessNotificationActions(NSDictionary userInfo)
    {
        if (userInfo == null)
            return;
    
        try
        {
            var actionValue = userInfo.ObjectForKey(new NSString("action")) as NSString;
    
            if (!string.IsNullOrWhiteSpace(actionValue?.Description))
                NotificationActionService.TriggerAction(actionValue.Description);
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }
    
  10. 重写 RegisteredForRemoteNotifications 方法,该方法将 deviceToken 参数传递给 CompleteRegistrationAsync 方法。

    public override void RegisteredForRemoteNotifications(
        UIApplication application,
        NSData deviceToken)
        => CompleteRegistrationAsync(deviceToken).ContinueWith((task)
            => { if (task.IsFaulted) throw task.Exception; });
    
  11. 重写 ReceivedRemoteNotification 方法,该方法将 userInfo 参数传递给 ProcessNotificationActions 方法。

    public override void ReceivedRemoteNotification(
        UIApplication application,
        NSDictionary userInfo)
        => ProcessNotificationActions(userInfo);
    
  12. 重写 FailedToRegisterForRemoteNotifications 方法以记录错误。

    public override void FailedToRegisterForRemoteNotifications(
        UIApplication application,
        NSError error)
        => Debug.WriteLine(error.Description);
    

    注意

    这在很大程度上是一个占位符。 你需要为生产方案实现正确的日志记录和错误处理。

  13. 更新 FinishedLaunching 方法,以在调用 Forms.InitIDeviceInstallationService 的特定于平台的实现后立即调用Bootstrap.Begin

    Bootstrap.Begin(() => new DeviceInstallationService());
    
  14. 在同一方法中,有条件地请求授权,并在 之后 Bootstrap.Begin立即注册远程通知。

    if (DeviceInstallationService.NotificationsSupported)
    {
        UNUserNotificationCenter.Current.RequestAuthorization(
                UNAuthorizationOptions.Alert |
                UNAuthorizationOptions.Badge |
                UNAuthorizationOptions.Sound,
                (approvalGranted, error) =>
                {
                    if (approvalGranted && error == null)
                        RegisterForRemoteNotifications();
                });
    }
    
  15. FinishedLaunching 中,如果 options 参数包含传入生成的 userInfo 对象的 UIApplication.LaunchOptionsRemoteNotificationKey,则立即在调用 LoadApplication 后调用 ProcessNotificationActions

    using (var userInfo = options?.ObjectForKey(
        UIApplication.LaunchOptionsRemoteNotificationKey) as NSDictionary)
            ProcessNotificationActions(userInfo);
    

测试解决方案

现在可以测试通过后端服务发送通知。

发送测试通知

  1. Postman 中打开新选项卡。

  2. 将请求设置为 POST,并输入以下地址:

    https://<app_name>.azurewebsites.net/api/notifications/requests
    
  3. 如果选择完成 “使用 API 密钥对客户端进行身份验证” 部分,请确保将请求标头配置为包含 apikey 值。

    apikey <your_api_key>
  4. 选择“正文的原始选项,然后从格式选项列表中选择“JSON”,然后包括一些占位符 JSON 内容:

    {
        "text": "Message from Postman!",
        "action": "action_a"
    }
    
  5. 选择窗口右上角的“保存”按钮下的“代码”按钮。 如果为 HTML (显示请求,则请求应类似于以下示例,具体取决于是否包含 apikey 标头) :

    POST /api/notifications/requests HTTP/1.1
    Host: https://<app_name>.azurewebsites.net
    apikey: <your_api_key>
    Content-Type: application/json
    
    {
        "text": "Message from backend service",
        "action": "action_a"
    }
    
  6. AndroidiOS) (一个或两个目标平台上运行 PushDemo 应用程序。

    注意

    如果在 Android 上测试,请确保未在 调试中运行,或者如果应用已通过运行应用程序部署,则强制关闭应用,然后从启动器重新启动它。

  7. PushDemo 应用中,点击“ 注册 ”按钮。

  8. 返回 Postman,关闭“ 生成代码片段 ”窗口 ((如果尚未这样做),) 然后单击“ 发送 ”按钮。

  9. 验证 Postman 中是否收到“200 正常”响应,并且警报显示在显示收到 ActionA 操作的应用中。

  10. 关闭 PushDemo 应用,然后在 Postman 中再次单击“发送”按钮。

  11. 验证是否再次在 Postman 中收到 200 OK 响应。 验证 PushDemo 应用的通知区域中是否显示正确的消息。

  12. 点击通知以确认它打开应用并显示 ActionA 操作收到 警报。

  13. 返回 Postman,修改上一个请求正文以发送指定action_b的静默通知,而不是为操作action_a

    {
        "action": "action_b",
        "silent": true
    }
    
  14. 在应用仍处于打开状态的情况下,单击 Postman 中的“发送”按钮。

  15. 验证 Postman 中是否收到“200 正常”响应,以及警报是否显示在应用中,显示已接收 ActionB 操作,而不是收到 ActionA 操作

  16. 关闭 PushDemo 应用,然后在 Postman 中再次单击“发送”按钮。

  17. 验证 Postman 中是否收到“200 正常”响应,以及通知区域中是否未显示无提示通知。

疑难解答

后端服务无响应

在本地测试时,请确保后端服务正在运行并使用正确的端口。

如果针对 Azure API 应用进行测试,检查服务正在运行且已部署并启动,且未出错。

请确保检查通过客户端进行测试时,已在 Postman 或移动应用配置中正确指定基址。 在本地测试时,基址应指示为 https://<api_name>.azurewebsites.net/https://localhost:5001/

启动或停止调试会话后,Android 上未收到通知

确保在启动或停止调试会话后再次注册。 调试器将导致生成新的 Firebase 令牌。 通知中心安装也必须更新。

从后端服务接收 401 状态代码

验证是否正在设置 apikey 请求标头,以及此值是否与为后端服务配置的标头匹配。

如果在本地测试时收到此错误,请确保在客户端配置中定义的密钥值与 API 使用的 Authentication:ApiKey 用户设置值匹配。

如果使用 API 应用进行测试,请确保客户端配置文件中的密钥值与你在 API 应用中使用的 Authentication:ApiKey 应用程序设置匹配。

注意

如果在部署后端服务后创建了或更改了此设置,则必须重启该服务才能使其生效。

如果选择不完成 使用 API 密钥对客户端进行身份验证 部分,请确保未将 Authorize 属性应用于 NotificationsController 类。

从后端服务接收 404 状态代码

验证终结点和 HTTP 请求方法是否正确。 例如,终结点应指示为:

  • [PUT]https://<api_name>.azurewebsites.net/api/notifications/installations
  • [DELETE]https://<api_name>.azurewebsites.net/api/notifications/installations/<installation_id>
  • [POST]https://<api_name>.azurewebsites.net/api/notifications/requests

或者在本地测试时:

  • [PUT]https://localhost:5001/api/notifications/installations
  • [DELETE]https://localhost:5001/api/notifications/installations/<installation_id>
  • [POST]https://localhost:5001/api/notifications/requests

在客户端应用中指定基址时,请确保它以 /结尾。 在本地测试时,基址应指示为 https://<api_name>.azurewebsites.net/https://localhost:5001/

无法注册,并显示通知中心错误消息

验证测试设备是否具有网络连接。 然后,通过设置断点来检查 HttpResponse 中的 StatusCode 属性值来确定 Http 响应状态代码。

根据状态代码查看前面的故障排除建议(如果适用)。

在返回相应 API 的这些特定状态代码的行上设置断点。 然后尝试在本地调试时调用后端服务。

使用适当的有效负载通过 Postman 验证后端服务是否按预期工作。 使用客户端代码为有问题的平台创建的实际有效负载。

查看特定于平台的配置部分,确保未遗漏任何步骤。 检查是否正在为 installation id 适当的平台解析合适的值,以及 token 针对相应平台的变量。

显示无法解析设备 ID 错误消息

查看特定于平台的配置部分,确保未遗漏任何步骤。

后续步骤

现在,应该有一个基本的 Xamarin.Forms 应用通过后端服务连接到通知中心,并且可以发送和接收通知。

可能需要调整本教程中使用的示例,以适应自己的方案。 还建议实现更可靠的错误处理、重试逻辑和日志记录。

Visual Studio App Center 可以快速合并到移动应用中,提供分析和诊断来帮助进行故障排除。