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.
Related
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.
I am trying to send a message to a SignalR client, but it should not broadcast to all, I went through many article about clientId and groups, but there are no straightforward examples or points given. My client code is below:
proxy: null,
//Getting the connection object
connection = .hubConnection('url')
//Creating proxy
this.proxy = connection.createHubProxy('SignalRServerHub');
this.proxy.on('displayStatus', function (a) {
SignalRMessageTableService.getDataFromServer()
});
//Starting connection
connection.start()
//we are able to capture connection.id here
//under .done function but not sure how to use
Please let me know step by step solution or any article reference so I can understand it easily.
To respond to a specific client with connectionId you can do something like this:
var connectionId = Context.ConnectionId;
Clients.Client(connectionId).someMethod(resultValue);
Or to respond only to the caller:
Clients.Caller.someMethod(resultValue);
These calls are made from within public methods in your hub class, and do the same.
EDIT:
It looks like the connection.start() should be expanded a bit. You do not need to handle the connectionId, if the server are going to return data or callback to an event at the caller. This will be done by the server, and handled by the methods posted above.
Try changing the connection.start() line to something like this:
connection.start().done(function() {
$('#someButton').click(function () {
var param1 = $('#someField').val();
var param2 = $('#someField2').val();
this.proxy.invoke('myHubMethod', param1, param2, paramN);
});
});
Do the neccessary changes to apply this to yout implementation. This code is based on the one you'll find in the documentation. Since it looks like you don't use the generated proxy, we use the invoke method on the proxy to tell which method, and which parameters we are going to send. This also wires up an event to a button with id=someButton, which will fire the myHubMethod on the hub when clicked.
The alternative would be something like (with generated proxy):
this.proxy.server.myHubMethod(param1, param2, paramN);
You should take a look on the ASP.NET SignalR Hubs API Guide - JavaScript Client
If you want to send message to a specific user, you can use the new method in version 2.0
Clients.User(userId).send(message);
by default, the user id used is the IPrincipal.Identity.Name. If you want to override the user id mechanism with your own mapping, refer to this reply - https://stackoverflow.com/a/21355406/2489038
I've just discovered you can't access the current session within the SignalR Hub.
Simplified my scenario: I've tried to write a chat.
The name of the current user was kept within the Session.
I've used SignalR to update (a group of connections) about every new message.
Now I see that I can't access the name of the current user through the hub.
I guess there might be some workarounds, but is that implement my design was wrong?
Should I've not used SignalR for that purpose? Or should I not use Session in this way?
You shouldn't use Session with SignalR (see SignalR doesn't use Session on server). You identify logical connections by their connection id which you can map to user names.
The underlying problem is that access to SessionState is serialized in ASP.NET to ensure state consistency, so each request to the hub would block other requests. In the past, limited read-only access (I assume (but can't confirm since the gist is gone) by setting EnableSessionstate to read-only, which prevents the locking problem I described) was possible, but support for this was dropped. Also see various other places where the SignalR team made similar statements. Lastly: there's a statement in the official documentation about HTTPContext.Current.Session.
You could send values from client to server hub via Query String.
Before the $.connection.hub.start() method you could add something like this:
Client JS Code:
// Replace "some value" with the session value or anything you want
$.connection.hub.qs = { "Name": "some value" };
$.connection.hub.start()...bla bla bla
On the server side on the Hub you could use this in any method:
string ClientValue= Context.QueryString["Name"].ToString();
I did not test with sessions, but of course on client side you could be as awesome as you can.
I was in a situation where I couldn't use the User/Identity because the session hadn't been authenticated yet so I just used the actual session cookie value.
[HubName("headerHub")]
public class HeaderHub : Hub
{
static HeaderHub()
{
EventManager.CartUpdated += Update;
EventManager.LoggedOut += Update;
EventManager.LoggedIn += Update;
}
public override Task OnConnected()
{
var group = Context.Request.Cookies["ASP.NET_SessionId"].Value;
Groups.Add(Context.ConnectionId, group);
return base.OnConnected();
}
private static void Update(object sender, SessionEventArgs e)
{
var hubContext = GlobalHost.ConnectionManager.GetHubContext<HeaderHub>();
hubContext.Clients.Group(e.SessionId).onUpdateHeader();
}
}
I am sure someone will find something wrong with this solution and if so feel free to comment because I would like a better way to accomplish tying a session to a set of clients to notify.
One the best approach is to use Cookie instead of session. because as mentioned above , You can not use session . so When user login to your system, put its unique identifier(such as username) in cookie . then work with cookie where ever you want access session . such below...
public class CustomUserIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
return request.Cookies["ODPUserID"].Value;
}
}
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!
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