#if !BESTHTTP_DISABLE_SIGNALR
using System;
using System.Collections.Generic;
using BestHTTP.SignalR.Messages;
using System.Text;
namespace BestHTTP.SignalR.Hubs
{
public delegate void OnMethodCallDelegate(Hub hub, string method, params object[] args);
public delegate void OnMethodCallCallbackDelegate(Hub hub, MethodCallMessage methodCall);
public delegate void OnMethodResultDelegate(Hub hub, ClientMessage originalMessage, ResultMessage result);
public delegate void OnMethodFailedDelegate(Hub hub, ClientMessage originalMessage, FailureMessage error);
public delegate void OnMethodProgressDelegate(Hub hub, ClientMessage originialMessage, ProgressMessage progress);
///
/// Represents a clientside Hub. This class can be used as a base class to encapsulate proxy functionalities.
///
public class Hub : IHub
{
#region Public Properties
///
/// Name of this hub.
///
public string Name { get; private set; }
///
/// Server and user set state of the hub.
///
public Dictionary State
{
// Create only when we need to.
get
{
if (state == null)
state = new Dictionary();
return state;
}
}
private Dictionary state;
///
/// Event called every time when the server sends an order to call a method on the client.
///
public event OnMethodCallDelegate OnMethodCall;
#endregion
#region Privates
///
/// Table of the sent messages. These messages will be removed from this table when a Result message is received from the server.
///
private Dictionary SentMessages = new Dictionary();
///
/// Methodname -> Callback delegate mapping. This table stores the server callable functions.
///
private Dictionary MethodTable = new Dictionary();
///
/// A reusable StringBuilder to save some GC allocs
///
private StringBuilder builder = new StringBuilder();
#endregion
Connection IHub.Connection { get; set; }
public Hub(string name)
:this(name, null)
{
}
public Hub(string name, Connection manager)
{
this.Name = name;
(this as IHub).Connection = manager;
}
#region Public Hub Functions
///
/// Registers a Callback function to the given method.
///
public void On(string method, OnMethodCallCallbackDelegate callback)
{
MethodTable[method] = callback;
}
///
/// Removes Callback from the given method.
///
///
public void Off(string method)
{
MethodTable[method] = null;
}
///
/// Orders the server to call a method with the given arguments.
///
/// True if the plugin was able to send out the message
public bool Call(string method, params object[] args)
{
return Call(method, null, null, null, args);
}
///
/// Orders the server to call a method with the given arguments.
/// The onResult Callback will be called when the server successfully called the function.
///
/// True if the plugin was able to send out the message
public bool Call(string method, OnMethodResultDelegate onResult, params object[] args)
{
return Call(method, onResult, null, null, args);
}
///
/// Orders the server to call a method with the given arguments.
/// The onResult Callback will be called when the server successfully called the function.
/// The onResultError will be called when the server can't call the function, or when the function throws an exception.
///
/// True if the plugin was able to send out the message
public bool Call(string method, OnMethodResultDelegate onResult, OnMethodFailedDelegate onResultError, params object[] args)
{
return Call(method, onResult, onResultError, null, args);
}
///
/// Orders the server to call a method with the given arguments.
/// The onResult Callback will be called when the server successfully called the function.
/// The onProgress Callback called multiple times when the method is a long running function and reports back its progress.
///
/// True if the plugin was able to send out the message
public bool Call(string method, OnMethodResultDelegate onResult, OnMethodProgressDelegate onProgress, params object[] args)
{
return Call(method, onResult, null, onProgress, args);
}
///
/// Orders the server to call a method with the given arguments.
/// The onResult Callback will be called when the server successfully called the function.
/// The onResultError will be called when the server can't call the function, or when the function throws an exception.
/// The onProgress Callback called multiple times when the method is a long running function and reports back its progress.
///
/// True if the plugin was able to send out the message
public bool Call(string method, OnMethodResultDelegate onResult, OnMethodFailedDelegate onResultError, OnMethodProgressDelegate onProgress, params object[] args)
{
IHub thisHub = this as IHub;
lock (thisHub.Connection.SyncRoot)
{
// Start over the counter if we are reached the max value if the UInt64 type.
// While we are using this property only here, we don't want to make it static to avoid another thread synchronization, neither we want to make it a Hub-instance field to achieve better deuggability.
thisHub.Connection.ClientMessageCounter %= UInt64.MaxValue;
// Create and send the client message
return thisHub.Call(new ClientMessage(this, method, args, thisHub.Connection.ClientMessageCounter++, onResult, onResultError, onProgress));
}
}
#endregion
#region IHub Implementation
bool IHub.Call(ClientMessage msg)
{
IHub thisHub = this as IHub;
lock (thisHub.Connection.SyncRoot)
{
if (!thisHub.Connection.SendJson(BuildMessage(msg)))
return false;
SentMessages.Add(msg.CallIdx, msg);
}
return true;
}
///
/// Return true if this hub sent the message with the given id.
///
bool IHub.HasSentMessageId(UInt64 id)
{
return SentMessages.ContainsKey(id);
}
///
/// Called on the manager's close.
///
void IHub.Close()
{
SentMessages.Clear();
}
///
/// Called when the client receives an order to call a hub-function.
///
void IHub.OnMethod(MethodCallMessage msg)
{
// Merge the newly received states with the old one
MergeState(msg.State);
if (OnMethodCall != null)
{
try
{
OnMethodCall(this, msg.Method, msg.Arguments);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("Hub - " + this.Name, "IHub.OnMethod - OnMethodCall", ex);
}
}
OnMethodCallCallbackDelegate callback;
if (MethodTable.TryGetValue(msg.Method, out callback) && callback != null)
{
try
{
callback(this, msg);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("Hub - " + this.Name, "IHub.OnMethod - Callback", ex);
}
}
else
HTTPManager.Logger.Warning("Hub - " + this.Name, string.Format("[Client] {0}.{1} (args: {2})", this.Name, msg.Method, msg.Arguments.Length));
}
///
/// Called when the client receives back messages as a result of a server method call.
///
void IHub.OnMessage(IServerMessage msg)
{
ClientMessage originalMsg;
UInt64 id = (msg as IHubMessage).InvocationId;
if (!SentMessages.TryGetValue(id, out originalMsg))
{
// This can happen when a result message removes the ClientMessage from the SentMessages dictionary,
// then a late come progress message tries to access it
HTTPManager.Logger.Warning("Hub - " + this.Name, "OnMessage - Sent message not found with id: " + id.ToString());
return;
}
switch(msg.Type)
{
case MessageTypes.Result:
ResultMessage result = msg as ResultMessage;
// Merge the incoming State before firing the events
MergeState(result.State);
if (originalMsg.ResultCallback != null)
{
try
{
originalMsg.ResultCallback(this, originalMsg, result);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("Hub " + this.Name, "IHub.OnMessage - ResultCallback", ex);
}
}
SentMessages.Remove(id);
break;
case MessageTypes.Failure:
FailureMessage error = msg as FailureMessage;
// Merge the incoming State before firing the events
MergeState(error.State);
if (originalMsg.ResultErrorCallback != null)
{
try
{
originalMsg.ResultErrorCallback(this, originalMsg, error);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("Hub " + this.Name, "IHub.OnMessage - ResultErrorCallback", ex);
}
}
SentMessages.Remove(id);
break;
case MessageTypes.Progress:
if (originalMsg.ProgressCallback != null)
{
try
{
originalMsg.ProgressCallback(this, originalMsg, msg as ProgressMessage);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("Hub " + this.Name, "IHub.OnMessage - ProgressCallback", ex);
}
}
break;
}
}
#endregion
#region Helper Functions
///
/// Merges the current and the new states.
///
#if BESTHTTP_SIGNALR_WITH_JSONDOTNET
private void MergeState(IDictionary state)
#else
private void MergeState(IDictionary state)
#endif
{
if (state != null && state.Count > 0)
foreach (var kvp in state)
this.State[kvp.Key] = kvp.Value;
}
///
/// Builds a JSon string from the given message.
///
private string BuildMessage(ClientMessage msg)
{
try
{
builder.Append("{\"H\":\"");
builder.Append(this.Name);
builder.Append("\",\"M\":\"");
builder.Append(msg.Method);
builder.Append("\",\"A\":");
string jsonEncoded = string.Empty;
// Arguments
if (msg.Args != null && msg.Args.Length > 0)
jsonEncoded = (this as IHub).Connection.JsonEncoder.Encode(msg.Args);
else
jsonEncoded = "[]";
builder.Append(jsonEncoded);
builder.Append(",\"I\":\"");
builder.Append(msg.CallIdx.ToString());
builder.Append("\"");
// State, if any
if (msg.Hub.state != null && msg.Hub.state.Count > 0)
{
builder.Append(",\"S\":");
jsonEncoded = (this as IHub).Connection.JsonEncoder.Encode(msg.Hub.state);
builder.Append(jsonEncoded);
}
builder.Append("}");
return builder.ToString();
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("Hub - " + this.Name, "Send", ex);
return null;
}
finally
{
// reset the StringBuilder instance, to reuse next time
builder.Length = 0;
}
}
#endregion
}
}
#endif