stop method on hub doesn't fire onDisconnect - c#

When a user logout from my web application, I explicitly end its hub's connection with the following code
$.connection.hub.stop();
On the server side, I would expect that my implementation of the Hub class would instantly reach my override of OnDisconnected() method wich is not the case. The method ends up by being called but only when the disconnection timeout is reached. What should I do to be notified instantly server side that the stop method was call on my client's hub ?

I struggled a few hours with this. I am using SignalR 2
On a basic script you need to define at least one client callback for events to be raised on the hub..... I eventually found this while running an example from here.
chat = $.connection.chatHub;
// Need at least one callback for events to be raised on the hub
chat.client.void = function () { };
$.connection.hub.logging = true;
$.connection.hub.start(); //.done(function () {
Now, after all that digging, my breakpoints finally get hit on the OWIN C# server.
public override Task OnConnected()
{
//logic code removed for brevity
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
//logic code removed for brevity
return base.OnDisconnected(stopCalled);
}

Scenario you've described should work.
If i were you, i would check if your override is correctly done plus i'd check if i have the connection at the moment of calling .stop() on the client.
You might want to do something like this to get some additional info:
$.connection.hub.stop()
.done(function(){ console.log('done'); })
.fail(function(){ console.log('Could not stop!'); });
});
SignalR docs about connections management

ppumkin, thanks for your answer!
I've faced same issue, the only difference is that we don't use autoproxies for our hubs.
So, if you create hub proxy in js on your own and expect OnDisconnected event on the hub without callbacks tied - add fake event subscription like in this code sample:
export default class MyHubProxy{
constructor(connection){
this._myHub = connection.createHubProxy('MyHub');
this._myHub.on('fake_event', function(){});
}
...
}

Related

SignalR: GetHubContext does not call client methods, but referencing the hub directly does

