XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

2019/05/28

Xamarin.Android 的遠端推播通知 Remote Push Notification - 使用 Azure Notification Hub

Xamarin.Android 的遠端推播通知 Remote Push Notification - 使用 Azure Notification Hub

本章節將會說明如何透過 Azure Notification Hub 服務,建立起一個具有遠端推播訊息功能的 Android App。

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

本教學課程示範如何使用 Azure 通知中樞將推播通知傳送至 Xamarin.Android 應用程式。 您會建立可使用 Firebase 雲端通訊 (FCM) 接收推播通知的空白 Xamarin.Android 應用程式。 您會使用通知中樞,將推播通知廣播到所有執行您的應用程式的裝置。 NotificationHubs 應用程式範例中提供完成的程式碼。
在本教學課程中,您會執行下列步驟:
  • 建立 Firebase 專案並啟用 Firebase 雲端通訊
  • 建立 Azure 通知中樞
  • 建立 Xamarin.Android 應用程式,並將其連線至通知中樞
  • 從 Azure 入口網站傳送測試通知

必要條件

  • Azure 訂用帳戶。 如果您沒有 Azure 訂用帳戶,請在開始前建立免費 Azure 帳戶。
  • Visual Studio 搭配 Xamarin (在 Windows 上) 或 Visual Studio for Mac (在 OS X 上)。
  • 有效的 Google 帳戶

建立 Firebase 專案並啟用 Firebase 雲端通訊

建立一個新的 Firebase 專案

  • 打開 Firebase 主控台 網頁,使用您的 Google 帳號,登入到 Firebase 系統內 。在登入首頁內,點選 [新增專案] 按鈕
    Firebase 主控台首頁
  • 在 [新增專案] 對話窗中的 [專案名稱] 欄位,輸入 Xamarin-Push-Notification
  • 勾選 [新增專案] 對話窗最下方的 我接受 ... 檢查盒
  • 最後,點選 [建立專案] 按鈕
    Firebase 建立專案 對話窗
  • 當此 Firebase 專案建立完成之後,點選 [繼續] 按鈕
    Firebase 建立專案完成
  • 現在,將會顯示 Xamarin-Push-Notification 這個專案頁面,找到 [首先請新增應用程式] 文字,點選該文字上方代表 Android 機器人的圖示,[將 Firebase 新增至 Android 應用程式]
    Firebase 專案首頁
  • 此時,將會顯示 [新增 Android 應用程式] 頁面,並且將會有四個步驟要進行,分別是 [註冊應用程式]、[下載設定檔]、[新增 Firebase SDK]、[執行應用程式以驗證是否安裝成功]
  • 在 [註冊應用程式] 步驟中,需要填寫底下兩個欄位資料
  • 在 [Android 套件名稱] 欄位中,輸入 com.vulcan.AzureNHub
把這個 Android 套件名稱 字串記錄下來
該 Android 套件名稱 將會於 Xamarin.Android 專案中用到,這將會用於 Xamarin.Android 的 [Xamarin.Android 專案屬性] > [Android 資訊清單] > [套件名稱] 欄位,因此,請將這個 套件名稱 記錄下來
  • 在 [應用程式暱稱 (選填)] 欄位中,輸入 Azure Notification Hub Lab
  • 點選 [註冊應用程式] 按鈕
    Firebase 新增 Android 應用程式 - 註冊應用程式
  • 在 [下載設定檔] 步驟中,請點選 [下載 google-servic.json] 按鈕,取得 google-servic.json 這個檔案,等下在 Xamarin.Android 專案內會使用的到。
請將這個 google-servic.json 妥善保存在適當的地方
google-servic.json 檔案 將會於 Xamarin.Android 專案中用到
  • 點選右下角的 [繼續] 按鈕,繼續下一個步驟
    Firebase 新增 Android 應用程式 - 下載設定檔
  • 在 [新增 Firebase SDK] 步驟中,點選右下角的 [繼續] 按鈕,繼續下一個步驟
    Firebase 新增 Android 應用程式 - 新增 Firebase SDK
  • 在 [執行應用程式以驗證是否安裝成功] 步驟中,點選右下角的 [略過此步驟] 連結,完成 新增 Android 應用程式 程序。
    Firebase 新增 Android 應用程式 - 執行應用程式以驗證是否安裝成功

Firebase 專案設定

  • 現在將會顯示出 [Xamarin-Push-Notification] 頁面,在該頁面左上方找到 [Project Overview] 文字的右方,有個齒輪圖示
  • 點選該齒輪圖示,在彈出子視窗中,點選 [專案設定]
    Firebase 專案設定
  • 此時,將會看到 [設定] 頁面的 [一般] 標籤,若剛剛忘記取得 google-servic.json 這個檔案,可以在這裡點選 [下載 google-servic.json] 按鈕,來重新取得
    Firebase 設定 - 一般
  • 點選 [Cloud Messaging] 標籤,將會看到有個 伺服器金鑰 欄位。
    Firebase 設定 - Cloud Messaging
把這個 伺服器金鑰 字串記錄下來
請把這個 伺服器金鑰 值記錄下來,等下在 Azure 通知中樞內會用到,因此,請將這個 伺服器金鑰 記錄下來

