教學課程:透過後端服務使用 Azure 通知中樞將推播通知傳送至 Xamarin.Forms 應用程式

下載範例 下載範例

在本教學課程中,您會使用 Azure 通知中樞將通知推送至以 AndroidiOS 為目標的 Xamarin.Forms 應用程式。

ASP.NET Core Web API 後端是用來使用最新且最佳的安裝方法來處理客戶端的裝置註冊。 服務也會以跨平臺方式傳送推播通知。

這些作業是使用 通知中樞 SDK 來處理後端作業。 如需整體方法的進一步詳細數據,請參閱 從您的應用程式後端註冊 檔。

本教學課程會引導您完成下列步驟:

Prerequisites

若要跟著做,您需要:

  • 您可以在其中建立和管理資源的 Azure 訂 用帳戶。
  • 已安裝 Visual Studio for Mac的 Mac 或執行 Visual Studio 2019 的電腦。
  • Visual Studio 2019 用戶也必須使用 .NET 進行行動裝置開發 ,並 安裝 ASP.NET 和 Web 開發 工作負載。
  • Android (實體或模擬器裝置上執行應用程式的能力,) 或 iOS (實體裝置僅) 。

針對 Android,您必須具備:

  • 開發人員已解除鎖定實體裝置或模擬器 , (執行 API 26 和更新版本,並安裝 google Play Services)

針對 iOS,您必須具有:

注意

iOS 模擬器不支援遠端通知,因此在 iOS 上探索此範例時需要實體裝置。 不過,您不需要在 AndroidiOS 上執行應用程式,即可完成本教學課程。

您可以遵循此第一個原則範例中的步驟,且沒有先前的經驗。 不過,您將受益於熟悉下列層面。

重要

提供的步驟專屬於 Visual Studio for Mac。 您可以使用 Visual Studio 2019 進行追蹤,但可能會有一些差異可協調。 例如,使用者介面和工作流程、範本名稱、環境組態等的描述。

設定推播通知服務和 Azure 通知中樞

在本節中,您會設定 Firebase 雲端通訊 (FCM) Apple 推播通知服務, (APNS) 。 接著,您會建立並設定通知中樞來使用這些服務。

建立 Firebase 專案並啟用適用於 Android 的 Firebase 雲端通訊

  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 布建入口網站應用程式識別碼頁面

  2. 在 [ 註冊新標識符 ] 畫面上,選取 [ 應用程式 標識符] 單選按鈕。 然後選取 [繼續]。

    iOS 布建入口網站註冊新的標識碼頁面

  3. 更新新應用程式的下列三個值,然後選取 [ 繼續]:

    • 描述:輸入應用程式的描述性名稱。

    • 套件組合標識碼:輸入 com.organization_identifier<窗體的套件組合>標識碼。<如應用程式發佈指南中所述的>product_name 在下列螢幕快照中,此值 mobcat 會當做組織標識碼使用, 並將 PushDemo 值當做產品名稱使用。

      iOS 布建入口網站註冊應用程式標識碼頁面

    • 推播通知:檢查 [功能] 區段中的 [推播通知] 選項。

      註冊新應用程式識別碼的表單

      此動作會產生您的應用程式識別碼,並要求您確認資訊。 選取 [繼續],然後選取 [ 註冊 ] 以確認新的應用程式標識符。

      確認新的應用程式識別碼

      選取 [ 註冊] 之後,您會在 [ 憑證]、[標識符] & [配置檔 ] 頁面中看到新的應用程式識別符作為明細專案。

  4. 在 [ 憑證] 的 [標識符] & [配置檔 ] 頁面的 [ 標識符] 底下,找出您建立的應用程式標識符行專案。 然後,選取其數據列以顯示 [ 編輯您的應用程式識別符 設定] 畫面。

建立通知中樞的憑證

需要憑證,才能讓通知中樞使用 Apple推播通知服務 (APNS) ,而且可以使用下列兩種方式之一提供:

  1. 建立可直接上傳至通知中樞的 p12 推播憑證 , (原始方法)

  2. 建立 p8 憑證,以用於令牌型驗證 , (較新且建議的方法)

較新的方法有一些優點,如 令牌型 (HTTP/2) APNS 驗證中所述。 需要較少的步驟,但也適用於特定案例。 不過,已針對這兩種方法提供步驟,因為任一方法都適用於本教學課程的目的。