I had a troubling issue dealing with SignalR (v 2.4.1) hubs last week, and despite doing enough by the documents, I could not broadcast messages without hacking my way through it. For some extra context, this is a self hosted (Owin) hub attached to a windows service.
MSDN documents, and runtime errors (Using a Hub instance not created by the HubPipeline is unsupported), suggest that we are supposed to get the hub context by calling GlobalHost.ConnectionManager.GetHubContext<ThisTypeOfHub>(), and at that point we can make calls to the client.
(https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-server#callfromoutsidehub)
When I originally set up calls to the clients, I was doing it this way:
public void OnDisplayMessage(string message)
{
var hub = GlobalHost.ConnectionManager.GetHubContext<ThisTypeOfHub>();
hub.Clients.All.BroadcastToClient(message);
}
The browsers that contained the method and had connected to signalR never got called, however. The clients could call server methods, and even the server methods would send out a callback which the clients responded to, but the hub context could never call out to the client methods when called from outside the hub. In the end, I directly brought back the hub reference in the IoC container, and called out to clients using that, as shown below.
public class LogicWithUI : Logic
{
Hub hub;
public LogicWithUI(IDependencyInjectionContainer container)
{
this.hub = container.Resolve<ThisTypeOfHub>(); // ThisTypeOfHub inherits from Hub
}
public class OnDisplayMessage(string message)
{
try
{
this.hub.Clients.All.DisplayMessage(string);
}
catch (Exception)
{
//do nothing, no webpage has connected yet
}
}
}
This way it is finally working fine (you can see the catch placed there for when an error occurs -- only when no webpages have connected to this yet), but it doesn't make sense.
What would cause the HubContext called from outside the hub to not actually broadcast the method?
Why would the unsupported error get thrown only when no clients are connected?
Are there any obvious mistakes I'm overlooking here?
The primary goal here is to have a functioning product, but I also want to do it the right/documented way. It's a little confusing when that way isn't working.
I also understand I might have left out some important details regarding the SignalR configuration, I can answer any follow up questions, but wanted to start with the basic explanation.

Pass a parameter value to the client

I have a button, that when clicked will/should notify the server. The server will then save a value to the database. If all went well, it should return true, otherwise return false.
I instantiate a hub in my view
var signalRhub = $.connection.hubSignalR;
Start the connection:
$.connection.hub.start().done(function () {
$("#submitBut").click(function () {
signalRhub.server.cardAdded();
});
});
Define the function that will be used by the server to return the boolean value:
signalRhub.client.cardAddedRes = function (isSuccess) {
alert("From server: " + isSuccss);
}
My Hub class:
public class HubSignalR : Hub
{
public bool isSuccess = false; <-- Will be set from controller
public void CardAdded()
{
Clients.Caller.CardAddedRes(isSuccess); <-- Notice the isSuccess
}
}
My problem is that the isSuccess value is coming from my controller, that interacts with the model/database.
So I get the error:
Using a Hub instance not created by the HubPipeline is unsupported.
I tried using: GlobalHost.ConnectionManager.GetHubContext<HubSignalR>()
but I can't make it work.
Here is the relevant code in my controller:
private HubSignalR signalR = new HubSignalR(); <-- Field variable
[HttpPost]
public ActionResult AttachCard(Card model, int MemberID)
{
var hub = GlobalHost.ConnectionManager.GetHubContext<HubSignalR>();
...
//We saved to the database, so we call the client function with bool = true
hub.Clients.All.CardAdded(true); <-- Actually I want to send to one client, NOT ALL
//Something like hub.Clients.Caller.CardAdded();
}
I'm forced to make the isSuccess field in my: HubSignalR class, since I need to return that as the parameter from my controller. But when the button is clicked, this value has not yet been set (I think).
I can see from the debugger, that I do reach: signalRhub.server.cardAdded();
But the server never responds, so I don't reach this function:
signalRhub.client.cardAddedRes = function (isSuccess) {
alert("From server: " + isSuccss);
}
I don't really get to call the CardAdded() method from my controller, cus of the GlobalHost.ConnectionManager.GetHubContext. But you can see
If you got a nicer solution than what I'm trying to do, please tell. I'm total new with SignalR and fairly new with ASP.net MVC
Hope I got the problem right: you want to set isSuccess in controller and send it to specific client, but don't reach specific client from outside the hub?
As to "how":
You should find a way to identify your client, i.e. implement some kind of authentication. More on this here: Authentication in .net core When clients connect to SignalR, they get a connection ID. You can map the connection ID to real client identity. More info here: Mapping clients to connections
Than in you server method:
Get authenticated client identity
Get signalR hub context
Map client identity to existing signalR connection ID
Send message to that signalR connection
As I can see from you code you might be working on some personal-use/exploratory project and probably not interested in implementing authentication/don't care about security. You could get signalR connection ID in your client after connecting through $.connection.hub.id. Than you could send this ID to the server method as parameter or header. This is by no way should be used in production environment, as you would trust the client who he is, and pass parameters that are not strictly needed by your method.
As to "why":
In fact I don't think you need signalR for your use case. You call a server method, it saves to DB and returns you OK or not OK, client is happy. No need to pass it through signalR.
You do need signalR e.g. when:
- Same client is logged in on several devices and wants to get updates if changes were made on one of them
- Client works on something and another one changes same data. You want to inform the first client.
- Inform your client of an event that was not triggered by him (new notification)
In all this cases you have some kind of authentication and sending signalR message to the right client is not a problem.
Answer to comment below
I have little experience with ajax, i guess it might work. Another idea, if you want to avoid authentication, is a subscription model with SignalR.
You have to find out which specific resources you have, let's say "game" in your case, which have IDs. Than clients, interested in that particular resource, shall subscribe to changes.
All participants of a particular card game instance shall call a hub method defined like this:
public async Task SubscribeToGameChanges(long id)
{
await this.Groups.AddToGroupAsync(this.Context.ConnectionId, Helper.GetGameGroupName(id));
}
public static class Helper
{
public static string GetGameGroupName(long id)
{
return $"Game_{id}";
}
}
Than call it from client. Note the AddToGroupAsync. SignalR shall create a group with given name and add a client to it. If group exists, it will just add another client. So you have a group per game instance with a list of interested clients (players).
Now when a change happens to the game, you notify all your clients by calling from your controller on hub context:
await hubContext.Clients.Groups(Helper.GetGameGroupName(id)).SendAsync("myNotifyMethod", myParameters);
You could pack all your changes into parameters, or just inform the clients that the state of the game (or whatever other resource) has changed and clients shall requery the state through normal API call.
Also I noticed you use GlobalHost in your code. My code samples are for .net core SignalR version and might slightly differ in your case. See here on differences between .net core and full .net SignalR.

SignalR OnConnected not called, connecting from .NET client

I'm using SignalR and have a Hub where I've overwritten the OnConnected method. The reason is, the server publishes data every 60 seconds. But when a new client connects, I want to publish the data immediately.
In a simplified form:
public class CustomerPublisher : Hub
{
public void Publish(Customer customer)
{
Clients.All.receive(customer);
}
public override Task OnConnected()
{
return base.OnConnected();
}
}
The OnConnected method is never called (I tested by debugging). I've read in several places (like here) that the client needs to have a connected handler, like:
$.connection.onlineHub.client.connected = function(){...}
However, I have a WPF client, so in .NET I tried adding this, on top of the event I was already handling:
hubProxy.On<object>("receive", OnCustomerReceived); // this line was already present
hubProxy.On<object>("connected", OnConnected); // I added this
This didn't help. But I do have a connection, because after some time, I start receiving data.
Is there anything I'm missing? How can I get SignalR to call the OnConnected method when my .NET client connects?
You can try to use StateChanged event like this :
Connection.StateChanged += Connection_StateChanged;
void Connection_StateChanged(StateChange obj)
{
if (obj.NewState == ConnectionState.Connected)
{
//do somethign here
}
}
You ll receive Connecting and Connected states
In my case, the problem was Autofac. I had set it up in my own way (due to the way I had set up WCF duplex before using SignalR). But it is important to follow the docs. Even though a connection was made, and my client received data, I never entered the OnConnected method. After following the documentation, I the method was called.

Is it possible to restart/reconnect a SignalR connection?

I am learning how to use AngularJS and SignalR together and I am wondering if I can restart a SignalR connection withot losing the connectionId on the server side. The reason I am asking this has got to do with the client methods that needed to be called serverside. I haven't tried anything out yet and I just was thinking about this situation and what is the best practice solution and I am hoping some could think along or might have the solution and would like to explain it.
For example: I have two angularJS controllers, controller1 and controller2 and two signalR hubs, hub1 and hub2. controller1 is started on opening the website and in the initialisation of controller1 I can bind a function to a client method that needs to be called in hub1 before SignalR is started. This works fine and even after signalR is started I can still bind functions to client methods with the on function even if the signalR is started, although this probably isn't nessecary because I can bind the functions to the client methods before starting the signalR connection.
Next, on a form I got a button and that button is starting another div which has controller2 as ng-controller. In the initialisation of controller2 I want to bind functions to client methods that needs to be called in hub2. But since the signalR connection is already started by controller1, I can't do hub2.client.AMethod = function () { }. I was thinking, would this be possible if I can restart a signalR connection without losing the connectionId on the server side and by doing the restart, also refresh all the client methods bindings? And if not, can I use the on function even if there hasn't been a function binded to client method on hub2 before? Or do I have to bind an empty function to a client method on hub2 as well before I start my signalR connection?
EDIT: I took the time to set up a code example.
I got the 2 hubs: Hub1
[HubName("Hub1")]
public class Hub1 : Hub
{
public void TestMethod1(string test)
{
Clients.All.TestMethod1Hub1("Testing Hub1 method1; " + test);
}
public void TestMethod2(string test)
{
Clients.All.TestMethod2Hub1("Testing Hub1 method2; " + test);
}
}
and hub2:
[HubName("Hub2")]
public class Hub2 : Hub
{
public void TestMethod1(string test)
{
Clients.All.TestMethod1Hub2("Testing Hub2 method1; " + test);
}
public void TestMethod2(string test)
{
Clients.All.TestMethod2Hub2("Testing Hub2 method2; " + test);
}
}
And I got my angularJS controller:
testApp.controller('controller1', ['$scope', 'signalRService', function ($scope, signalRService) {
var self = this;
$scope.logs = [];
self.TestMethod = function(testString) {
$scope.logs.push({ text: testString });
$scope.$apply();
};
$scope.initialize = function() {
signalRService.connection.Hub1.client.TestMethod1Hub1 = self.TestMethod;
//signalRService.connection.Hub2.client.TestMethod1Hub2 = self.TestMethod;
signalRService.initialize();
};
$scope.addHandlers = function () {
//this will call the client method cause it is set before the connection start.
signalRService.connection.Hub1.server.testMethod1("Test 1");
//This is working, so on function isn't required?
signalRService.connection.Hub1.client.TestMethod2Hub1 = self.TestMethod;
signalRService.connection.Hub1.server.testMethod2("Test 2");
//So you don't need the on method (anymore?). (By the way, this is working as well ofcourse)
signalRService.connection.Hub1.on("TestMethod2Hub1", self.TestMethod);
signalRService.connection.Hub1.server.testMethod2("Test 3");
//this doesn't work (obviously?) unless the line in the initalize method is uncommented
signalRService.connection.Hub2.client.TestMethod1Hub2 = self.TestMethod;
signalRService.connection.Hub2.server.testMethod1("Test 4");
//but this doesn't work either. Same: works if line in the initialize method is uncommented
signalRService.connection.Hub2.on("TestMethod1Hub2", self.TestMethod);
signalRService.connection.Hub2.server.testMethod1("Test 5");
//Also, I get the test 4 and test 5 twice, so the event handlers are added, not replaced.
};
}]);
In the example, the binding to the client methods happens much later after the signalR is started (In this case, by pressing the button as example, but in a live example it could be when a user navigates to a different ng-view template and with that starts a different angular controller which also depends on client methods). In the test I see I have to add a client method (dummy or not) to every hub so I can add extra client methods later at a start up of another angular controller. I wonder if this could be done otherwise, so you don't get a long list of binding dummy functions to client methods?
Also, it doesn't seems to be nessecary to use the on function, binding straight away seems to work as well after the connection is started. Perhaps this is changed in SignalR version 2.0.0
Gonna give this a shot, I think I understand what you're asking so I'll provide some guidance:
It's not a good idea to restart a SignalR connection with the intent of maintaining a connection id. Instead track users in your hub via some sort of static concurrent dictionary. This way when a connection is established from a specific user you can associate them with your dictionary version of that user.
Prior to starting a SignalR connection (JavaScript) you MUST have at least 1 client side function bound; this process allows SignalR which hubs you want to subscribe to.
Hope this helps!

How to communicate Server to Client with SignalR in Nancy with ASP.NET Hosting?

Most of the examples I've found for SignalR are assuming ASP.NET (MVC or not). I'm using NancyFX. I'm having just one problem, so I'm hoping there's something I'm overlooking or some thing I need to do in Nancy to compensate for not being ASP.NET.
My one goal is to be able to notify the client browsers when a server event happens. I don't plan on replacing my Nancy routes with hub methods. But I would like the ability to call into the browser from my routes (actions).
I have very simple Hub that I created following the example in the SignalR Wiki. I'm not even sure I need it, since I don't plan on calling client to server.
public interface IUserNotifier
{
void Start();
void Notify(object #event);
}
I used an interface in hopes that I would be able to inject the same hub later on to use in my nancy routes... I'm not sure that is in the cards.
[HubName("userNotifier")]
public class UserNotifier : Hub, IUserNotifier
{
public void Start()
{
Notify(new {Status = "Started"});
}
public void Notify(object #event)
{
Clients.notification(#event);
}
}
When I have the following code in my html file, I can see that it executes the Start() method, and then the Notify() method, delivering content to my client.
var communicator = $.connection.userNotifier;
$.extend(communicator, {
Notification: function(event) {
alert("notification received from server!");
console.log(event);
}
});
$.connection.hub.start()
.done(function() {
communicator.start();
});
Like I said, "starting" the hub works and sends a notification to the client. Very cool. But, then, my primary goal hasn't been accomplished yet. I need to initiate these notifications from other places in my code where they might not be directly associated with a "request".
I tried injecting my IUserNotifier in my nancy modules for use in the routes, but when the Notify() is fired, I get:
That's because the Clients property on the Hub base class is null (hasn't been initialized). So, I switched gears. I tried to follow multiple examples, including the example from the wiki page about hubs in the section called "Broadcasting over a Hub from outside of a Hub":
public class NotifierModule : NancyModule
{
public NotifierModule(){
Get["/notify/{message}"] = p => {
var context = GlobalHost.ConnectionManager.GetHubContext<UserNotifier>();
context.Clients.notification(new { Message = p.message });
};
}
}
My Nancy route executes without throwing errors. Except my browser never receives the message. If I set a breakpoint in the route, I can see that Clients is initialized. Maybe the collection of clients is initialized but empty. Who knows? Maybe you do. :)
Again, my main goal is to be able to send events/notifications to the browser from anywhere in my code, any time. Is that too much to ask? What should I be doing here?
I'm sure you must have found the answer already. However, I figured I could try and help out in case anyone else runs into a similar problem. In order for your server on the .NET side to send messages to clients, it would also need to have a connection made to the hub.
var connection = new HubConnection("http://localhost/");
connection.Start();
connection.Notify("Hello");
Check out an official example at:
https://github.com/SignalR/SignalR/blob/master/samples/Microsoft.AspNet.SignalR.Client.Samples/Program.cs

Categories

Resources