チュートリアル:バックエンド サービス経由で Azure Notification Hubs を使用して Xamarin.Forms アプリにプッシュ通知を送信する

Download Sample サンプルをダウンロードします

このチュートリアルでは、Azure Notification Hubs を使用して、AndroidiOS をターゲットとする Xamarin.Forms アプリケーションにプッシュ通知を送信します。

ASP.NET Core Web API バックエンドを使用して、最新かつ最適なインストール方法を使用しているクライアントのデバイス登録を処理します。 また、サービスは、クロスプラットフォーム方式でプッシュ通知を送信します。

これらの操作はバックエンド操作用の Notification Hubs SDK を使用して処理されます。 全体的なアプローチの詳細については、アプリ バックエンドからの登録に関するドキュメントを参照してください。

このチュートリアルでは、次の手順について説明します。

前提条件

手順を進めてゆくには、以下が必要です。

  • リソースを作成および管理できる Azure サブスクリプション
  • Visual Studio for Mac がインストールされている Mac または Visual Studio 2019 を実行している PC。
  • Visual Studio 2019 のユーザーは、 .NET によるモバイル開発ASP.NET および Web の開発のワークロードもインストールしておく必要があります。
  • Android (物理デバイスまたはエミュレーター デバイス) または iOS (物理デバイスのみ) のいずれかでアプリを実行する機能。

Android の場合は次のものが必要です。

  • 開発者用のロック解除済みの物理デバイスまたはエミュレーター (API 26 以降を実行していて Google Play 開発者サービスがインストールされているもの)

iOS の場合は次のものが必要です。

注意

iOS シミュレーターではリモート通知がサポートされていないため、このサンプルを iOS で試すときには物理デバイスが必要です。 ただし、このチュートリアルを完了するために、アプリを AndroidiOS の両方で実行する必要はありません。

以前に経験がなくとも、この第一原理の例に記載されている手順に従うことができます。 ただし、以下の面について知識があると役立ちます。

重要

示されている手順は、Visual Studio for Mac に固有のものです。 Visual Studio 2019 を使用して従うこともできますが、調整すべきいくつかの違いがある場合があります。 たとえば、ユーザー インターフェイスとワークフローの説明、テンプレート名、環境構成などです。

プッシュ通知サービスと Azure Notification Hubs を設定する

このセクションでは、 Firebase Cloud Messaging (FCM)Apple Push Notification Services (APNS) をセットアップします。 次に、通知ハブを作成し、それらのサービスと連携するように構成します。

Firebase プロジェクトを作成し、Android 用に Firebase Cloud Messaging を有効にする

  1. Firebase コンソールにサインインします。 新しい Firebase プロジェクトを作成し、 [プロジェクト名] に「PushDemo」と入力します。

    注意

    一意の名前が生成されます。 既定では、指定した名前を小文字にしたものと生成された数値がダッシュで区切られます。 グローバルに一意になるように指定する場合は、この設定を変更できます。

  2. プロジェクトを作成した後、 [Add Firebase to your Android app](Android アプリに Firebase を追加する) を選択します。

    Add Firebase to your Android app

  3. [Android アプリへの Firebase の追加] ページで、次の手順を実行します。

    1. [Android パッケージ名] に、お使いのパッケージの名前を入力します。 (例: com.<organization_identifier>.<package_name>)。

      Specify the package name

    2. [アプリの登録] を選択します。

    3. [google-services.json のダウンロード] を選択します。 次に、後で使用するためにファイルをローカル フォルダーに保存し、 [次へ] を選択します。

      Download google-services.json

    4. [次へ] を選択します。

    5. [Continue to console] (コンソールに続行する) を選択します。

      注意

      インストールの確認のために [Continue to console] (コンソールに続行する) ボタンが有効になっていない場合は、 [この手順をスキップする] を選択します。

  4. Firebase コンソールで、プロジェクトの歯車アイコンを選択します。 次に、 [Project Settings](プロジェクト設定) を選択します。

    Select Project Settings

    注意

    google-services.json ファイルをダウンロードしていない場合は、このページでダウンロードできます。

  5. 上部にある [クラウド メッセージング] タブに切り替えます。 後で使用するために、サーバー キーをコピーし、保存します。 この値を使用して通知ハブを構成します。

    Copy server key

iOS アプリをプッシュ通知に登録する

プッシュ通知を iOS アプリに送信するには、アプリケーションを Apple に登録すると共に、プッシュ通知にも登録します。

  1. ご自分のアプリをまだ登録していない場合は、Apple Developer Center の iOS Provisioning Portal に移動します。 Apple ID を使用してポータルにサインインし、[Certificates, Identifiers & Profiles](証明書、識別子、およびプロファイル) に移動して、[識別子] を選択します。 + をクリックして新しいアプリを登録します。

    iOS Provisioning Portal App IDs page

  2. [Register a New Identifier](新しい識別子の登録) 画面で、 [App IDs](アプリ ID) を選択します。 その後 [続行] を選択します。

    iOS Provisioning Portal register new ID page

  3. 新しいアプリで次の 3 つの値を更新してから、 [Continue](続行) を選択します。

    • 説明:アプリのわかりやすい名前を入力します。

    • Bundle ID: アプリ ディストリビューション ガイドの説明のとおりに、com.<組織 ID>.<製品名>の形式のバンドル ID を入力します。 次のスクリーンショットでは、mobcat 値が組織 ID として使用され、PushDemo 値が製品名として使用されています。

      iOS Provisioning Portal register app ID page

    • [Push Notifications](プッシュ通知) : [Capabilities](機能) セクションの [Push Notifications](プッシュ通知) オプションをオンにします。

      Form to register a new App ID

      このアクションにより、アプリ ID が生成され、その情報を確認するよう求められます。 [Continue](続行) を選択し、 [Register](登録) を選択して新しいアプリ ID を確認します。

      Confirm new App ID

      [Register](登録) を選択すると、新しいアプリ ID が [Certificates, Identifiers & Profiles](証明書、識別子、およびプロファイル) ページに 1 行の項目として表示されます。

  4. [Certificates, Identifiers & Profiles](証明書、識別子、およびプロファイル) ページの [識別子] で、作成したアプリ ID の行項目を見つけます。 次に、その行を選択して [Edit your App ID Configuration](App ID 構成の編集) 画面を表示します。

