Implementing Azure Notification Hubs – Part 2 (Shared and App Projects)

The complete solution for this can be found on my GitHub account.

We will start work in the shared project as the classes we create here will be required by both the API and the Xamarin application projects. 

DTOs 

In the root directory of the project, create a folder called Dtos which will contain DTO classes that will be used in the API requests. The DTO’s we’ll require are: 

ResponseDeviceDto.cs 

It will contain data about a user’s device, including the RegistrationId on the Notification Hub as well as the PnsToken. This is only used for checking that the devices were registered correctly.

public class ResponseDeviceDto 
{ 
    public string DeviceId { get; set; } 
    public string Platform { get; set; } 
    public string RegistrationId { get; set; } 
    public string PnsToken { get; set; } 

    public ResponseDeviceDto(Guid deviceId, Platform platform, string registrationId, string pnsToken) 
    { 
        DeviceId = deviceId.ToString(); 
        Platform = platform.ToString(); 
        RegistrationId = registrationId; 
        PnsToken = pnsToken; 
    }
} 

It contains a non-default constructor to ensure everything is set correctly before returning the response. 

ResponseUserDto 

This is very similar to the ResponseDeviceDto except that it contains user data instead of device data. It does however contain a list of ResponseDeviceDto’s which are the user’s devices. 

public class ResponseUserDto 
{ 
    public string Username { get; set; } 
    public List<ResponseDeviceDto> Devices { get; set; } = new List<ResponseDeviceDto>(); 

    public ResponseUserDto(string username) 
    { 
        if (string.IsNullOrWhiteSpace(username)) 
        { 
            throw new ArgumentNullException(nameof(username)); 
        } 

        Username = username; 
    } 
} 

SendNotifcationDto 

This is a simple class containing the data the API requires to send a notification to a user. 

public class SendNotificationDto 
{ 
    [Required] 
    public string Username { get; set; } 
    public string Message { get; set; } = string.Empty; 
}

SignInUserDto 

This contains all the data required to create a user and register their device with the Notification Hub. You will notice that all the properties are required. 

public class SignInUserDto 
{ 
    [Required] 
    public string Username { get; set; } 

    [Required] 
    public Guid? DeviceId { get; set; }

    [Required] 
    public Platform? Platform { get; set; } 

    [Required] 
    public string PnsToken { get; set; }    // Called GcmRegistrationId (Android), DeviceToken (iOS) and ChannelUri (UWP) 
} 

SignOutUserDto 

Similar, but simpler than SignInUserDto, this contains the Username and DeviceId from the device a user is signing out from. This is all that is needed by the API. Again, all the properties are required. 

public class SignOutUserDto 
{ 
    [Required] 
    public string Username { get; set; }   

    [Required] 
    public Guid? DeviceId { get; set; } 
} 

Models 

Create another folder in the root directory of the shared project called Models. Within this folder, create a file called Platform.cs which will contain an enumeration for the platform a device is running. 

public enum Platform 
{ 
    Android, 
    iOS, 
    UWP 
} 

Main Xamarin Project 

Replace the XML within MainPage.xml with the following: 

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="LearningNotificationHubs.Xam.MainPage">
    <StackLayout>
        <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0">
            <Label Text="Learning Notification Hubs" HorizontalTextAlignment="Center" TextColor="White" FontSize="36" />
        </Frame>
        <Label x:Name="PlatformLabel" Text="Platform: " />
        <Label x:Name="DeviceIdLabel" Text="DeviceId: " />
        <Label x:Name="PnsTokenLabel" Text="PnsToken: " />
        <Entry x:Name="UsernameEntry" Placeholder="Username" />
        <Button x:Name="SignInButton" Text="Sign In" Clicked="SignInButton_Clicked" />
        <Button x:Name="SignOutButton" Text="Sign Out" Clicked="SignOutButton_Clicked" IsEnabled="false" />
    </StackLayout>
</ContentPage> 

This will create the user-interface for the application and set up the input elements with Name’s so that they can be accessed from the code-behind file. The labels provide us with some information about the device that will be transmitted to the API. This is useful for testing. There is an Entry for providing a username and a Sign In and Sign Out button.  

We won’t be making use of MVVM for this application as it is just for learning and code-behind is simpler for this use-case. The UI will look as follows (on UWP): 