建立 Azure 通知中樞

  • 打開 Azure 入口網站 網頁,使用您的 Microsoft 帳號,登入到 Azure 系統內 。
    Azure 入口網站
  • 在 Azure 入口網站 左方,點選 [所有服務],當 所有服務 的彈出子畫面出現後,在該子畫面最上方輸入 hub ,將會得到與 hub 有關的 Azure 產品。
    此時,請選擇 [Notification Hubs] 這個項目
    Azure 所有服務
  • 現在將會顯示 [Notification Hubs] 頁面,請點選上方工具列的 [新增] 按鈕,準備新增一個 Notification Hubs 服務
    Azure Notification Hubs
  • 當顯示新 刀鋒 視窗,分別在底下欄位填入適當的欄位值
    • Notification Hub
      這裡需要填入這個通知中樞服務的名稱,請輸入 azure-notification-hub-lab
    • Create a new namespace
      在這裡填入一個通知中樞的命名空間,由於在這裡僅是做練習,因此請在此輸入 azure-notification-hub-lab ,建立一個新的通知中樞命名空間
    • 位置
      在這裡選擇 東南亞
    • Resource Group
      由於在這裡僅是做練習,在這裡點選 [新建] 連結,當顯示文字 資源群組是能夠存放 Azure 解決方案相關資源的容器。 的彈出視窗,在該彈出視窗的 名稱 欄位內,填入 azure-notification-hub-lab,完成後,點選 [確定] 按鈕。
    • 訂用帳戶
      依據登入帳號可以使用訂用帳戶,選擇合適的帳戶項目
    • Pricing tier
      由於在這裡僅是做練習,在這裡就僅選擇 Free
把這個 通知中樞服務的名稱 字串記錄下來
通知中樞服務的名稱 等下在 Xamarin.Android 專案內會用到,因此,請將這個通知中樞服務的名稱 字串記錄下來
  • 最後,在該 刀鋒 視窗下方,點選 [建立] 按鈕
    新建一個通知中樞
  • 當通知中樞建立完成之後,可以點選 [通知] 鈴鐺圖示,然後選取 [前往資源] 按鈕,或重新整理 [通知中樞] 頁面中的清單,然後選取您的通知中樞
    通知中樞完成的提示訊息
  • 當進入到剛剛建立的 [azure-notification-hub-lab] 通知中樞項目頁面,在左方的功能表清單中,選取 [Access Policies]。
    通知中樞 - Access Policies
把這個 DefaultListenSharedAccessSignature 字串記錄下來
這裡存在兩個連接字串,請記下 [DefaultListenSharedAccessSignature] 這個連結字串,因為,等下在 Xamarin.Android 專案中會使用到

設定通知中樞的 GCM 設定

  • 在剛剛的 [azure-notification-hub-lab] 通知中樞項目頁面,從左方功能表清單內,點選 [Google (GCM/FCM)] 這個項目
  • 找出剛剛在 Firebase 專案內的 伺服器金鑰 ,將這個 伺服器金鑰 值填入到 [API Key] 欄位內,然後點選 [Save] 按鈕,儲存這個設定
    通知中樞 - Google (GCM/FCM) - API Key

建立 Xamarin.Android 應用程式,並將其連線至通知中樞

建立 Xamarin.Forms 專案

請依照底下說明步驟,建立一個使用 Prism 開發框架的 Xamarin.Forms 練習專案
  • 啟動 Visual Studio 2017 應用程式
  • 點選功能表 [檔案] > [新增] > [專案]
  • 在 [新增專案] 對話窗左方,點選 [Prism] 項目
  • 在 [新增專案] 對話窗中間,點選 [Prism Blank App (Xamarin.Forms)],
  • 在 [新增專案] 對話窗下方的 [名稱] 欄位,輸入 XFAzureNHub
  • 點選 [新增專案] 對話窗右下方的 [確定] 按鈕
  • 當 [PRISM PROJECT WIZARD] 對話窗出現之後,請確定有勾選 [ANDROID] 與 [iOS] 這兩個項目,而 [UWP] 這個項目則不要勾選
  • 確定 [Container] 下拉選單欄位,選擇的是 [Unity]
  • 最後,點選 [CREATE PROJECT] 按鈕
  • 現在,請等候 Xamarin.Forms 專案建立完成

套用 Firebase 設定到 Xamarin.Android 專案

  • 展開 [XFAzureNHub.Android] 專案,使用滑鼠雙擊 [Properties] 節點
  • 點選 [Android 資訊清單] 標籤頁次,將剛剛在 Firebase 專案中設定的套件名稱,也就是 com.vulcan.AzureNHub ,填入到 [套件名稱] 欄位內。
    Xamarin.Android 的套件名稱修正
需要設定與 Firebase 專案上相同的 套件名稱
若 Xamarin.Android 專案屬性中的套件名稱與 Firebase 專案內的套件名稱兩者不一致,將會造成這個 Xamarin.Android 專案無法正常接收到來自遠端的推播通知。

