Live Charts Using Azure Cosmos DB, Azure Functions, SignalR And WPF - Part Four

Introduction

This article series is aimed at teaching you how to use Cosmos DB trigger and HttpTrigger in an Azure function to create a serverless app that can broadcast the data to clients using SignalR. The client can be a desktop/web/mobile client. For this article, I will be using a simple desktop client created using WPF with Material Design. To follow along please make sure you have an Azure account. If you don't have it already you can create one for free by visiting Cloud Computing Services | Microsoft Azure.

This article is part 4 of the series and is aimed at teaching the readers how to create an Azure SignalR and send live data to clients using SignalR when there is an update/insert in Cosmos DB. To follow along please make sure to refer to Part 1 and Part 2 and Part 3 of this article series to learn how to create Cosmos DB and Azure functions and publish them on Azure Cloud.

Step 1 - Create a new SignalR on Azure Cloud

Navigate to Azure Portal and go to your Resource Group where you want to add the SignalR (for step-by-step instructions on how to create a Resource group please refer to Part 1 of this article series) and click Create on Top Menu

On the Create Resource window search for SignalR Service

Click Create on the new window that opens

On the Create window choose your Subscription, the Resource group, give a proper resource name, choose free pricing tier, and serverless service mode. Click on the Review+Create button

Click on Create button to create the SignalR resource

Once the SignalR resource is created successfully you will get a page like below. Click on go to resource button

On the resource page click on keys on the left side menu, we will need this update our JSON file in the azure function

We will need the connection string so make a note of it

Step 2: Azure Function changes

Open the Azure function we created in Part 2 of this article series and edit the local.settings.json file to add "AzureSignalRConnectionString"

{
    "IsEncrypted": false,
    "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "CosmosDbConnectionString": "<PRIMARY CONNECTION STRING>"
    "DatabaseEndpoint": "<URI>",
    "AzureSignalRConnectionString":"<SingalR Connection string>"
    "DatabaseAccountKey": "<PRIMARY KEY>"
  }
}

We need to install two Nuget packages for SignalR - Microsoft.AspNetCore.SignalR.Client and Microsoft.Azure.WebJobs.Extensions.SignalRService. The package version should be less than the version of the .NET core your azure function is running on. The application might not behave properly otherwise because of compatibility issues. Since my .NET core version is 3.1 I specifically chose version 3.1.21 for Microsoft.AspNetCore.SignalR.Client although the latest version right now is 6.0.0

Add a new class called Negotiate.cs. Copy-paste the following code to the class. Negotiate class is needed so the client can obtain SignalR service client hub URL and access token that is needed for the client to connect.

using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;

