#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