安裝相關 NuGet 套件

  • 滑鼠右擊 [XFAzureNHub.Android] 專案的 [參考] 節點,選取 [管理 NuGet 套件] 選項
  • 切換到 [瀏覽] 標籤頁次
  • 搜尋 [Xamarin.GooglePlayServices.Base] 套件,安裝到 [XFAzureNHub.Android] 專案
    安裝 Xamarin.GooglePlayServices.Base 套件
  • 搜尋 [Xamarin.Firebase.Messaging] 套件,安裝到 [XFAzureNHub.Android] 專案
    安裝 Xamarin.Firebase.Messaging 套件
  • 搜尋 [Xamarin.Azure.NotificationHubs.Android] 套件,安裝到 [XFAzureNHub.Android] 專案
    安裝 Xamarin.Azure.NotificationHubs.Android 套件

新增 Google Services JSON 檔案

接下來要來處理一個 Firebase 很重要的檔案,也就是剛剛從 Google Firebase 主控台下載下來的 google-services.json 檔案
  • 找到剛剛下載下來的 google-services.json,把它拖拉到方案總管的 [XFAzureNHub.Android] 專案內
  • 點選 google-services.json 檔案,並且在 [屬性] 窗格中,將 [建置動作] 設定為 GoogleServicesJson。
    設定 google-services.json 建置動作為 GoogleServicesJson
看不到 GoogleServicesJson 選項之處置做法
如果您未看到 GoogleServicesJson 選項,請關閉 Visual Studio 再加以重新啟動,並重新開啟專案,然後重複上述步驟,就會看到了 GoogleServicesJson 選項。

開始設計 Firebase 推播通知的相關程式碼

  • 展開 [XFAzureNHub.Android] 專案內的 [Properties] 節點,打開 [AndroidManifest.xml] 檔案
    設定 google-services.json 建置動作為 GoogleServicesJson
  • 將下列 <receiver> 元素插入 <application> 元素中
 
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
    <action android:name="com.google.android.c2dm.intent.RECEIVE" />
    <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
    <category android:name="${applicationId}" />
    </intent-filter>
</receiver>
  • 底下是修正後的 [AndroidManifest.xml] 檔案內容
 
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.vulcan.AzureNHub" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
    <uses-permission android:name="android.permission.INTERNET" />
    <application android:label="XFAzureNHub.Android" android:icon="@mipmap/ic_launcher">
    <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
    <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
      <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="${applicationId}" />
      </intent-filter>
    </receiver>
  </application>
</manifes>

建立 Constants.cs 類別

  • 在 [XFAzureNHub.Android] 專案內,建立一個 Constants.cs 類別,並定義類別中的下列常數值。
    其中,[ListenConnectionString] 這個常數,需要到 Azure 通知中樞中,找到 [DefaultListenSharedAccessSignature] 這個欄位值來填入進來,這個欄位值可以從 [azure-notification-hub-lab] 這個通知中樞設定頁面,在左方功能清單的最下方,找到 [Manage] > [Access Policies] 連結,點擊之後,就會在右方視窗中看到 [DefaultListenSharedAccessSignature] 欄位。
    在 Azure Notification Hub 的 DefaultListenSharedAccessSignature 值
    另外一個 [NotificationHubName] 常數,就是這個 Azure 通知中樞的名稱,也就是 [azure-notification-hub-lab]
    在 Azure Notification Hub 的該中樞的名稱
 Constants.cs
public static class Constants
{
    public const string ListenConnectionString = "請在這裡填入 Azure Notification Hub 的 DefaultListenSharedAccessSignature 值";
    public const string NotificationHubName = "這裡填入 Azure Notificaion Hub 的名稱";
    public const string ChannelName = "azure-notification-hub-lab";
    public const string CHANNEL_ID = "location_notification";
    public const string NotificationTitle = "Azure 通知中樞訊息";
}

修正 MainActivity 類別

  • 打開 [MainActivity.cs] 檔案,在最前面加入底下的命名空間參考
 MainActivity.cs
using Android.Util;
在這個 [MainActivity] 類別,加入底下的常數欄位宣告
 MainActivity.cs
public const string TAG = "MainActivity";
  • 將下列程式碼新增到 base.OnCreate(savedInstanceState) 之後的 OnCreate
 MainActivity.cs
if (Intent.Extras != null)
{
    foreach (var key in Intent.Extras.KeySet())
    {
        if(key!=null)
        {
            var value = Intent.Extras.GetString(key);
            Log.Debug(TAG, "Key: {0} Value: {1}", key, value);
        }
    }
}

建立 MyFirebaseIIDService 類別

  • 在 [XFAzureNHub.Android] 專案內,建立一個 MyFirebaseIIDService.cs 類別
  • 使用底下程式碼替換掉檔案內容
 MyFirebaseIIDService.cs
using System.Collections.Generic;

using Android.App;
using Android.Content;
using Android.Util;
using WindowsAzure.Messaging;
using Firebase.Iid;

namespace XFAzureNHub.Droid
{
    [Service]
    [IntentFilter(new[] { "com.google.firebase.INSTANCE_ID_EVENT" })]
    public class MyFirebaseIIDService : FirebaseInstanceIdService
    {
        const string TAG = "MyFirebaseIIDService";
        NotificationHub hub;

        public override void OnTokenRefresh()
        {
            var refreshedToken = FirebaseInstanceId.Instance.Token;
            Log.Debug(TAG, "FCM token: " + refreshedToken);
            SendRegistrationToServer(refreshedToken);
        }

