Hub methods on the server are able to have return values for the client to consume, but not the other way around. This means that the server cannot call a client and get a value from the method invocation. It would be great if this could be added as it would make server and client more symmetrical in their abilities.
Maybe sometime in the far future. Moving to backlog.
I would still like to see this feature implemented at some point.
Me too.
While I understand this would be hard for "broadcasts" it would be great to have this for "single receiver calls".
Technically possible, but very easily mimicked in app code without SignalR having to do some heavy lifting under the covers.
We'll move this to the new repo as a candidate for v3 or beyond.
How would I mimic this in my app code?
Good question. How would I mimic this in my app code? Any hints?
All you would need to do is have a call back method defined in your hub, so that the client instead of returning a value per se, would be calling a new method on the hub and passing the "return" value as the method parameter. You could then bundle this all together with the TPL so that both the initial call and the call back could be awaited using the EAP pattern as illustrated here: https://msdn.microsoft.com/en-us/library/ee622454(v=vs.110).aspx
OK
I tried the following way.
On Server
```C#
public class MyHub : Hub
{
private static readonly ConcurrentDictionary
public bool SendClientResponse(Guid Identificador, Object Response)
{
return ClientResponses.TryUpdate(Identificador, Response, null);
}
public TResponse GetClientResponse<TResponse>(Guid Identificador, int Attempts = 5, int SecondsBetweenAttempts = 1)
{
object _response = null;
int _attempts = 0;
TResponse _retorno;
do
{
if (!ClientResponses.TryGetValue(Identificador, out _response))
{
break;
}
_attempts += 1;
if (_response == null && _attempts < Attempts)
{
System.Threading.Thread.Sleep(SecondsBetweenAttempts*1000);
}
} while (_response == null && _attempts <= Attempts);
bool remocao = ClientResponses.TryRemove(Identificador, out _response);
_retorno = (TResponse)_response;
return _retorno;
}
public string GetIpClientLoja()
{
Guid _identificador = Guid.NewGuid();
ClientResponses.TryAdd(_identificador, null);
Clients.Client(ConnectionIdClientLoja).GetIp(_identificador);
return GetClientResponse<string>(_identificador);
}
}
**On Client 1**
```C#
public static async Task GetIP(Guid Identificador)
{
//System.Threading.Thread.Sleep(100000);
string _response = "192.168.13.200";
bool retorno = await _proxy.Invoke<bool>("SendClientResponse", Identificador, _response);
if (!retorno) Console.WriteLine("Erro ao Responder a Solicita莽茫o do Servidor Central");
}
On Client 2
```C#
string ip = _proxy.Invoke
Console.WriteLine("Resultado:");
Console.WriteLine(ip);
Console.WriteLine("Tecle qualquer coisa para continuar....");
```
First observation
It works.
Questions
1) Is there any better (and safer) way of doing this from the point of view of using threads?
2) An improvement would be welcome, I would like to optionally have the "SendClientResponse" method only terminate, and release the client if a response consumption method was correctly executed from the Server Side, is this possible?
Victor Perez
@victorperez2911 You should be using a TaskCompletionSource instead of sleeping waiting for results. What you have is extremely inefficient.
@davidfowl
Thanks For The Help
Made this change to the side of the Hub, what do you think?
```C#
public class MyHub : Hub
{
private static readonly ConcurrentDictionary
public bool SendClientResponse(Guid Identificador, Object Response)
{
return ClientResponses.TryUpdate(Identificador, Response, null);
}
public TResponse GetClientResponse<TResponse>(Guid Identificador, int Attempts = 5, int SecondsBetweenAttempts = 1)
{
object _response = null;
int _attempts = 0;
TResponse _retorno;
do
{
if (!ClientResponses.TryGetValue(Identificador, out _response))
{
break;
}
_attempts += 1;
if (_response == null && _attempts < Attempts)
{
System.Threading.Thread.Sleep(SecondsBetweenAttempts*1000);
}
} while (_response == null && _attempts <= Attempts);
bool remocao = ClientResponses.TryRemove(Identificador, out _response);
_retorno = (TResponse)_response;
return _retorno;
}
public Task<string> GetIpClientLoja()
{
Guid _identificador = Guid.NewGuid();
ClientResponses.TryAdd(_identificador, null);
Clients.Client(ConnectionIdClientLoja).GetIp(_identificador);
return getClientResponseAsync<string>(_identificador);
}
private Task<TResponse> getClientResponseAsync<TResponse>(Guid Identificador)
{
TaskCompletionSource<TResponse> tcs = new TaskCompletionSource<TResponse>();
tcs.SetResult(GetClientResponse<TResponse>(Identificador));
return tcs.Task;
}
}
```
@victorperez2911 I still see a sleep.
ok @davidfowl rrsrsrs
And now ?
```C#
public class MyHub : Hub
{
//Static ?
public static event EventHandler<GetClientResponseEventArgs> OnGetClientResponse;
public void SendClientResponse(Guid Identificador, string Response)
{
//using ? make this Thread-Safe ?
OnGetClientResponse?.Invoke(this, new GetClientResponseEventArgs(Identificador, Response));
}
public Task<string> GetIpClientLoja()
{
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
Guid _identificador = Guid.NewGuid();
OnGetClientResponse += (s, args) => {
if (args.Identificador != _identificador) return; // Do I need this for many clients?
tcs.TrySetResult(args.Response);
};
Clients.Client(ConnectionIdClientLoja).GetIp(_identificador);
return tcs.Task;
}
public class GetClientResponseEventArgs : EventArgs
{
public GetClientResponseEventArgs(Guid Identificador, string Response)
{
identificador = Identificador;
response = Response;
}
private Guid identificador;
private string response;
public Guid Identificador
{
get { return identificador; }
}
public string Response
{
get { return response; }
}
}
}
```
Much better, but you can do better:
```C#
public class MyHub : Hub
{
private static readonly ConcurrentDictionary
public void SendClientResponse(Guid identificador, string response)
{
TaskCompletionSource<object> tcs;
if (ClientResponses.TryGetValue(out tcs))
{
// Trigger the task continuation
tcs.TrySetResult(response);
}
else
{
// Client response for something that isn't being tracked, might be an error
}
}
public async Task<string> GetIpClientLoja()
{
// Create an entry in the dictionary that will be used to track the client response
var tcs = new TaskCompletionSource<string>();
var identificador = Guid.NewGuid();
ClientResponses.Add(identificador, tcs);
// Call GetIp on the client passing the identifier
Clients.Client(ConnectionIdClientLoja).GetIp(identificador);
try
{
// Wait for the client to respond
// TODO: Cancel this task if the client disconnects (potentially by just adding a timeout)
return await tcs.Task;
}
finally
{
// Remove the tcs from the dictionary so that we don't leak memory
ClientResponses.TryRemove(identificador, out tcs);
}
}
}
```
I left a TODO as an exercise for the reader 馃槃
OMG!
I can not believe I was able to pull lines of code from you @davidfowl
Following new version.
```C#
public class MyHub : Hub
{
private static readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> ClientResponses = new ConcurrentDictionary<Guid, TaskCompletionSource<object>>();
public void SendClientResponse(Guid Identificador, string Response)
{
TaskCompletionSource<object> tcs;
if (ClientResponses.TryGetValue(Identificador, out tcs))
{
// Trigger the task continuation
tcs.TrySetResult(Response);
}
else
{
// Client response for something that isn't being tracked, might be an error
//Test Only
throw new Exception("Resposta n茫o Esperada.");
}
}
public async Task<string> GetIpClientLoja()
{
// Create an entry in the dictionary that will be used to track the client response
var tcs = new TaskCompletionSource<object>();
var identificador = Guid.NewGuid();
ClientResponses.TryAdd(identificador, tcs);
// Call GetIp on the client passing the identifier
Clients.Client(ConnectionIdClientLoja).GetIp(identificador);
try
{
// Wait for the client to respond
// TODO: Cancel this task if the client disconnects (potentially by just adding a timeout)
int timeout = 5000;
var task = tcs.Task;
if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
{
return (string)await task;
}
else
{
return "TimedOut";//Only Test
}
}
finally
{
// Remove the tcs from the dictionary so that we don't leak memory
ClientResponses.TryRemove(identificador, out tcs);
}
}
}
```
Hello @davidfowl
Could you give me a hint?
I changed the method that calls the Client to be a bit more generic, and I don't have to redo the process of tracking down the answer with TaskCompletitionSource all the time, and it worked perfectly.
```C#
private async Task
{
Guid _identificador = Guid.NewGuid();
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
bool responseTracked = ClientResponses.TryAdd(_identificador, tcs);
IClientProxy proxy = Clients.Client(ConnectionId);
//I combined that all methods that will be used with this must have mechanisms as last parameter the Guid for tracker.
Array.Resize(ref parameters, parameters.Length + 1);
parameters[parameters.Length - 1] = _identificador;
await proxy.Invoke(Method, parameters);
try
{
int timeout = SecondsWaitFromResponse * 1000;
var task = tcs.Task;
if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
{
return (TResponse)await task;
}
else
{
return TimeOutResponse;
}
}
finally
{
ClientResponses.TryRemove(_identificador, out tcs);
}
}
```
But right now, I need to use this mechanism outside the Hub.
Is this possible?
An Extended Method of some type that can be accessed with the Context generated by the GetHubContext method would be great.
Thank you
Most helpful comment
OMG!
I can not believe I was able to pull lines of code from you @davidfowl
Following new version.
```C#
public class MyHub : Hub
{
}
```