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

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 5 of the series and is aimed at teaching the readers how to create a WPF application and display charts that are updated in real-time as the data changes happen in Azure Cosmos DB.  To follow along please make sure to refer to Part 1 and Part 2 and Part 3  and Part 4 of this article series.

Step 1 - Create a new WPF project

Open Visual Studio and search for template WPF. Choose .Net core WPF application and click Next

Give it an appropriate Project name and solution name. Click the Next button

Choose a target framework and click Create 

Step 2 - Add NuGet Packages

Add the following NuGet packages to your project

  1. LiveCharts.Wpf (version 0.9.7)
  2. Microsoft.AspNet.WebApi.Client (version 5.2.7)
  3. Microsoft.AspNetCore.SignalR.Client (version 6.0.0)

Step 3 - Add a user control - CancelledOrders

Right-click on your project -> Add -> User Control (WPF)

Give an appropriate name and click Add

Copy-paste the following code in your CancelledOrders.xaml file. Let us take a look at some of the important pieces of code here

  1. Line 7: we have added the Live chart namespace.
  2. Line 13: we have set the "Series" attribute to bind to Property CancelledOrdersByProvince. We will be creating this property in our view model in the upcoming steps.
  3. Line 15: we have set the X-axis "Labels" attribute to bind to LabelsCancelledOrderByProvince. We will be creating this property in our view model in the upcoming steps.

If you want to learn more about how to configure the Live charts in your WPF application you can visit their official website.

<UserControl x:Class="LiveChartsDesktopApp.CancelledOrders"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:LiveChartsDesktopApp"
             xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf" 
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <GroupBox Header="Cancelled Orders By Province" Margin="5 5 5 5" >

            <lvc:CartesianChart Series="{Binding CancelledOrdersByProvince}" LegendLocation="None" >
                <lvc:CartesianChart.AxisX>
                    <lvc:Axis Title="Province" Labels="{Binding LabelsCancelledOrderByProvince}" FontSize="12"  Foreground="Black">
                        <lvc:Axis.Separator>
                            <lvc:Separator Step="1" />
                        </lvc:Axis.Separator>
                    </lvc:Axis>
                </lvc:CartesianChart.AxisX>
                <lvc:CartesianChart.AxisY>
                    <lvc:Axis Title="Count" LabelFormatter="{Binding Formatter}" FontSize="12" Foreground="Black" >
                        <lvc:Axis.Separator>
                            <lvc:Separator Step="1" />
                        </lvc:Axis.Separator>
                    </lvc:Axis>
                </lvc:CartesianChart.AxisY>
            </lvc:CartesianChart>

        </GroupBox>
    </Grid>
</UserControl>

Step 4 - Add Model and View Model Classes

Add a new folder called Models and add a class called DashboardModel inside the folder

Copy-paste the following code into DashboardModel class. This class will be used to deserialize and map the data received from Azure SignalR.

using System.Collections.Generic;

namespace LiveChartsDesktopApp.Models
{
    public class DashboardModel
    {
        public Dictionary<string, string> CompletedOrdersByProvince { get; set; }

        public Dictionary<string, string> DraftOrdersByProvince { get; set; }

        public Dictionary<string, string> CancelledOrdersByProvince { get; set; }
    }
}

Add another class to Models folder called ChartModel. Copy paste the following code into ChartModel class. This class will be used for mapping purposes to show data in the chart.

namespace LiveChartsDesktopApp.Models
{
    public class ChartModel
    {
        public int Data { get; set; }
        public string Label { get; set; }
    }
}

Add a new folder called ViewModels and add a class to it called OrdersViewModel. This class will be used as Data Context class to display data in real time on our UI. Copy paste the following code into OrdersViewModel class. In this class we have three properties - CancelledOrdersByProvince (this is used to map the numbers on Y-axis), LabelsCancelledOrderByProvince (this is used to map labels on X axis) and Formatter (is used to define how the Y axis label will look like, in our case we need numeric labels). Also notice this class implements INotifyPropertyChanged interface, this is needed to update the data on UI when the property value changes (in our case it will happen when there is an update in Cosmos DB)

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

namespace LiveChartsDesktopApp.ViewModels
{
    public class OrdersViewModel : INotifyPropertyChanged
    {
        private SeriesCollection cancelledOrdersByProvince;
        public SeriesCollection CancelledOrdersByProvince
        {
            get { return cancelledOrdersByProvince; }
            set
            {
                cancelledOrdersByProvince = value;
                OnPropertyChanged();
            }
        }

        private string[] labelsCancelledOrderByProvince;
        public string[] LabelsCancelledOrderByProvince
        {
            get { return labelsCancelledOrderByProvince; }
            set
            {
                labelsCancelledOrderByProvince = value;
                OnPropertyChanged();
            }
        }
        public Func<double, string> Formatter { get; set; }

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

The next thing we need is a service class to connect to SignalR and receive data. Create a new class in the Models folder called OrderService. Copy-paste the following code into OrderService class.