        void SendRegistrationToServer(string token)
        {
            // Register with Notification Hubs
            hub = new NotificationHub(Constants.NotificationHubName,
                                        Constants.ListenConnectionString, this);

            var tags = new List<string>() { };
            var regID = hub.Register(token, tags.ToArray()).RegistrationId;

            Log.Debug(TAG, $"Successful registration of ID {regID}");
        }
    }
}

建立 MyFirebaseMessagingService 類別

  • 在 [XFAzureNHub.Android] 專案內,建立一個 MyFirebaseMessagingService.cs 類別
  • 使用底下程式碼替換掉檔案內容
 MyFirebaseMessagingService.cs
using System;
using System.Linq;

using Android.App;
using Android.Content;
using Android.Media;
using Android.OS;
using Android.Support.V4.App;
using Android.Util;
using Firebase.Messaging;

namespace XFAzureNHub.Droid
{
    [Service]
    [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
    public class MyFirebaseMessagingService : FirebaseMessagingService
    {
        const string TAG = "MyFirebaseMsgService";
        public override void OnMessageReceived(RemoteMessage message)
        {
            Log.Debug(TAG, "From: " + message.From);
            if (message.GetNotification() != null)
            {
                //These is how most messages will be received
                Log.Debug(TAG, "Notification Message Body: " + message.GetNotification().Body);
                createNotification(Constants.NotificationTitle, message.GetNotification().Body);
            }
            else
            {
                //Only used for debugging payloads sent from the Azure portal
                createNotification(Constants.NotificationTitle, message.Data.Values.First());
            }
        }

        void createNotification(string title, string desc)
        {
            var intent = new Intent(this, typeof(MainActivity));
            intent.AddFlags(ActivityFlags.ClearTop);
            var pendingIntent = PendingIntent.GetActivity(this, RandomGenerator(), intent, PendingIntentFlags.OneShot);

            NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .SetSmallIcon(Resource.Drawable.ic_launcher)
                .SetContentTitle(title)
                .SetContentText(desc)
                .SetSound(RingtoneManager.GetDefaultUri(RingtoneType.Notification))
                .SetAutoCancel(true)
                .SetContentIntent(pendingIntent);

            NotificationManager notificationManager;
            notificationManager = (NotificationManager)GetSystemService(Context.NotificationService);

            if (Build.VERSION.SdkInt < BuildVersionCodes.O)
            {
                notificationManager.Notify(RandomGenerator(), notificationBuilder.Build());
            }
            else
            {
                notificationBuilder.SetChannelId(Constants.CHANNEL_ID);
                var name = Constants.ChannelName;
                var description = desc;
                var channel = new NotificationChannel(Constants.CHANNEL_ID, name, NotificationImportance.Default)
                {
                    Description = description, 
                };

                notificationManager.CreateNotificationChannel(channel);
                notificationManager.Notify(RandomGenerator(), notificationBuilder.Build());
            }
        }
        private int RandomGenerator()
        {
            return new Random().Next(int.MinValue, int.MaxValue);
        }
    }
}

開始進行測試