Notification Hubs の証明書を作成する

通知ハブが Apple プッシュ通知サービス (APNS) で動作できるようにするには、証明書が必要です。証明書は次の 2 つの方法のいずれかで提供できます。

  1. 通知ハブに直接アップロードできる p12 プッシュ証明書を作成する (当初のアプローチ)

  2. トークンベースの認証に使用できる p8 証明書を作成する (新しい推奨されるアプローチ)

APNS のトークンベース (HTTP/2) 認証」にも記載されているように、新しい方のアプローチには多くの利点があります。 必要な手順は少数ですが、特定のシナリオで必須になることもあります。 ただし、どちらの方法もこのチュートリアルの目的で動作するため、両方の手順が提供されています。

オプション 1: 通知ハブに直接アップロードできる p12 プッシュ証明書を作成する
  1. Mac で、キーチェーン アクセス ツールを実行します。 これは、Launchpad のユーティリティ フォルダーまたはその他フォルダーから開くことができます。

  2. [キーチェーン アクセス] を選択し、 [証明書アシスタント] を展開して、 [認証局に証明書を要求] を選択します。

    Use Keychain Access to request a new certificate

    注意

    既定では、キーチェーン アクセスによってリスト内の最初の項目が選択されます。 これは、 [証明書] カテゴリで、 [Apple Worldwide Developer Relations Certification Authority] が一覧の最初の項目ではない場合に問題になることがあります。 CSR (証明書署名要求) を生成する前に、キー以外の項目があること、または [Apple Worldwide Developer Relations Certification Authority] キーが選択されていることを確認します。

  3. [ユーザーのメール アドレス] を選択し、 [通称] の値を入力します。 [ディスクに保存] を指定したことを確認してから、 [続ける] を選択します。 [CA のメール アドレス] は、必要がないため空白のままにします。

    Expected certificate information

  4. [名前を付けて保存]証明書署名要求 (CSR) ファイルの名前を入力し、 [場所] で保存場所を選択して [保存] を選択します。

    Choose a file name for the certificate

    このアクションにより、選択した場所に CSR ファイルが保存されます。 既定の場所は [デスクトップ] です。 ファイル用に選択した場所を忘れないでください。

  5. iOS プロビジョニング ポータル[Certificates, Identifiers & Profiles](証明書、識別子、およびプロファイル) ページに戻り、選択された [プッシュ通知] オプションまで下にスクロールし、[構成] を選択して証明書を作成します。

    Edit App ID page

  6. [Apple Push Notification service TLS/SSL Certificates](Apple Push Notification Service の TLS/SSL 証明書) ウィンドウが表示されます。 [Development TLS/SSL Certificate](開発 TLS/SSL 証明書) セクションで [Create Certificate](証明書の作成) ボタンを選択します。

    Create certificate for App ID button

    [Create a new Certificate](新しい証明書の作成) 画面が表示されます。

    注意

    このチュートリアルでは開発証明書を使用します。 運用証明書の場合も同じ処理を行います。 通知の送信と同じ証明書の種類を使用するようにします。

  7. [Choose File](ファイルの選択) を選択して、CSR ファイルを保存した場所を参照し、証明書名をダブルクリックして読み込みます。 その後 [続行] を選択します。

  8. ポータルで証明書が作成されたら、 [Download](ダウンロード) ボタンを選択します。 この証明書を保存し、この保存場所を覚えておいてください。

    Generated certificate download page

    証明書がダウンロードされ、自分のコンピューターのダウンロード フォルダーに保存されます。

    Locate certificate file in the Downloads folder

    注意

    既定では、ダウンロードした開発証明書の名前は aps_development.cer になっています。

  9. ダウンロードしたプッシュ証明書 aps_development.cer をダブルクリックします。 このアクションで、以下の図のように、新しい証明書がキーチェーンにインストールされます:

    Keychain access certificates list showing new certificate

    注意

    証明書の名前は場合によって異なりますが、名前の前には Apple Development iOS Push Services が付き、適切なバンドル ID が関連付けられます。

  10. キーチェーン アクセスの [Certificates] (証明書) カテゴリで、作成した新しいプッシュ証明書に [Control] + クリックを実行します。 [書き出す] を選択し、ファイルに名前を付けて、p12 形式を選択します。次に、 [保存] を選択します。

    Export certificate as p12 format

    パスワードを使用して証明書を保護することもできますが、パスワードはオプションです。 パスワードの作成を省略する場合は、 [OK] をクリックします。 エクスポートした p12 証明書のファイル名と場所を書き留めます。 これらは、APNs での認証を有効にするために使用されます。

    注意

    実際の p12 ファイルの名前と場所は、このチュートリアルの図に示されているものと異なる場合があります。

