前回(第4回) で温度が読めるようになりました。ただ、各画面が個別に1秒ごとにポーリングするのは効率が良くありません。今回は 1か所で計測し、全クライアントにリアルタイムで反映する形に作り替え、ライブグラフにします。
Table of contents
Open Table of contents
Blazor ServerとSignalRの関係
実は Blazor Server の画面更新は、最初から SignalR(回線=circuit)の上で動いています。サーバーでUIの差分を計算し、SignalR経由でブラウザに送って描画しているのです。
つまり、サーバー側で状態を変えて StateHasChanged を呼べば、その変化はSignalRで各クライアントに届きます。今回はこの仕組みに乗せて、
- バックグラウンドサービスが温度を定期計測
- 更新をイベントで通知
- 各ページが受け取って再描画 → SignalRで画面反映
という流れを作ります。各画面が個別にポーリングするより効率的で、複数タブ・複数端末が同時に同じ値を見られます。
バックグラウンドで計測するサービス
BackgroundService で1秒ごとに温度を読み、直近の履歴を保持して、更新イベントを発火します。
// Services/TemperatureMonitor.cs
public sealed class TemperatureMonitor : BackgroundService
{
private const int Capacity = 60; // 直近60秒ぶん
private readonly TemperatureService _temp;
private readonly Queue<double> _history = new();
private readonly object _gate = new();
public double Current { get; private set; }
public event Action? Changed;
public TemperatureMonitor(TemperatureService temp) => _temp = temp;
public double[] Snapshot()
{
lock (_gate) return _history.ToArray();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
Current = _temp.ReadCelsius();
lock (_gate)
{
_history.Enqueue(Current);
while (_history.Count > Capacity) _history.Dequeue();
}
Changed?.Invoke();
}
}
}
Program.cs に、シングルトン兼ホステッドサービスとして登録します。
builder.Services.AddSingleton<TemperatureMonitor>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<TemperatureMonitor>());
Queueは複数スレッドから触るのでlockで保護し、描画側には配列のスナップショットを渡します。
ライブグラフを表示する(依存ライブラリ無し)
外部チャートライブラリを使わず、SVGの折れ線で描きます。イベントを購読し、更新のたびに再描画します。
@* Components/Pages/Live.razor *@
@page "/live"
@rendermode InteractiveServer
@implements IDisposable
@inject TemperatureMonitor Monitor
<h1>温度ライブグラフ</h1>
<p style="font-size:2rem"><b>@Monitor.Current.ToString("F1") ℃</b></p>
<svg width="600" height="200" style="border:1px solid #ccc">
@if (!string.IsNullOrEmpty(_points))
{
<polyline points="@_points" fill="none" stroke="#006cac" stroke-width="2" />
}
</svg>
@code {
private string _points = "";
protected override void OnInitialized() => Monitor.Changed += OnChanged;
private void OnChanged()
{
_points = BuildPoints(Monitor.Snapshot());
InvokeAsync(StateHasChanged);
}
private static string BuildPoints(double[] data)
{
if (data.Length < 2) return "";
const int w = 600, h = 200;
double min = data.Min(), max = data.Max();
double range = max - min < 0.1 ? 1 : max - min; // 平坦なとき対策
return string.Join(" ", data.Select((v, i) =>
{
double x = (double)i / (data.Length - 1) * w;
double y = h - (v - min) / range * h; // 上下反転
return $"{x:F1},{y:F1}";
}));
}
public void Dispose() => Monitor.Changed -= OnChanged;
}
実機で起動し、/live を開くと、温度が1秒ごとに折れ線で伸びていくはずです。センサーを指で触れて温めると、グラフが上がるのが確認できます。別のタブや別PCで同時に開いても、同じ動きになります(=SignalRで全クライアントに配信)。

発展:明示的なSignalR Hubを使う場合
今回はBlazor Server内で完結しましたが、Blazor以外(素のJavaScript、WASM、モバイル)にも配信したい場合は、明示的なSignalR Hubを立て、IHubContext からブロードキャストします。第7回のMQTTと組み合わせると、他デバイスへの配信も見えてきます。本連載のダッシュボードは Blazor Server なので、まずは上記のシンプルな形で十分です。
つまずきポイント
- 画面が更新されない:
Changedの購読解除(Dispose)忘れや、InvokeAsync(StateHasChanged)の呼び忘れ - 例外でアプリが落ちる:
ExecuteAsync内は try/catch でセンサー読み取り失敗を握る(1回の失敗で計測を止めない) - グラフが平坦:
rangeの下限処理(上のコード)で対応済み
まとめ
- Blazor Serverの画面更新はSignalR回線で動いている。その仕組みに乗せる
- バックグラウンドサービスで一括計測 → イベント通知 →
StateHasChangedで全クライアントにリアルタイム反映 - グラフは依存ライブラリ無しのSVG折れ線でも十分作れる
次回(第6回)は、この温度をSQLite+EF Coreで蓄積し、過去の推移を時系列グラフで振り返れるようにします。