  • 滑鼠右擊專案 [XFAzureNHub.Android],點選 [設定為起始專案] 選項
  • 在 Azure 通知中樞 (azure-notification-hub-lab) 的網頁上,點選 [Test Send] 按鈕
    Azure 通知中樞 (azure-notification-hub-lab)
  • 切換 [Platform] 欄位值為 [Android]
  • 點選下方的 [Send] 按鈕
    準備進行 Android 平台的通知測試
  • 當最下方的 [Result] 列表出現新的紀錄,則表示剛剛的通知推播已經傳送到遠端裝置上了
    推播通知傳送結果
  • 從模擬器上就可以看到這個遠端推播訊息通知
    接收到推播通知
選擇模擬器或者實體裝置
當要執行這個練習專案,並且可以接收到來自於 Azure 通知中樞的遠端推播內容,需要在目的模擬器或者實體裝置上有安裝 Google Play Service 這個軟體,否則,是無法接收到來自於遠端推播通知訊息。
因此,在 Android Device Manager 對話窗中,請針對要進行除錯的模擬器,使用滑鼠右擊該裝置項目,再彈出對話窗中,選擇 [編輯] 選項
Android Device Manager
當該模擬器裝置的屬性對話窗出現之後,在其裝置屬性對話窗的左下方,將會看到有個 [Google Play Store] 選項,請勾選它;若無法勾選這個選項,請將 處理器 這個下拉選單,選擇 x86 這個項目即可。
Android Device Manager




2019/05/27

使用 PushSharp 來推送 Push Notification 到 Xamarin.Forms 開發的 Android 或者 iOS App 上

使用 PushSharp 來推送 Push Notification 到 Xamarin.Forms 開發的 Android 或者 iOS App 上

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式


最近在幫客戶處理 Xamarin.Forms 推播 Push Notification 需求的時候,產生了一個問題,那就是原先的軟體都可以正常收到來自 Apple APNS 的推播訊息,突然間,客戶向我反映,現在正在開發的測試版本 App,突然間收不到來自後台伺服器推送出來的推播訊息,詢問我是不是我這裡程式做了甚麼修正,導致這樣的問題。由於整個系統規劃架構是,當 App 第一次啟動的時候,從來自遠端 PNS 取得了該裝置的 Token,會於使用者登入系統的時候,將此 Token 傳送到遠端 Web API 服務,接著,後端的 Web API 服務,則會將此 Token 再向 Azure Notification Hub 進行註冊;而當有狀況需要推送到 App 的時候,後端 API 服務需要呼叫 Azure Notification Hub 的 SDK,請求 Azure Notification Hub 幫忙向 Apple APNS 推送出這個訊息的推播內容。
因此,首先就透過 Azure Notification Hub 的測試推播功能,發送出一個該 App 使用者可以接收的推播訊息,可是,該 App 還是沒有收到任何推播內容,最後,是在客戶的幫忙,將 App 取得的推播 Token,使用他寫的程式,直接向 Apple APNS 發送推播通知請求,結果,我開發的 App 是可以接收到這個推播通知,所以,可以證明 Xamarin.Forms App 對於來自遠端 Apple APNS 推播內容,是可以正常運作的。
經過這次的經驗,我覺得我這裡還是需要有個這樣的直接對於 Google / Apple 的 PNS 系統,直接送出推播通知請求,以便日後方便針對這樣的問題進行除錯。
要完成這樣的需求,首先需要安裝 PushSharp 的 NuGet 套件,這裡將會建立一個 Console App,完成底下的測試推播程式碼(在該篇文章的最後面)。
在這裡的程式碼將會由 PushSharp 所提供的範例說明程式碼,整理出可以對於 Android / iOS App 推送出 Push Notification 的程式碼,因此,想要對 iOS App 推送出一個推播內容,就可以呼叫 PushiOS 這個方法,而需要將該 App 取得的推播 Token,傳送進去,若想要對 Android App 推送出一個推播內容,就可以呼叫 PushAndroid 這個方法,當然,這裡也需要將該 App 取得的推播 Firebase 的 Token,傳送進去。

針對 iOS 裝置進行訊息推播

在 PushiOS 方法中,這個敘述 string sendMessage = File.ReadAllText("iOS.json", System.Text.Encoding.UTF8); 將會從這個專案目錄下,讀取出要發送出去的推播內容,這裡是一個 JSON 格式內容,如下所示:
C Sharp / C#
{
  "aps": {
    "alert": {
      "title": "推播主題",
      "body": "Notification Hub test notification"
    },
    "sound": "default"
  }
}
透過 apnsBroker.QueueNotification(new ApnsNotification{DeviceToken = deviceid, Payload = JObject.Parse(sendMessage)}); 方法呼叫,可以將指定 App 推播 Token ,也就是 deviceid 這個參數,與上述指定的推播內容,傳送到 Apple APNS ( Apple Push Notification Service ) 上,進而 iOS App 就會收到這個推播通知。
在建立可以用於 Apple APNS 推播的物件時候,會需要使用到 var config = new ApnsConfiguration(ApnsConfiguration.ApnsServerEnvironment.Sandbox, Constants.APNS_P12FILENAME, Constants.PUSH_CERT_PWD); 敘述,在這裡的第一個參數使用了 ApnsConfiguration.ApnsServerEnvironment.Sandbox ,表示使用的是開發模式下的推播,因此,需要從 Apple Developer 蘋果開發人員網頁中,點選設定的 App ID,下載出該推播憑證檔案,轉換成為 .p12 憑證格式;在這裡是將這個憑證檔案放入測試專案內,指定 輸出到目錄屬性為一律複製,因此,第二個引述將會指向這個檔案所在的路徑,而第三個引數則是要使用這個憑證的時候,需要使用到的密碼。
在這個測試專案中,對於 Android.json (Android 推播內容), iOS.json (iOS 推播內容) 與 com.vulcan.azurehub.p12 (iOS 推播憑證) 都已經放置到該專案內。
想要進行 iOS 的推播測試,需要使用實體裝置,例如,這裡使用的一台 iPhone 手機 (不像 Android 平台,可以使用模擬器來進行測試,只要該模擬器上有安裝 Google Play 軟體即可),底下為進行 iOS 裝置下的推送測試結果,這個範例程式是可以送出讓 iOS App 上收到該推播通知。
在底下,測試了當送出推播訊息之後,App 正好在前景狀態與在背景狀態情境下,所看到收到推播訊息的畫面。

針對 Android 裝置進行訊息推播

在 PushAndroid 方法中,這個敘述 string sendMessage = File.ReadAllText("Android.json", System.Text.Encoding.UTF8); 將會從這個專案目錄下,讀取出要發送出去的推播內容,這裡是一個 JSON 格式內容,如下所示:
C Sharp / C#
{
  "data": {
    "title": "推播主題",
    "message": "Notification Hub test notification"
  }
}
透過 gcmBroker.QueueNotification(new GcmNotification{ RegistrationIds = new List<string> { regId }, Data = JObject.Parse(sendMessage) }); 方法呼叫,可以將指定 App 推播 Token ,也就是 regId 這個參數,與上述指定的推播內容,傳送到 FCM ( Firebase Cloud Messaging ) 上,進而 Google App 就會收到這個推播通知。
在建立可以用於 Apple APNS 推播的物件時候,會需要使用到 var config = new GcmConfiguration(Constants.GCM_SENDER_ID, Constants.AUTH_TOKEN, null); 敘述,第一個引數,GCM_SEND_ID 與第二個引數, AUTH_TOKEN 可以從 Firebase Console 網站中取得。
底下為進行 Android 裝置下的推送測試結果,這個範例程式是可以送出讓 Android App 上收到該推播通知。

發送 iOS / Android 推播的測試程式原始碼

C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        // 進行 iOS 裝置的推播
        PushiOS("ad1942af16e2d85d40fbc7e186555eb276ce67a8112fe5fbe0b6b33a1d404a3f");
        // 進行 Android 裝置的推播
        //PushAndroid("eJTHqO53Tbk:APA91bGUWCleYTdY39Ws1JmbtMnJm80RRsWm8sE5zINsd5YP4dDi_lqp1MHnD38Hr-x7UpIdx0VP3TEuOgpQ7W77bE_c0k4G2FGLK73GAiHsdqEJZhyF8rBIjC7hmfBHOoqteE_cWCbC");
    }

    static void PushiOS(string deviceid)
    {
        // Configuration (NOTE: .pfx can also be used here)
        var config = new ApnsConfiguration(ApnsConfiguration.ApnsServerEnvironment.Sandbox,
            Constants.APNS_P12FILENAME, Constants.PUSH_CERT_PWD);

        // Create a new broker
        var apnsBroker = new ApnsServiceBroker(config);

        // Wire up events
        apnsBroker.OnNotificationFailed += (notification, aggregateEx) =>
        {

            aggregateEx.Handle(ex =>
            {

                // See what kind of exception it was to further diagnose
                if (ex is ApnsNotificationException notificationException)
                {

                    // Deal with the failed notification
                    var apnsNotification = notificationException.Notification;
                    var statusCode = notificationException.ErrorStatusCode;

                    Console.WriteLine($"Apple Notification Failed: ID={apnsNotification.Identifier}, Code={statusCode}");

                }
                else
                {
                    // Inner exception might hold more useful information like an ApnsConnectionException            
                    Console.WriteLine($"Apple Notification Failed for some unknown reason : {ex.InnerException}");
                }

                // Mark it as handled
                return true;
            });
        };

        apnsBroker.OnNotificationSucceeded += (notification) =>
        {
            Console.WriteLine("Apple Notification Sent!");
        };

        // Start the broker
        apnsBroker.Start();

        string sendMessage = File.ReadAllText("iOS.json", System.Text.Encoding.UTF8);

        // Queue a notification to send
        apnsBroker.QueueNotification(new ApnsNotification
        {
            DeviceToken = deviceid,
            Payload = JObject.Parse(sendMessage)
        });

        // Stop the broker, wait for it to finish   
        // This isn't done after every message, but after you're
        // done with the broker
        apnsBroker.Stop();
    }

    static void PushAndroid(string regId)
    {
        // Configuration GCM (use this section for GCM)
        var config = new GcmConfiguration(Constants.GCM_SENDER_ID, Constants.AUTH_TOKEN, null);
        var provider = "GCM";

        // Configuration FCM (use this section for FCM)
        // var config = new GcmConfiguration("APIKEY");
        // config.GcmUrl = "https://fcm.googleapis.com/fcm/send";
        // var provider = "FCM";

        // Create a new broker
        var gcmBroker = new GcmServiceBroker(config);

        // Wire up events
        gcmBroker.OnNotificationFailed += (notification, aggregateEx) =>
        {

            aggregateEx.Handle(ex =>
            {

                // See what kind of exception it was to further diagnose
                if (ex is GcmNotificationException notificationException)
                {

                    // Deal with the failed notification
                    var gcmNotification = notificationException.Notification;
                    var description = notificationException.Description;

                    Console.WriteLine($"{provider} Notification Failed: ID={gcmNotification.MessageId}, Desc={description}");
                }
                else if (ex is GcmMulticastResultException multicastException)
                {

                    foreach (var succeededNotification in multicastException.Succeeded)
                    {
                        Console.WriteLine($"{provider} Notification Succeeded: ID={succeededNotification.MessageId}");
                    }

                    foreach (var failedKvp in multicastException.Failed)
                    {
                        var n = failedKvp.Key;
                        var e = failedKvp.Value;

                        Console.WriteLine($"{provider} Notification Failed: ID={n.MessageId}, Desc={e.Message}");
                    }

                }
                else if (ex is DeviceSubscriptionExpiredException expiredException)
                {

                    var oldId = expiredException.OldSubscriptionId;
                    var newId = expiredException.NewSubscriptionId;

                    Console.WriteLine($"Device RegistrationId Expired: {oldId}");

                    if (!string.IsNullOrWhiteSpace(newId))
                    {
                        // If this value isn't null, our subscription changed and we should update our database
                        Console.WriteLine($"Device RegistrationId Changed To: {newId}");
                    }
                }
                else if (ex is RetryAfterException retryException)
                {

                    // If you get rate limited, you should stop sending messages until after the RetryAfterUtc date
                    Console.WriteLine($"{provider} Rate Limited, don't send more until after {retryException.RetryAfterUtc}");
                }
                else
                {
                    Console.WriteLine("{provider} Notification Failed for some unknown reason");
                }

                // Mark it as handled
                return true;
            });
        };

        gcmBroker.OnNotificationSucceeded += (notification) =>
        {
            Console.WriteLine("{provider} Notification Sent!");
        };

        // Start the broker
        gcmBroker.Start();

        string sendMessage = File.ReadAllText("Android.json", System.Text.Encoding.UTF8);

        // Queue a notification to send
        gcmBroker.QueueNotification(new GcmNotification
        {
            RegistrationIds = new List<string> { regId },
            Data = JObject.Parse(sendMessage)
        });

        // Stop the broker, wait for it to finish   
        // This isn't done after every message, but after you're
        // done with the broker
        gcmBroker.Stop();
    }
}

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式




