通过 Xamarin 中的 Azure 模板进行推送通知






4.89/5 (5投票s)
如何在 Xamarin 中通过 Azure 模板实现推送通知。
我的抱怨
没有什么比文档更让我恼火的了,它们要么过于模糊,要么过于复杂,而你只需要一些直接和实际的例子。
就通过 Azure 向移动设备推送消息而言,它两者兼有。
我需要做的是为我正在为客户编写的应用程序支持推送通知。他们的员工可能使用 Android 或 iOS 设备。我的应用程序是使用 Xamarin 的 C#。
我遇到的所有示例要么不包含我需要的所有信息,要么提供了太多信息,以至于淹没了所有重要细节。
我的一大痛点是那些基本没用的帮助文档。
如果你看一下 NotificationHubClient.GetRegistrationsByChannelAsync 的帮助(https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.notificationhubs.notificationhubclient.getregistrationsbychannelasync?view=azure-dotnet),你会看到它可以在构造函数中接受几个参数。我需要的版本是
GetRegistrationsByChannelAsync (string pnsHandle, int top);
第一个参数我知道是 Azure 提供给我的句柄。没问题,易于理解。下一个参数 TOP 我必须假设它的意思是返回前 N 个注册,然而,该参数的描述是这样的
Top Int32 The location of the registration.
好吧,现在我想,那到底是什么意思?注册的位置?他们的意思是我想从 TOP 开始的所有注册,还是我想为这个 PSN 找到前 TOP 个注册?
抱怨结束……是时候进入正题了。
在我们开始之前
几个假设。
第一个是您已经访问了特定平台站点和 Azure 以设置推送通知。本文档是关于在客户端(目前是 Android)和服务器端(您想在哪里执行,服务、应用程序等等……)实现推送通知代码的。
要设置 Hub 端,请参见此处
https://console.firebase.google.com/
和
其次,您希望使用模板,这样您就可以通过 Azure 使用 C# 进行设备无关的推送,并且客户端正在使用 Xamarin。
如果您使用 C# 和 Xamarin 以外的不同工具,其中一些可能仍然对您有用。
让我们开始吧
推送通知有 3 个主要部分。
Azure(和特定平台站点)
在继续之前,这应该已经设置好了。这是您的服务器和特定平台推送服务(即 Firebase)以及您的设备之间的中介。
在 Visual Studio(我使用的是 2019 Enterprise)中,有一个用于 Azure 的服务器资源管理器,您应该确保已安装它,因为它对于故障排除和测试以确保所有配置正确非常有用。如果您的应用程序没有收到通知,并且您可以通过 Azure 资源管理器发送测试消息,那么您的应用程序很可能出了问题。
当您的设备正确注册后(这将在文档下方的“客户端”部分进一步说明),它应该在“设备注册”选项卡上显示,类似于这样
在这种情况下,平台是 Google (GCM),因为我正在使用 Android 设备进行测试。类型是“Template”,因为我使用模板方法进行了注册。您需要这个,因为这意味着当您发送消息时,它将是设备/平台无关的。如果类型显示“Native”,则意味着您必须发送专门针对该平台类型的消息,并且消息格式也专门针对该平台。
标签是设备特有的,并用作发送通知时的过滤机制。任何带有特定标签(每个值用逗号分隔)的注册设备都将收到该通知。我放在标签中的内容包括用户的唯一 ID、他们可能所属的组(管理、技术等)。据我了解,标签的数量限制为 10 个,但我从未需要超过少数几个,所以我没有验证这一点。
服务器端
服务器端实际上非常简单。每当我第一次尝试弄清楚某件事时,我通常会创建一个简单的测试应用程序并在其中编写代码以证明它有效并确保我考虑了所有方面。一旦完成,我就会创建一个可重用类,以一种几乎可以在任何地方使用的方式实现它。
抱怨时间……
在我见过的所有示例中,他们都倾向于使事情过于复杂。他们使用多个类、单例模式等……这真的会因过多的噪声而使事情变得缓慢。只需给我一个小的干净示例,我就可以将其重构为我自己的模式。更糟糕的是,在我见过的 Azure 推送通知示例中,它们都是“这是基于上一个示例的……”,然后你去看,它也基于上一个示例,如此反复!唉。
无论如何……以下是服务器端发送推送通知所需的所有内容。
首先,您需要在发送通知的项目中安装名为“Microsoft.Azure.NotificationHubs”的 NuGet 包。
请参阅此处:https://azure.microsoft.com/en-us/services/notification-hubs/
然后我们只需要以下代码
public async void SendPushNotification(string p_title, string p_message, int p_assignmentID, string p_tags)
{
Dictionary<string, string> notificationParameters = new Dictionary<string, string>();
Microsoft.Azure.NotificationHubs.NotificationHubClient hub = null;
hub = Microsoft.Azure.NotificationHubs.NotificationHubClient.CreateClientFromConnectionString(“<DefaultFullSharedAccessSignature>”, “<hub name>”);
notificationParameters.Add("Title", p_title);
notificationParameters.Add("Message", p_message);
notificationParameters.Add("AssignmentID", p_assignmentID.ToString());
await hub.SendTemplateNotificationAsync(notificationParameters, p_tags);
}
现在对正在发生的事情进行一些解释……
行:
Microsoft.Azure.NotificationHubs.NotificationHubClient.CreateClientFromConnectionString
创建一个 NotificationHubClient
类的实例,该实例用于发送实际通知。
第一个参数是 DefaultFullSharedAccessSignature
,可以在 Azure 通知中心项目页面的“访问策略”下找到。
第二个参数是您的通知中心的名称。如果您在 Azure 页面上查看通知中心,它将位于顶部,格式为 <namespace>/<hubname>。您只需要 Hub Name 部分。
我不知道为什么他们有一个命名空间,因为我从不需要它。它可能用于更高级的场景。
字典 notificationParameters
只是一个参数字典,您将传递给 Azure,以便它可以使用注册的模板创建消息,并填充您传递的值。
注意:参数名称区分大小写,因此请确保它们与您在客户端注册的名称匹配。
您可以传入任意数量的参数,但我通常只传入 3 个。我只需要一个标题、通知正文以及我的应用程序可能需要执行操作(例如显示特定任务或类似内容)的任何数据。
要实际发送通知,您需要调用该方法
await hub.SendTemplateNotificationAsync(notificationParameters, p_tags);
第一个参数是我们的参数集合。第二个参数是表示“标签表达式”的字符串。目前我只需要一次发送一个标签。但是,阅读文档和示例,表达式似乎是简单的布尔逻辑。
例如:<tag A> || <tag B>
或 <Tag A> && <Tag B>
欲了解更多信息,请参见此处
就是这样。非常简单,非常直接。
客户端
这是大部分工作需要完成的地方。我使用的是 Xamarin Forms,所以这适用于它,但如果您以不同的方式使用 Xamarin,这可能仍然对您有用。
这个例子以 Android 为中心,所以您需要自己处理 iOS 方面的事情,因为我目前还没有做。但是,相同的原则应该适用,我怀疑不会有太多额外的代码。
客户端需要几项才能正常工作。
- 一个我们可以实现用于注册设备的接口
- 该接口在设备特定项目中的实现
- Xamarin Forms 项目中调用实现该接口的类的代码。
- 对于 Android 项目,您需要安装以下 NuGet 包
- Xamarin.Firebase.Messaging
- Xamarin.Firebase.Common
我们将从定义接口开始。
using System;
using System.Collections.Generic;
using System.Text;
namespace MyApp.MultiPlatformInterfaces
{
public interface IRegisterNotifications
{
void RegisterDevice(string p_psnHandle, List<string> p_tags);
void UnRegisterDevice();
}
}
在这个接口中,我有两个方法将在设备特定项目中实现。
RegisterDevice
是最重要的一个。第一个参数是 Azure 将提供给您的客户端应用程序的 PSN 句柄。第二个参数是您希望为该设备注册的标签列表。
标签是设备特定的,因此您可以为不同的设备注册不同的标签,或者您可以为所有设备注册相同的标签。在我的情况下,我希望在用户的所有设备上为相同的用户注册相同的标签,这样当我向该用户发送推送通知时,无论他们当前使用的是什么设备,都没有关系。
现在我们需要在我们的设备特定项目(本例中为 Android)中实现该接口。
using MyApp.MultiPlatformInterfaces;
using System;
using System.Collections.Generic;
using System.Linq;
[assembly: Xamarin.Forms.Dependency(typeof(MyApp.Droid.RegisterNotifications))]
namespace MyApp.Droid
{
public class RegisterNotifications : MultiPlatformInterfaces.IRegisterNotifications
{
public void RegisterDevice(string p_psnHandle, List<string> p_tags)
{
AndroidFireBaseMessagingService.SendRegistrationToServer(p_psnHandle, p_tags);
}
void IRegisterNotifications.UnRegisterDevice()
{
AndroidFireBaseMessagingService.UnRegisterDevice();
}
}
}
这个类使用依赖注入来完成这项任务。
[assembly: Xamarin.Forms.Dependency(typeof(MyApp.Droid.RegisterNotifications))]
这就是 Xamarin Forms 处理需要从 Xamarin Forms 项目调用的平台特定代码的方式。
这个类基本上是一个包装器,它调用真正将设备注册到 Azure 的代码。
下一个类有点复杂,但大部分工作都在这里完成。这需要一些解释,所以这是完整的代码,然后我将对其进行分解。
注意:此代码是新的,可能可以做得更好一些,但它有效,并且应该说明需要做的事情。
using System;
using Android.Util;
using Android.Support.V4.App;
using Android.App;
using System.Collections.Generic;
using Android.Content;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
namespace MyApp.Droid
{
[Service]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
[IntentFilter(new[] { "com.google.firebase.INSTANCE_ID_EVENT" })]
public class AndroidFireBaseMessagingService : Firebase.Messaging.FirebaseMessagingService
{
public override void OnNewToken(string token)
{
UtilityFunctions.SetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TOKEN, token);
UtilityFunctions.DeleteStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TAGS);
}
public override void OnMessageReceived(Firebase.Messaging.RemoteMessage message)
{
if (message.GetNotification() != null)
{
SendNotification(message.GetNotification().Body);
}
else
{
SendNotification(message.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
}
}
private void SendNotification(string p_message)
{
SendNotification(new Dictionary<string, string> { { "Message", p_message } });
}
private async void SendNotification(Dictionary<string, string> p_values)
{
try
{
await Task.Run(() =>
{
Intent intent = null;
PendingIntent pendingIntent = null;
intent = new Intent(this, typeof(MainActivity));
intent.AddFlags(ActivityFlags.ClearTop);
if (p_values.ContainsKey("AssignmentID"))
{
intent.PutExtra("AssignmentID", p_values["AssignmentID"]);
}
pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.OneShot);
var notificationBuilder = new NotificationCompat.Builder(this, MainActivity.CHANNEL_ID);
notificationBuilder.SetContentTitle("Message Title");
notificationBuilder.SetSmallIcon(Resource.Drawable.ic_launcher);
if (p_values.ContainsKey("Title"))
{
notificationBuilder.SetContentTitle(p_values["Title"]);
}
if (p_values.ContainsKey("Message"))
{
notificationBuilder.SetContentText(p_values["Message"]);
}
notificationBuilder.SetAutoCancel(true);
notificationBuilder.SetShowWhen(false);
notificationBuilder.SetContentIntent(pendingIntent);
var notificationManager = NotificationManager.FromContext(this);
notificationManager.Notify(0, notificationBuilder.Build());
});
}
catch (Exception ex)
{
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION, ex.ToString());
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION_DATE, DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff"));
}
}
public static async Task<string> GetRegistrationIdAsync(string p_handle, Microsoft.Azure.NotificationHubs.NotificationHubClient p_hub)
{
Microsoft.Azure.NotificationHubs.CollectionQueryResult<Microsoft.Azure.NotificationHubs.RegistrationDescription> registrations = null;
string psnHandle = "";
string newRegistrationId = null;
if (string.IsNullOrEmpty(p_handle))
{
throw new ArgumentNullException("handle could not be empty or null");
}
psnHandle = p_handle.ToUpper();
registrations = await p_hub.GetRegistrationsByChannelAsync(psnHandle, 100);
foreach (Microsoft.Azure.NotificationHubs.RegistrationDescription registration in registrations)
{
if (newRegistrationId == null)
{
newRegistrationId = registration.RegistrationId;
}
else
{
await p_hub.DeleteRegistrationAsync(registration);
}
}
if (newRegistrationId == null)
{
newRegistrationId = await p_hub.CreateRegistrationIdAsync();
}
return newRegistrationId;
}
public static async void SendRegistrationToServer(string p_token, List<string> p_tags)
{
Microsoft.Azure.NotificationHubs.NotificationHubClient hub;
Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription registration;
Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription registrationResult;
string messageTemplate = "";
string tags = "";
foreach (var item in p_tags)
{
if (tags != "")
{
tags += ",";
}
tags += item;
}
hub = Microsoft.Azure.NotificationHubs.NotificationHubClient.CreateClientFromConnectionString(UtilityFunctions.AZURE_FULL_ENDPOINT, UtilityFunctions.AZURE_NOTIFICATION_HUB_NAME);
messageTemplate = "{\"data\":{\"Title\":\"$(title)\",\"Message\":\"$(message)\", \"AssignmentID\":\"$(assignmentID)\"}}";
registration = new Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription(p_token, messageTemplate);
try
{
registration.RegistrationId = await GetRegistrationIdAsync(p_token, hub);
registration.Tags = new HashSet<string>(p_tags);
registrationResult = await hub.CreateOrUpdateRegistrationAsync(registration);
UtilityFunctions.SetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TAGS, tags);
UtilityFunctions.SetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_REGID, registration.PnsHandle);
}
catch (Exception ex)
{
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION, ex.ToString());
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION_DATE, DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff"));
}
}
public static async void UnRegisterDevice()
{
//TODO: Implment later
}
}
}
此类别定义为服务,并处理来自 Android 的两个意图。
[Service]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
[IntentFilter(new[] { "com.google.firebase.INSTANCE_ID_EVENT" })]
public class AndroidFireBaseMessagingService : Firebase.Messaging.FirebaseMessagingService
当您的应用程序首次运行时,它将调用此方法
public override void OnNewToken(string token)
{
UtilityFunctions.SetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TOKEN, token);
UtilityFunctions.DeleteStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TAGS);
}
此方法被调用并传入一个来自 Firebase 的令牌。我只是将此令牌保存起来以备后用(我有一个处理加载和保存值等实用程序类,这就是 UtilityFunction.SetStoredValue 的用途)。您可以将值存储在任何您想要的位置,或者您也可以直接从这里调用 Register 方法。
我保存它是因为我需要先允许用户在 Android 应用程序中进行身份验证,这样我才能知道他们是谁。在他们身份验证后,我稍后会需要这个令牌。
当您的应用程序收到推送通知时,将调用以下代码。
public override void OnMessageReceived(Firebase.Messaging.RemoteMessage message)
{
if (message.GetNotification() != null)
{
SendNotification(message.GetNotification().Body);
}
else
{
SendNotification(message.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
}
}
这条线的原因
SendNotification(message.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
是因为 message.Data
是一个 IDictionary,我更喜欢使用 Dictionary,所以我用一点 Lambda 将它从一个转换为另一个。
我有两个方法来处理这个问题。第一个只是一个重载方法,它调用执行实际工作的主方法。
private void SendNotification(string p_message)
{
SendNotification(new Dictionary<string, string> { { "Message", p_message } });
}
这是第二个进行实际工作的。与任何移动设备一样,您需要对用户友好,不要锁定您的用户界面,所以我们将其放在任务中运行。
p_values
是服务器发送的参数集合。我不会详细介绍这部分,只说这是打开应用程序 (MainActivity) 并显示实际通知的部分。
private async void SendNotification(Dictionary<string, string> p_values)
{
try
{
await Task.Run(() =>
{
Intent intent = null;
PendingIntent pendingIntent = null;
intent = new Intent(this, typeof(MainActivity));
intent.AddFlags(ActivityFlags.ClearTop);
if (p_values.ContainsKey("AssignmentID"))
{
intent.PutExtra("AssignmentID", p_values["AssignmentID"]);
}
pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.OneShot);
var notificationBuilder = new NotificationCompat.Builder(this, MainActivity.CHANNEL_ID);
notificationBuilder.SetContentTitle("Message Title");
notificationBuilder.SetSmallIcon(Resource.Drawable.ic_launcher);
if (p_values.ContainsKey("Title"))
{
notificationBuilder.SetContentTitle(p_values["Title"]);
}
if (p_values.ContainsKey("Message"))
{
notificationBuilder.SetContentText(p_values["Message"]);
}
notificationBuilder.SetAutoCancel(true);
notificationBuilder.SetShowWhen(false);
notificationBuilder.SetContentIntent(pendingIntent);
var notificationManager = NotificationManager.FromContext(this);
notificationManager.Notify(0, notificationBuilder.Build());
});
}
catch (Exception ex)
{
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION, ex.ToString());
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION_DATE, DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff"));
}
}
现在,我将在我的应用程序中稍后调用的实际方法(用户验证后)是这个
public static async void SendRegistrationToServer(string p_token, List<string> p_tags)
{
Microsoft.Azure.NotificationHubs.NotificationHubClient hub;
Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription registration;
Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription registrationResult;
string messageTemplate = "";
string tags = "";
foreach (var item in p_tags)
{
if (tags != "")
{
tags += ",";
}
tags += item;
}
hub = Microsoft.Azure.NotificationHubs.NotificationHubClient.CreateClientFromConnectionString(UtilityFunctions.AZURE_FULL_ENDPOINT, UtilityFunctions.AZURE_NOTIFICATION_HUB_NAME);
messageTemplate = "{\"data\":{\"Title\":\"$(title)\",\"Message\":\"$(message)\", \"AssignmentID\":\"$(assignmentID)\"}}";
registration = new Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription(p_token, messageTemplate);
try
{
registration.RegistrationId = await GetRegistrationIdAsync(p_token, hub);
registration.Tags = new HashSet<string>(p_tags);
registrationResult = await hub.CreateOrUpdateRegistrationAsync(registration);
UtilityFunctions.SetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TAGS, tags);
UtilityFunctions.SetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_REGID, registration.PnsHandle);
}
catch (Exception ex)
{
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION, ex.ToString());
UtilityFunctions.SetStoredValue(UtilityFunctions.LAST_EXCEPTION_DATE, DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff"));
}
}
重要的代码是这部分。这定义了特定于平台的消息。对于其他设备类型,您将使用 FcmTemplateRegistrationDescription
以外的类,并且您的消息模板外观将是特定于平台的。
messageTemplate = "{\"data\":{\"Title\":\"$(title)\",\"Message\":\"$(message)\", \"AssignmentID\":\"$(assignmentID)\"}}";
registration = new Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription(p_token, messageTemplate);
对于 Android,它是一个 Json 消息。它是一个 <name>/<token> 的字典。Token 只能是 $(<token name>)。如果你输入“这是我的 token $(blah)”,那么“这是我的 token $(blah)”就会显示在设备上,而不是你可能期望的内容。
只要遵循该格式,您就可以在模板中添加任何需要的内容。键区分大小写,请记住这一点。
这一行很重要!
registration.RegistrationId = await GetRegistrationIdAsync(p_token, hub);
这会调用此方法
public static async Task<string> GetRegistrationIdAsync(string p_handle, Microsoft.Azure.NotificationHubs.NotificationHubClient p_hub)
{
Microsoft.Azure.NotificationHubs.CollectionQueryResult<Microsoft.Azure.NotificationHubs.RegistrationDescription> registrations = null;
string psnHandle = "";
string newRegistrationId = null;
if (string.IsNullOrEmpty(p_handle))
{
throw new ArgumentNullException("handle could not be empty or null");
}
psnHandle = p_handle.ToUpper();
registrations = await p_hub.GetRegistrationsByChannelAsync(psnHandle, 100);
foreach (Microsoft.Azure.NotificationHubs.RegistrationDescription registration in registrations)
{
if (newRegistrationId == null)
{
newRegistrationId = registration.RegistrationId;
}
else
{
await p_hub.DeleteRegistrationAsync(registration);
}
}
if (newRegistrationId == null)
{
newRegistrationId = await p_hub.CreateRegistrationIdAsync();
}
return newRegistrationId;
}
此方法会在 Azure 上查找该 PSN 句柄的注册 ID,如果存在,则将其删除并重新创建。如果您的设备注册了多次,您的应用程序可能会收到重复的推送通知。
该方法基于 Premchandra Singh 在此处的帖子
之后,我们设置标签并调用实际将设备注册到 Azure 的方法。
registration.Tags = new HashSet<string>(p_tags);
registrationResult = await hub.CreateOrUpdateRegistrationAsync(registration);
在我的代码中,我没有对 registrationResult
进行任何操作,但如果您需要,可以保留该信息。
一旦完成,我就会将 PnsHandle 保存起来以备后用。
这里缺少的是实际调用 SendRegistrationToServer
的代码。这发生在我的 Xamarin Forms 页面中,在用户验证身份后。我这样做是因为我需要知道他们是谁,这样我才能为该设备上的该用户设置一个特定的标签。
我有一个方法叫做
public static void RegisterDeviceForPushNotifications()
{
MultiPlatformInterfaces.IRegisterNotifications reg = null;
List<string> tagList = new List<string>();
string token = "";
string tags = "";
tags = UtilityFunctions.GetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TAGS, "");
if (tags == "")
{
token = UtilityFunctions.GetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TOKEN, "");
tagList.Add(App.LoggedInDcw.DirectCareWorkerID.ToString());
tagList.Add("DCW");
reg = Xamarin.Forms.DependencyService.Get<MultiPlatformInterfaces.IRegisterNotifications>();
reg.RegisterDevice(token, tagList);
}
}
它调用注册代码的特定平台版本,该版本执行实际注册。
差不多就是这样了。希望我涵盖了足够多的内容来帮助您实现推送通知。