Next, go to the code-behind file (MainPage.xaml.cs). Add properties to the MainPage class for the HttpClient that will be used for making requests to the API, as well as for the DeviceIdPnsToken and DevicePlatform which will be sent.

private HttpClient _httpClient; 

public Guid DeviceId 
{ 
    get 
    { 
        string deviceId = Preferences.Get("deviceId", string.Empty); 
        if (string.IsNullOrWhiteSpace(deviceId)) 
        { 
            deviceId = Guid.NewGuid().ToString(); 
            Preferences.Set("deviceId", deviceId); 
        } 

        return Guid.Parse(deviceId); 
    } 
} 

public string PnsToken 
{ 
    get 
    { 
        return Preferences.Get("pnsToken", string.Empty); 
    } 

    set 
    { 
        Preferences.Set("pnsToken", value); 
        PnsTokenLabel.Text = $"PnsToken: {value}";
    } 
} 

public string DevicePlatform 
{ 
    get 
    { 
        return DeviceInfo.Platform.ToString(); 
    } 
} 

In the constructor, set up the HttpClient and update the labels with values. You also need to set the URL that your API will be available to the HttpClient BaseAddress property. 

public MainPage() 
{ 
    InitializeComponent(); 

    _httpClient = new HttpClient(); 
    _httpClient.BaseAddress = new Uri("http://localhost:5000");     

    PlatformLabel.Text = $"Platform: {DevicePlatform}"; 
    DeviceIdLabel.Text = $"DeviceId: {DeviceId}"; 
    PnsTokenLabel.Text = $"PnsToken: {PnsToken}"; 
} 

Next, implement the SignInButton_Clicked method. This method will get the username from the Entry as well as the DeviceId and PnsToken from the properties. It will then try to get the device platform using Xamarin.Essentials. If everything works successfully, it will make a request to the API to “sign the user in”. Once this is successful, an alert is shown, and the UI is updated. The code to do this is as follows: 

private async void SignInButton_Clicked(object sender, EventArgs e) 
{ 
    string username = UsernameEntry.Text; 

    if (string.IsNullOrWhiteSpace(username)) 
    { 
        await DisplayAlert("Error", "You need to provide a username.", "OK"); 
        return; 
    } 

    SignInUserDto signInUserDto = new SignInUserDto 
    { 
        Username = UsernameEntry.Text, 
        DeviceId = DeviceId,               
        PnsToken = PnsToken 
    }; 

    if (!Enum.TryParse(DevicePlatform, out Platform platform)) 
    { 
        await DisplayAlert("Error", $"{DevicePlatform} is not supported.", "OK"); 
        return; 
    } 

    signInUserDto.Platform = platform; 

    StringContent jsonContent = new StringContent(JsonSerializer.Serialize(signInUserDto), Encoding.UTF8, "application/json"); 

    HttpResponseMessage response = await _httpClient.PostAsync("/auth/signuserin", jsonContent); 

    if (!response.IsSuccessStatusCode) 
    { 
        await DisplayAlert("Error", response.ReasonPhrase, "OK"); 
        return; 
    } 

    UsernameEntry.IsEnabled = false; 
    SignInButton.IsEnabled = false; 
    SignOutButton.IsEnabled = true; 

    await DisplayAlert("Success", $"You were signed in.", "OK"); 
} 

Finally, implement the SignOutButton_Clicked method. This method again, obtains the username and the device id, and after passing validation, makes a request to the API. If the request is successful, an alert is displayed, and the UI is updated. 

private async void SignOutButton_Clicked(object sender, EventArgs e) 
{ 
    string username = UsernameEntry.Text; 

    if (string.IsNullOrWhiteSpace(username)) 
    { 
        await DisplayAlert("Error", "You need to provide a username.", "OK"); 
        return; 
    } 

    SignOutUserDto signOutUserDto = new SignOutUserDto 
    { 
        Username = UsernameEntry.Text, 
        DeviceId = DeviceId, 
    }; 

    StringContent jsonContent = new StringContent(JsonSerializer.Serialize(signOutUserDto), Encoding.UTF8, "application/json"); 
    HttpResponseMessage response = await _httpClient.PostAsync("/auth/signuserout", jsonContent); 

    if (!response.IsSuccessStatusCode) 
    { 
        await DisplayAlert("Error", response.ReasonPhrase, "OK"); 
        return; 
    } 

    UsernameEntry.IsEnabled = true; 
    SignInButton.IsEnabled = true; 
    SignOutButton.IsEnabled = false; 

    await DisplayAlert("Success", $"You were signed out.", "OK"); 
}

