ASP.NET Core 6 SignalR: Creating Real-Time Charts in Blazor Web Assembly using C3.js
In this article, we will implement the Real-Time Charts in Blazor WebAssembly Application using ASP.NET Core 6 SignalR. Creating Real-Time charts is one of the frequent requirements in Moden-Web Applications. This means that the chart must be generated without page postback. This must be implemented using SignalR. I have already published an article on Chart Creation in Blazor WebAssembly using C3.js. You can read this article from this link.
What is SignalR?
ASP.NET Core SignalR is an Open-Source library that is used to add simplified real-time functionality to the Moden-Web Applications. The advantage of using Real-Time functionality is that it enables the content generated after server-side after execution to the client-side instantly. The ideal scenarios of using the SignalR are discussed in the following points
- Dashboard applications where the updates from the server are immediately pushed to the client.
- Gaming, Social App where high-frequency updates are pushed to the client
- Video-based Team meeting apps
- It scales up to handle increasing traffic
- Sends messages to all connected clients simultaneously
- Handling automatic connection management
public class Market { public string? CompanyName { get; set; } public int Volume { get; set; } }
public class MarketHub : Hub { public async Task AcceptData(List<Market> data) => await Clients.All.SendAsync("CommunicateMarketData", data); }
namespace ChartServer.DataProvider { /// <summary> /// This call will be used to send the data after each second to the client /// </summary> public class TimeWatcher { private Action? Executor; private Timer? timer; // we need to auto-reset the event before the execution private AutoResetEvent? autoResetEvent; public DateTime WatcherStarted { get; set; } public bool IsWatcherStarted { get; set; } /// <summary> /// Method for the Timer Watcher /// This will be invoked when the Controller receives the request /// </summary> public void Watcher(Action execute) { int callBackDelayBeforeInvokeCallback = 1000; int timeIntervalBetweenInvokeCallback = 2000; Executor = execute; autoResetEvent = new AutoResetEvent(false); timer = new Timer((object? obj) => { Executor(); if ((DateTime.Now - WatcherStarted).TotalSeconds > 60) { IsWatcherStarted = false; timer.Dispose(); } }, autoResetEvent, callBackDelayBeforeInvokeCallback, timeIntervalBetweenInvokeCallback); WatcherStarted = DateTime.Now; IsWatcherStarted = true; } } }
using SharedModels; namespace ChartServer.DataProvider { public static class MarketDataProvider { public static List<Market> GetMarketData() { var random = new Random(); var marketData = new List<Market>() { new Market() { CompanyName = "MS-IT Services", Volume = random.Next(1,900)}, new Market() { CompanyName = "TS-IT Providers", Volume = random.Next(1,900)}, new Market() { CompanyName = "LS-SL Sales", Volume = random.Next(1,900)}, new Market() { CompanyName = "MS-Electronics", Volume = random.Next(1,900)}, new Market() { CompanyName = "TS-Electrical", Volume = random.Next(1,900)}, new Market() { CompanyName = "LS-Foods", Volume = random.Next(1,900)}, new Market() { CompanyName = "MS-Healthcare", Volume = random.Next(1,900)}, new Market() { CompanyName = "LS-Pharmas", Volume = random.Next(1,900)}, new Market() { CompanyName = "TS-Healthcare", Volume = random.Next(1,900)} }; return marketData; } } }
using ChartServer.DataProvider; using ChartServer.RHub; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; namespace ChartServer.Controllers { [Route("api/[controller]")] [ApiController] public class MarketController : ControllerBase { private IHubContext<MarketHub> marketHub; private TimeWatcher watcher; public MarketController(IHubContext<MarketHub> mktHub, TimeWatcher watch) { marketHub = mktHub; watcher = watch; } [HttpGet] public IActionResult Get() { if(!watcher.IsWatcherStarted) { watcher.Watcher(()=>marketHub.Clients.All.SendAsync ("SendMarketStatusData" ,MarketDataProvider.GetMarketData())); } return Ok(new { Message = "Request Completed" }); } } }
using ChartServer.DataProvider; using ChartServer.RHub; using SharedModels; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Add CORS Policy builder.Services.AddCors(option => { option.AddPolicy("cors", policy => { policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyHeader(); }); }); builder.Services.AddSignalR(); // Register the Watcher builder.Services.AddScoped<TimeWatcher>(); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseCors("cors"); app.UseAuthorization(); app.MapControllers(); // Add the SignalR Hub app.MapHub<MarketHub>("/marketdata"); app.Run();
function marketLineChart([]) { let data = []; let labels = []; // data points for chart range on Y-Axis data = arguments[0]; // labels on X-Axis labels = arguments[1]; var chart1 = c3.generate({ bindto: '#market', data: { columns: [ data ] }, axis: { x: { type: 'category', categories: labels, label: { text: 'CompanyName', position: 'outer-center' } }, y: { label: { text: 'Volume', position: 'outer-center' } } } }); } function marketBarChart([]) { let data = []; let labels = []; data = arguments[0]; labels = arguments[1]; var chart = c3.generate({ bindto: '#market', data: { columns: [ data ], type: 'bar' }, bar: { width: { ratio: 0.5 } }, axis: { x: { type: 'category', categories: labels, label: { text: 'CompanyName', position: 'outer-center' } }, y: { label: { text: 'Volume', position: 'outer-center' } } } }) }
namespace MarketChartClient.HttpCaller { public class MarketDataCaller { private HttpClient httpClient; public MarketDataCaller(HttpClient http) { httpClient = http; } public async Task GetMarketDataAsync() { try { var response = await httpClient.GetAsync("marketdata"); if (!response.IsSuccessStatusCode) throw new Exception("Something is wrong with the connection make sure that the server is running."); } catch (Exception ex) { Console.WriteLine(ex.Message); throw ex; } } public async Task GetMarketEndpoint() { try { var response = await httpClient.GetAsync ("https://localhost:7193/api/Market"); if (!response.IsSuccessStatusCode) throw new Exception("Something is wrong with the connection so get call is not executing."); } catch (Exception ex) { Console.WriteLine(ex.Message); throw ex; } } } }
builder.Services.AddScoped<MarketDataCaller>();
@page "/chartui" @using Microsoft.AspNetCore.SignalR.Client; @using SharedModels @using System.Text.Json @inject IJSRuntime js @inject MarketChartClient.HttpCaller.MarketDataCaller service;
<div> <div class="container"> <table class="table table-bordered table-striped"> <tbody> <tr> <td> <button class="btn btn-success" @onclick="@generateLineChartTask">Line Chart</button> </td> <td> <button class="btn btn-danger" @onclick="@generateBarChartTask">Bar Chart</button> </td> </tr> </tbody> </table> <div id="market"></div> <table class="table table-bordered table-striped"> <thead> <tr> <th>Company Name</th> <th>Volume</th> </tr> </thead> <tbody> @foreach (var item in MarketData) { <tr> <td>@item.CompanyName</td> <td>@item.Volume</td> </tr> } </tbody> </table> <hr/> <div class="container"> @ConnectionStatusMessage </div> </div> </div>
@code { private HubConnection? hubConn; private string? ConnectionStatusMessage; public List<Market> MarketData = new List<Market>(); public List<Market> MarketReceivedData = new List<Market>(); private List<string> xSource; private List<int> ySource; private List<object> source; protected override async Task OnInitializedAsync() { xSource = new List<string>(); ySource = new List<int>(); source = new List<object>(); await service.GetMarketEndpoint(); await Start(); } private async Task Start() { hubConn = new HubConnectionBuilder().WithUrl ("https://localhost:7193/marketdata").Build(); await hubConn.StartAsync(); if(hubConn.State == HubConnectionState.Connected ) ConnectionStatusMessage = "Connection is established Successfully..."; else ConnectionStatusMessage = "Connection is not established..."; } private void MarketDataListener(string chartType) { hubConn.On<List<Market>>("SendMarketStatusData", async (data) => { MarketData = new List<Market>(); foreach (var item in data) { Console.WriteLine($"Company Name: {item.CompanyName}, Volumn: {item.Volume}"); xSource.Add(item.CompanyName); ySource.Add(item.Volume); } source.Add(ySource); source.Add(xSource); MarketData = data; StateHasChanged(); await js.InvokeAsync<object>(chartType, source.ToArray()); xSource.Clear(); ySource.Clear(); }); } private void ReceivedMarketDataListener() { hubConn.On<List<Market>>("CommunicateMarketData", (data) => { MarketReceivedData = data; StateHasChanged(); }); } public async Task Dispose() { await hubConn.DisposeAsync(); } async Task generateLineChartTask() { MarketDataListener("marketLineChart"); ReceivedMarketDataListener(); await service.GetMarketDataAsync(); } async Task generateBarChartTask() { MarketDataListener("marketBarChart"); ReceivedMarketDataListener(); await service.GetMarketDataAsync(); } }
<div class="nav-item px-3"> <NavLink class="nav-link" href="chartui"> <span class="oi oi-list-rich" aria-hidden="true"></span> Chart </NavLink> </div>