選項 1:建立可直接上傳至通知中樞的 p12 推播憑證
  1. 在您的 Mac 上,執行 Keychain 存取工具。 它可以從 [公用程式 ] 資料夾或啟動列上的 [其他 ] 資料夾開啟。

  2. 選取 [金鑰鏈存取],展開 [ 憑證小幫手],然後 選取 [向證書頒發機構單位要求憑證]。

    使用 Keychain 存取來要求新的憑證

    注意

    根據預設,Keychain Access 會選取清單中的第一個專案。 如果您位於 [憑證 ] 類別,而 Apple 全球開發人員關係證書頒發機構單位 不是清單中的第一個專案,則這可能會是問題。 產生 CSR (憑證簽署要求) 之前,請確定您已選取非密鑰專案或 Apple 全球開發人員關係證書頒發機構單位 密鑰。

  3. 選取 [使用者 Email 位址],輸入您的 [一般名稱] 值,確定您指定 [已儲存到磁碟],然後選取 [繼續]。 將 CA Email 位址保留空白,因為不需要。

    預期的憑證資訊

  4. 在 [另存新檔] 中輸入憑證簽署要求 (CSR) 檔案的名稱,選取[位置] 中的位置,然後選取 [儲存]。

    選擇憑證的檔名

    此動作會將 CSR 檔案 儲存在選取的位置。 默認位置為 Desktop。 請記住為檔案選擇的位置。

  5. 回到iOS布建入口網站中的 [憑證]、[標識符 & 配置檔] 頁面,向下卷動至核取的 [推播通知] 選項,然後選取 [設定] 以建立憑證。

    編輯應用程式識別碼頁面

  6. [Apple 推播通知服務 TLS/SSL 憑證] 視窗隨即出現。 選取 [開發 TLS/SSL 憑證] 區段底下的 [建立憑證] 按鈕。

    [建立應用程式識別碼] 按鈕的憑證

    隨即顯示 [建立新的憑證 ] 畫面。

    注意

    本教學課程使用開發憑證。 註冊生產憑證時會使用相同的程式。 只要確定您在傳送通知時使用相同的憑證類型。

  7. 選取 [選擇檔案],流覽至您儲存 CSR 檔案的位置,然後按兩下憑證名稱以載入它。 然後選取 [繼續]。

  8. 在入口網站建立憑證之後,選取 [ 下載 ] 按鈕。 儲存憑證,並記住其儲存位置。

    產生的憑證下載頁面

    憑證會下載並儲存到 [ 下載 ] 資料夾中的電腦。

    在 [下載] 資料夾中找出憑證檔案

    注意

    根據預設,下載的開發憑證會命名 為 aps_development.cer

  9. 按兩下下載的推播憑證 aps_development.cer。 此動作會在 Keychain 中安裝新的憑證,如下圖所示:

    顯示新憑證的金鑰鏈存取憑證清單

    注意

    雖然憑證中的名稱可能不同,但名稱前面會加上 Apple Development iOS Push Services ,並具有與其相關聯的適當套件組合標識碼。

  10. 在 [金鑰鏈存取] 中,按下 + 您在 [憑證] 類別中建立的新推播憑證。 選取 [匯出],將檔案命名為 p12 格式,然後選取 [ 儲存]。

    將憑證匯出為 p12 格式

    您可以選擇使用密碼來保護憑證,但密碼是選擇性的。 如果您想要略過密碼建立,請按兩下 [ 確定 ]。 記下匯出 p12 憑證的檔名和位置。 它們用來啟用APN的驗證。

    注意

    您的 p12 檔名和位置可能與本教學課程中所說明的內容不同。