オプション 2: トークンベースの認証に使用できる p8 証明書を作成する
  1. 次の情報をメモしておきます。

    • アプリ ID プレフィックス (チーム ID)
    • バンドル ID
  2. [Certificates, Identifiers & Profiles](証明書、識別子、およびプロファイル) に戻って [Keys](キー) をクリックします。

    注意

    APNS 用に構成されたキーが既にある場合は、作成直後にダウンロードした p8 証明書を再利用できます。 その場合、手順 3. から手順 5. は無視してかまいません。

  3. [+] ボタン (または [Create a key](キーの作成) ボタン) をクリックして新しいキーを作成します。

  4. [Key Name](キー名) に適切な値を入力し、 [Apple Push Notifications service (APNS)] オプションをオンにして、 [続行] をクリックし、次の画面で [登録] をクリックします。

  5. [ダウンロード] をクリックして p8 ファイル (AuthKey_ で始まるファイル) を安全なローカル ディレクトリに移動し、 [Done](完了) をクリックします。

    注意

    p8 ファイルは必ず安全な場所に保管してください (さらにバックアップを保存すること)。 キーのダウンロード後は、サーバー コピーが削除されるため、再ダウンロードすることはできません。

  6. [キー] で、作成したキー (または既存のキーを使用するように選択した場合はそのキー) をクリックします。

  7. [Key ID](キー ID) の値をメモしておきます。

  8. 任意の適切なアプリケーション (Visual Studio Code など) で p8 証明書を開きます。 -----BEGIN PRIVATE KEY----------END PRIVATE KEY----- で挟まれたキーの値をメモします。

    -----BEGIN PRIVATE KEY-----
    <key_value>
    -----END PRIVATE KEY-----

    注意

    これは、後で Notification Hub の構成に使用するトークン値です。

以上の手順を終えると、次の情報が確認済みとなります。これらの情報は、後出の「APNS 情報で通知ハブを構成する」で使用します。

  • チーム ID (手順 1. を参照)
  • バンドル ID (手順 1. を参照)
  • キー ID (手順 7. を参照)
  • トークン値 (手順 8. で取得した p8 キー値)

アプリケーションのプロビジョニング プロファイルを作成する

  1. iOS Provisioning Portal に戻り、[Certificates, Identifiers & Profiles](証明書、識別子、およびプロファイル) を選択します。左側のメニューから [Profiles](プロファイル) を選択し、[+] を選択して新しいプロファイルを作成します。 [Register a New Provisioning Profile](新しいプロビジョニング プロファイルの登録) 画面が表示されます。

  2. [Development](開発) で、プロビジョニング プロファイルの種類として [iOS App Development](iOS アプリ開発) を選択し、 [Continue](続行) を選択します。

    Provisioning profile list

  3. 次に、 [App ID](アプリ ID) ドロップダウン リストで、自分が作成したアプリ ID を選択し、 [Continue](続行) を選択します。

    Select the App ID

  4. [Select certificates](証明書の選択) ウィンドウで、コード署名に使用する開発証明書を選択し、 [Continue](続行) を選択します。

    注意

    これは証明書であり、前の手順で作成したプッシュ証明書ではありません。 これは開発証明書です。 このチュートリアルでは前提条件であるため、存在しない場合は作成する必要があります。 開発者証明書は、Apple Developer ポータルXcode、または Visual Studio で作成できます。

  5. [Certificates, Identifiers & Profiles](証明書、識別子、およびプロファイル) ページに戻り、左側のメニューから [Profiles](プロファイル) を選択し、[+] を選択して新しいプロファイルを作成します。 [Register a New Provisioning Profile](新しいプロビジョニング プロファイルの登録) 画面が表示されます。

  6. [Select certificates](証明書の選択) ウィンドウで、作成した開発証明書を選択します。 その後 [続行] を選択します。

  7. 次に、テストに使用するデバイスを選択し、 [Continue](続行) を選択します。

  8. 最後に、 [Provisioning Profile Name](プロビジョニング プロファイル名) でプロファイルの名前を選択し、 [Generate](生成) を選択します。

    Choose a provisioning profile name

  9. 新しいプロビジョニング プロファイルが作成されたら、 [Download](ダウンロード) を選択します。 この保存場所を覚えておいてください。

  10. プロビジョニング プロファイルの場所を参照し、それをダブルクリックして開発マシンにインストールします。

通知ハブの作成

このセクションでは、通知ハブを作成し、APNS で認証を構成します。 p12 プッシュ証明書またはトークンベースの認証を使用できます。 既に作成した通知ハブを使用する場合は、手順 5. に進んでください。

  1. Azure にサインインします。

  2. [リソースの作成] をクリックし、通知ハブを検索して選択し、 [作成] をクリックします。

  3. 次のフィールドを更新してから、 [作成] をクリックします。

    基本的な詳細

    サブスクリプション:[サブスクリプション] ボックスの一覧で、ターゲット サブスクリプションを選択します。
    [リソース グループ]: 新しい [リソース グループ] を作成します (または、既存のリソース グループを選択します)。

    名前空間の詳細

    [Notification Hub の名前空間]:通知ハブ名前空間のグローバルに一意な名前を入力します。

    注意

    このフィールドに [新規作成] オプションが選択されていることを確認します。

    通知ハブの詳細

    [通知ハブ]:[通知ハブ] の名前を入力します。
    [場所]: ボックスの一覧から適切な場所を選択します。
    [価格レベル]: 既定の [無料] オプションのままにします。

    注意

    無料レベルのハブの最大数に達している場合を除きます。

  4. 通知ハブがプロビジョニングされたら、そのリソースに移動します。

  5. 新しい [通知ハブ] に移動します。

  6. ( [管理] の下の) リストから [アクセス ポリシー] を選択します。

  7. [ポリシー名] の値を、対応する [接続文字列] の値と一緒にメモしておきます。

APNS 情報を使用して通知ハブを構成する

[Notification Services][Apple] を選択し、先ほど「Notification Hubs の証明書を作成する」セクションで選択したアプローチに応じて適切な手順に従います。

注意

