2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Blazor Server SignalR でリアルタイムプッシュ更新(株価ボード)

Last updated at Posted at 2022-04-27

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");  // 追加

・・・

引用

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?