選項 2:建立可用於令牌型驗證的 p8 憑證
  1. 記下下列詳細資料:

    • 應用程式標識碼前置詞 (小組標識碼)
    • 套件組合標識碼
  2. 回到 [憑證],[標識符] & [配置檔],按兩下 [ 金鑰]。

    注意

    如果您已經為 APNS 設定金鑰,您可以重複使用您在建立 APNS 後立即下載的 p8 憑證。 如果是,您可以忽略步驟 35

  3. +按鍵 (或 [建立金鑰] 按鈕) 來建立新的金鑰。

  4. 提供適當的 [金鑰名稱] 值,然後核取 [Apple Push Notifications 服務] ([APNS) ] 選項,然後按兩下 [ 繼續],然後在下一個畫面上按兩下 [ 註冊 ]。

  5. 按兩下 [下載],然後將前面加上 AuthKey_) p8 (檔案移至安全本機目錄,然後按兩下 [完成]。

    注意

    請務必將 p8 檔案保留在安全的位置 (,並儲存備份) 。 下載金鑰之後,就無法在移除伺服器複本時重新下載。

  6. [金鑰] 上,如果您選擇改用) ,請按下您建立 (或現有金鑰的金鑰。

  7. 記下 金鑰識別碼 值。

  8. 在您選擇的適當應用程式中開啟 p8 憑證,例如 Visual Studio Code。 記下 -----BEGIN 私鑰----------END 私鑰 之間的金鑰 (值-----) 。

    -----BEGIN 私鑰-----
    <key_value>
    -----END 私鑰-----

    注意

    這是稍後將用來設定通知中樞令牌值

在這些步驟結束時,您應該會有下列資訊,以供稍後 使用APNS資訊設定通知中樞

  • 小組標識子 (請參閱步驟 1)
  • 套件組合標識碼 (請參閱步驟 1)
  • 密鑰標識碼 (請參閱步驟 7)
  • 在步驟 8) 中取得的令牌值 (p8 索引鍵值

建立應用程式的布建配置檔

  1. 返回 iOS 布建入口網站,選取 [ 憑證]、[標識符 & 配置檔]、從左側功能表中選取 [ 配置檔 ],然後選取 + 以建立新的配置檔。 [ 註冊新的布建配置檔 ] 畫面隨即出現。

  2. 選取 [開發] 下的 [iOS 應用程式開發] 作為布建配置檔類型,然後選取 [繼續]。

    布建配置檔清單

  3. 接下來,從 [ 應用程式 標識符] 下拉式清單中選取您建立的應用程式識別碼,然後選取 [ 繼續]。

    選取應用程式識別碼

  4. 在 [ 選取憑證 ] 視窗中,選取您用於程式代碼簽署的開發憑證,然後選取 [ 繼續]。

    注意

    此憑證不是您在 上一個步驟中建立的推送憑證。 這是您的開發憑證。 如果不存在,您必須建立它,因為這是本教學課程 的必要條件 。 開發人員憑證可以透過 XcodeVisual Studio,在 Apple Developer Portal 中建立。

  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. 輸入您稍早取得的下列值:

    • 金鑰識別碼
    • 套件組合標識碼
    • 小組標識碼
    • 令牌
  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. 開始偵錯 (命令 + 輸入) 以測試樣板化應用程式。

    注意

    範本化應用程式已設定為使用 WeatherForecastController 作為 launchUrl。 這會在 [屬性>] launchSettings.json中設定。

    如果系統提示您 找到無效的開發憑證 訊息:

    1. 按兩下 [是 ] 同意執行 'dotnet dev-certs https' 工具來修正此問題。 'dotnet dev-certs https' 工具會提示您輸入憑證的密碼和密鑰鏈的密碼。

    2. 當系統提示您安裝並信任新憑證時,請按兩下 [],然後輸入密鑰鏈的密碼。

  6. 展開 Controllers 資料夾,然後刪除 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 App 服務 應用程式設定。

使用 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 和 app 之前呼叫這些方法 。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.cs 的Models 資料夾,然後新增下列實作。

    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.cs 的Models 資料夾,然後新增下列實作。

    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.cs 的Models 資料夾,然後新增下列實作。

    using System.ComponentModel.DataAnnotations;
    
    namespace PushDemoApi.Models
    {
        public class NotificationHubOptions
        {
            [Required]
            public string Name { get; set; }
    
            [Required]
            public string ConnectionString { get; set; }
        }
    }
    
  10. 將新的資料夾新增至名為 ServicesPushDemoApi 專案。

  11. 空白介面新增至名為 INotificationService.cs的服務資料夾,然後新增下列實作。

    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,但表示式只包含 (|| 的 OU在此情況下) 。 如果要求中有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 控制器類別],針對 [名稱] 輸入 NotificationsController,然後按兩下 [新增]。

    注意

    如果您遵循 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. 開始偵錯 (命令 + 輸入) 驗證應用程式正在使用新的 NotificationsController ,並傳回 401 未經授權的 狀態。

    注意

    Visual Studio 可能不會在瀏覽器中自動啟動應用程式。 您將使用 Postman 從這個點開始測試 API。

  9. 在新的 Postman 索引標籤上,將要求設定為 GET。 輸入下列位址,以屬性>launchSettings.json中找到的 HTTPs applicationUrl 取代佔位元 <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 OK 狀態。

    如果您收到 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 App 服務 中建立 API 應用程式,以裝載後端服務。

  1. 登入 Azure 入口網站

  2. 按兩下 [建立資源],然後搜尋並選擇 [API 應用程式],然後按兩下 [ 建立]。

  3. 更新下列欄位,然後按兩 下列欄位,然後按兩下列欄位。。

    應用程式名稱:
    輸入 API 應用程式的全域唯一名稱

    訂閱:
    選擇您在中建立通知中樞的相同目標 用帳戶。

    資源群組:
    選擇您在中建立通知中樞的相同 資源群組

    App Service 方案/位置:
    建立新的 App Service方案

    注意

    從預設選項變更為包含 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 App 服務

  1. 如果您尚未這麼做,請將組態從 [偵 錯] 變更為 [發行 ]。

  2. 控制 + 單擊PushDemoApi 項目,然後從 [發佈] 功能表中選擇 [發佈至 Azure...]。

  3. 如果系統提示您這麼做,請遵循驗證流程。 使用您在上一個 建立 API 應用程式 一節中使用的帳戶。

  4. 選取您先前從清單中建立的 Azure App 服務 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. 按兩下 [傳送]。

    注意

    您應該會收到來自服務的 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. 將新的資料夾新增至名為 ServicesPushDemo 專案,然後使用下列實作,將名為 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.cs空白介面新增至衍生自 INotificationActionServiceServices 資料夾。

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

    注意

    此類型專屬於 PushDemo 應用程式,並使用 PushDemoAction 列舉來識別以強型別方式觸發的動作。

  10. 使用下列程序代碼空類別新增至名為 NotificationRegistrationService.cs實作 INotificationRegistrationServiceServices 資料夾。

    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. 使用下列程序代碼,將空類別新增至名為 PushDemoNotificationActionService.cs實作 IPushDemoNotificationActionServiceServices 資料夾。

    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));
            }
        }
    }
    

    注意

    當應用程式啟動傳入 IDeviceInstallationService 的平臺特定實作時,每個平台都會呼叫 Begin 方法。

    只有在您選擇完成使用 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. 實作 RegisterButtonDeregisterButton 按鈕的事件處理程式 Clicked 事件,呼叫對應的 Register/Deregister 方法。

    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. 請確定已在 [必要許可權] 下啟用因特網READ_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. 控制 + 專案內PushDemo.Android按兩下google-services.json檔案,然後確定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) 作為通知中樞註冊承載的一部分。

  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 的新方法,以檢查指定的 意圖 是否有額外的具名 動作值。 使用 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);
    }
    

    注意

    由於 ActivityLaunchMode 設定為 SingleTop因此意圖會透過 OnNewIntent 方法傳送至現有的 Activity 實例,而不是 OnCreate 方法,因此您必須在 OnCreateOnNewIntent 方法中處理傳入意圖。

  11. 更新 OnCreate 方法,以在呼叫之後立即呼叫 Bootstrap.Begin ,以 base.OnCreate 傳入 IDeviceInstallationService 的平臺特定實作

    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 ,並確定 BundleIdentifier 符合 Apple 開發人員入口網站中個別布建配置檔所使用的值。 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 值) 和通知中樞註冊承載,提供唯一標識碼 (。

  4. 將新的資料夾新增至名為 ExtensionsPushDemo.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 方法,以在呼叫之後立即呼叫 Bootstrap.Begin ,以 Forms.Init 傳入 IDeviceInstallationService 的平臺特定實作

    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 自變數包含 UIApplication.LaunchOptionsRemoteNotificationKey,則呼叫 ProcessNotificationActions,並在產生的 userInfo 物件中傳入時立即呼叫 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. 選取 [ 程序代碼 ] 按鈕,其位於視窗右上方的 [ 儲存 ] 按鈕底下。 根據您是否包含 apikey 標頭) 而定,針對 HTML (顯示時,要求看起來應該類似下列範例:

    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 OK 回應,且警示會出現在應用程式中,其中顯示已收到的 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 OK 回應,且警示出現在應用程式中,其中顯示已收到的 ActionB 動作,而不是收到 ActionA 動作

  16. 關閉 PushDemo 應用程式,然後在 Postman 中再次按兩下 [傳送] 按鈕。

  17. 驗證您在 Postman 中收到 200 OK 回應,且無訊息通知不會出現在通知區域中。

故障排除

後端服務沒有回應

在本機測試時,請確定後端服務正在執行,並使用正確的埠。

如果針對 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 之這些特定狀態代碼的行上設定斷點。 然後在本機偵錯時嘗試呼叫後端服務。

使用適當的承載,驗證後端服務是否如預期般運作。 針對有問題的平臺,使用用戶端程式代碼所建立的實際承載。

檢閱平臺特定的組態區段,以確保未遺漏任何步驟。 檢查是否要針對 installation id 適當的平臺解析適當的值和 token 變數。

無法解析裝置錯誤訊息的識別碼

檢閱平臺特定的組態區段,以確保未遺漏任何步驟。

後續步驟

您現在應該已透過後端服務連線到通知中樞的基本 Xamarin.Forms 應用程式,而且可以傳送和接收通知。

您可能需要調整本教學課程中使用的範例,以符合您自己的案例。 也建議您實作更強固的錯誤處理、重試邏輯和記錄。

Visual Studio App Center 可以快速併入行動裝置應用程式中,提供分析和診斷,以協助進行疑難解答。