2

Let me start by saying this all works perfectly at the moment except for one thing - the notification update for the progress bar goes to all clients (as you would expect given that the code example I used sends to ...Clients.All).

All I want to do is send the notification back to the client that initiated the current call to the hub. That's it, nothing else. There is no concept of "logging in" on this website so there's no user identity stuff to work with.

My method is:

public void NotifyUpdates(decimal val)
{
    var hubContext = GlobalHost.ConnectionManager.GetHubContext<EventsForceHub>();

    if (hubContext != null)
    {
        //"updateProgress" is javascript event trigger name
        await hubContext.Clients.All.updateProgress(val); 
    }
}

So back in my view I subscribe to "updateProgress" and it works fine - the progress bar updates as desired. But if another client happens to be connected to the hub, when one runs the async task and causes the NotifyUpdates method to run, then ALL connected clients see the taskbar update, which is a bit confusing for them!

In debug, if I inspect hubContext at runtime I can see a Clients property and that has a Connection property which has an Identity property with a unique GUID. Perfect! Just what I want to use. But... I cannot access it! If I try:

var currentConnection = hubContext.Clients.Connection;

...then I simply get a

"does not contain a definition for 'Connection'"

error, which I simply don't understand.

I've tried accessing Context.ConnectionId from the method too, but Context is null at that point so I'm a bit confused. The server method that uses NotifyUpdates to send information back to the client is called via a normal asp.net button, not via AJAX.

Clarification on structure

I think there is a degree of confusion here. It's a very simply webpage with an asp.net button control on it. The eventhandler for that button invokes an async method to return data from the server via a service call/repository.

Inside the async method, it has to process each returned data line and make three or four remote web api calls. On each loop I make a call back to the NotifyUpdates SignalR method shown above with a percentage complete number so this can update back to the client via an eventhandler for the method name specified (updateProgress as shown above). There could be dozens of data lines and each data line requires several Web API calls to a remote server to add data. This can take several seconds per iteration, hence me sending back the "update progress" to the client via that updateProgress method call.

TheMook
  • 1,531
  • 4
  • 20
  • 58
  • Why are you checking if `hubContext` is null? Have you had it return a null `HubContext` before? I doubt this situation will arise. – mason Apr 28 '17 at 13:41
  • Was just in the code sample I started with. No reason other than that. – TheMook Apr 28 '17 at 13:41
  • Don't be a [copy and paste programmer](https://en.wikipedia.org/wiki/Copy_and_paste_programming). Verify that the code you're introducing to your application does what you want it to do, and understand the parts and complexities you're introducing. There's a lot of bad code out there, don't automatically trust it. – mason Apr 28 '17 at 13:43
  • 2
    Agreed, but in this case it is making zero difference to the problem! – TheMook Apr 28 '17 at 13:44
  • I didn't say it was related to your problem. I'm just pointing out that you're introducing what is probably needless code into your application. – mason Apr 28 '17 at 13:44
  • @TheMook give each client a unique id and include that in the broadcast. have the client compare its id to the one in the broadcast and if it matches perform desired functionality. – Nkosi May 07 '17 at 10:34
  • @TheMook, just for clarification of your problem. I get the impression that the above code snippet is called outside the Hub class. Also in terms of the flow. When is the above called in relation to when the client made the call? Documentation indicates `that the context is not associated with a particular call from a client, so any methods that require knowledge of the current connection ID, such as *Clients.Others*, or *Clients.Caller*, or *Clients.OthersInGroup*, are not available` – Nkosi May 07 '17 at 11:11

2 Answers2

1

NEW ANSWER

Based on your comments I made the following little test:

It will allow clients to connect to the hub with a clientName, and every client is listening to updates send to them. We will have a group defined for them to be able to notify them from the server side.

I made a dummy progress simulator class to throw some update values to the users.

The code:

Hub class:

public class EventsForceHub : Hub {
    public static IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<EventsForceHub>();

    // allow users to join to hub and get s dedicated group/channel for them, so we can update them
    public async Task JoinGroup(string clientName) {
        string clientID = Context.ConnectionId;
        ClientInfo.clients.Add(clientID, new MyAppClient(clientID, clientName));
        await Groups.Add(clientID, clientName);

        // this is just mockup to simulate progress events (this uis not needed in real application)
        MockupProgressGenerator.DoJob(clientName, 0);
    }

    public static void NotifyUpdates(decimal val, string clientName) {
        // update the given client on his group/channel
        hubContext.Clients.Group(clientName).updateProgress(val);
    }
}

