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

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 add material design to 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 1Part 2Part 3Part 4, and Part 5 of this article series.

Step 1 - Add Material Design Nuget Packages

Open WPF project we created in the last article in Visual Studio. Add a MaterialDesignThemes Nuget Package to the project.

Step 2 - Add WPF user control - Draft Orders By Province

Copy-paste the following code into DraftOrdersByProvince.xaml file. We are trying to generate a Pie chart here

<UserControl x:Class="LiveChartsDesktopApp.DraftOrdersByProvince"
             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="Draft Orders By Province" Margin="5 5 5 5" >
            <lvc:PieChart LegendLocation="Bottom"  Hoverable="False" DataTooltip="{x:Null}" Series="{Binding DraftOrdersByProvince}">
            </lvc:PieChart>
        </GroupBox>
    </Grid>
</UserControl>

Step 3 - Edit OrdersViewModel.cs file

Edit OrdersViewModel.cs file to include two new fields - draftOrdersByProvince and labelsDraftOrderByProvince. Replace the OrdersViewModel class with the following code

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();
            }
        }

        private SeriesCollection draftOrdersByProvince;
        public SeriesCollection DraftOrdersByProvince
        {
            get { return draftOrdersByProvince; }
            set
            {
                draftOrdersByProvince = value;
                OnPropertyChanged();
            }
        }

        private string[] labelsDraftOrderByProvince;
        public string[] LabelsDraftOrderByProvince
        {
            get { return labelsDraftOrderByProvince; }
            set
            {
                labelsDraftOrderByProvince = 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));
        }
    }
}

Step 4 - Edit OrderService Class

1) Add two new properties - DraftOrdersByProvinceList and LabelsDraftOrdersByProvinceList

2) Add Three methods - MapToChartModelDraftOrdersByProvince, GetChartValuesDraftOrdersByProvince, UpdateChartDataDraftOrdersByProvince

Replace OrderService class with the following code 

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>();
        List<ObservableValue> DraftOrdersByProvinceList = new List<ObservableValue>();
        public List<string> LabelsDraftOrdersByProvinceList { 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);
                                        MapToChartModelDraftOrdersByProvince(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);
                MapToChartModelDraftOrdersByProvince(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 UpdateChartDataDraftOrdersByProvince()
        {
            orderViewModel.DraftOrdersByProvince = new SeriesCollection();
            int i = 0;
            foreach (var r in DraftOrdersByProvinceList)
            {
                orderViewModel.DraftOrdersByProvince.Add(new PieSeries()
                {
                    Title = LabelsDraftOrdersByProvinceList[i],
                    Values = new ChartValues<ObservableValue> { DraftOrdersByProvinceList[i] },
                    DataLabels = true
                });
                i++;
            }

            orderViewModel.Formatter = value => value.ToString("N");

        }
        private void GetChartValuesDraftOrdersByProvince(List<ChartModel> chartModels)
        {
            DraftOrdersByProvinceList = new List<ObservableValue>();
            LabelsDraftOrdersByProvinceList = new List<string>();
            int i = 0;
            for (int k = 0; k < chartModels.Count; k++)
            {
                DraftOrdersByProvinceList.Add(new ObservableValue(0));
            }
            foreach (var cm in chartModels)
            {
                DraftOrdersByProvinceList[i].Value = cm.Data;
                LabelsDraftOrdersByProvinceList.Add(cm.Label);
                i++;
            }
        }
        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);
        }

        private void MapToChartModelDraftOrdersByProvince(DashboardModel data)
        {
            List<ChartModel> chartModels = new List<ChartModel>();
            foreach (var d in data.DraftOrdersByProvince)
            {
                chartModels.Add(new ChartModel { Data = Convert.ToInt32(d.Value), Label = d.Key });
            }
            GetChartValuesDraftOrdersByProvince(chartModels);
            UpdateChartDataDraftOrdersByProvince();
        }
    }
}

Step 5 - Update Main.xaml and App.xaml to include Material Design Support

Copy paste the following code into Main.xaml file. Line 10 to 17 is added for Material design. Also we have added a header (line 32) and new user control for draft orders (line 38)

<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"
           xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
     TextElement.Foreground="{DynamicResource MaterialDesignBody}"
     TextElement.FontWeight="Regular"
     TextElement.FontSize="13"
     TextOptions.TextFormattingMode="Ideal"
     TextOptions.TextRenderingMode="Auto"
     Background="{DynamicResource MaterialDesignPaper}"
     FontFamily="{DynamicResource MaterialDesignFont}">
    <Grid>

        <ScrollViewer>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="50"></RowDefinition>
                    <RowDefinition Height="300"></RowDefinition>
                    <RowDefinition Height="300"></RowDefinition>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="50*"></ColumnDefinition>

                </Grid.ColumnDefinitions>
                <Grid Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Background="Blue">
                    <TextBlock  Margin= "10 0 0 0" VerticalAlignment="Center" HorizontalAlignment="Left" FontSize="20"  Foreground="White">Real Time Monitoring App</TextBlock>
                </Grid>
                <Grid Grid.Column="0" Grid.Row="1">
                    <local:CancelledOrders></local:CancelledOrders>
                </Grid>
                <Grid Grid.Column="0" Grid.Row="2">
                    <local:DraftOrdersByProvince></local:DraftOrdersByProvince>
                </Grid>
            </Grid>
        </ScrollViewer>
    </Grid>
</Window>

Copy-paste the following code into App.xaml. We have added new Resource dictionary (line 7 to line 15)

<Application x:Class="LiveChartsDesktopApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:LiveChartsDesktopApp"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.LightBlue.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.Lime.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Card.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Step 6 - Running and Testing the Application

1) Run Azure function and the WPF app 

2) WPF app should look similar to the screenshot below on the initial load

3) Add a Draft order for Quebec using Data explorer in Azure Cosmos DB (please refer to Part 1 of this article series for a quick refresher on how to do this) and notice the changes in pie chart

Summary and Next steps

At the end of this article, you should have a working WPF client app with a bar graph and pie chart with material design theme. 

Thank you for reading, this should be the last article of this series. Here we have used WPF for client app but the client app can be a web app or a mobile app also. Please let me know in comments if you would like me to write another article for web app (Angular) or mobile app (Xamarin Android Native app).

Source Code

WPF app - https://github.com/tanujgyan/LiveChartsDesktopApp

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