[ASP.NET Core MVC][SignalR][Socket] 網頁即時更新最新股價教學 #CH2
此範例會教學如何在 ASP.NET Core MVC 網頁上即時顯示股價,這讓網頁看起來很像看盤軟體,輸入商品代碼後,網頁會即時更新最新的報價。
在上一章節我們建立了報價機,利用 Console 串接群益 API 讀取報價,然後再建立 Socket Server 提供其他程式串接報價。
這章節我們會建立網頁串接報價機的 Socket 接口,讀取報價後同步顯示到網頁上。
Contents
建立專案
開啟 Visual Studio 2022,建立新專案為「ASP.NET Core Web 應用程式 (Model-View-Controller)」。
輸入專案名稱,我們這次的範例名稱是 “TeachQuoteWeb”、路徑,架構選擇「.NET 6.0」版本,按下「建立」就會建立此專案。
加入 Vue3 套件
Vue3 是前端控制欄位的框架類別庫,打開 \Views\Shared\_Layout.cshtml 檔案,在下方 JavaScript 引用增加 Vue3 類別庫語法,順序的要求要放在 jQuery 之後才行。
<script src="https://unpkg.com/vue@3"></script>
當在 Layout 加上 Vue3 引用後,我們就可以在所有的頁面使用 Vue3 語法了,此引用語法來源可參考官方文件。
停用 Json 回傳預設小寫設定
.NET Core 在 Controller 回傳 Json 時,會將變數修改為開頭預設小寫 (駝峰式命名),這設定容易造成前端取值大小寫問題,所以我會停用此設定,讓前端與後端維持相同大小寫設定。
在 Program.cs 加入以下語法:
1 2 3 4 5 |
// 維持 Json 回傳大小寫與 ViewModel 相同 builder.Services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); |
新增 SignalR 用戶端程式庫
SignalR 前端需要 JavaScript 的類別庫才能運作,而後端的 SignalR 已包含在 ASP.NET Core 共用架構中。
這裡要先下載最新版的 signalr.js 到我們的專案內。
在「方案總管」中,以滑鼠右鍵按一下專案,然後選取「加入 > 用戶端程式庫」。
針對提供者選取「unpkg」
針對「程式庫」輸入「@microsoft/signalr@latest」
選取「選擇特定檔案」,展開「dist/browser」資料夾,然後選取「signalr.js」和「signalr.min.js」。
將「目標位置」設定為「wwwroot/js/signalr/」
安裝之後,在你的專案目錄下就會找到這些檔案。
建立 SignalR Hub 類別
SignalR 的 Hub 類別可以處理網頁上用戶端與伺服器端之間的溝通橋樑。
在專案的目錄中新增目錄,名稱為「Hubs」。
然後在「Hubs」目錄下新增新類別,名稱為「QuoteHub」。
然後在 QuoteHub 類別貼上以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
using Microsoft.AspNetCore.SignalR; namespace TeachQuoteWeb.Hubs { public class QuoteHub : Hub { #region 屬性 public static List<string> ConnIDList = new List<string>(); //用戶連線 ID 列表 #endregion #region 方法 /// <summary> /// 連線事件 /// </summary> /// <returns></returns> public override async Task OnConnectedAsync() { if (ConnIDList.Count == 0) { QuoteUtil.Init(); // 連線報價伺服器 string msg = QuoteUtil.Connect(); if (msg != "") { Clients.Client(Context.ConnectionId).SendAsync("Alert", msg); } } // 加入用戶連線列表 if (ConnIDList.Any(w => w == Context.ConnectionId) == false) { ConnIDList.Add(Context.ConnectionId); } // 更新連線 ID await Clients.Client(Context.ConnectionId).SendAsync("SetHubConnId", Context.ConnectionId); await base.OnConnectedAsync(); } /// <summary> /// 離線事件 /// </summary> /// <param name="ex"></param> /// <returns></returns> public override async Task OnDisconnectedAsync(Exception ex) { string id = ConnIDList.Where(p => p == Context.ConnectionId).FirstOrDefault(); if (id != null) { ConnIDList.Remove(id); QuoteUtil.UserDisconnect(id); } await base.OnDisconnectedAsync(ex); } #endregion } } |
報價操作類別 QuoteUtil
在程式碼中有用到新類別 QuoteUtil,我將所有與報價機連線要用到的方法都封裝寫在 QuoteUtil 裡面。
QuoteUtil 新類別同樣新增放在 /Hubs 目錄內。
在 QuoteUtil 類別內加入此語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
using Microsoft.AspNetCore.SignalR; using Newtonsoft.Json; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; using TeachQuoteWeb.Hubs.Models; namespace TeachQuoteWeb.Hubs { public class QuoteUtil { #region 屬性 public static IHubContext<QuoteHub> hubContext; public static List<UserRquest> listUserRquest = new List<UserRquest>(); //使用者訂閱名單 public static ClientInfo? clientInfo = null; static TcpClient TCPClient = new TcpClient(); static bool isTCPListen = false; static Thread? ThreadTCPListen; #endregion #region 建構子 /// <summary> /// 報價連線初始化 /// </summary> public static void Init() { if (clientInfo == null) { string ID = DateTime.Now.ToString("ss") + DateTime.Now.Millisecond; clientInfo = new ClientInfo(ID); } } #endregion #region 方法 /// <summary> /// 連線伺服器 /// </summary> /// <returns></returns> public static string Connect() { // 連線報價伺服器 if (!TCPClient.Connected) { string serverIp = "127.0.0.1"; int serverPort = 8888; try { IPAddress address; if (!IPAddress.TryParse(serverIp, out address)) { address = Dns.Resolve(serverIp).AddressList[0]; } IPEndPoint ServerEndpoint = new IPEndPoint(address, serverPort); TCPClient.Client.Connect(ServerEndpoint); TCPClient.Client.IOControl(IOControlCode.KeepAliveValues, GetKeepAliveData(), null); isTCPListen = true; ListenTCP(); SendTCP(clientInfo); } catch (Exception ex) { return "連線錯誤: " + ex.Message; } } return ""; } /// <summary> /// 使用者離線 /// </summary> public static void UserDisconnect(string connID) { UserRquest item = listUserRquest.FirstOrDefault(w => w.ID == connID); if (item != null) { listUserRquest.Remove(item); } } /// <summary> /// 監聽 TCP /// </summary> private static void ListenTCP() { ThreadTCPListen = new Thread(new ThreadStart(delegate { byte[] receiveBuffer = new byte[0]; byte[] processBuffer = new byte[0]; byte[] packet = new byte[1024]; byte[] lenPacket = new byte[8]; int size = 0; int bytesRead = 0; while (isTCPListen) { try { bytesRead = TCPClient.GetStream().Read(packet, 0, packet.Length); if (bytesRead > 0) { receiveBuffer = MargeByte(receiveBuffer, packet, bytesRead); if (receiveBuffer.Length < 8 && bytesRead < 8) { continue; } lenPacket = GetByteData(receiveBuffer, 0, 8); size = int.Parse(Encoding.UTF8.GetString(lenPacket)); while (size > 0) { if (size <= receiveBuffer.Length - 8) { processBuffer = GetByteData(receiveBuffer, 8, size); IPacket Item = ByteToPacket(processBuffer); ProcessReceive(Item); receiveBuffer = GetByteData(receiveBuffer, 8 + size, receiveBuffer.Length - size - 8); if (receiveBuffer.Length < 8) { break; } lenPacket = GetByteData(receiveBuffer, 0, 8); size = int.Parse(Encoding.UTF8.GetString(lenPacket)); } else { break; } } } else { isTCPListen = false; break; } } catch (Exception ex) { ShowAlert("TCP 接收錯誤: " + ex.Message); break; } } // 停止連線 isTCPListen = false; TCPClient.Client.Disconnect(true); })); ThreadTCPListen.IsBackground = true; if (isTCPListen) { ThreadTCPListen.Start(); } } /// <summary> /// 處理接收項目 /// </summary> /// <param name="Item"></param> private static void ProcessReceive(IPacket Item) { if (isTCPListen == false) { return; } if (Item.GetType() == typeof(TickPacket)) { // Tick 回報 TickPacket packet = (TickPacket)Item; List<UserRquest> items = listUserRquest.Where(w => w.Symbol == packet.Symbol).ToList(); foreach (var item in items) { // 傳送至前端 hubContext.Clients.Client(item.ID).SendAsync("UpdTick", packet.Symbol, packet.Close, packet.Qty); } } else if (Item.GetType() == typeof(Best5Packet)) { //Best5 回報 Best5Packet packet = (Best5Packet)Item; List<UserRquest> items = listUserRquest.Where(w => w.Symbol == packet.Symbol).ToList(); foreach (var item in items) { // 傳送至前端 hubContext.Clients.Client(item.ID).SendAsync("UpdBest5", packet.Symbol, JsonConvert.SerializeObject(packet)); } } } /// <summary> /// 傳送 TCP /// </summary> /// <param name="Item"></param> public static void SendTCP(IPacket Item) { if (TCPClient.Connected) { byte[] Data = PacketToByteArray(Item); NetworkStream NetStream = TCPClient.GetStream(); NetStream.Write(Data, 0, Data.Length); } } /// <summary> /// 傳送物件轉 Byte /// </summary> /// <param name="packet"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public static byte[] PacketToByteArray(IPacket packet) { string type = packet.GetType().Name; string jsonString = JsonConvert.SerializeObject(packet); byte[] data = Encoding.UTF8.GetBytes(type + "|" + jsonString); int len = data.Length; if (len > 99999999) { throw new Exception("傳送字串超過長度限制"); } byte[] lenData = Encoding.UTF8.GetBytes(len.ToString("00000000")); byte[] newData = MargeByte(lenData, data, 0); return newData; } /// <summary> /// 用戶訂閱報價 /// </summary> /// <param name="id"></param> /// <param name="reqSymbol"></param> /// <returns></returns> public static string GetRequestSymbol(string id, string symbol) { // 檢查是否已連接 if (isTCPListen == false) { return "報價伺服器未連線"; } // 檢查存在訂閱商品代碼列表 listUserRquest.RemoveAll(w => w.ID == id); listUserRquest.Add(new UserRquest() { ID = id, Symbol = symbol, }); RequestQuotePacket reqQuote = new RequestQuotePacket(clientInfo.ID); reqQuote.Symbol = new List<string>(); reqQuote.Symbol.Add(symbol); SendTCP(reqQuote); return ""; } /// <summary> /// 連線心跳檢測 /// </summary> /// <returns></returns> public static byte[] GetKeepAliveData() { uint dummy = 0; byte[] inOptionValues = new byte[Marshal.SizeOf(dummy) * 3]; BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0); BitConverter.GetBytes((uint)3000).CopyTo(inOptionValues, Marshal.SizeOf(dummy));//keep-alive間隔 BitConverter.GetBytes((uint)500).CopyTo(inOptionValues, Marshal.SizeOf(dummy) * 2);// 嘗試間隔 return inOptionValues; } /// <summary> /// 合併位元組 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <param name="bsz"></param> /// <returns></returns> public static byte[] MargeByte(byte[] a, byte[] b, int bsz) { using (MemoryStream ms = new MemoryStream()) { ms.Write(a, 0, a.Length); ms.Write(b, 0, (bsz == 0) ? b.Length : bsz); return ms.ToArray(); } } /// <summary> /// 取得位元組 /// </summary> /// <param name="buf"></param> /// <param name="pos"></param> /// <param name="length"></param> /// <returns></returns> public static byte[] GetByteData(byte[] buf, int pos, int length) { byte[] b = new byte[length]; Array.Copy(buf, pos, b, 0, length); return b; } /// <summary> /// Byte 轉傳送物件 /// </summary> /// <param name="bytes"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public static IPacket ByteToPacket(byte[] bytes) { string jsonString = Encoding.UTF8.GetString(bytes); string type = jsonString.Split('|')[0]; try { switch (type) { case "TickPacket": return JsonConvert.DeserializeObject<TickPacket>(jsonString.Split('|')[1]); case "Best5Packet": return JsonConvert.DeserializeObject<Best5Packet>(jsonString.Split('|')[1]); default: throw new Exception("Not Support Type, \nSource:" + jsonString); } } catch (Exception ex) { throw new Exception("Convert Error: " + ex.Message + "\nSource:" + jsonString); } } /// <summary> /// 廣播顯示訊息 /// </summary> /// <param name="msg"></param> public static void ShowAlert(string msg) { hubContext.Clients.All.SendAsync("Alert", msg); } #endregion } } |
Model 與 Socket 傳遞類別
我將 Socket 所傳遞的內容都宣告成不同的類別,在傳送時,先將物件轉換成 Json,再轉換成 Byte[] 後傳送。
當收到 Byte[] 時反向處理,將 Byte[] 轉 Json,再轉成物件。
我在 /Hubs 底下建了一個 “Models” 新目錄,將會用到類別都放在這裡,此部份跟第一章節用到的類別是一樣的。
在這個範例我會用到兩個報價類別,分別是 Tick 和 Best5 ,一個使用者類別 ClientInfo,一個報價需求類別 RequestQuotePacket,但是我針對傳送的類別宣告一個介面 (interface) 來規範必要欄位。
新增介面: IPacket
1 2 3 4 5 6 7 |
namespace TeachQuoteWeb.Hubs.Models { public interface IPacket { string ID { get; set; } } } |
新增類別: TickPacket
1 2 3 4 5 6 7 8 9 10 11 |
namespace TeachQuoteWeb.Hubs.Models { [Serializable] public class TickPacket : IPacket { public string ID { get; set; } public string Symbol { get; set; } public double Close { get; set; } public int Qty { get; set; } } } |
新增類別: Best5Packet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
namespace TeachQuoteWeb.Hubs.Models { [Serializable] public class Best5Packet : IPacket { public string ID { get; set; } public string Symbol { get; set; } public double Bid1Price { get; set; } public double Bid1Qty { get; set; } public double Bid2Price { get; set; } public double Bid2Qty { get; set; } public double Bid3Price { get; set; } public double Bid3Qty { get; set; } public double Bid4Price { get; set; } public double Bid4Qty { get; set; } public double Bid5Price { get; set; } public double Bid5Qty { get; set; } public double Ask1Price { get; set; } public double Ask1Qty { get; set; } public double Ask2Price { get; set; } public double Ask2Qty { get; set; } public double Ask3Price { get; set; } public double Ask3Qty { get; set; } public double Ask4Price { get; set; } public double Ask4Qty { get; set; } public double Ask5Price { get; set; } public double Ask5Qty { get; set; } } } |
新增使用者類別: ClientInfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System.Net.Sockets; namespace TeachQuoteWeb.Hubs.Models { [Serializable] public class ClientInfo : IPacket { public string ID { get; set; } [NonSerialized] public TcpClient Client; public ClientInfo(string id) { this.ID = id; } } } |
新增報價需求類別: RequestQuotePacket
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
namespace TeachQuoteWeb.Hubs.Models { [Serializable] public class RequestQuotePacket : IPacket { public string ID { get; set; } public List<string> Symbol { get; set; } public RequestQuotePacket(string id) { this.ID = id; } } } |
新增使用者訂閱類別: UserRquest
1 2 3 4 5 6 7 8 |
namespace TeachQuoteWeb.Hubs.Models { public class UserRquest { public string ID { get; set; } public string Symbol { get; set; } } } |
接著就可以把 QuoteUtil 有用到的類別加入引用。
註冊 SignalR 服務
開啟 Program.cs 檔案,這裡要啟用 SignalR 服務,然後註冊 Hub 路由。
在 Program.cs 的 var app = builder.Build();
語法之前,加入此語法:
//加入 SignalR
builder.Services.AddSignalR();
在 app.Run(); 語法之前,加入此語法:
//加入 Hub
app.MapHub<QuoteHub>("/quoteHub");
前端 View 頁面
接著我在 MVC 的主頁面 Home/Index 設計範例,因為 MVC 是分層架構,所以寫程式碼的位置也會跳來跳去的,我會依順序在不同頁面慢慢增加程式碼。
打開 \Views\Home\Index.cshtml,先清空原有程式碼,然後貼上此語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
<h2>網頁即時更新最新股價範例</h2> <div id="app"> <div class="row"> <div class="col-auto"> <label class="col-form-label">商品代碼 (群益)</label> </div> <div class="col-auto"> <input type="text" id="txtSymbol" class="form-control" v-model="queryForm.Symbol"> </div> <div class="col-auto"> <button type="button" class="btn btn-primary" v-on:click="RequestQuote()">訂閱報價</button> </div> </div> <hr /> <h4>即時價格</h4> <div class="row"> <div class="col-auto"> <label class="col-form-label">價格</label> </div> <div class="col-auto"> <input type="text" class="form-control" readonly="readonly" v-bind:value="Tick.Close"> </div> <div class="col-auto"> <label class="col-form-label">單量</label> </div> <div class="col-auto"> <input type="text" class="form-control" readonly="readonly" v-bind:value="Tick.Qty"> </div> </div> <hr /> <h4>即時最佳五檔</h4> <div class="table-responsive"> <table class="table table-bordered table-striped"> <thead> <tr> <th scope="col" style="width: 25%">買價</th> <th scope="col" style="width: 25%">買量</th> <th scope="col" style="width: 25%">賣價</th> <th scope="col" style="width: 25%">賣量</th> </tr> </thead> <tbody> <tr class="table-danger"> <th scope="row">{{Best5.Bid1Price}}</th> <td>{{Best5.Bid1Qty}}</td> <th scope="row">{{Best5.Ask1Price}}</th> <td>{{Best5.Ask1Qty}}</td> </tr> <tr> <td>{{Best5.Bid2Price}}</td> <td>{{Best5.Bid2Qty}}</td> <td>{{Best5.Ask2Price}}</td> <td>{{Best5.Ask2Qty}}</td> </tr> <tr> <td>{{Best5.Bid3Price}}</td> <td>{{Best5.Bid3Qty}}</td> <td>{{Best5.Ask3Price}}</td> <td>{{Best5.Ask3Qty}}</td> </tr> <tr> <td>{{Best5.Bid4Price}}</td> <td>{{Best5.Bid4Qty}}</td> <td>{{Best5.Ask4Price}}</td> <td>{{Best5.Ask4Qty}}</td> </tr> <tr> <td>{{Best5.Bid5Price}}</td> <td>{{Best5.Bid5Qty}}</td> <td>{{Best5.Ask5Price}}</td> <td>{{Best5.Ask5Qty}}</td> </tr> </tbody> </table> </div> </div> @section scripts{ <script src="~/js/signalr/dist/browser/signalr.js"></script> <script> const app = Vue.createApp({ data() { return { hub: { connection: {} , HubConnId: '' } , queryForm: { Symbol:'TX00' } , Tick: { Close: '' , Qty: '' } , Best5: { Bid1Price: '' , Bid1Qty: '' , Bid2Price: '' , Bid2Qty: '' , Bid3Price: '' , Bid3Qty: '' , Bid4Price: '' , Bid4Qty: '' , Bid5Price: '' , Bid5Qty: '' } } } , created() { var self = this; self.hub.connection = new signalR.HubConnectionBuilder().withUrl("/quoteHub").build(); //與Server建立連線 self.hub.connection.start().then(function() { console.log("連線完成"); }).catch(function(err) { alert('連線錯誤: ' + err.toString()); }); // 連線ID self.hub.connection.on("SetHubConnId", function(id) { self.hub.HubConnId = id; }); // 顯示訊息事件 self.hub.connection.on("Alert", function(message) { alert(message); }); // 更新報價 self.hub.connection.on("UpdTick", function(Symbol, close, qty) { self.Tick.Close = close; self.Tick.Qty = qty; }); //更新Best5 self.hub.connection.on("UpdBest5", function(Symbol, jsonBest5) { var obj = JSON.parse(jsonBest5); self.Best5 = obj; }); } , methods: { // 訂閱報價 RequestQuote() { var self = this; // 組合表單資料 var postData = {}; postData["HubConnId"] = self.hub.HubConnId; postData["Symbol"] = self.queryForm.Symbol; // 使用 jQuery Ajax 傳送至後端 $.ajax({ url: '@Url.Content("~/Home/RequestQuote")', method: 'POST', dataType: 'json', data: { inModel: postData, __RequestVerificationToken: $('@Html.AntiForgeryToken()').val() }, success: function(datas) { if (datas.ErrMsg) { alert(datas.ErrMsg); return; } }, error: function(err) { alert(err.status + " " + err.statusText + '\n' + err.responseText); } }); } } }); const vm = app.mount('#app'); </script> } |
Controller 頁面
在 View 按下「訂閱報價」後,會呼叫「~/Home/RequestQuote」,所以打開 \Controllers\HomeController.cs,然後加入以下 Action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/// <summary> /// 訂閱報價 /// </summary> /// <param name="inModel"></param> /// <returns></returns> public IActionResult RequestQuote(Dictionary<string, string> inModel) { Dictionary<string, string> outModel = new Dictionary<string, string>(); outModel["ErrMsg"] = ""; // 訂閱報價需求 string msg = QuoteUtil.GetRequestSymbol(inModel["HubConnId"], inModel["Symbol"]); if (msg != "") { outModel["ErrMsg"] = msg; } return Json(outModel); } |
Controller 引用 Hub
我們在 Controller 收到的報價需求會傳送到 QuoteUtil 此類別去執行,同時將 Hub 物件傳送到 QuoteUtil 裡面去,將收到報價後,在 QuoteUtil 就直接回傳到前端 SignalR 顯示畫面。
在 HomeController 的建構子要引用 QuoteHub 物件,再丟到 QuoteUtil 裡面。
修改後的 HomeController 建構子是:
1 2 3 4 5 |
public HomeController(ILogger<HomeController> logger, IHubContext<QuoteHub> quoteHubContext) { _logger = logger; QuoteUtil.hubContext = quoteHubContext; } |
測試專案
寫到這裡,我們的網頁客戶端程式碼就完成了,要測試此程式,要搭配上一章的伺服器程式才行,先將上一個範例 “TeachQuoteServer” 程式執行,執行前記得修改群益的帳號密碼,可以正常登入。
執行後就會有此畫面。
接著執行我們這次的範例,網頁打開後,輸入要查詢的商品代碼後,按「訂閱報價」,就會即時更新股價了。
完成此範例,單純顯示股價,其實沒有大太實際的幫助,但這是往下一步開發之前,最重要的一步,可以在網頁即時更新股價後,就可以做很多的即時應用,也祝各位在程式交易都可以順利。
範例下載
相關學習文章
- 【C# 群益 API 開發教學】取得商品報價、Tick、最佳 5 檔教學 #CH3
- [ASP.NET Core][SignalR] 即時對話聊天室教學 #CH1
- [C#][群益 Api]計算 1 分 K 線與產生 KD 技術指標
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