[アプリケーション モード][Production](運用) は、ストアからアプリを購入したユーザーにプッシュ通知を送信する場合にのみ使用します。

オプション 1: .p12 プッシュ証明書を使用する

  1. [Certificate] を選択します。

  2. ファイル アイコンを選択します。

  3. 先ほどエクスポートした .p12 ファイルを選択し、 [Open](開く) を選択します。

  4. 必要に応じて、適切なパスワードを指定します。

  5. [サンドボックス] モードを選択します。

  6. [保存] を選択します。

オプション 2: トークンベースの認証を使用する

  1. [トークン] を選択します。

  2. 先ほど取得した次の値を入力します。

    • キー ID
    • バンドル ID
    • チーム ID
    • トークン
  3. [サンドボックス] を選択します。

  4. [保存] を選択します。

FCM 情報を使用して通知ハブを構成する

  1. 左側のメニューの [設定] で、 [Google (GCM/FCM)] を選択します。
  2. Google Firebase Console からメモしたサーバー キーを入力します。
  3. ツールバーの [保存] を選択します。

ASP.NET Core Web API バックエンド アプリケーションを作成する

このセクションでは、デバイスの登録と、Xamarin.Forms モバイル アプリへの通知の送信を処理する ASP.NET Core Web API バックエンドを作成します。

Web プロジェクトの作成

  1. Visual Studio で、 [ファイル]>[新しいソリューション] を選択します。

  2. [.NET Core]>[アプリ]>[ASP.NET Core]>[API]>[次へ] を選択します。

  3. [Configure your new ASP.NET Core Web API] (新しい ASP.NET Core Web API の構成) ダイアログで、 .NET Core 3.1[ターゲット フレームワーク] を選択します。

  4. [プロジェクト名] に「PushDemoApi」と入力して、 [作成] を選択します。

  5. デバッグを開始して ( [Command] + [Enter] )、テンプレート アプリをテストします。

    注意

    テンプレート アプリは、launchUrl として WeatherForecastController を使用するように構成されています。 これは [プロパティ]>[launchSettings.json] で設定されます。

    [Invalid development certificate found] (無効な開発証明書が見つかりました) というメッセージが表示された場合は、以下を実行します。

    1. 'dotnet dev-certs https' ツールを実行してこの問題を解決することに同意するには、 [はい] をクリックします。 証明書のパスワードと、キーチェーンのパスワードを入力するように求められます。

    2. [Install and trust the new certificate] (新しい証明書をインストールして信頼する) の確認を求められたら、 [はい] をクリックします。次に、キーチェーンのパスワードを入力します。

  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 で確認できます。

    NotificationsHub:Name:
    [概要] の上部にある [要点] サマリーの [名前] を参照してください。

    NotificationHub:ConnectionString:
    アクセス ポリシーDefaultFullSharedAccessSignature を参照してください。

    注意

    運用環境のシナリオでは、Azure KeyVault などのオプションを参照して、接続文字列を安全に格納することができます。 わかりやすくするために、シークレットは Azure App Service のアプリケーション設定に追加されます。

API キーを使用してクライアントを認証する (オプション)

API キーはトークンほど安全ではありませんが、このチュートリアルの目的には十分です。 API キーは、ASP.NET ミドルウェアを介して簡単に構成できます。

  1. API キーをローカル構成値に追加します。

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

    注意

    プレースホルダーの値を実際の値に置き換え、メモする必要があります。

  2. PushDemoApi プロジェクトで [Control] + クリックを実行し、 [追加] メニューから [新しいフォルダー] を選択します。次に、 [フォルダー名] として「Authentication」を使用して [追加] をクリックします。

  3. Authentication フォルダーで [Control] + クリックを実行し、 [追加] メニューから [新しいファイル...] を選択します。

  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.cs という別の [空のクラス]Authentication フォルダーに追加し、次の実装を追加します。

    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.csConfigureServices メソッドを更新して、services.AddControllers メソッドの呼び出しの下で API キー認証を構成します。

    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.csConfigure メソッドを更新して、アプリの IApplicationBuilderUseAuthentication および UseAuthorization 拡張メソッドを呼び出します。 これらのメソッドが 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) を実現するための技術です。