Android Project 

You need to install the Xamarin.Firebase.Messaging NuGet package which provides the bindings to work with the Firebase Messaging Service. 

In the root of the Android project, paste the google-services.json file you obtained when setting up the Firebase project. Set the Build Action of the file to GoogleServicesJson. If you do not have this option, you may need to install the Xamarin.GooglePlayServices.Base NuGet package which adds it. 

Next, create a new folder in the root of the project called Services. Inside this folder, create a new file called FirebaseMessagingService.cs which will contain a class called MyFirebaseMessagingService. We shouldn’t use FirebaseMessagingService as our class must inherit from that class and so should have a different name. Ensure the class is annotated with:

[Service] 
[IntentFilter(new[] {"com.google.firebase.MESSAGING_EVENT"})] 

Inside MyFirebaseMessagingService, you need to create a string to hold the notification channel ID. This application will only have one channel so the code can simply be: 

internal static readonly string NOTIFICATION_CHANNEL_ID = "NOTIFICATION_CHANNEL_ID";

Next you need to override OnNewToken. This method receives a new token from the Firebase Messaging Service which must be provided to the MainPage which will pass it on to the API which will provide it to the Notification Hub. Implement it as follows: 

public override void OnNewToken(string newToken) 
{ 
    if (string.IsNullOrWhiteSpace(newToken)) 
    { 
        return; 
    } 

    if (!(Xamarin.Forms.Application.Current?.MainPage is MainPage mainPage)) 
    { 
        return; 
    }      

    mainPage.PnsToken = newToken; 
} 

Next, you need to override OnMessageReceived which will process the message received from the Firebase Messaging Service. Below is a simple implementation that displays the message: 

public override void OnMessageReceived(RemoteMessage remoteMessage) 
{ 
    base.OnMessageReceived(remoteMessage); 

    string messageBody = remoteMessage.GetNotification()?.Body; 

    if (messageBody is null) 
    { 
        messageBody = remoteMessage.Data.Values.FirstOrDefault() ?? string.Empty; 
    } 

    Intent intent = new Intent(this, typeof(MainActivity)); 
    intent.AddFlags(ActivityFlags.ClearTop); 
    intent.PutExtra("message", messageBody); 

    // Unique require code to avoid PendingIntent collision 
    int requestCode = new Random().Next(); 
    PendingIntent pendingIntent = PendingIntent.GetActivity(this, requestCode, intent, PendingIntentFlags.OneShot); 

    NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) 
        .SetContentTitle("Learning Notification Hubs") 
        .SetSmallIcon(Resource.Mipmap.icon) 
        .SetContentText(messageBody) 
        .SetContentIntent(pendingIntent); 

    NotificationManager notificationManager = NotificationManager.FromContext(this); 
    notificationManager.Notify(0, notificationBuilder.Build()); 
} 

If you get the Java.Lang.NoClassDefFoundError: ‘Failed resolution of: Lcom/google/android/datatransport/runtime/dagger/internal/Factory;’ exception when running the application, install the Xamarin.Google.Dagger nuget package – https://github.com/xamarin/GooglePlayServicesComponents/issues/410 

iOS Project 

In the iOS project, open the Entitlements.plist file. Look for Push Notifications in the list and enable it. 

Next, open Info.plist and ensure that the Bundle Identifier exactly matches that set in your provisioning profile. If it does not, the push notifications will not work. 

Create a new folder in the root of the project called Extensions. Within this folder, create an internal static class called NSDataExtension. Within this class you need to implement an extension method that will convert the data within an NSData class into a Hex string that is required for the Notification Hub.  

The implementation looks like: 

internal static class NSDataExtensions 
{ 
    internal static string ToHexString(this NSData data) 
    { 
        byte[] bytes = data.ToArray(); 

        if (bytes == null) 
        { 
            return null; 
        } 

        StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2); 

        foreach (byte b in bytes) 
        { 
            stringBuilder.AppendFormat("{0:x2}", b); 
        } 

        return stringBuilder 
            .ToString() 
            .ToUpperInvariant(); 
    }
}

I obtained the code above from: https://docs.microsoft.com/en-us/azure/developer/mobile-apps/notification-hubs-backend-service-xamarin-forms#configure-the-native-ios-project-for-push-notifications 