Some little helper classes:

// client "storage"
public static class ClientInfo  {
    public static Dictionary<string, MyAppClient> clients = new Dictionary<string, MyAppClient>();
    // .. further data and methods
}

// client type
public class MyAppClient {
    public string Id { get; set; }
    public string Name { get; set; }
    // .. further prooerties and methods

    public MyAppClient(string id, string name) {
        Id = id;
        Name = name;
    }
}

// this a completely made up and dumb class to simulate slow process and give some simple progress events
public static class MockupProgressGenerator {
    public static void DoJob(string clientName, int status) {
        if (status < 100) {
            Task.Delay(1000).ContinueWith(a =>
            {
                EventsForceHub.NotifyUpdates(status += 20, clientName);
                DoJob(clientName, status);
            });
        }
    }
}

Let's see two simple clients in JS:

    $(function () {
        var eventsForceHub = $.connection.eventsForceHub;

        $.connection.hub.start().done(function () {
            $('body').append("Joining with Name: Jerry");
            eventsForceHub.server.joinGroup("Jerry");
        });

        eventsForceHub.client.updateProgress = function (val) {
            // message received
            $('body').append('<br>').append("New Progress message: " + val);
        };
    });

For simplicity, same code, with different params, I even put this in two different html pages and stated execution in slightly different timing.

    $(function () {
        var eventsForceHub = $.connection.eventsForceHub;

        $.connection.hub.start().done(function () {
            $('body').append("Joining with Name: Tom");
            eventsForceHub.server.joinGroup("Tom");
        });

        eventsForceHub.client.updateProgress = function (val) {
            // message received
            $('body').append('<br>').append("New Progress message: " + val);
        };
    });

See it in action: enter image description here


FIRST ANSWER

I made a small web application to verify your claim. You may create the following to be able to isolate the issue from other possible problems.

I created an empty Web Application and included SignalR.

This is the hub class:

public class EventsForceHub : Hub {
    public void NotifyUpdates(decimal val) {
        var hubContext = GlobalHost.ConnectionManager.GetHubContext<EventsForceHub>();

        if (Context != null) {
            string clientID = Context.ConnectionId;         // <-- on debug: Ok has conn id.
            object caller = Clients.Caller;                 // <-- on debug: Ok, not null
            object caller2 = Clients.Client(clientID);      // <-- on debug: Ok, not null
            Clients.Caller.updateProgress(val);             // Message sent
            Clients.Client(clientID).updateProgress(val);   // Message sent
        }

        if (hubContext != null) {
            //"updateProgress" is javascript event trigger name
            hubContext.Clients.All.updateProgress(val);     // Message sent
        }
    }
}

This is the web page:

<script src="Scripts/jquery-1.10.2.min.js"></script>
<script src="Scripts/jquery.signalR-2.2.2.min.js"></script>
<script src="signalr/hubs"></script>
<script type="text/javascript">
    $(function () {
        var eventsForceHub = $.connection.eventsForceHub;

        $.connection.hub.start().done(function () {
            // send mock message on start
            console.log("Sending mock message: " + 42);
            eventsForceHub.server.notifyUpdates(42);
        });

        eventsForceHub.client.updateProgress = function (val) {
            // message received
            console.log("New Progress message: " + val);
        };
    });
</script>

Try to build an application as little as this to isolate the issue. I have not had any of the issues you mentioned.

For the sake of simplicity and using the debugger I took away the await and async.

Actually, SignalR will take care of that for you. You will get a new instance of your Hub class at every request, no need to force asynchrony into the methods.

Also, GlobalHost is defined as static which should be shared between instances of your Hub class. Using in an instance method does not seem like a very good idea. I think you want to use the Context and the Clients objects instead. However, while debugging we can verify that using GlobalHost also works.

Some debugger screenshots showing runtime values of callerId, Clients.Caller and Clients.Client(clientID): enter image description here enter image description here enter image description here

Understanding SignalR better will help you a lot in achieving your goal.

Happy debugging!