通知ハブおよび Notification Hubs SDK のバックエンド操作への使用は、サービス内にカプセル化されます。 サービスが登録され、適切な抽象化によって使用できるようになります。

  1. Dependencies フォルダーで [Control] + クリックを実行し、 [NuGet パッケージの管理...] を選択します。

  2. Microsoft.Azure.NotificationHubs を検索し、オンになっていることを確認します。

  3. [パッケージの追加] をクリックし、ライセンス条項への同意を求めるメッセージが表示されたら、 [同意する] をクリックします。

  4. PushDemoApi プロジェクトで [Control] + クリックを実行し、 [追加] メニューから [新しいフォルダー] を選択します。次に、 [フォルダー名] として「Models」を使用して [追加] をクリックします。

  5. Control + キーを押しながら Models フォルダーをクリックし、 [追加] メニューから [新しいファイル...] を選択します。

  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. PushDemoApi プロジェクトに Services という名前の新しいフォルダーを追加します。

  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 に制限されていますが、この場合の式には、OR (||) のみが含まれています。 要求に 20 個を超えるタグがある場合は、複数の要求に分割する必要があります。 詳細については、「ルーティングとタグ式」のドキュメントを参照してください。

  13. Startup.csConfigureServices メソッドを更新して、NotificationHubsServiceINotificationService のシングルトン実装として追加します。

    
    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. Controllers フォルダーで [Control] + クリックを実行し、 [追加] メニューから [新しいファイル...] を選択します。

  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 キーを使用してクライアントを認証する」セクションを完了した場合は、NotificationsControllerAuthorize 属性でも装飾する必要があります。

    [Authorize]
    
  6. コンストラクターを更新して、INotificationService の登録済みインスタンスを引数として受け取り、それを readonly メンバーに割り当てます。

    readonly INotificationService _notificationService;
    
    public NotificationsController(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }
    
  7. launchSettings.json (Properties フォルダー内) で、RegistrationsControllerRoute 属性に指定されている URL と一致するように launchUrlweatherforecast から api/notifications に変更します。

  8. デバッグを開始して ( [Command] + [Enter] )、アプリが新しい NotificationsController で動作し、401 Unauthorized 状態を返すことを確認します。

    注意

    Visual Studio では、ブラウザーでアプリが自動的に起動しない場合があります。 この時点から API をテストするには Postman を使用します。

  9. 新しい [Postman] タブで、要求を [GET] に設定します。 次のアドレスを入力して、プレースホルダー <applicationUrl> を、[プロパティー]>launchSettings.json にある https applicationUrl に置き換えます。

    <applicationUrl>/api/notifications
    

    Note

    既定のプロファイルでは、applicationUrl は "https://localhost:5001" になります。 IIS (Windows 上の Visual Studio 2019 での既定) を使用している場合は、代わりに iisSettings 項目に指定されている applicationUrl を使用してください。 アドレスが正しくない場合は、404 応答が返されます。

  10. API キーを使用してクライアントを認証する」セクションを完了した場合は、apikey 値を含めるように要求ヘッダーを構成してください。

    Key
    apikey <your_api_key>
  11. [Send] ボタンをクリックします。

    注意

    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 ServiceAPI アプリを作成します。

  1. Azure portal にサインインします。

  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. 以下の各設定について、 [新しいアプリケーション設定] をクリックして、 [名前][値] を入力してから、 [OK] をクリックします。

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

    注意

    これらは、以前にユーザー設定で定義したものと同じ設定です。 それをコピーすることができます。 Authentication:ApiKey 設定は、「API キーを使用してクライアントを認証する」セクションを完了した場合にのみ必要です。 運用環境のシナリオでは、Azure KeyVault などのオプションを参照できます。 この場合、わかりやすくするためにアプリケーション設定として追加されています。

  8. すべてのアプリケーション設定が追加されたら、 [保存][続行] の順にクリックします。

バックエンド サービスの発行

次に、このアプリを API アプリにデプロイして、すべてのデバイスからアクセスできるようにします。

注意

次の手順は、Visual Studio for Mac に固有のものです。 Windows で Visual Studio 2019 を使用している場合は、発行フローが異なります。 「Windows 上の Azure App Service へ発行する」を参照してください。

  1. 構成を [デバッグ] から [リリース] に変更します (まだ行っていない場合)。

  2. PushDemoApi プロジェクトで [Control] + クリックを実行し、 [発行] メニューから [Azure への発行...] を選択します。

  3. メッセージが表示されたら、認証フローに従います。 前の「API アプリの作成」セクションで使用したアカウントを使用します。

  4. 発行先としてリストから以前に作成した Azure App Service API アプリを選択し、 [発行] をクリックします。

ウィザードの完了後に、Azure にアプリを発行してからアプリを開きます。 まだ行っていない場合は、URL をメモしておきます。 この URL は、後で使用するバックエンド エンドポイントです。

発行された API の検証

  1. [Postman] で新しいタブを開いて要求を [PUT] に設定し、以下のアドレスを入力します。 プレースホルダーを、前の「バックエンド サービスの発行」セクションでメモしたベース アドレスに置き換えます。

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

    注意

    ベース アドレスは、https://<app_name>.azurewebsites.net/ の形式にする必要があります。

  2. API キーを使用してクライアントを認証する」セクションを完了した場合は、apikey 値を含めるように要求ヘッダーを構成してください。

    Key
    apikey <your_api_key>
  3. [Body][raw] オプションを選択し、書式オプションの一覧から [JSON] を選択します。次に、いくつかのプレースホルダー JSON コンテンツを含めます。

    {}
    
  4. [送信] をクリックします。

    注意

    サービスから 422 UnprocessableEntity 状態が返されます。

  5. もう一度手順 1 から 4 を実行しますが、今度は要求エンドポイントを指定して、400 Bad Request 応答が返されることを確認します。

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

注意

有効な要求データを使用して API をテストすることはできません。これは、クライアント モバイル アプリからのプラットフォーム固有の情報が必要になるためです。

クロスプラットフォームの Xamarin.Forms アプリケーションを作成する

このセクションでは、クロスプラットフォーム方式でプッシュ通知を実装する Xamarin.Forms モバイル アプリケーションを構築します。

これにより、作成したバックエンド サービスを使用して、通知ハブから登録と登録解除を行うことができます。

アクションが指定され、アプリがフォアグラウンドにある場合は、アラートが表示されます。 それ以外の場合は、通知センターに通知が表示されます。

注意

通常、明示的なユーザー登録/登録解除の入力を行わずに、アプリケーション ライフサイクルの適切な時点で (または初回起動時に)、登録 (および登録解除) アクションを実行します。 ただし、この例では、この機能をより簡単に調査およびテストできるように、明示的なユーザー入力が必要です。

