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>