I have been doing a lot of research on finding a working example of SignalR implementation where status updates are sent only to an instance of a browser tab. My application extracts web data, and that process takes a lot of time.
I have been able to send status updates, but only to all users that have the webpage open.
//Server side (works just fine)
public class SendCustomText : Hub
{
public string myStatus;
public void CurrentStatus()
{
var context = GlobalHost.ConnectionManager.GetHubContext<SendCustomText>();
context.Clients.All.setStatus(myStatus);
}
}
//Client side
<script type="text/javascript">
$(function () {
//Declare a proxy to the reference hub
var currentStatus = $.connection.sendCustomText;
currentStatus.client.setStatus = function (value) {
$('#signalr_french_status').text(value.toString());
}
$.connection.hub.start().done(function () {
$('pandora_french_panel_extract_button').click(function () {
currentStatus.server.send();
})
})
$.connection.hub.disconnected(function () {
$.connection.hub.start();
})
})
What I, however, need is a way to update progress only in a tab that is open. The logged-in user may open another tab and process another job, and that new tab should display its own progress.
I did try successfully getting the connection ID client side and then passing it through AJAX to the server code. I need your help with an example where connection ID is used in the hub.
context.Clients.Client(connectionID).setStatus();
I could solve the problem myself. (I had made a very basic error initially. The first time, I used public string myConnectionID, that is, without the static keyword.)
public class SendCustomText : Hub
{
public string myStatus;
public static string myConnectionID;
public void CurrentStatus()
{
var context = GlobalHost.ConnectionManager.GetHubContext<SendCustomText>();
context.Clients.Client(myConnectionID).setStatus(myStatus);
}
public override System.Threading.Tasks.Task OnConnected()
{
myConnectionID = Context.ConnectionId;
return base.OnConnected();
}
}
Related
EDITED: see at the bottom
I'm new to SignalR and trying to implement with it a simple scenario with Angular7 client using this library, and ASP.NET Core web API. All what I need is to use SignalR to notify the client about the progress of some lengthy operations in methods of the API controllers.
After a number of attempts, I got to a point where apparently the connection is established, but then when the long task starts running and sending messages, my client does not seem to receive anything, nor any traffic appears in web sockets (Chrome F12 - Network - WS).
I post here the details, which might also be useful to other newcomers (full source code at https://1drv.ms/u/s!AsHCfliT740PkZh4cHY3r7I8f-VQiQ). Probably I'm just making some obvious error, yet in the docs and googling around I could not find a code fragment essentially different from mine. Could anyone give a hint?
My start point for the server side was https://msdn.microsoft.com/en-us/magazine/mt846469.aspx, plus the docs at https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-2.2. I tried to create a dummy experimental solution with that.
My code snippets in form of a recipe follow.
(A) Server Side
1.create a new ASP.NET core web API app. No authentication or Docker, just to keep it minimal.
2.add the NuGet package Microsoft.AspNetCore.SignalR.
3.at Startup.cs, ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
// CORS
services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{
builder.AllowAnyMethod()
.AllowAnyHeader()
// https://github.com/aspnet/SignalR/issues/2110 for AllowCredentials
.AllowCredentials()
.WithOrigins("http://localhost:4200");
}));
// SignalR
services.AddSignalR();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
and the corresponding Configure method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
// CORS
app.UseCors("CorsPolicy");
// SignalR: add to the API at route "/progress"
app.UseSignalR(routes =>
{
routes.MapHub<ProgressHub>("/progress");
});
app.UseHttpsRedirection();
app.UseMvc();
}
4.add a ProgressHub class, which just derives from Hub:
public class ProgressHub : Hub
{
}
5.add a TaskController with a method to start some lengthy operation:
[Route("api/task")]
[ApiController]
public class TaskController : ControllerBase
{
private readonly IHubContext<ProgressHub> _progressHubContext;
public TaskController(IHubContext<ProgressHub> progressHubContext)
{
_progressHubContext = progressHubContext;
}
[HttpGet("lengthy")]
public async Task<IActionResult> Lengthy([Bind(Prefix = "id")] string connectionId)
{
await _progressHubContext
.Clients
.Client(connectionId)
.SendAsync("taskStarted");
for (int i = 0; i < 100; i++)
{
Thread.Sleep(500);
Debug.WriteLine($"progress={i}");
await _progressHubContext
.Clients
.Client(connectionId)
.SendAsync("taskProgressChanged", i);
}
await _progressHubContext
.Clients
.Client(connectionId)
.SendAsync("taskEnded");
return Ok();
}
}
(B) Client Side
1.create a new Angular7 CLI app (without routing, just to keep it simple).
2.npm install #aspnet/signalr --save.
3.my app.component code:
import { Component, OnInit } from '#angular/core';
import { HubConnectionBuilder, HubConnection, LogLevel } from '#aspnet/signalr';
import { TaskService } from './services/task.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
private _connection: HubConnection;
public messages: string[];
constructor(private _taskService: TaskService) {
this.messages = [];
}
ngOnInit(): void {
// https://codingblast.com/asp-net-core-signalr-chat-angular/
this._connection = new HubConnectionBuilder()
.configureLogging(LogLevel.Debug)
.withUrl("http://localhost:44348/signalr/progress")
.build();
this._connection.on("taskStarted", data => {
console.log(data);
});
this._connection.on("taskProgressChanged", data => {
console.log(data);
this.messages.push(data);
});
this._connection.on("taskEnded", data => {
console.log(data);
});
this._connection
.start()
.then(() => console.log('Connection started!'))
.catch(err => console.error('Error while establishing connection: ' + err));
}
public startJob() {
this.messages = [];
this._taskService.startJob('zeus').subscribe(
() => {
console.log('Started');
},
error => {
console.error(error);
}
);
}
}
Its minimalist HTML template:
<h2>Test</h2>
<button type="button" (click)="startJob()">start</button>
<div>
<p *ngFor="let m of messages">{{m}}</p>
</div>
The task service in the above code is just a wrapper for a function which calls HttpClient's get<any>('https://localhost:44348/api/task/lengthy?id=' + id).
EDIT 1
After some more experimenting, I came with these changes:
use .withUrl('https://localhost:44348/progress') as suggested. It seems that now it no more triggers 404. Note the change: I replaced http with https.
do not make the API method async as it seems that the await are not required (i.e. set the return type to IActionResult and remove async and await).
With these changes, I can now see the expected log messages on the client side (Chrome F12). Looking at them, it seems that the connection gets bound to a generated ID k2Swgcy31gjumKtTWSlMLw:
Utils.js:214 [2019-02-28T20:11:48.978Z] Debug: Starting HubConnection.
Utils.js:214 [2019-02-28T20:11:48.987Z] Debug: Starting connection with transfer format 'Text'.
Utils.js:214 [2019-02-28T20:11:48.988Z] Debug: Sending negotiation request: https://localhost:44348/progress/negotiate.
core.js:16828 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
Utils.js:214 [2019-02-28T20:11:49.237Z] Debug: Selecting transport 'WebSockets'.
Utils.js:210 [2019-02-28T20:11:49.377Z] Information: WebSocket connected to wss://localhost:44348/progress?id=k2Swgcy31gjumKtTWSlMLw.
Utils.js:214 [2019-02-28T20:11:49.378Z] Debug: Sending handshake request.
Utils.js:210 [2019-02-28T20:11:49.380Z] Information: Using HubProtocol 'json'.
Utils.js:214 [2019-02-28T20:11:49.533Z] Debug: Server handshake complete.
app.component.ts:39 Connection started!
app.component.ts:47 Task service succeeded
So, it might be the case that I get no notification because my client ID does not match the ID assigned by SignalR (from the paper quoted above I had the impression that it was my duty to provide an ID, given that it is an argument of the API controller). Yet, I cannot see any available method or property in the connection prototype allowing me to retrieve this ID, so that I can pass it to the server when launching the lengthy job. Could this be the reason of my issue? If this is so, there should be a way of getting the ID (or setting it from the client side). What do you think?
It seems I've finally found it. The issue was probably caused by the wrong ID, so I started looking for a solution. A post (https://github.com/aspnet/SignalR/issues/2200) guided me to the usage of groups, which seems the recommended solution in these cases. So, I changed my hub so that it automatically assign the current connection ID to a "progress" group:
public sealed class ProgressHub : Hub
{
public const string GROUP_NAME = "progress";
public override Task OnConnectedAsync()
{
// https://github.com/aspnet/SignalR/issues/2200
// https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/working-with-groups
return Groups.AddToGroupAsync(Context.ConnectionId, "progress");
}
}
Now, my API controller method is:
[HttpGet("lengthy")]
public async Task<IActionResult> Lengthy()
{
await _progressHubContext
.Clients
.Group(ProgressHub.GROUP_NAME)
.SendAsync("taskStarted");
for (int i = 0; i < 100; i++)
{
Thread.Sleep(200);
Debug.WriteLine($"progress={i + 1}");
await _progressHubContext
.Clients
.Group(ProgressHub.GROUP_NAME)
.SendAsync("taskProgressChanged", i + 1);
}
await _progressHubContext
.Clients
.Group(ProgressHub.GROUP_NAME)
.SendAsync("taskEnded");
return Ok();
}
And of course I updated the client code accordingly, so that it does no more have to send an ID when invoking the API method.
Full demo repository available at https://github.com/Myrmex/signalr-notify-progress.
You set the route for the hub as /progress, but then you're attempting to connect to /signalr/progress, which is going to be a 404. If you open the developer console, you should have an connection error there telling you as much.
Just wanted to add that OP was on the right track with the connection ID.
I send it along optionally in the form data.
I'm just reporting progress while uploading to AWS and I handle with SignalR like this:
Controller
[HttpPost("MyPostRoute")]
public async Task<ActionResult> UploadFiles([FromForm] List<IFormFile> files, [FromForm] string? connectionId)
{
await _logic.UploadFiles(files, connectionId);
return Ok();
}
Logic
public async Task<bool> UploadFiles(List<IFormFile> files, string? connectionId)
{
foreach (var file in files)
{
Guid fileGuid = Guid.NewGuid();
string extension = Path.GetExtension(file.FileName);
using (var transferUtility = new TransferUtility(_awsClient))
{
var request = new TransferUtilityUploadRequest()
{
BucketName = _configuration["AwsBucket"],
Key = fileGuid + extension,
InputStream = file.OpenReadStream()
};
if (connectionId != null)
{
AwsFileProgress progress = new()
{
ConnectionId = connectionId,
FileName = file.FileName
};
request.UploadProgressEvent += (sender, e) => Request_UploadProgressEvent(sender, e, progress);
}
await transferUtility.UploadAsync(request);
}
}
return true;
}
private async void Request_UploadProgressEvent(object? sender, UploadProgressArgs e, AwsFileProgress progress)
{
progress.ProgressPercent = e.PercentDone;
await _hub.Clients.Client(progress.ConnectionId).SendAsync("AWSProgress", progress);
}
I made a model for the progress stuff:
public class AwsFileProgress
{
public string ConnectionId { get; set; }
public string FileName { get; set; }
public int ProgressPercent { get; set; } = 0;
}
And finally, the front-end:
import { Injectable } from '#angular/core';
import * as signalR from "#microsoft/signalr";
import { environment } from "#environment";
import { AwsFileProgress } from "#models/signalr/awsFileProgress";
#Injectable({
providedIn: 'root',
})
export class AwsSignalRService {
private hubConnection!: signalR.HubConnection
public data!: AwsFileProgress;
public startConnection = () => {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(`${environment.signalRhubRoot}/awsprogresshub`)
.build();
this.hubConnection
.start()
.then(() => { console.log('AWS SignalR Service Connection started') })
.catch(err => console.log('Error while starting AWS SignalR Service WebSocket: ' + err))
}
/////////////////////////////////////////////////////////
//This is all you have to do to get the connection ID:
public getConnectionId() : string | null {
return this.hubConnection.connectionId;
}
/////////////////////////////////////////////////////////
public addAwsProgressListener = () => {
this.hubConnection.on('AWSProgress', (awsFileProgress : AwsFileProgress) => {
this.data = awsFileProgress;
console.log(awsFileProgress.progressPercent);
});
}
}
Then the actual component it's injected into:
import { AwsSignalRService } from "#shared/services/aws-signalr-service";
#Component({
selector: 'settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
})
export class SettingsComponent implements OnInit {
constructor(
private awsSignalRService: AwsSignalRService
) {
awsSignalRService.startConnection();
awsSignalRService.addAwsProgressListener();
}
^ Whatever is in our progress listener is handled here.
Making the request (and reporting client-side upload progress) is handled like this.
Note that I just use a basic (change) handler on a file input (<input type="file" (change)="fileUploadTest($event)"/>)
fileUploadTest(e: any) {
const formData = new FormData();
var cId = this.awsSignalRService.getConnectionId();
for(let file of e.target.files as File[]) {
formData.append('files', file, file.name);
};
if(cId != null)
formData.append('connectionId',cId);
const url: string = `${environment.ApiRoot}/MyPostRoute`;
this.http.post<any>(url,formData, {
reportProgress: true,
observe: 'events'
}).subscribe({
next: (response) => console.log(response),
error: (error) => console.log(error)
});
}
And the results in console of my API uploading data to AWS:
I have Xamarin.Forms frontend app and Web API .NET Core backend and I use SignalR. I am able to make chat - userA sends a message and all connected users can see it.
And now I need to display specific page to all connected users after userA clicks the button. I have created DisplayPageToAllConnectedUsers SignalR client method but I don’t know how to tell there to display specific page to all connected users. Thanks for any advice
Xamarin.Forms - SignalRClient.cs
public class SignalRClient : INotifyPropertyChanged
{
private HubConnection Connection;
public delegate void MessageReceived(string username, string message);
public event MessageReceived OnMessageReceived;
public SignalRClient(string url)
{
Connection = new HubConnectionBuilder()
.WithUrl(url)
.Build();
Connection.On<string, string>("ReceiveMessage", (username, text) =>
{
OnMessageReceived?.Invoke(username, text);
});
Connection.On("DisplayPageToAllConnectedUsers", () =>
{
//??display specific page to all connected users??
});
}
public void SendMessage(string username, string text)
{
Connection.InvokeAsync("SendMessage", username, text);
}
public void DisplayPageToAllConnectedUsers()
{
Connection.InvokeAsync("DisplayPageToAllConnectedUsers");
}
public Task Start()
{
return Connection.StartAsync();
}
Backend - ChatHub.cs
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task DisplayPageToAllConnectedUsers()
{
await Clients.All.SendAsync("DisplayPageToAllConnectedUsers");
}
}
* Edit *
Instead of MainPage I have to put SignalRClient and it is working if I use MessagingCenter without using SignalR. See bellow
In SignalRClient.cs
public void DisplayPageToAllConnectedUsers()
{
//Connection.InvokeAsync("DisplayPageToAllConnectedUsers");
MessagingCenter.Send<SignalRClient>(this, "MyPage"); //working
}
But it is not working as soon as I use SignalR
Connection.On("DisplayPageToAllConnectedUsers", () =>
{
MessagingCenter.Send<SignalRClient>(this, "MyPage");
});
You can use the MessagingCenter, to send a message to your MainPage, to redirect the application to whatever page you want. For example:
In SignalRClient.cs
MessagingCenter.Send<MainPage> (this, "ChangeToYourPage");
In your MainPage.xaml.cs, you subscribe to this event:
MessagingCenter.Subscribe<MainPage> (this, "ChangeToYourPage", (sender) => {
// do something whenever the "ChangeToYourPage" message is sent
Navigation.PushAsync(YourNewPage);
});
I have a full engine that relies on abstractions based on user interactions. This works great with WPF/Xamarin app, cause I can implements this abstractions with window/form.
I have a little problem for porting this engine into ASP MVC.
A simple example can be show as this.
Abstraction interface (simplified)
public interface IQuestionBox
{
Task<bool> ShowYesNoQuestionBox(string message);
}
For WPF, it's really simple, I implement this interface as return the result of a window by calling ShowDialog().
In a simple business class, I can have this kind of calls (simplified) :
public async Task<string> GetValue(IQuestionBox qbox)
{
if(await qbox.ShowYesNoQuestionBox("Question ?"))
{
return "Ok";
}
return "NOk";
}
I really don't see how can I implement this kind of behavior in ASP, due to stateless of HTTP, knowing that this kind of call can be as various as domain/business need. The way I think it should be done is by returning a PartialView to inject into popup, but I don't see how to do this without breaking all the process ...
Anyone has ever done this ?
as I've said, I strongly doesn't recommend this pratice, but its possible, bellow the code that allows to do it, let's go:
To become it's possible I abused the use from TaskCompletionSource, this class allow us to set manually result in a task.
First we need to create a structure to encapsulate the process:
public class Process
{
// this dictionary store the current process running status, you will use it to define the future answer from the user interaction
private static Dictionary<string, Answare> StatusReport = new Dictionary<string, Answare>();
// this property is the secret to allow us wait for the ShowYesNoQuestion call, because til this happen the server doesn't send a response for the client.
TaskCompletionSource<bool> AwaitableResult { get; } = new TaskCompletionSource<bool>(true);
// here we have the question to interact with the user
IQuestionBox QuestionBox { get; set; }
// this method, receive your bussiness logical the receive your question as a parameter
public IQuestionBox Run(Action<IQuestionBox> action)
{
QuestionBox = new QuestionBox(this);
// here we create a task to execute your bussiness logical processment
Task.Factory.StartNew(() =>
{
action(QuestionBox);
});
// and as I said we wait the result from the processment
Task.WaitAll(AwaitableResult.Task);
// and return the question box to show the messages for the users
return QuestionBox;
}
// this method is responsable to register a question to receive future answers, as you can see, we are using our static dictionary to register them
public void RegisterForAnsware(string id)
{
if (StatusReport.ContainsKey(id))
return;
StatusReport.Add(id, new Answare()
{
});
}
// this method will deliver an answer for this correct context based on the id
public Answare GetAnsware(string id)
{
if (!StatusReport.ContainsKey(id))
return Answare.Empty;
return StatusReport[id];
}
// this method Releases the processment
public void Release()
{
AwaitableResult.SetResult(true);
}
// this method end the process delivering the response for the user
public void End(object userResponse)
{
if (!StatusReport.ContainsKey(QuestionBox.Id))
return;
StatusReport[QuestionBox.Id].UserResponse(userResponse);
}
// this method define the answer based on the user interaction, that allows the process continuing from where it left off
public static Task<object> DefineAnsware(string id, bool result)
{
if (!StatusReport.ContainsKey(id))
return Task.FromResult((object)"Success on the operation");
// here I create a taskcompletaionsource to allow get the result of the process, and send for the user, without it would be impossible to do it
TaskCompletionSource<object> completedTask = new TaskCompletionSource<object>();
StatusReport[id] = new Answare(completedTask)
{
HasAnswared = true,
Value = result
};
return completedTask.Task;
}
}
After that the question implementation
public interface IQuestionBox
{
string Id { get; }
Task<bool> ShowYesNoQuestionBox(string question);
HtmlString ShowMessage();
}
class QuestionBox : IQuestionBox
{
Process CurrentProcess { get; set; }
public string Id { get; } = Guid.NewGuid().ToString();
private string Question { get; set; }
public QuestionBox(Process currentProcess)
{
CurrentProcess = currentProcess;
CurrentProcess.RegisterForAnswer(this.Id);
}
public Task<bool> ShowYesNoQuestionBox(string question)
{
Question = question;
CurrentProcess.Release();
return AwaitForAnswer();
}
public HtmlString ShowMessage()
{
HtmlString htm = new HtmlString(
$"<script>showMessage('{Question}', '{Id}');</script>"
);
return htm;
}
private Task<bool> AwaitForAnswer()
{
TaskCompletionSource<bool> awaitableResult = new TaskCompletionSource<bool>(true);
Task.Factory.StartNew(() =>
{
while (true)
{
Thread.Sleep(2000);
var answare = CurrentProcess.GetAnswer(this.Id);
if (!answare.HasAnswered)
continue;
awaitableResult.SetResult(answare.Value);
break;
}
});
return awaitableResult.Task;
}
}
The differences for yours implementaion are:
1 - I create an Identifier to know for who I have to send the aswer, or just to stop the process.
2 - I receive a Process as parameter, because this allows us to call the method
CurrentProcess.Release(); in ShowYesNoQuestion, here in specific, releases the process to send the response responsable to interact with the user.
3 - I create the method AwaitForAnswer, here one more time we use from the TaskCompletionSource class. As you can see in this method we have a loop, this loop is responsable to wait for the user interaction, and til receive a response it doesn't release the process.
4 - I create the method ShowMessage that create a simple html script alert to simulate the user interaction.
Then a simple process class as you should be in your bussiness logical:
public class SaleService
{
public async Task<string> GetValue(IQuestionBox qbox)
{
if (await qbox.ShowYesNoQuestionBox("Do you think Edney is the big guy ?"))
{
return "I knew, Edney is the big guy";
}
return "No I disagree";
}
}
And then the class to represent the user answer
public class Answer
{
// just a sugar to represent empty responses
public static Answer Empty { get; } = new Answer { Value = true, HasAnswered = true };
public Answer()
{
}
// one more time abusing from TaskCompletionSource<object>, because with this guy we are abble to send the result from the process to the user
public Answer(TaskCompletionSource<object> completedTask)
{
CompletedTask = completedTask;
}
private TaskCompletionSource<object> CompletedTask { get; set; }
public bool Value { get; set; }
public bool HasAnswered { get; set; }
// this method as you can see, will set the result and release the task for the user
public void UserResponse(object response)
{
CompletedTask.SetResult(response);
}
}
Now we use all the entire structure create for this:
[HttpPost]
public IActionResult Index(string parametro)
{
// create your process an run it, passing what you want to do
Process process = new Process();
var question = process.Run(async (questionBox) =>
{
// we start the service
SaleService service = new SaleService();
// wait for the result
var result = await service.GetValue(questionBox);
// and close the process with the result from the process
process.End(result);
});
return View(question);
}
// here we have the method that deliver us the user response interaction
[HttpPost]
public async Task<JsonResult> Answer(bool result, string id)
{
// we define the result for an Id on the process
var response = await Process.DefineAnswer(id, result);
// get the response from process.End used bellow
// and return to the user
return Json(response);
}
and in your view
<!-- Use the question as the model page -->
#model InjetandoInteracaoComUsuario.Controllers.IQuestionBox
<form asp-controller="Home" asp-action="Index">
<!-- create a simple form with a simple button to submit the home -->
<input type="submit" name="btnDoSomething" value="All about Edney" />
</form>
<!-- in the scripts section we create the function that we call on the method ShowMessage, remember?-->
<!-- this method request the action answer passing the questionbox id, and the result from a simple confirm -->
<!-- And to finalize, it just show an alert with the process result -->
#section scripts{
<script>
function showMessage(message, id) {
var confirm = window.confirm(message);
$.post("/Home/Answer", { result: confirm, id: id }, function (e) {
alert(e);
})
}
</script>
#Model?.ShowMessage()
}
As I've said, I realy disagree with this pratices, the correct should to write a new dll, to support the web enviroment, but I hope it help you.
I put the project on github to you can download an understand all the solution
I realy hope it can help you
You can create a web socket connection from client side to server side. And work with front-end content with web socket request. It could be implemented as following:
Client side:
$app = {
uiEventsSocket : null,
initUIEventsConnection : function(url) {
//create a web socket connection
if (typeof (WebSocket) !== 'undefined') {
this.uiEventsSocket = new WebSocket(url);
} else if (typeof (MozWebSocket) !== 'undefined') {
this.uiEventsSocket = new MozWebSocket(url);
} else {
console.error('WebSockets unavailable.');
}
//notify if there is an web socket error
this.uiEventsSocket.onerror = function () {
console.error('WebSocket raised error.');
}
this.uiEventsSocket.onopen = function () {
console.log("Connection to " + url + " established");
}
//handling message from server side
this.uiEventsSocket.onmessage = function (msg) {
this._handleMessage(msg.data);
};
},
_handleMessage : function(data){
//the message should be in json format
//the next line fails if it is not
var command = JSON.parse(data);
//here is handling the request to show prompt
if (command.CommandType == 'yesNo') {
var message = command.Message;
var result = confirm(message);
//not sure that bool value will be successfully converted
this.uiEventsSocket.send(result ? "true" : "false");
}
}
}
And init it from ready or load event:
window.onload = function() { $app.initUIEventsConnection(yourUrl); }
Note that you url should begin with ws:// instead of http:// and wss:// instead of https:// (Web Sockets and Web Sockets Secure).
Server side.
Here is a good article for how to setup web sockets at asp.net core application or you could find another one. Note that you should group web socket connections from single user and if you want to send a message to the concrete user, you should send message for every connection from this user.
Every web socket you should accept with AcceptWebSocketAsync() method call and then add instance of this web socket to singleton, which contains a set of web sockets connection groupped by user.
The following class will be used to operate commands:
public class UICommand
{
public string CommandType { get; set; }
public string Message { get; set; }
public Type ReturnType { get; set; }
}
And a full code of singleton for handling sockets
public class WebSocketsSingleton
{
private static WebSocketsSingleton _instance = null;
//here stored web sockets groupped by user
//you could use user Id or another marker to exactly determine the user
private Dictionary<string, List<WebSocket>> _connectedSockets;
//for a thread-safety usage
private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();
public static WebSocketsSingleton Instance {
get {
if (this._instance == null)
{
this._instance = new WebSocketsSingleton();
}
return this._instance;
}
}
private WebSocketsSingleton()
{
this._connectedSockets = new Dictionary<string, List<WebSocket>>();
}
/// <summary>
/// Adds a socket into the required collection
/// </summary>
public void AddSocket(string userName, WebSocket ws)
{
if (!this._connectedSockets.ContainsKey(userName))
{
Locker.EnterWriteLock();
try
{
this._connectedSockets.Add(userName, new List<WebSocket>());
}
finally
{
Locker.ExitWriteLock();
}
}
Locker.EnterWriteLock();
try
{
this._connectedSockets[userName].Add(ws);
}
finally
{
Locker.ExitWriteLock();
}
}
/// <summary>
/// Sends a UI command to required user
/// </summary>
public async Task<string> SendAsync(string userName, UICommand command)
{
if (this._connectedSockets.ContainsKey(userName))
{
var sendData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(command));
foreach(var item in this._connectedSockets[userName])
{
try
{
await item.SendAsync(new ArraySegment<byte>(sendData), WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (ObjectDisposedException)
{
//socket removed from front end side
}
}
var buffer = new ArraySegment<byte>(new byte[1024]);
var token = CancellationToken.None;
foreach(var item in this._connectedSockets[userName])
{
await Task.Run(async () => {
var tempResult = await item.ReceiveAsync(buffer, token);
//result received
token = new CancellationToken(true);
});
}
var resultStr = Encoding.Utf8.GetString(buffer.Array);
if (command.ReturnType == typeof(bool))
{
return resultStr.ToLower() == "true";
}
//other methods to convert result into required type
return resultStr;
}
return null;
}
}
Explanation:
on establishing connection from web socket it will be added with
AddSocket method
on sending request to show a message, the required command will be passed into SendAsync method
the command will be serialized to JSON (using Json.Net, however you could serialize in your way) and send to all sockets, related to the required user
after the command sent, application will wait for respond from front end side
the result will be converted to required type and sent back to your IQuestionBox
In the web socket handling your should add some kind of the following code:
app.Use(async (http, next) =>
{
if (http.WebSockets.IsWebSocketRequest)
{
var webSocket = await http.WebSockets.AcceptWebSocketAsync();
var userName = HttpContext.Current.User.Identity.Name;
WebSocketsSingleton.Instance.AddSocket(userName, webSocket);
while(webSocket.State == WebSocketState.Open)
{
//waiting till it is not closed
}
//removing this web socket from the collection
}
});
And your method implementation of ShowYesNoQuestionBox should be kind of following:
public async Task<bool> ShowYesNoQuestionBox(string userName, string text)
{
var command = new UICommand
{
CommandType = "yesNo",
Message = text,
ReturnType = typeof(bool)
};
return await WebSocketsSingleton.Instance.SendAsync(string userName, command);
}
Note that there should be added userName to prevent sending the same message to all of the connected users.
WebSocket should create the persistent connection between server and client sides, so you could simply send commands in two ways.
I am kindly new to Asp.Net Core, so the final implementation could be a bit different from this.
It's actually much the same, except your UI is sort of disconnected and proxied with the HTTP protocol for the most part.
you essentially need to build the same code as your WPF code but then in the browser construct ajax calls in to the controller actions to apply your logic.
To clarify ...
so lets say you are building up a process over a series of questions that based on the users answer you put different steps in to the process.
You can either ...
build the process in the database
build it in session on the server
build it on the client as a js object
then do a post for execution ofthe constructed process.
think of the "statelessness" as a series of short interactions, but the state you keep between them can be done either on the client, in a db or in the users logged in session on the web server.
In your controller you can add an ActionResult that will give you the html response to your jquery modal popup request. Here is an example
public class MController : Controller {
public ActionResult doWork(requirement IQuestionBox)
{
// model is already modelBound/IOC resolved
return PartialView("_doWork", requirement );
}
}
//scripts
$(function(){
$.ajax({
url:"/m/doWork",
type:"get",
success:function(data){
$modal.html(data); // bind to modal
}
});
});
Apologies for not fully understanding the question.
hope this helps!
I am developing 2 applications, the first being a C# console application and the other an Asp.net web application. I am using SignalR to connect the two.
This is my C# console application (Client)
public class RoboHub
{
public static IHubProxy _hub;
public RoboHub()
{
StartHubConnection();
_hub.On("GetGoals", () => GetGoals());
_hub.On("PrintMessageRobot", x => PrintMessageRobot(x));
Thread thread = new Thread(MonitorHubStatus);
thread.Start();
}
public void GetGoals()
{
//TODO: Does stuff
}
public void PrintMessageRobot(string msg)
{
Console.WriteLine(msg);
}
public void StartHubConnection()
{
Console.WriteLine("Robo Hub Starting");
string url = #"http://localhost:46124/";
var connection = new HubConnection(url);
_hub = connection.CreateHubProxy("WebHub");
connection.Start().Wait();
Console.WriteLine("Robo Hub Running");
}
public void MonitorHubStatus()
{
while (true)
{
Thread.Sleep(1000);
_hub.Invoke("Ping", "ping").Wait();
Console.WriteLine("WebHub Pinged : " + DateTime.Now);
}
}
}
When the console application runs, it creates an instance of the RoboHub class. Which in turn starts a connection to the SignalR hub and on a separate thread starts the method MonitorHubStatus which is something I implemented to check if the C# console application client is still actively connected to the hub.
This is my Web hub (within the Asp.net Web application)
public class WebHub : Hub
{
/// <summary>
/// This method should be called by the Web Clients.
/// This method should call the method on the robot clients.
/// </summary>
public void GetGoalsHub()
{
lock (UserHandler.Connections)
{
if (UserHandler.Connections.Any(connection => connection.Contains("Robot")))
{
Clients.All.GetGoals();
}
}
//TODO add Error method to call on the client
}
/// <summary>
/// Override Methods
/// </summary>
/// <returns></returns>
public override Task OnConnected()
{
lock (UserHandler.Connections)
{
//Find out who is connecting based on the User-Agent header
var query = (from r in Context.Headers
where r.Key == "User-Agent"
select r).SingleOrDefault().ToString();
if (query.Contains("SignalR.Client.NET45"))
{
UserHandler.Connections.Add("Robot : " + Context.ConnectionId);
}
else
{
UserHandler.Connections.Add("Web Application : " + Context.ConnectionId);
GetGoalsHub();
}
}
Clients.All.UpdateConnections(UserHandler.Connections);
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
lock (UserHandler.Connections)
{
for (int i = 0; i < UserHandler.Connections.Count; i++)
{
if (UserHandler.Connections[i].Contains(Context.ConnectionId))
{
UserHandler.Connections.Remove(UserHandler.Connections[i]);
}
}
}
Clients.All.UpdateConnections(UserHandler.Connections);
return base.OnDisconnected(stopCalled);
}
public void Ping(string msg)
{
Clients.All.PrintMessageRobot("Pong : " + DateTime.Now);
}
}
public static class UserHandler
{
public static List<string> Connections = new List<string>();
}
Currently the 2 applications seem to work for a time, until after a while this error randomly appears:
Connection started reconnecting before invocation result was received.
Further more should the web hub call any other method on the C# console client such as the GetGoals method. The 'Ping Pong' method freezes and after time a similar exception is thrown. throughout this the web client continues to function perfectly and the web client can communicate back and forth with the hub server.
Can anyone suggest what the issue could be?
Edit: Further investigation leads me to believe it to be something to do with threading, however it is difficult to find the source of the issues.
The problem is with the invoke call:
_hub.Invoke("MethodName", "Parameters").Wait();
Here I am telling it to wait for a response however I did not program any reply mechanism in the web server.
This error was fix by switching to:
_hub.Invoke("MethodName", "Parameters");
Now it follows a 'fire and forget' methodology and it now no longer gets the error. Should anyone else get this error be sure to check whether you need a response or not.
You will get the same error message if the data being sent to server side is a 'Non-serializable' (e.g.) List of business objects which don't have [Serializable] attribute
I got this same exception when the payload was to big. I fixed it by changing the following line, in the Startup declaration for signlr.net. It removes the size limit
GlobalHost.Configuration.MaxIncomingWebSocketMessageSize = null;
I defined 2 separate hub in my project, Which is seem they parse into single proxy... and i'm not against it, let it be.
Next i defined my client, the time i had one client i had no issue with it,
but now that i have two hub, and both come to work at a single page, once the first hub start it work well, request goes to server and back.
but once i call the partial page, which connect and talk to second hub, it goes to connection.start, but it wont break on server, mean server is not notified of this action... . now can any one help me?
Controller1:
var app = angular.module("chatApplication", []);
var myHub = $.connection.chatHub;
app.controller("ChatController", [
"$scope", "$timeout", "$templateCache",
function ($scope, $timeout, $templateCache) {
...
$scope.RegisterClientMethod(myHub);
myHub.connection.start().done(function () {
//Already uses OnConnect Override
//TODO: Link Events To Required Buttons And Actions
//like: $(x).click(fnX);
//TODO: Call Started Events
//myHub.server.hello();
}).fail(function (e) {
$scope.connectionMessage = "Connection Failed" + e.toString();
$scope.$apply();
});
}
]);
Controller2:
var myUserHub = $.connection.userHub;
app.controller("UserController", [
"$scope", "$timeout", "$templateCache",
function($scope, $timeout, $templateCache) {
...
$scope.RegisterClientMethod(myUserHub);
$scope.RegisterWatchers();
myUserHub.server.getUsers();
myUserHub.connection.start().done(function () {
//Since Connection is already open, by chatHub, we cannot relay on that
myUserHub.server.getUsers();
}).fail(function (e) {
$scope.connectionMessage = "Connection Failed" + e.toString();
$scope.$apply();
});
}
]);
Hub1:
namespace SignalRChatSystem.Hubs
{
[ChatAuthorize]
public class ChatHub : Hub
{
...
public override Task OnConnected()
{
//client.doSomething
return base.OnConnected();
}
...
}
}
Hub2:
namespace SignalRChatSystem.Hubs
{
[UserAuthorize]
public class UserHub : Hub
{
...
public override Task OnConnected()
{
//client.doSomething
return base.OnConnected();
}
...
}
}
Mapping
[assembly: OwinStartupAttribute(typeof(SignalRChatSystem.Startup))]
namespace SignalRChatSystem
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
app.MapSignalR();
}
}
}
You should call $.connection.hub.start() only once, even if using multiple hubs. See: http://www.asp.net/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client#establishconnection
If you are not sure if connection was started earlier on not, you could check before starting it, using the connectionState object.
$.signalR.connectionState
Object {connecting: 0, connected: 1, reconnecting: 2, disconnected: 4}
So your start method could be like this:
if ($.connection.hub && $.connection.hub.state === $.signalR.connectionState.disconnected) {
$.connection.hub.start();
//...
}
If your connection is already open, you can call directly what is in the .done() { body.
Maybe you can check before:
if ($.connection.hub && $.connection.hub.state === $.signalR.connectionState.connected) {
// ... logic for second hub here
}