Xamarin.Forms ソリューションを作成する

  1. Visual Studio で、空のフォーム アプリをテンプレートとして使用し、 [プロジェクト名] に「PushDemo」と入力して新しい Xamarin. Forms ソリューションを作成します。

    注意

    [Configure your Blank Forms App] (空のフォームアプリの構成) ダイアログで、組織 ID が前に使用した値と一致していること、およびターゲットとして AndroidiOS の両方のチェック ボックスがオンになっていることを確認します。

  2. Control キーを押しながら PushDemo ソリューションをクリックし、 [Update NuGet Packages] (NuGet パッケージの更新) を選択します。

  3. Control キーを押しながら PushDemo ソリューションをクリックし、 [NuGet パッケージの管理...] を選択します。

  4. Newtonsoft.Json を探し、そのチェック ボックスがオンになっていることを確認します。

  5. [パッケージの追加] をクリックし、ライセンス条項への同意を求めるメッセージが表示されたら、 [同意する] をクリックします。

  6. 各ターゲット プラットフォームでアプリをビルドして実行し (Command + Enter キー)、テンプレート化されたアプリがデバイスで実行されることをテストします。

クロスプラットフォーム コンポーネントを実装する

  1. Control キーを押しながら PushDemo プロジェクトをクリックし、 [追加] メニューから [新しいフォルダー] を選択します。次に、 [フォルダー名] として「Models」を使用して [追加] をクリックします。

  2. Control + キーを押しながら Models フォルダーをクリックし、 [追加] メニューから [新しいファイル...] を選択します。

  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.cs という名前の空の列挙型Models フォルダーに追加します。

    namespace PushDemo.Models
    {
        public enum PushDemoAction
        {
            ActionA,
            ActionB
        }
    }
    
  5. PushDemo.iOS プロジェクトに Services という名前の新しいフォルダーを追加してから、以下の実装を使用してそのフォルダーに 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. 次の実装を使用して、INotificationActionService から派生する IPushDemoNotificationActionService.cs という名前の空のインターフェイスServices フォルダーに追加します。

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

    注意

    この型は PushDemo アプリケーションに固有のもので、PushDemoAction 列挙型を使用して、厳密に型指定された方法でトリガーされるアクションを識別します。

  10. 以下のコードにより、NotificationRegistrationService.cs という名前の空のクラスServices フォルダーに追加し、INotificationRegistrationService を実装します。

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

    注意

    apiKey 引数が必要なのは、「API キーを使用してクライアントを認証する」セクションを完了した場合のみです。

  11. 以下のコードにより、PushDemoNotificationActionService.cs という名前の空のクラスServices フォルダーに追加し、IPushDemoNotificationActionService を実装します。

    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.cs という名前の空のクラスPushDemo プロジェクトに追加します。

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

    注意

    これは、機密をソース管理の対象外にしておくための簡単な方法として利用されます。 これらの値は、自動化されたビルドの一部として置き換えることも、ローカルの部分クラスを使用してオーバーライドすることもできます。 これは次の手順で実行します。

    ApiKey フィールドが必要なのは、「API キーを使用してクライアントを認証する」セクションを完了した場合のみです。

  13. 次の実装で、今回は Config.local_secrets.cs という名前の別の空のクラスを、PushDemo プロジェクトに追加します。

    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/ である必要があります。 このファイルがコミットされないように、お使いの gitignore ファイルに必ず *.local_secrets.* を追加してください。

    ApiKey フィールドが必要なのは、「API キーを使用してクライアントを認証する」セクションを完了した場合のみです。

  14. 次の実装で、Bootstrap.cs という名前の空のクラスPushDemo プロジェクトに追加します。

    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 の実装を渡し始めたときに、各プラットフォームによって呼び出されます。

    NotificationRegistrationServiceapiKey コンストラクターが必要なのは、「API キーを使用してクライアントを認証する」セクションを完了した場合のみです。

クロスプラットフォーム 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 で、readonly のバッキング フィールドを追加して、INotificationRegistrationService 実装への参照を格納します。

    readonly INotificationRegistrationService _notificationRegistrationService;
    
  3. MainPage コンストラクター内で、ServiceContainer を使用して INotificationRegistrationService 実装を解決し、それを notificationRegistrationService バッキング フィールドに代入します。

    public MainPage()
    {
        InitializeComponent();
    
        _notificationRegistrationService =
            ServiceContainer.Resolve<INotificationRegistrationService>();
    }
    
  4. 対応する Register/Deregister メソッドを呼び出す、RegisterButton ボタンと DeregisterButton ボタンの 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. App コンストラクター内で、ServiceContainer を使用して IPushNotificationActionService 実装を解決し、IPushDemoNotificationActionServiceActionTriggered イベントにサブスクライブします。

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

    注意

    これは単に、プッシュ通知アクションの受信と伝達を示すためのものです。 通常、これらは暗黙のうちに処理されます。たとえば、特定のビューへの移動や、この場合の、ルートの PageMainPage を介してアラートを表示するのではなく、一部のデータを更新する場合です。

プッシュ通知用のネイティブ Android プロジェクトを構成する

パッケージ名とアクセス許可を検証する

  1. PushDemo.Android で、 [Project Options] (プロジェクト オプション) を開き、 [ビルド] セクションから [Android アプリケーション] を開きます。

  2. パッケージ名が、Firebase ConsolePushDemo プロジェクトで使用した値と一致していることを確認します。 パッケージ名は、com.<organization>.pushdemo という形式でした。

  3. [Minimum Android Version] (最小 Android バージョン)[Android 8.0 (API level 26)] (Android 8.0 (API レベル 26)) に設定し、 [Target Android Version] (対象の Android バージョン) を最新の API レベルに設定します。

    注意

    このチュートリアルでは、API レベル 26 以上が実行されているデバイスのみがサポートされていますが、より前のバージョンを実行しているデバイスをサポートするように拡張できます。

  4. [必要なアクセス許可] で、 [INTERNET][READ_PHONE_STATE] のアクセス許可が有効になっていることを確認します。

  5. [OK]