2019/04/26

使用 StackLayout 配合 BindableLayout.ItemTemplateSelector 建立一個動態資料樣板的呈現的效果

使用 StackLayout 配合 BindableLayout.ItemTemplateSelector 建立一個動態資料樣板的呈現的效果

在 Xamarin.Forms 3.5 版本推出的時候,有一個相當好用的功能,那就是 BindableLayout ,有興趣的人可以參考 Xamarin.Forms 3.5: A Little Bindable Love 這篇文章,只要是版面配置繼承於 Layout ,都可以使用 BindableLayout 這個附加屬性 Attached Property,這包括了:AbsoluteLayuot, FlexLayout, Grid, RelativeLayout, StackLayout。
在這篇文章中,將要來練習使用 StackLayout 這個版面配置,但是不會在 StackLayout 版面配置內指定這些仔檢視,而是透過 BindableLayout.ItemsSource 來指定要顯示在 StackLayout 內的子檢視 View 的物件,並且每個子檢視的樣貌都會不相同,會依據當時該物件值來決定要顯示甚麼樣貌,也就是要顯示哪個 DataTemplate,所以,這裡將會使用 BindableLayout.ItemTemplateSelector
該文件的專案原始碼可以透過 GitHub 來取得

建立一個 使用 StackLayout 專案

  • 開啟 Visual Studio 2019 程式
  • 當 Visual Studio 2019 開始 視窗 出現之後,請點選左下角的 [建立新專案] 選項
  • 當 [建立新專案] 對話窗出現之後,請在中間最上方的搜尋文字輸入盒中輸入 [prism] 關鍵字,搜尋所有與 Prism 有關的專案樣板
  • 請選擇 [Prism Blank App (Xamarin.Forms)] 這個專案樣板
  • 當出現 [設定新的專案] 對話窗,請在 [專案名稱] 輸入 [XF3003]
  • 最後點選該對話窗右下方的 [建立] 按鈕
  • 現在將會看到 [PRISM PROJECT WIZARD] 對話窗,請勾選 ANDROID, iOS, UWP 三個行動裝置平台,接著在底下 [Container] 下拉選單,選擇 Unity 項目
  • 最後,點選 [CREATE PROJECT] 按鈕,以便產生 Xamarin.Forms 專案

