WebSurfer's Home さんのサンプルをBlazor Server で動かしてみる
アプリ作成
- Visual Studio 2022 v17.1.4
- Blazor Server アプリ .NET6 認証なし 'BlazorStockTicker' 作成
- NuGet でインストール
Microsoft.AspNetCore.SignalR.Client
Syncfusion.Blazor.Grid
Syncfusion.Blazor.Themes
疑似株価発生クラス作成
Stocks/Stock.cs
namespace BlazorStockTicker.Stocks
{
public class Stock
{
private decimal _price;
public string Symbol { get; set; }
public decimal Price
{
get {
return _price;
}
set {
if (_price == value)
{
return;
}
_price = value;
if (DayOpen == 0)
{
DayOpen = _price;
}
}
}
public decimal DayOpen { get; set; }
public decimal Change
{
get {
return Price - DayOpen;
}
}
public double PercentChange
{
get {
return (double)Math.Round(Change / Price, 4);
}
}
}
}
Stocks/StockTicker.cs
using System.Collections.Concurrent;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using BlazorStockTicker.Hubs;
namespace BlazorStockTicker.Stocks
{
public class StockTicker
{
private readonly IHubContext<StockTickerHub> _hubContext;
private readonly ConcurrentDictionary<string, Stock> _stocks;
private readonly double _rangePercent;
private readonly TimeSpan _updateInterval;
private readonly Timer _timer;
private volatile bool _updatingStockPrices;
// コンストラクタ
public StockTicker(IHubContext<StockTickerHub> hubContext)
{
// SignalR コンテキストを DI により取得して設定
_hubContext = hubContext;
// 株価情報を保持する
_stocks = new ConcurrentDictionary<string, Stock>();
// 株価情報の初期値を設定
InitializeStockPrices(_stocks);
// 株価は下の TryUpdateStockPrice メソッドでランダムに
// 変更されるが、その上限・下限を 2% に設定している
_rangePercent = .002;
// Timer を使って UpdateStockPrices メソッドを呼ぶ間隔
_updateInterval = TimeSpan.FromMilliseconds(100);
#nullable disable
// _updateInterval (250 ミリ秒間隔) で UpdateStockPrices
// メソッドが呼ばれるよう Timer を設定
_timer = new Timer(UpdateStockPrices,
null,
_updateInterval,
_updateInterval);
#nullable enable
// 株価情報が更新中であることを示すフラグ
_updatingStockPrices = false;
}
private void _timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
UpdateStockPrices(new object());
}
// 株価情報の初期値を設定するヘルパメソッド
private void InitializeStockPrices(
ConcurrentDictionary<string, Stock> stocks)
{
var init = new List<Stock>
{
new Stock { Symbol = "MSFT", Price = 30.31m },
new Stock { Symbol = "APPL", Price = 578.18m },
new Stock { Symbol = "GOOG", Price = 570.30m }
};
stocks.Clear();
// bool TryAdd (TKey key, TValue value) メソッドで
// キー/値ペアを追加する。成功すると true を返す。
// キーが既に存在する場合は false を返す
init.ForEach(init => _stocks.TryAdd(init.Symbol, init));
}
// ConcurrentDictionary<string, Stock> のインスタンス
// から Value プロパティで IEnumerable<Stock> を取得
public IEnumerable<Stock> GetAllStocks()
{
return _stocks.Values;
}
public async Task<IEnumerable<Stock>> GetAllStocksAsync()
{
return _stocks.Values;
}
private readonly object _updateStockPricesLock = new object();
// Timer を使って 250ms 毎に以下のメソッドが呼ばれる。
// 株価に変更があった場合は BroadcastStockPrice メソッ
// ドで株価情報をクライアントへ送信する
private void UpdateStockPrices(object state)
{
lock (_updateStockPricesLock)
{
if (!_updatingStockPrices)
{
_updatingStockPrices = true;
// _stocks.Values は IEnumerable<Stock>
foreach (var stock in _stocks.Values)
{
if (TryUpdateStockPrice(stock))
{
// 株価に変更があった場合は株価
// 情報をクライアントへ送信
BroadcastStockPrice(stock);
}
}
_updatingStockPrices = false;
}
}
}
private readonly Random _updateOrNotRandom = new Random();
private bool TryUpdateStockPrice(Stock stock)
{
// 0.0 以上 1.0 未満のランダム値
var r = _updateOrNotRandom.NextDouble();
if (r > .1)
{
return false;
}
// 株価をランダムに変更する
var random = new Random((int)Math.Floor(stock.Price));
var percentChange = random.NextDouble() * _rangePercent;
var pos = random.NextDouble() > .51;
var change = Math.Round(stock.Price * (decimal)percentChange, 2);
change = pos ? change : -change;
stock.Price += change;
return true;
}
// 株価情報をクライアントへ送信する
private void BroadcastStockPrice(Stock stock)
{
_hubContext.Clients.All.SendAsync("UpdateStockPrice", stock);
}
}
}
HUB 作成
Hubs/StockTickerHub.cs
using Microsoft.AspNetCore.SignalR;
using BlazorStockTicker.Stocks;
namespace BlazorStockTicker.Hubs
{
public class StockTickerHub : Hub
{
private readonly StockTicker _stockTicker;
public StockTickerHub(StockTicker stockTicker)
{
// Broadcaster インスタンスを DI により取得して設定。
// Program.cs で AddSingleton メソッドを使ってシング
// ルトンになるようにしている
_stockTicker = stockTicker;
}
// StockTicker.GetAllStock メソッドは株価情報を
// IEnumerable<Stock> として返す
public IEnumerable<Stock> GetAllStocks()
{
return _stockTicker.GetAllStocks();
}
public async Task<IEnumerable<Stock>> GetAllStocksAsync()
{
return await _stockTicker.GetAllStocksAsync();
}
}
}
株価ボード作成
Pages/StockTicker.razor@.html
@page "/stockticker"
@using Microsoft.AspNetCore.SignalR.Client
@using Syncfusion.Blazor.Grids
@using BlazorStockTicker.Stocks
@inject NavigationManager NavigationManager
@implements IAsyncDisposable
<h1>ASP.NET SignalR Stock Ticker Sample</h1>
<h2>Live Stock Table</h2>
<div>
<SfGrid DataSource="@stocks.Values.ToList()" AllowPaging="true" >
<GridEvents QueryCellInfo="CellInfoHandler" TValue="Stock"></GridEvents>
<GridColumns>
<GridColumn Field=@nameof(Stock.Symbol) HeaderText="銘柄" TextAlign="TextAlign.Center" Width="70" ></GridColumn>
<GridColumn Field=@nameof(Stock.DayOpen) HeaderText="始値" Type="ColumnType.Number" TextAlign="TextAlign.Right" Width="70"></GridColumn>
<GridColumn Field=@nameof(Stock.Price) HeaderText="価格" Type="ColumnType.Number" TextAlign="TextAlign.Right" Width="70"></GridColumn>
<GridColumn Field=@nameof(Stock.Change) HeaderText="増減" Type="ColumnType.Number" Format="▲0.00;▼0.00" TextAlign="TextAlign.Right" Width="70"></GridColumn>
<GridColumn Field=@nameof(Stock.PercentChange) HeaderText="増減率" Type="ColumnType.Number" Format="p2" TextAlign="TextAlign.Right" Width="70"></GridColumn>
</GridColumns>
</SfGrid>
</div>
<style>
.negative-color{
color: red;
}
.positive-color{
color: lime;
}
.e-grid .e-spinner-pane{ /** Grid スピナーを非表示*/
display:none;
}
</style>
@code {
private HubConnection? connection;
private Dictionary<string, Stock> stocks = new Dictionary<string, Stock>();
protected override async Task OnInitializedAsync()
{
// 接続を作成。"/stockTickerHub" は Program.cs で
// app.MapHub<StockTickerHub>("/stockTickerHub");
// としてマップしたエンドポイントらしい
connection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/stockTickerHub"))
.Build();
// StockTicker クラスの BroadcastStockPrice メソッドの
// SendAsync("UpdateStockPrice", stock); で SignalR コ
// ンテキストを通じて下の On<Stock>() { ... } が起
// 動される。引数の stock に含まれる情報により株価情報
// の表示を更新する。
connection.On<Stock>("UpdateStockPrice", (stock) =>
{
if (stocks.ContainsKey(stock.Symbol))
{
stocks[stock.Symbol] = stock;
} else {
stocks.Add(stock.Symbol, stock);
}
InvokeAsync(StateHasChanged);
});
await connection.StartAsync();
}
// セル変更イベント、マイナスなら赤にする
private void CellInfoHandler(QueryCellInfoEventArgs<Stock> e)
{
bool? isPositive = e.Column.Field == nameof(Stock.Change) ? 0 <= e.Data.Change
: e.Column.Field == nameof(Stock.PercentChange) ? 0 <= e.Data.PercentChange : null;
e.Cell.AddStyle(new string[] { isPositive == true ? "color:lime"
: isPositive == false ? "color:red" : "" });
}
public async ValueTask DisposeAsync()
{
if (connection is not null)
{
await connection.DisposeAsync();
}
}
}
ナビに「株価ボード」メニュー追加 及び SyncFusion
Shared/NavNenu.razor@.html
・・・
<div class="nav-item px-3">
<NavLink class="nav-link" href="stockticker">
<span class="oi oi-list-rich" aria-hidden="true"></span> 株価ボード
</NavLink>
</div>
</nav>
</div>
- SyncFusion の Script と css を追加
Pages/_Layout.csthml@.html
・・・
<link id="theme" href="_content/Syncfusion.Blazor.Themes/bootstrap4.css" rel="stylesheet" />
<script src="_content/Syncfusion.Blazor.Core/scripts/syncfusion-blazor.min.js" type="text/javascript"></script>
</head>
・・・
サービスの追加
Programs.cs
// 追加
using BlazorStockTicker.Stocks;
using BlazorStockTicker.Hubs;
・・・
// *** 追加 ***
builder.Services.AddSignalR();
// StockTickerHub でコンストラクタ経由 DI によりシングル
// トンインスタンスを取得できるよう以下の設定を行う
builder.Services.AddSingleton<StockTicker>();
// SyncFusion Service を追加
builder.Services.AddSyncfusionBlazor(options => { options.IgnoreScriptIsolation = true; });
var app = builder.Build();
・・・
app.MapBlazorHub();
app.MapHub<StockTickerHub>("/stockTickerHub"); // 追加
・・・