Xamarin Google Play 開発者サービス ベースおよび Xamarin.Firebase.Messaging の各パッケージを追加する

  1. PushDemo.Android で、Control キーを押しながら Packages フォルダーをクリックし、 [NuGet パッケージの管理...] を選択します。

  2. Xamarin.GooglePlayServices.Base (Basement ではありません) を探し、チェック ボックスがオンになっていることを確認します。

  3. Xamarin.Firebase.Messaging を探し、チェック ボックスがオンになっていることを確認します。

  4. [パッケージの追加] をクリックし、ライセンス条項への同意を求めるメッセージが表示されたら、 [同意する] をクリックします。

Google Services JSON ファイルを追加する

  1. Control キーを押しながら PushDemo.Android プロジェクトをクリックし、 [追加] メニューから [既存のファイル...] を選択します。

  2. Firebase Console で以前 PushDemo プロジェクトを設定したときにダウンロードした google-services.json ファイルを選択し、 [開く] をクリックします。

  3. メッセージが表示されたら、ファイルをディレクトリにコピーするように選択します。

  4. Controlキーを押しながら PushDemo.Android プロジェクト内からの google-services.jsonクリックして、GoogleServicesJsonビルド アクションとして設定されていることを確認します。

Android 用のプッシュ通知を処理する

  1. Control キーを押しながら PushDemo.Android プロジェクトをクリックし、 [追加] メニューから [新しいフォルダー] を選択します。次に、 [フォルダー名] として「Services」を使用して [追加] をクリックします。

  2. Control + キーを押しながら Services フォルダーをクリックし、 [追加] メニューから [新しいファイル...] を選択します。

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

    注意

    このクラスでは、通知ハブの登録ペイロードの一部として一意の ID (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 で、LaunchModeSingleTop に設定し、開かれたときに再度 MainActivity が作成されないようにします。

    [Activity(
        Label = "PushDemo",
        LaunchMode = LaunchMode.SingleTop,
        Icon = "@mipmap/icon",
        Theme = "@style/MainTheme",
        MainLauncher = true,
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    
  7. プライベート プロパティとそれらに対応するバッキング フィールドを追加して、IPushNotificationActionService および IDeviceInstallationService の実装への参照を格納します。

    IPushDemoNotificationActionService _notificationActionService;
    IDeviceInstallationService _deviceInstallationService;
    
    IPushDemoNotificationActionService NotificationActionService
        => _notificationActionService ??
            (_notificationActionService =
            ServiceContainer.Resolve<IPushDemoNotificationActionService>());
    
    IDeviceInstallationService DeviceInstallationService
        => _deviceInstallationService ??
            (_deviceInstallationService =
            ServiceContainer.Resolve<IDeviceInstallationService>());
    
  8. Firebase トークンを取得して格納する IOnSuccessListener インターフェイスを実装します。

    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 という名前の新しいメソッドを追加します。このメソッドでは、指定された Intentaction という名前の追加の値があるかどうかを調べます。 その 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. ProcessNotificationActions メソッドを呼び出すように OnNewIntent メソッドをオーバーライドします。

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

    注意

    ActivityLaunchModeSingleTopに設定されているので、Intent は、OnCreate メソッドではなく OnNewIntent メソッドによって既存の Activity インスタンスに送信されます。そのため、OnCreate メソッドと OnNewIntent メソッドの両方で、到着する意図を処理する必要があります。

  11. base.OnCreate の呼び出しの直後に Bootstrap.Begin を呼び出すように OnCreate メソッドを更新して、IDeviceInstallationService のプラットフォーム固有の実装を渡します。

    Bootstrap.Begin(() => new DeviceInstallationService());
    
  12. 同じメソッドで、Bootstrap.Begin の呼び出しの直後に、条件に基づいて FirebaseApp インスタンスに対して GetInstanceId を呼び出して、IOnSuccessListener として MainActivity を追加します。

    if (DeviceInstallationService.NotificationsSupported)
    {
        FirebaseInstanceId.GetInstance(Firebase.FirebaseApp.Instance)
            .GetInstanceId()
            .AddOnSuccessListener(this);
    }
    
  13. 引き続き OnCreate 内で、LoadApplication の呼び出し直後に ProcessNotificationActions を呼び出して、現在の Intent を渡します。

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

注意

プッシュ通知を受信し続けるには、アプリを実行して停止するたびにデバッグ セッションからアプリを再登録する必要があります。

プッシュ通知用のネイティブ iOS プロジェクトを構成する

Info.plist と Entitlements.plist を構成する

  1. Visual Studio>[ユーザー設定...]>[発行]>[Apple Developer Accounts] (Apple Developer アカウント) で、お使いの Apple Developer アカウントにサインインしていること、および適切な証明書プロビジョニング プロファイルがダウンロードされていることを確認します。 これらのアセットは、前もっての手順の一部として作成しておく必要があります。

  2. PushDemo.iOSInfo.plist を開き、BundleIdentifier が、Apple Developer ポータルの対応するプロビジョニング プロファイルで使用されていた値と一致していることを確認します。 BundleIdentifier は、com.<organization>.PushDemo という形式でした。

  3. 同じファイルで、Minimum system version13.0 に設定します。

    注意

    このチュートリアルでは、iOS 13.0 以降が実行されているデバイスのみがサポートされていますが、より前のバージョンを実行しているデバイスをサポートするように拡張できます。

  4. PushDemo.iOSプロジェクト オプションを開きます (プロジェクトをダブルクリックします)。

  5. [Project Options] (プロジェクト オプション)[ビルド] > [iOS Bundle Signing] (iOS バンドル署名) で、[チーム] の下にある開発者アカウントが選択されていることを確認します。 次に、[Automatically manage signing] (署名を自動管理する) が選択されていて、署名証明書とプロビジョニング プロファイルが自動的に選択されていることを確認します。

    注意

    署名証明書プロビジョニング プロファイルが自動的に選択されていない場合は、 [Manual Provisioning] (手動プロビジョニング) を選択してから [Bundle Signing Options] (バンドル署名オプション) をクリックします。 [Signing Identity] (署名 ID) には [チーム] が選択されていて、 [デバッグ] 構成と [リリース] 構成の両方でプロビジョニング プロファイルには PushDemo 固有のプロビジョニング プロファイルが選択されていることを確認します。そうすることで、両方の場合に [プラットフォーム] には [iPhone] が選択されるようにします。

  6. [PushDemo.iOS]Entitlements.plist を開き、 [権利] タブで表示したときに [プッシュ通知を有効にする] チェック ボックスがオンになっていることを確認します。次に、 [ソース] タブで表示したときに [APS Environment] (APS 環境) の設定が [開発] に設定されていることを確認します。

iOS 用のプッシュ通知を処理する

  1. Control キーを押しながら [PushDemo.iOS] プロジェクトをクリックし、 [追加] メニューから [新しいフォルダー] を選択します。次に、 [フォルダー名] として「Services」を使用して [追加] をクリックします。

  2. Control + キーを押しながら Services フォルダーをクリックし、 [追加] メニューから [新しいファイル...] を選択します。

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

    注意

    このクラスでは、一意の ID (UIDevice.IdentifierForVendor 値を使用) と通知ハブの登録ペイロードが提供されます。

  4. PushDemo.iOS プロジェクトに Extensions という名前の新しいフォルダーを追加してから、以下の実装を使用してそのフォルダーに 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. プライベート プロパティとそれらに対応するバッキング フィールドを追加して、IPushDemoNotificationActionServiceINotificationRegistrationService、および IDeviceInstallationService の実装への参照を格納します。

    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. ユーザー通知設定を登録してから、APNS を使用するリモート通知のための登録を行う RegisterForRemoteNotifications メソッドを追加します。

    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. NSDictionary 通知データを処理し、条件に基づいて NotificationActionService.TriggerAction を呼び出すための ProcessNotificationActions メソッドを追加しします。

    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. Forms.Init の呼び出しの直後に Bootstrap.Begin を呼び出すように FinishedLaunching メソッドを更新して、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 が含まれている場合は、LoadApplication を呼び出した直後に ProcessNotificationActions を呼び出して、結果の userInfo オブジェクトを渡します。

    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 値を含めるように要求ヘッダーを構成してください。

    Key
    apikey <your_api_key>
  4. [Body][raw] オプションを選択し、書式オプションの一覧から [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. PushDemo アプリケーションを、ターゲット プラットフォーム (Android および iOS) の一方または両方で実行します。

    注意

    Android でテストしている場合は、デバッグで実行していないことを確認します。または、アプリケーションを実行してアプリが展開されている場合は、アプリを強制的に閉じてから、ランチャーからもう一度起動します。

  7. PushDemo アプリで、 [登録] ボタンをタップします。

  8. Postman に戻り、 [Generate Code Snippets] (コード スニペットの生成) ウィンドウを閉じ (まだ閉じていない場合)、 [送信] ボタンをクリックします。

  9. Postman200 OK 応答を受信し、ActionA アクションの受信を示すアラートがアプリに表示されたことを確認します。

  10. PushDemo アプリを閉じ、 Postman でもう一度 [送信] ボタンをクリックします。

  11. Postman で再度 200 OK 応答が返されたことを確認します。 PushDemo アプリの通知領域に、正しいメッセージを含む通知が表示されていることを確認します。

  12. 通知をタップしてアプリを開き、ActionA アクションの受信を示すアラートが表示されていることを確認します。

  13. Postman に戻り、前の要求本文を変更して、action 値に action_a ではなく action_b を指定したサイレント通知を送信します。

    {
        "action": "action_b",
        "silent": true
    }
    
  14. アプリを開いたままの状態で、 Postman[送信] ボタンをクリックします。

  15. Postman200 OK 応答を受信し、ActionA アクションの受信ではなく ActionB アクションの受信を示すアラートがアプリに表示されたことを確認します。

  16. PushDemo アプリを閉じ、 Postman でもう一度 [送信] ボタンをクリックします。

  17. Postman200 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/ である必要があります。

登録できず、通知ハブのエラー メッセージが表示される

テスト デバイスがネットワーク接続を備えていることを確認します。 次に、HttpResponseStatusCode プロパティ値を検査するようにブレークポイントを設定して、Http 応答の状態コードを確認します。

状態コードに基づいて、該当する場合は前のトラブルシューティングの推奨事項を確認します。

各 API の特定の状態コードを返す行にブレークポイントを設定します。 次に、ローカルでデバッグするときにバックエンド サービスを呼び出します。

適切なペイロードを使用して、 Postman でバックエンド サービスが予期したとおりに動作していることを確認します。 対象のプラットフォームのクライアント コードによって作成された実際のペイロードを使用します。

プラットフォーム固有の構成セクションを確認して、実行されなかったステップがないことを確認します。 適切な値が、適切なプラットフォームの installation id および token 変数に対して解決されていることを確認します。

デバイスの ID を解決できず、エラー メッセージが表示される

プラットフォーム固有の構成セクションを確認して、実行されなかったステップがないことを確認します。

次のステップ

バックエンド サービスを介して通知ハブに接続された基本的な Xamarin.Forms アプリがあって、通知を送受信できるようになっているはずです。

このチュートリアルで使用している例は、実際のシナリオに合わせて調整する必要があります。 より堅牢なエラー処理、再試行ロジック、ログ記録も実装することをお勧めします。

Visual Studio App Center は、分析および診断を提供するモバイル アプリに簡単に組み込むことができ、トラブルシューティングに役立ちます。