安裝需要用到的 PropertyChanged.Fody NuGet 套件

  • 當這個 Xamarin.Forms 專案建立成功之後,請在該方案中,找到 Xamarin.Forms 使用的專案(這是一個 .NET Standard 類別庫,簡稱為 SCL ),請在該專案中,使用滑鼠右擊 [相依性] 節點,選擇 [管理 NuGet 套件] 選項
  • 在 [NuGet: XXX] 視窗中,點選 [瀏覽] 標籤頁次,並且在下方的搜尋文字輸入盒中,輸入 [propertychanged.fody] 關鍵字,搜尋出這個 NuGet 套件
  • 當出現 [PropertyChanged.Fody] NuGet 套件,請點選該套件,並且點選右方的 [安裝] 按鈕,將這個套件安裝到 Xamarin.Forms 專案內
  • 請查看 Xamarin.Forms 專案內,並沒有 [FodyWeavers.xml] 這個檔案,因此,使用滑鼠右擊 Xamarin.Forms 專案節點,選擇 [建置] 選項
  • 當建置完成之後,在這個 Xamarin.Forms 專案內將會出現 [FodyWeavers.xml] 檔案

升級 Xamarin.Forms 套件到大於 3.5 板本以上

因為 BindableLayout 將會在 Xamarin.Forms 3.5 版才有支援,因此,現在這個時間點,將會升級到最新的 3.6 版本
  • 使用滑鼠右擊方案節點(方案總管最上方的那個節點),選擇 [管理方案的 NuGet 套件]
  • 點選 [更新] 標籤頁次
  • 勾選該標籤頁次內的所有項目
  • 點選右上方的更新按鈕,就可以升級這些套件到最新版本了

建立資料模型

  • 滑鼠右擊 Xamarin.Forms 專案,選擇 [加入] > [新增資料夾]
  • 將新增資料夾的名稱設定為 [Models]
  • 滑鼠右擊剛剛建立的 [Models] 資料夾,選擇 [加入] > [類別]
  • 在 [新增項目] 對話窗下方的 [名稱] 欄位中,輸入 [ItemBlock]
  • 點選右下方的 [新增] 按鈕
  • 將底下程式碼填入到這個新建立的類別檔案內
C Sharp / C#
namespace XF3003.Models
{
    public enum ItemBlockTypeEnum
    {
        Label,
        BoxView,
        Entry,
    }
    public class ItemBlock
    {
        public ItemBlockTypeEnum ShowViewType { get; set; }
        public string LabelText { get; set; }
        public int CountIndex { get; set; }
    }
}

