This provides most of the information you'll need to implement Push Notifications through Azure using C# and Xamarin.
My Rant
Nothing annoys me more than documentation that is either super vague or exceedingly complex when you just need something straightforward and realistic in examples.
In the case of pushing messages to mobile devices via Azure, it’s both.
What I needed to do was to support Push Notifications for an Application I am writing for a customer. Their employees might be using Android or an iOS device. My application is C# using Xamarin.
All the examples I ran across either didn’t contain all the information I needed or they provided way too much information that buried all the important details.
A big pet-peeve of mine is help documents that are basically worthless.
If you take a look at the help for NotificationHubClient.GetRegistrationsByChannelAsync (https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.notificationhubs.notificationhubclient.getregistrationsbychannelasync?view=azure-dotnet ) you’ll see that it can take a couple of parameters in the constructor. The version I needed was
GetRegistrationsByChannelAsync (string pnsHandle, int top);
The first Parameter I knew was the handle provided to me by Azure. No problem easy to understand. The next parameter TOP I have to assume means return me the TOP N number of registrations, however, the description for that parameter is this:
Top Int32
The location of the registration.
Ok, now I’m thinking WTF does that mean? The Location of the registration? Do they mean I want all registrations starting at TOP or do I want the first TOP number of registrations for this PSN?
Rant over… Time to get to the meat of this.
Before we begin
A couple of assumptions.
First one is that you have gone to the Platform Specific Sites and Azure to set them up for Push Notifications. This document is about the implementation of the Push Notification code on the Client Side (Android currently) and the Server Side (Where ever you want to do it, Service, App, whatever...)
To setup the Hub Side see here:
https://console.firebase.google.com/
and
https://portal.azure.com/#blade/HubsExtension/BrowseResourceBlade/resourceType/Microsoft.NotificationHubs%2Fnamespaces%2FnotificationHubs
Second you are looking to use the Templates so you can do a device agnostic push through Azure using C# and the client side is using Xamarin.
If you’re using different tools other than C# and Xamarin, some of this might still be useful for you.
Let’s get started
There are 3 major parts to Push Notifications.
Azure (and Platform specific sites)
This should already be set up before you continue. This is the intermediary between your server and the platform specific push services (i.e. Firebase) and your devices.
In Visual Studio (I’m using 2019 Enterprise), there is a Server Explorer for Azure, you should make sure that is installed as it comes in very handy for troubleshooting and testing to make sure everything is configured correctly. If you’re not getting notifications from your app and you can send a test message through the Azure Explorer, then chances are good that your application is at fault.
When your device is registered correctly (which will happen further down the document under the Client section) it should show up on the “Device Registration” tab similar to this:
The platform is Google (GCM) in this case because I’m using an Android device to test with. The type is “Template” because I registered using the Template methods. You want this because it means when you send a message it’s going to be device/platform agnostic. If the Type says “Native” then that means you have to send messages specifically for that platform type with messages formatted specifically for that platform.
The tags are specific to the device and are used as a filtering mechanism when sending notifications. Any registered device with a specific TAG (each value separated by a comma) will receive that notification. Things I put in the tags are unique ID for the user, groups that they may belong to (Management, Tech, etc.…). From my understanding there is a limit of 10 tags, but I’ve never needed more than a handful so I haven’t verified that.
Server Side
The server side is actually pretty trivial. Whenever I’m trying to figure something out for the first time, I usually create a simple test app and write the code in there to prove that it works and make sure I’ve covered all my bases. Once that is done, I then create a reusable class to implement it in way that can be used pretty much anywhere.
Rant Time…
In all the examples I’ve seen they tend to over complicate matters. They use multiple classes, singleton patterns, etc.… Which can really bog things down with excess noise. Just show me a small clean example and I can refactor that into my own patterns. What’s worse is in the case of the Azure Push Notification Examples I’ve seen they are “This is based on a previous example…” which then you go look at and it’s based on previous example as well, rinse and repeat! Ugh.
Anyway…. Here is basically all you need on the server side to send a Push Notification.
First thing you will want to do is install the NuGet Package called “Microsoft.Azure.NotificationHubs” in the project that you are sending the notifications from.
See here: https://azure.microsoft.com/en-us/services/notification-hubs/
Then we just need the following code:
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);
}
Now some explanation of what is going on…
The line:
Microsoft.Azure.NotificationHubs.NotificationHubClient.CreateClientFromConnectionString
Creates an instance of the NotificationHubClient
class that is used to send the actual notification.
The first parameter is the DefaultFullSharedAccessSignature
which can be found on the Azure Notification Hub project page under “Access Policies”.
The second parameter is the name of your Notification Hub. If you’re on the Azure Page for the notification hub, it will be right at the top and it will be <namespace>/<hubname>. You just need the Hub Name portion.
I have no idea why they have a Name Space as I’ve never needed it for anything. It might be used for more advanced scenarios.
The dictionary notificationParameters
is simply a dictionary of the parameters you are going to pass to Azure so it can create a message using the registered template filling in the values you passed.
NOTE: Parameter names are case sensitive, so make sure they match what you registered in the client side of things.
You can pass in any number of parameters you want, but I typically only pass in 3. I just need a Title, the Body of the Notification and any data my application might need to perform an action such as displaying a specific Assignment or what not.
To actually send the notification, you need to call the method
await hub.SendTemplateNotificationAsync(notificationParameters, p_tags);
The first parameter is our collection of parameters. The second parameter is a string representing the “Tag Expression”. At this point I’ve only needed to send a single tag at a time. However, reading the documentation and examples the expression appears to be simple Boolean logic.
Something like: <tag A> || <tag B>
Or <Tag A> && <Tag B>
For more information see here:
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.notificationhubs.notificationhubclient.sendtemplatenotificationasync?view=azure-dotnet#:~:text=String%2CString%3E)-,SendTemplateNotificationAsync(IDictionary%2C%20IEnumerable%3CString,(%22%7C%7C%22).
That’s it. Very simple, very straight forward.
Client Side
This is where the majority of the work has to be done. I am using Xamarin Forms so this applies to that, but if you’re using Xamarin in a different way, this might still be useful for you.
This example is Android centric, so you’ll have to work out the iOS side of things as I haven’t done so at this time. The same principles should apply though and I suspect it won’t be much extra code.
The client side requires several things to work.
- An interface that we can implement for registering the device
- The implementation of that Interface in the Device Specific Projects
- Code in the Xamarin Forms project that calls the class that implements the interface.
- For the Android project you will want to install the following NuGet Packages:
- Xamarin.Firebase.Messaging
- Xamarin.Firebase.Common
We’re going to start by defining the Interface.
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();
}
}
In this interface, I have two methods that I’m going to implement in the Device Specific projects.
The RegisterDevice
is the most important one. The first parameter is the PSN Handle that Azure will provide to your client application. The second parameter is a list of Tags that you want to register this device with.
Tags are device specific so you can register different devices with different tags or you can register all devices with the same tags. In my case I want to register the same tags with the same users on all of their devices so when I send a push notification to that user, it doesn’t matter what device they are currently using.
Now we need to implement the interface in our Device Specific project (Android in this case).
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();
}
}
}
This class uses Dependency Injection to accomplish this task.
[assembly: Xamarin.Forms.Dependency(typeof(MyApp.Droid.RegisterNotifications))]
This is how Xamarin Forms deals with platform specific code that needs to be called from the Xamarin Forms project.
This class is basically a wrapper that calls the real code that does the registration of the device with Azure.
The next class is a bit complicated, but it’s where the majority of the work is done. This is going to take some explaining to do, so here is the whole thing, and then I’ll break it down after that.
NOTE: This code is new and could probably be done a bit better, but it works and should illustrate what needs to be done.
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()
{
}
}
}
This class is defined as a service and handles 2 intents from Android.
[Service]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
[IntentFilter(new[] { "com.google.firebase.INSTANCE_ID_EVENT" })]
public class AndroidFireBaseMessagingService : Firebase.Messaging.FirebaseMessagingService
When your application is first run, it’s going to call this method:
public override void OnNewToken(string token)
{
UtilityFunctions.SetStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TOKEN, token);
UtilityFunctions.DeleteStoredValue(UtilityFunctions.AZURE_NOTIFICATIONS_REGISTRATION_TAGS);
}
This method is called and passes in a Token from Firebase. I am simply saving this token for later use (I have a Utility Class that handles things like loading and saving values, that’s what the UtilityFunction.SetStoredValue is for). You can store the value anywhere you want, or you could simply call the Register method from here.
I save it because I need to allow the user to authenticate themselves in the Android app first so I know who they are. After they authenticate then I’ll need this token later.
The following code is called anytime your application receives a Push Notification.
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));
}
}
The reason for this line:
SendNotification(message.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
is because message.Data
is an IDictionary and I prefer using Dictionary, so I use a bit of Lambda to convert it from one to the other.
I have two methods that then handle this. The first one is just an overloaded method that calls the main method that does the actual work.
private void SendNotification(string p_message)
{
SendNotification(new Dictionary<string, string> { { "Message", p_message } });
}
This is the second one that does the actual work. As with anything Mobile, you need to be User Friendly and not lock your UI up, so we run it in a Task.
p_values
is the collection of Parameters that the Server has sent over. I’m not going to go into this part in detail other to say that it this is the part that opens the application (MainActivity) and displays the actual notification.
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"));
}
}
Now, the actual method that will be called later in my Application (after the user has authenticated themselves) is this:
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"));
}
}
The import bit of code is this. This defines the message that is platform specific. For other Device types you'll use a different class other than FcmTemplateRegistrationDescription
and what your message template looks like will be platform specific.
messageTemplate = "{\"data\":{\"Title\":\"$(title)\",\"Message\":\"$(message)\", \"AssignmentID\":\"$(assignmentID)\"}}";
registration = new Microsoft.Azure.NotificationHubs.FcmTemplateRegistrationDescription(p_token, messageTemplate);
For Android it’s a Json message. It’s a dictionary of <name>/<token>. Token can only be $(<token name>). If you put something like “this is my token $(blah)”, then “this is my token $(blah)” is what will show up on device rather than what you might expect.
You can add what ever you need in template as long as it follows that format. The Keys are case sensitive, so keep that in mind.
This line is important!:
registration.RegistrationId = await GetRegistrationIdAsync(p_token, hub);
This calls this method:
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;
}
This method looks for a Registration ID already on Azure for that PSN Handle and if it exists it deletes them and then recreates it. If you register your device multiple times it’s possible that your application will receive duplicate push notifications.
That method was based on the post from Premchandra Singh here:
https://stackoverflow.com/questions/31912711/azure-notification-hub-tags-not-creating-nor-updating-to-target-specific-user
After that, we then set the tags and call the method that will actually register the device with Azure.
registration.Tags = new HashSet<string>(p_tags);
registrationResult = await hub.CreateOrUpdateRegistrationAsync(registration);
In my code I’m not doing anything with registrationResult
but you can keep that information if you need it.
Once that is done, I save off the PnsHandle for later use if I need it.
The missing piece here is the code that actually calls SendRegistrationToServer
. This happens in my Xamarin Forms page after the user has authenticated themselves. The reason I do this is I need to know who they are so I can set a specific tag for that user on the device.
I have a method that I call:
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);
}
}
That calls the Platform specific version of the Registration code which does the actual registration.
And that's pretty much it. Hopefully I covered enough of this to help you get push notifications working.