  1. StartConnection - This method is used to start the connection with the Azure function and will also implement the "On" handler to receive the data from the function.
  2. MapToChartModelCancelledByProvince - This method will receive the raw data from the Azure function and map it to the ChartModel object we created
  3. UpdateChartDataCancelledOrderByProvince - To show the chart on UI we need SeriesCollection object and label and formatter. This method will give us that.
  4. GetChartValuesForCancelledOrderByProvince - To display the data we need to update ColumnSeries class, the value for the Value property will be assigned here.
  5. GetDataInitial - This method will call the HTTP Trigger to get the initial data from Cosmos DB.
using LiveCharts;
using LiveCharts.Defaults;
using LiveCharts.Wpf;
using LiveChartsDesktopApp.ViewModels;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Threading;

namespace LiveChartsDesktopApp.Models
{
    public class OrderService
    {
        HubConnection connection = null;
        HttpClient client = new HttpClient();
        DashboardModel dashboardModel = new DashboardModel();
        List<ObservableValue> CancelledOrdersByProvinceList = new List<ObservableValue>();
        OrdersViewModel orderViewModel;
        public List<string> LabelsCancelledOrderByProvinceList { get; set; } = new List<string>();
        public OrderService(OrdersViewModel ordersViewModel)
        {
            orderViewModel = ordersViewModel;
        }
        public async Task StartConnection()
        {
            connection = new HubConnectionBuilder()
                 .WithUrl("http://localhost:7071/api")
                 .Build();
            await connection.StartAsync();

            connection.Closed += async (error) =>
            {
                await Task.Delay(new Random().Next(0, 5) * 1000);
                await connection.StartAsync();
            };
            connection.On<string>("target",
                                (value) =>
                                {
                                    Dispatcher.CurrentDispatcher.BeginInvoke((Action)(() =>
                                    {
                                        dashboardModel = System.Text.Json.JsonSerializer.Deserialize<DashboardModel>(value);
                                        MapToChartModelCancelledByProvince(dashboardModel);
                                    }));
                                });
            _ = GetDataInitial();


        }
        public async Task GetDataInitial()
        {
            try
            {
                client.BaseAddress = new Uri("http://localhost:7071/api/");
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
                var response = await client.GetStringAsync("DashboardLoadFunction");
                dashboardModel = System.Text.Json.JsonSerializer.Deserialize<DashboardModel>(response);
                MapToChartModelCancelledByProvince(dashboardModel);

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }


        }
        private ChartValues<ObservableValue> GetChartValuesForCancelledOrderByProvince(List<ChartModel> chartModels)
        {
            ChartValues<ObservableValue> chartValues = new ChartValues<ObservableValue>();
            CancelledOrdersByProvinceList = new List<ObservableValue>();
            LabelsCancelledOrderByProvinceList = new List<string>();
            int i = 0;
            for (int k = 0; k < chartModels.Count; k++)
            {
                CancelledOrdersByProvinceList.Add(new ObservableValue(0));
            }
            foreach (var cm in chartModels)
            {
                CancelledOrdersByProvinceList[i].Value = cm.Data;
                LabelsCancelledOrderByProvinceList.Add(cm.Label);
                chartValues.Add(CancelledOrdersByProvinceList[i]);
                i++;
            }
            return chartValues;


        }
        private void UpdateChartDataCancelledOrderByProvince(List<ChartModel> chartModels)
        {
            orderViewModel.CancelledOrdersByProvince = new SeriesCollection
            {
                new ColumnSeries
                {
                    Title = "Registration By Province",
                    Values = GetChartValuesForCancelledOrderByProvince(chartModels),

                }
            };
            orderViewModel.LabelsCancelledOrderByProvince = LabelsCancelledOrderByProvinceList.ToArray();
            orderViewModel.Formatter = value => value.ToString("N");
        }
        private void MapToChartModelCancelledByProvince(DashboardModel data)
        {
            List<ChartModel> chartModels = new List<ChartModel>();
            foreach (var d in data.CancelledOrdersByProvince)
            {
                chartModels.Add(new ChartModel { Data = Convert.ToInt32(d.Value), Label = d.Key });
            }
            UpdateChartDataCancelledOrderByProvince(chartModels);
        }
    }
}

Step 5 - Edit Main.xaml and Main.xaml.cs 

Copy-paste the following code into Main.xaml.cs

using LiveChartsDesktopApp.Models;
using LiveChartsDesktopApp.ViewModels;
using System;
using System.Windows;

namespace LiveChartsDesktopApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        OrderService orderService;
        OrdersViewModel orderViewModel;
        public MainWindow()
        {
            try
            {

                InitializeComponent();
                orderViewModel = new OrdersViewModel();
                orderService = new OrderService(orderViewModel);
                orderService.StartConnection();
                this.DataContext = orderViewModel;
            }
            catch (Exception ex)
            {

            }
        }
    }
}

Copy-paste the following code into MainWindow.xaml

<Window x:Class="LiveChartsDesktopApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:LiveChartsDesktopApp"
       
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <local:CancelledOrders></local:CancelledOrders>
    </Grid>
</Window>

Step 6 - Running and Testing the Changes

1) Run Azure function in Visual Studio and then run WPF client app in Visual Studio

2) When WPF main window opens it will display a chart with current records in Cosmos DB

3) Go to Cosmos DB Data explorer and add/edit records. The charts should be updated automatically.

Summary and Next Steps

At the end of this article, you should have a working WPF project with one chart that will display live data from Azure Cosmos DB. You should be able to update/insert records in DB to see the changes in rea-time. In the next article, we will include more charts and also add material design UI.

Thanks for reading and stay tuned for the next article.

For Source code please visit my Github repo

Client App - https://github.com/tanujgyan/LiveChartsDesktopApp

Azure function - https://github.com/tanujgyan/OrderFunction

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