namespace OrderFunction
{
    public static class Negotiate
    {
        [FunctionName("negotiate")]
        public static SignalRConnectionInfo Run(
            [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req,
            [SignalRConnectionInfo(HubName = "ordersdashboard")] SignalRConnectionInfo connectionInfo)
        {
            return connectionInfo;
        }
    }
}

Edit the OrderTriggerFunction class, full code is pasted below. Notice lines 32 to 36. This piece of code sends a signalR message when there is an update/insert in the Cosmos DB. 

Also, notice the change in method signature on line 22. HubName should match the HubName from Negotiate.cs class

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;
using OrderFunction.Services;

namespace OrderFunction
{
    public static class OrderTriggerFunction
    {
        [FunctionName("OrderTriggerFunction")]
        public static async Task RunAsync([CosmosDBTrigger(
            databaseName: "OrderDB",
            collectionName: "Order",
            ConnectionStringSetting = "CosmosDbConnectionString",
            LeaseCollectionName = "leases", CreateLeaseCollectionIfNotExists =true)]IReadOnlyList<Document> input, ILogger log,
            [SignalR(HubName = "ordersdashboard")] IAsyncCollector<SignalRMessage> signalRMessages)
            {
            try
            {
                var viewModel = new DashboardService().GetDashboardViewModel();
                if (input != null && input.Count > 0)
                {
                    log.LogInformation("Documents modified " + input.Count);
                    log.LogInformation("First document Id " + input[0].Id);
                }
                await signalRMessages.AddAsync(new SignalRMessage
                {
                    Target = "target",
                    Arguments = new[] { JsonSerializer.Serialize(viewModel) }
                });
            }
            catch(Exception ex)
            {
                log.LogError(ex.Message);
            }
        }
    }
}

Step 3: Create a Test Client using WPF

Open Visual Studio and create a new WPF Project

Add two Nuget packages  - Microsoft.AspNet.WebApi.Client (version: 5.2.7) and Microsoft.AspNetCore.SignalR.Client (version 6.0.0)

Open MainWindow.xaml.cs and create a new method StartConnection() [Line 53]. We will be using this method to start a connection with the Azure function. This method will be called in the Constructor [Line 32]. We will also define an event to try and restart the connection if it is closed [Line 33]. The last step is to register the "On" handler which will be called when the hub method with a specified method name is called [Line 38]. Replace MainWindow.xaml.cs code with the following code

using LiveChartsDesktopClient.Models;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace LiveChartsDesktopClient
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        HubConnection connection = null;
        public MainWindow()
        {
            try
            {
                InitializeComponent();
                testdata.DataContext = orderDataModel;
                StartConnection();
                connection.Closed += async (error) =>
                {
                    await Task.Delay(new Random().Next(0, 5) * 1000);
                    await connection.StartAsync();
                };
                connection.On<string>("target",
                                    (value) =>
                                    {
                                        Dispatcher.BeginInvoke((Action)(() =>
                                        {
                                           orderDataModel.OrderData = value;
                                            
                                        }));
                                    });
            }
            catch(Exception ex)
            {
               
            }
        }
        public async Task StartConnection()
        {
            connection = new HubConnectionBuilder()
                 .WithUrl("http://localhost:7071/api")
                 .Build();
            await connection.StartAsync();
        }
    }
}

For the purpose of testing, we will create a textblock on MainWindow.xaml

Notice the Binding value in Textblock. We need a binding variable called OrderData.

Create a new class called OrderDataModel. Copy-paste the following code there.

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace LiveChartsDesktopClient
{
    public class OrderDataModel : INotifyPropertyChanged
    {
        private string orderData;

        public string OrderData
        {
            get { return orderData; }
            set
            {
                orderData = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

If you notice class implements interface INotifyPropertyChanged which will ensure if there is a change in OrderData Property the UI will be updated accordingly. If you notice line 31 of MainWindow.xaml.cs we have set the data context of textblock to object of OrderDataModel class and in XAML file Binding for "testdata" textblock is set to the property "OrderData" of the OrderDataModel class. All this configuration ensures data changes are reflected on UI in a timely manner.

In the On handler, we are setting the OrderData to value which is returned by SignalR whenever there is an update/insert in Cosmos DB.

With this, we are ready to execute the test to see if the client can receive data when Cosmos DB is updated.

Step 5: Test Run

Stop the Azure function running on Azure Cloud and run the function in Visual Studio. Start the WPF client. Initially, you should get a blank window 

Open Azure Portal and navigate to Azure Cosmos DB in your resource group. Open Data Explorer and insert new data or update an existing record. Once the DB is updated Azure function will send a SignalR communication and the WPF client will receive the data and textblock should be updated. If everything goes fine you should see a window similar to this. Here we are just getting the raw JSON object and dumping it on the screen

Add or update a couple of more rows to ensure the data is getting updated on the client in real-time.

Now we are able to receive live data from the server without any refresh or manual intervention from the client. Our next step will be to use the data to display and update charts on the client.

Summary and Next Steps

At the end of this tutorial, you should be able to create a new Azure SignalR on Cloud, send live data from Azure function when the Cosmos DB is updated, create a WPF client, and receive live updates from the Azure function, and display raw JSON on screen in a textblock.

After this, we will work on updating our WPF client to display charts using the data we receive. 

Thanks for reading and stay tuned for the next article.

Links for other parts: Part 1, Part 2, Part 3, Part 5, Part 6