Skip to content

Commit 78c4364

Browse files
authored
Merge pull request #100 from JialinXin/blazorchat
Update BlazorServerSide sample
2 parents 26283aa + a500674 commit 78c4364

18 files changed

+737
-12
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ packages/
66
**.csproj.user
77
node_modules/
88
asrs.log.txt
9-
**/global.json
9+
**/global.json
10+
PublishProfiles/
25.2 KB
Loading
66.8 KB
Loading
27.6 KB
Loading
56.1 KB
Loading
49.7 KB
Loading
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.JSInterop;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Threading.Tasks;
8+
9+
namespace ServerSideBlazor
10+
{
11+
/// <summary>
12+
/// Generic client class that interfaces .NET Standard/Blazor with SignalR Javascript client
13+
/// </summary>
14+
public class ChatClient : IDisposable
15+
{
16+
17+
#region static methods
18+
19+
/// <summary>
20+
/// internal dictionary of SignalR clients by Key
21+
/// </summary>
22+
private static readonly Dictionary<string, ChatClient> _clients = new Dictionary<string, ChatClient>();
23+
24+
25+
/// <summary>
26+
/// Inbound message handler
27+
/// </summary>
28+
/// <param name="key"></param>
29+
/// <param name="method"></param>
30+
/// <param name="username"></param>
31+
/// <param name="message"></param>
32+
/// <remarks>
33+
/// This method is called from Javascript when amessage is received
34+
/// </remarks>
35+
[JSInvokable]
36+
public static void ReceiveMessage(string key, string method, string username, string message)
37+
{
38+
if (_clients.ContainsKey(key))
39+
{
40+
var client = _clients[key];
41+
switch (method)
42+
{
43+
case "ReceiveMessage":
44+
client.HandleReceiveMessage(username, message);
45+
return;
46+
47+
default:
48+
throw new NotImplementedException(method);
49+
}
50+
}
51+
else
52+
{
53+
// unable to match the message to a client
54+
Console.WriteLine($"ReceiveMessage: unable to find {key}");
55+
}
56+
}
57+
58+
#endregion
59+
60+
/// <summary>
61+
/// Ctor: create a new client for the given hub URL
62+
/// </summary>
63+
/// <param name="hubUrl"></param>
64+
public ChatClient(string username, IJSRuntime JSRuntime)
65+
{
66+
_JSruntime = JSRuntime;
67+
// save username
68+
if (string.IsNullOrWhiteSpace(username))
69+
throw new ArgumentNullException(nameof(username));
70+
_username = username;
71+
// create a unique key for this client
72+
_key = Guid.NewGuid().ToString();
73+
// add to the list of clients
74+
_clients.Add(_key, this);
75+
}
76+
77+
/// <summary>
78+
/// The Hub URL for chat client
79+
/// </summary>
80+
const string HUBURL = "/chathub";
81+
82+
/// <summary>
83+
/// Our unique key for this client instance
84+
/// </summary>
85+
/// <remarks>
86+
/// We cannot pass JS objects to Blazor/C# so we use a unique key
87+
/// to reference each instance. The JS client will store the object
88+
/// under our key so we can reference it
89+
/// </remarks>
90+
private readonly string _key;
91+
92+
/// <summary>
93+
/// Name of the chatter
94+
/// </summary>
95+
private readonly string _username;
96+
97+
/// <summary>
98+
/// Flag to show if started
99+
/// </summary>
100+
private bool _started = false;
101+
102+
/// <summary>
103+
/// JS runtime from DI
104+
/// </summary>
105+
private readonly IJSRuntime _JSruntime;
106+
107+
108+
/// <summary>
109+
/// Start the SignalR client on JS
110+
/// </summary>
111+
public async Task Start()
112+
{
113+
if (!_started)
114+
{
115+
// the callback method for inbound messages
116+
const string assembly = "ServerSideBlazor";
117+
const string method = "ReceiveMessage";
118+
// invoke the JS interop start client method
119+
Console.WriteLine("ChatClient: calling Start()");
120+
var _ = await _JSruntime.InvokeAsync<object>("ChatClient.Start", _key, HUBURL, assembly, method);
121+
Console.WriteLine("ChatClient: Start returned");
122+
_started = true;
123+
124+
// register user
125+
await _JSruntime.InvokeAsync<object>("ChatClient.Register", _key, _username);
126+
}
127+
}
128+
129+
/// <summary>
130+
/// Handle an inbound message from a hub
131+
/// </summary>
132+
/// <param name="method">event name</param>
133+
/// <param name="message">message content</param>
134+
private void HandleReceiveMessage(string username, string message)
135+
{
136+
// raise an event to subscribers
137+
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(username, message));
138+
}
139+
140+
/// <summary>
141+
/// Event raised when this client receives a message
142+
/// </summary>
143+
/// <remarks>
144+
/// Instance classes should subscribe to this event
145+
/// </remarks>
146+
public event MessageReceivedEventHandler MessageReceived;
147+
148+
/// <summary>
149+
/// Send a message to the hub
150+
/// </summary>
151+
/// <param name="message">message to send</param>
152+
public async Task Send(string message)
153+
{
154+
// check we are connected
155+
if (!_started)
156+
throw new InvalidOperationException("Client not started");
157+
// send the message
158+
await _JSruntime.InvokeAsync<object>("ChatClient.Send", _key, _username, message);
159+
}
160+
161+
/// <summary>
162+
/// Stop the client (if started)
163+
/// </summary>
164+
public async Task Stop()
165+
{
166+
if (_started)
167+
{
168+
// disconnect the client
169+
await _JSruntime.InvokeAsync<object>("ChatClient.Stop", _key);
170+
_started = false;
171+
}
172+
}
173+
174+
/// <summary>
175+
/// Dispose of client
176+
/// </summary>
177+
public void Dispose()
178+
{
179+
Console.WriteLine("ChatClient: Disposing");
180+
// ensure we stop if connected
181+
if (_started)
182+
{
183+
Task.Run(async () =>
184+
{
185+
await Stop();
186+
}).Wait();
187+
}
188+
189+
// remove this key from the list of clients
190+
if (_clients.ContainsKey(_key))
191+
_clients.Remove(_key);
192+
}
193+
}
194+
195+
/// <summary>
196+
/// Delegate for the message handler
197+
/// </summary>
198+
/// <param name="sender">the SignalRclient instance</param>
199+
/// <param name="e">Event args</param>
200+
public delegate void MessageReceivedEventHandler(object sender, MessageReceivedEventArgs e);
201+
202+
/// <summary>
203+
/// Message received argument class
204+
/// </summary>
205+
public class MessageReceivedEventArgs : EventArgs
206+
{
207+
public MessageReceivedEventArgs(string username, string message)
208+
{
209+
Username = username;
210+
Message = message;
211+
}
212+
213+
/// <summary>
214+
/// Name of the message/event
215+
/// </summary>
216+
public string Username { get; set; }
217+
218+
/// <summary>
219+
/// Message data items
220+
/// </summary>
221+
public string Message { get; set; }
222+
223+
}
224+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.AspNetCore.SignalR;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Threading.Tasks;
8+
9+
namespace ServerSideBlazor
10+
{
11+
/// <summary>
12+
/// The SignalR hub
13+
/// </summary>
14+
public class ChatHub : Hub
15+
{
16+
/// <summary>
17+
/// connectionId-to-username lookup
18+
/// </summary>
19+
/// <remarks>
20+
/// Needs to be static as the chat is created dynamically a lot
21+
/// </remarks>
22+
private static readonly Dictionary<string, string> userLookup = new Dictionary<string, string>();
23+
24+
/// <summary>
25+
/// Send a message to all clients
26+
/// </summary>
27+
/// <param name="username"></param>
28+
/// <param name="message"></param>
29+
/// <returns></returns>
30+
public async Task SendMessage(string username, string message)
31+
{
32+
await Clients.All.SendAsync("ReceiveMessage", username, message);
33+
}
34+
35+
/// <summary>
36+
/// Register username
37+
/// </summary>
38+
/// <param name="username"></param>
39+
/// <returns></returns>
40+
public async Task Register(string username)
41+
{
42+
var currentId = Context.ConnectionId;
43+
if (!userLookup.ContainsKey(currentId))
44+
{
45+
// maintain a lookup of connectionId-to-username
46+
userLookup.Add(currentId, username);
47+
// re-use existing message for now
48+
await Clients.AllExcept(currentId).SendAsync("ReceiveMessage", username, $"{username} joined the chat");
49+
}
50+
}
51+
52+
/// <summary>
53+
/// Log connection
54+
/// </summary>
55+
/// <returns></returns>
56+
public override Task OnConnectedAsync()
57+
{
58+
Console.WriteLine("Connected");
59+
return base.OnConnectedAsync();
60+
}
61+
62+
/// <summary>
63+
/// Log disconnection
64+
/// </summary>
65+
/// <param name="e"></param>
66+
/// <returns></returns>
67+
public override async Task OnDisconnectedAsync(Exception e)
68+
{
69+
Console.WriteLine($"Disconnected {e?.Message}");
70+
// try to get connection
71+
string id = Context.ConnectionId;
72+
if (userLookup.TryGetValue(id, out string username))
73+
{
74+
userLookup.Remove(id);
75+
await Clients.AllExcept(Context.ConnectionId).SendAsync("ReceiveMessage", username, $"{username} has left the chat");
76+
}
77+
await base.OnDisconnectedAsync(e);
78+
}
79+
80+
81+
}
82+
}

0 commit comments

Comments
 (0)