DDan
  • 8,068
  • 5
  • 33
  • 52
  • Ah. Well it's a fabulous answer but there's one (crucial) factor that is being overlooked here. It's my fault entirely as I haven't fully explained in the question. I did put in more detail (I think) in the comments to a post someone made then deleted in which I mentioned that this is being initiated by the server, not the client directly. It's a standard .net web page with a button on it. That calls a "getEvents" method in my service which is implemented in a repository that goes to the server, retrieves the list, then calls "notifyUpdates" on each processing loop as more data is fetched. – TheMook May 08 '17 at 14:45
  • This means that the client side calls you have in your example aren't happening - it's being kicked off by a server repository method. This appears to be the crucial thing that is causing the Context to be null in my hub method. – TheMook May 08 '17 at 14:46
  • I've tried using `public override Task OnConnected()` in my hub to store the connection IDs (which are present at that point) but even though these are then available to me at `notifyUpdates` it doesn't seem to be the RIGHT connectionID and it still fails to update. The only reliable method appears to be the "Clients.All" it does, which is really, really annoying! – TheMook May 08 '17 at 14:48
  • Ah - it's at the bottom of my OP: "The server method that uses NotifyUpdates to send information back to the client is called via a normal asp.net button, not via AJAX." – TheMook May 08 '17 at 14:55
  • I see @TheMook I have to say I was surprised by the sample that the client is saying how the progress is going. Please see one of my earlier answers where I give example of calling Hub method from client and from server differently: http://stackoverflow.com/questions/7549179/signalr-posting-a-message-to-a-hub-via-an-action-method/31063193#31063193 (Notice the `static` on the server side call.) – DDan May 08 '17 at 15:00
  • How about this: Every client would have its own channel/group, and you can send progress to those channels knowing to which client you are sending. (the channel id could even be the connection id of the client) – DDan May 08 '17 at 15:02
  • I've added a bit more clarity on structure to the OP at the bottom. I hope! I'll take a look at this again tomorrow - end of day now. Thanks for the help. – TheMook May 08 '17 at 15:06
  • End of the day here too. I can draft up the idea for you tomorrow with some code. – DDan May 08 '17 at 15:18
  • Thanks @DDan. I appreciate your help here - it's helping me understand a new subject to me. I've awarded your answer the bonus as it expires soon! However, I still think I have the same problem. If I connect from 3 different clients, I end up with 3 "clients" in the clients dictionary. So at the point in my server method when I call `notifyUpdates` how do I know which client to send to the method in that string parameter? – TheMook May 09 '17 at 11:23
  • Thank you very much @TheMook! That's great! I made a seemingly valid assumption that when you start the long running task for a client, you would know which client is that task running for, so you can specify in the `ContinueWith` part who to notify. Like in the example. Maybe putting a button on the page which invokes the task would have been a better idea to show a more realistic use case, (You always know which client requested for the long running task) however the "drift" is the same. – DDan May 09 '17 at 13:32
  • I still don't understand that, sorry. Where would I get the clientname or ID from when they press the button to start the server process off? As I said, there is no logging in on this app. If person 1 presses the button and gets the list returned, then sits on that page and on a different computer person 2 does the same thing, then at the point the `notifyUpdates` fires, there are two clients in the static list. How do I know which one of the clients to notify as I don't see I have the name/ID for them anywhere? I may be missing the point here entirely, sorry. – TheMook May 10 '17 at 08:50
  • Let's say you add a hub method `buttonPressed()` which would be called from client side. One thing is certain. The `clientId`, (`Context.ConnectionId`) sticks with the client. Any method is called, the same client will always have the same `clientId`, so you always know which one of them requested. This is provided internally by SignalR. – DDan May 10 '17 at 09:27
  • But it's not called from the client side - well, not via AJAX anyway. This is an async server method called from the button click handler. It has to be in asp.net code behind as it then does lots of formatting and button showing/hiding for each row of a gridview. – TheMook May 10 '17 at 10:19
-1

If you want to send the notification back to the client you should not call

hubContext.Clients.All.updateProgress(val);

instead try

accessing the current user's ConnectionId and use Clients.Client

hubContext.Clients.Client(Context.ConnectionId);
mason
  • 31,774
  • 10
  • 77
  • 121
kostas.kapasakis
  • 920
  • 1
  • 11
  • 26
  • Clients.Caller = "'IHubConnectionContext' does not contain a definition for 'Caller' and no extension method 'Caller' accepting a first argument of type 'IHubConnectionContext' could be found (are you missing a using directive or an assembly reference?" – TheMook Apr 28 '17 at 13:24
  • You are right for the first answer check the answer now,i 've edited – kostas.kapasakis Apr 28 '17 at 13:37
  • Context is also null. I don't understand why as the broadcast to all clients works fine. – TheMook Apr 28 '17 at 13:49