建立資料樣板選擇器的類別

  • 滑鼠右擊 Xamarin.Forms 專案,選擇 [加入] > [新增資料夾]
  • 將新增資料夾的名稱設定為 [DataTemplateSelectors]
  • 滑鼠右擊剛剛建立的 [DataTemplateSelectors] 資料夾,選擇 [加入] > [類別]
  • 在 [新增項目] 對話窗下方的 [名稱] 欄位中,輸入 [MyItemTemplateSelector]
  • 點選右下方的 [新增] 按鈕
  • 將底下程式碼填入到這個新建立的類別檔案內
這個 MyItemTemplateSelector 類別將需要繼承 DataTemplateSelector 這個類別,並且要覆寫實作出 OnSelectTemplate 這個方法,而這個方法的主要作用在於會接收一個型別為 object 的 item 物件,該物件就是當時要顯示在螢幕上的物件,這個時候,可以根據該物件屬性值,決定當時要顯示出哪個資料樣板 DataTemplate,如此,就可以做出動態顯示不同檢視項目的效果了。
在這個類別類,將會宣告三個 DataTemplate 屬性,這三個 DataTemplate 屬性將會在該頁面的 XAML 中來宣告與指定,因此,在這裡將不會有任何的定義。
C Sharp / C#
public class MyItemTemplateSelector : DataTemplateSelector
{
    public DataTemplate LabelDataTemplate { get; set; }
    public DataTemplate BoxViewDataTemplate { get; set; }
    public DataTemplate EntryDataTemplate { get; set; }
    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        ItemBlock foo = (ItemBlock)item;
        if (foo.ShowViewType == ItemBlockTypeEnum.Label)
        {
            return LabelDataTemplate;
        }
        else if (foo.ShowViewType == ItemBlockTypeEnum.BoxView)
        {
            return BoxViewDataTemplate;
        }
        else
        {
            return EntryDataTemplate;
        }
    }
}

建立使用 BindableLayout.ItemTemplateSelector 的頁面與商業邏輯

  • 在 [Views] 資料夾內,打開 [MainPage.xaml] 檔案
  • 修正使用底下的 XAML 語言宣告
在這裡將會看到在 StackLayout 版面配置內,並沒有指定任何的子檢視,而是透過 BindableLayout.ItemsSource 來指定要顯示的物件,而該物件要顯示那些 XAML 檢視項目,這裡是透過 BindableLayout.ItemTemplateSelector 來決定。
設計技巧就是要使用到 XAML 的 資源 Resource,在此會使用 ContentPage.Resources 來宣告這些資源;在前面提到過的三種資料樣板 LabelDataTemplate BoxViewDataTemplate EntryDataTemplate 將會在這裡進行定義,另外,也需要建立一個 XAML 命名空間 xmlns:LocalDataTemplates="clr-namespace:XF3003.DataTemplateSelectors" ,將會透過該命名空間可以參考到上面所設計的 MyItemTemplateSelector 類別,也會在這裡分別指定該類別中的三個屬性所要參考的物件值。
xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:LocalDataTemplates="clr-namespace:XF3003.DataTemplateSelectors"
             x:Class="XF3003.Views.MainPage"
             Title="StackLayout的ItemTemplateSelector 應用練習">

    <ContentPage.Resources>
        <ResourceDictionary>
            <DataTemplate x:Key="labelTemplate">
                <Grid>
                    <Label Text="{Binding LabelText}"/>
                </Grid>
            </DataTemplate>
            <DataTemplate x:Key="boxViewTemplate">
                <Grid>
                    <BoxView Color="LightPink"/>
                </Grid>
            </DataTemplate>
            <DataTemplate x:Key="entryTemplate">
                <Grid>
                    <Entry Placeholder="{Binding LabelText}"/>
                </Grid>
            </DataTemplate>
            <LocalDataTemplates:MyItemTemplateSelector 
                x:Key="myItemTemplateSelector"
                LabelDataTemplate="{StaticResource labelTemplate}"
                BoxViewDataTemplate="{StaticResource boxViewTemplate}"
                EntryDataTemplate="{StaticResource entryTemplate}"/>
        </ResourceDictionary>
    </ContentPage.Resources>

    <Grid>
        <StackLayout
            Orientation="Vertical"
            BindableLayout.ItemsSource="{Binding myItemList}"
            BindableLayout.ItemTemplateSelector="{StaticResource myItemTemplateSelector}">
        </StackLayout>
    </Grid>
</ContentPage>
  • 在 [ViewModels] 資料夾內,打開 [MainPageViewModel.xaml] 檔案
  • 修正使用底下的 C# 程式碼
C Sharp / C#
namespace XF3003.ViewModels
{
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using Prism.Events;
    using Prism.Navigation;
    using Prism.Services;
    using XF3003.Models;

    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public ObservableCollection<ItemBlock> myItemList { get; set; } = new ObservableCollection<ItemBlock>();
        private readonly INavigationService navigationService;

        public MainPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;

        }

        public void OnNavigatedFrom(INavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(INavigationParameters parameters)
        {
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Label,
                LabelText = "User Account"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Entry,
                LabelText = "Please Enter Account"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Label,
                LabelText = "User Password"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Entry,
                LabelText = "Please Enter Password"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.BoxView,
            });
        }

        public void OnNavigatingTo(INavigationParameters parameters)
        {
        }

    }
}

執行結果