Next, open AppDelegate.cs. After the LoadApplication() method call, but before the return statement in FinishedLaunching(), add the following code that will register the device to receive push notifications. It contains various if blocks to cater for different iOS versions.

if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0)) 
{ 
    UNUserNotificationCenter.Current.RequestAuthorization(UNAuthorizationOptions.Alert | UNAuthorizationOptions.Sound, (granted, error) => 
    { 
        if (granted) 
        { 
            InvokeOnMainThread(UIApplication.SharedApplication.RegisterForRemoteNotifications); 
        } 
    }); 
} 
else if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0)) 
{ 
    UIUserNotificationSettings notificationSettings = UIUserNotificationSettings.GetSettingsForTypes(UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound, null); 
    UIApplication.SharedApplication.RegisterUserNotificationSettings(notificationSettings); 
    UIApplication.SharedApplication.RegisterForRemoteNotifications(); 
} 
else 
{ 
    UIRemoteNotificationType notificationTypes = UIRemoteNotificationType.Alert | UIRemoteNotificationType.Badge | UIRemoteNotificationType.Sound; 
    UIApplication.SharedApplication.RegisterForRemoteNotificationTypes(notificationTypes); 
} 

Next, create an override for RegisteredForRemoteNotifications that uses the ToHexString extension method to supply the deviceToken to the MainPage. 

public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) 
{ 
    string deviceTokenHexString = deviceToken.ToHexString(); 

    if (string.IsNullOrWhiteSpace(deviceTokenHexString)) 
    { 
        return; 
    } 

    if (!(Xamarin.Forms.Application.Current?.MainPage is MainPage mainPage)) 
    { 
        return; 
    } 

    mainPage.PnsToken = deviceToken.ToString(NSStringEncoding.UTF8); 
} 

Finally, override ReceivedRemoteNotification which is the method that will process the notification received from the Apple Push Notification System. 

public override void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo) 
{ 
    if (userInfo is null || !userInfo.ContainsKey(new NSString("aps"))) 
    { 
        return; 
    } 

    NSDictionary aps = userInfo.ObjectForKey(new NSString("aps")) as NSDictionary; 

    string payload = string.Empty; 

    NSString payloadKey = new NSString("alert"); 

    if (aps.ContainsKey(payloadKey)) 
    { 
        payload = aps[payloadKey].ToString(); 
    } 

    if (!string.IsNullOrWhiteSpace(payload)) 
    { 
        UILocalNotification notification = new UILocalNotification(); 
        notification.AlertAction = "Learning Notification Hubs"; 
        notification.AlertBody = payload; 
        UIApplication.SharedApplication.ScheduleLocalNotification(notification); 
    }                            
} 

UWP Project 

Ensure your UWP application is registered with the Microsoft Store and enabled for push notifications. You will also need to associate the application with the Store in Visual Studio. This can be done by right-clicking on the project in the Solution Explorer, going to Publish and then on to Associate App with the Store…

Next, open App.xaml.cs. Create a new private method inside the App class called InitNotificationsAsync. This method will obtain the push notification channel from the Windows Notification Service and then provide it to the MainPage. 

private async void InitNotificationsAsync() 
{ 
    PushNotificationChannel channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync(); 

    if (channel.Uri == null) 
    { 
        return; 
    } 

    if (!(Xamarin.Forms.Application.Current?.MainPage is Xam.MainPage mainPage)) 
    { 
        return; 
    } 

    mainPage.PnsToken = channel.Uri; 
} 

Inside OnLaunched(), right at the beginning of the method, add a new method call to InitNotificationsAsync()

protected override void OnLaunched(LaunchActivatedEventArgs e) 
{ 
    InitNotificationsAsync(); 
// …rest of method 

All done 

That’s the application side of things set up. The next post will cover setting up the API and testing. 

1 comment

  1. Morne,

    I wanted to thank you sooo much for sharing your code for getting messaging functioning from apps with Notification Hubs. The official MS docs on this are trash, but you got it working and generously shared this with all. It is an excellent resource and I believe unique on the internet.

    I had to still do a lot of work to get it working, but there’s no way I could have done so without your excellent example. I learned a ton too.

    Be happy to buy you a cup of coffee (or beer) for your efforts. 🙂

    Thanks,

    Joe

Leave a comment

Your email address will not be published. Required fields are marked *