|
|
|
|
using System;
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using HuizhongLibrary.Log;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
|
|
|
|
namespace HuizhongLibrary.Network
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Represents a callback used to inform a listener that a ServerConnection has received data.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="sender">The sender of the callback.</param>
|
|
|
|
|
/// <param name="e">The DataEventArgs object containging the received data.</param>
|
|
|
|
|
public delegate void DataReceivedCallback(ServerConnection sender, DataEventArgs e);
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Represents a callback used to inform a listener that a ServerConnection has disconnected.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="sender">The sender of the callback.</param>
|
|
|
|
|
/// <param name="e">The SocketAsyncEventArgs object used by the ServerConnection.</param>
|
|
|
|
|
public delegate void DisconnectedCallback(ServerConnection sender, SocketAsyncEventArgs e);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// A connection to our server.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class ServerConnection
|
|
|
|
|
{
|
|
|
|
|
public string IpAddress { get; set; }
|
|
|
|
|
public int Port { get; set; }
|
|
|
|
|
public string DeviceNo { get; set; }
|
|
|
|
|
public DateTime PrevTime { get; set; }
|
|
|
|
|
public List<byte> SocketBuffer { get; set; }
|
|
|
|
|
public SocketMessages ListMessage { get; set; }
|
|
|
|
|
public bool Enabled { get; set; }
|
|
|
|
|
public bool IsLogin { get; set; }
|
|
|
|
|
public string ConnectIp { get; set; }
|
|
|
|
|
public int ConnectPort { get; set; }
|
|
|
|
|
public int FileLength { get; set; }
|
|
|
|
|
public int ReceiveLen { get; set; }
|
|
|
|
|
public Byte[] Data { get; set; }
|
|
|
|
|
public string BuffData { get; set; }
|
|
|
|
|
public int RefshSecond { get; set; }
|
|
|
|
|
public bool IsSyncSend { get; set; }
|
|
|
|
|
public int CheckConnectNumber { get; set; } //连接状态验证次数
|
|
|
|
|
public int WaitMsgNumber = 0;
|
|
|
|
|
public bool IsWebSocket = false; //WebSocket握手协议是否完成
|
|
|
|
|
public FileStream fs = null;
|
|
|
|
|
|
|
|
|
|
private int MaxSendNumber = 0;
|
|
|
|
|
private int MaxWaitMsgNumber = 0;
|
|
|
|
|
private DateTime CacheDataTime = DateTime.Now;
|
|
|
|
|
public SocketAsyncEventArgs eventArgs;
|
|
|
|
|
public Socket socket;
|
|
|
|
|
|
|
|
|
|
public event Action<ServerConnection> SendCompleted;
|
|
|
|
|
public event Action<ServerConnection, DataEventArgs> DataReceived;
|
|
|
|
|
public event Action<ServerConnection, SocketAsyncEventArgs> CloseSocketed;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region 初始化
|
|
|
|
|
public ServerConnection()
|
|
|
|
|
{
|
|
|
|
|
CheckConnectNumber = 3;
|
|
|
|
|
FileLength = 0;
|
|
|
|
|
IsLogin = false;
|
|
|
|
|
PrevTime = DateTime.Now;
|
|
|
|
|
WaitMsgNumber = 0;
|
|
|
|
|
SocketBuffer = new List<byte>();
|
|
|
|
|
ListMessage = new SocketMessages();
|
|
|
|
|
//Data = new byte[2048];
|
|
|
|
|
}
|
|
|
|
|
public ServerConnection(Socket socket, SocketAsyncEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
//Data = new byte[2048];
|
|
|
|
|
FileLength = 0;
|
|
|
|
|
IsLogin = false;
|
|
|
|
|
PrevTime = DateTime.Now;
|
|
|
|
|
WaitMsgNumber = 0;
|
|
|
|
|
SocketBuffer = new List<byte>();
|
|
|
|
|
ListMessage = new SocketMessages();
|
|
|
|
|
if (socket.AddressFamily == AddressFamily.InterNetwork || socket.AddressFamily == AddressFamily.Unspecified)
|
|
|
|
|
{
|
|
|
|
|
string[] RemoteEndPoint = socket.RemoteEndPoint.ToString().Split(':');
|
|
|
|
|
this.IpAddress = RemoteEndPoint[0];
|
|
|
|
|
this.Port = Convert.ToInt32(RemoteEndPoint[1]);
|
|
|
|
|
ErrorFollow.TraceWrite("请求连接", "", IpAddress);
|
|
|
|
|
//this.DeviceNo = this.IpAddress.Split(Convert.ToChar("."))[3];
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
this.IpAddress = "";
|
|
|
|
|
}
|
|
|
|
|
this.Enabled = true;
|
|
|
|
|
this.socket = socket;
|
|
|
|
|
|
|
|
|
|
eventArgs = args;
|
|
|
|
|
eventArgs.Completed += ReceivedCompleted;
|
|
|
|
|
ListenForData(eventArgs);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 连接
|
|
|
|
|
public void Start(Socket socket, SocketAsyncEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
if (socket.AddressFamily == AddressFamily.InterNetwork || socket.AddressFamily == AddressFamily.Unspecified)
|
|
|
|
|
{
|
|
|
|
|
string[] RemoteEndPoint = socket.RemoteEndPoint.ToString().Split(':');
|
|
|
|
|
this.IpAddress = RemoteEndPoint[0];
|
|
|
|
|
this.Port = Convert.ToInt32(RemoteEndPoint[1]);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
this.IpAddress = "";
|
|
|
|
|
}
|
|
|
|
|
this.Enabled = true;
|
|
|
|
|
if (this.socket != null) this.socket = null;
|
|
|
|
|
if (this.eventArgs != null)
|
|
|
|
|
{
|
|
|
|
|
eventArgs.Dispose();
|
|
|
|
|
eventArgs = null;
|
|
|
|
|
}
|
|
|
|
|
this.socket = socket;
|
|
|
|
|
eventArgs = args;
|
|
|
|
|
eventArgs.Completed += ReceivedCompleted;
|
|
|
|
|
ListenForData(eventArgs);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region 返回要发送的消息
|
|
|
|
|
public SocketMessage GetNextSocketMessage(int RefshSecond)
|
|
|
|
|
{
|
|
|
|
|
var item=ListMessage.GetNextSocketMessage(RefshSecond);
|
|
|
|
|
if (item == null) return null;
|
|
|
|
|
if (item.SendNumber > 0)
|
|
|
|
|
{
|
|
|
|
|
ErrorFollow.TraceWrite("重发", "", item.FunNo);
|
|
|
|
|
DecrementWaitMsgNumber();
|
|
|
|
|
}
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 移除已发送消息
|
|
|
|
|
public void RemoveSocketMessage()
|
|
|
|
|
{
|
|
|
|
|
ListMessage.RemoveEmploy();
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 增加等待回复的消息数
|
|
|
|
|
public void IncrementWaitMsgNumber()
|
|
|
|
|
{
|
|
|
|
|
Interlocked.Increment(ref WaitMsgNumber);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 减少等待回复的消息数
|
|
|
|
|
public void DecrementWaitMsgNumber()
|
|
|
|
|
{
|
|
|
|
|
Interlocked.Decrement(ref WaitMsgNumber);
|
|
|
|
|
//Interlocked.Exchange<string>(ref 变量,11);
|
|
|
|
|
}
|
|
|
|
|
#endregion、
|
|
|
|
|
#region 消除已回复消息
|
|
|
|
|
public SocketMessage EndWaitMsg(string SequenceID)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
DecrementWaitMsgNumber();
|
|
|
|
|
SocketMessage msg = ListMessage.CheckOut(SequenceID);
|
|
|
|
|
return msg;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ErrorFollow.TraceWrite(ex.TargetSite.Name, ex.StackTrace, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 消除已回复消息
|
|
|
|
|
public SocketMessage EndFirstWaitMsg()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
DecrementWaitMsgNumber();
|
|
|
|
|
SocketMessage msg = ListMessage.CheckOut();
|
|
|
|
|
return msg;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ErrorFollow.TraceWrite(ex.TargetSite.Name, ex.StackTrace, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 返回消息
|
|
|
|
|
public SocketMessage GetSocketMessage(string SequenceID)
|
|
|
|
|
{
|
|
|
|
|
return ListMessage.GetSocketMessage(SequenceID);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 合并缓存数据
|
|
|
|
|
public void AddData(byte[] RevData, int Offset, int Len)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (CacheDataTime.AddSeconds(15) < DateTime.Now)
|
|
|
|
|
{
|
|
|
|
|
SocketBuffer.Clear();
|
|
|
|
|
}
|
|
|
|
|
this.ReceiveLen = Len + SocketBuffer.Count;
|
|
|
|
|
if (Data.Length < this.ReceiveLen) Data = new byte[ReceiveLen];
|
|
|
|
|
for (int i = 0; i < SocketBuffer.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
Data[i] = SocketBuffer[i];
|
|
|
|
|
}
|
|
|
|
|
Buffer.BlockCopy(RevData, Offset, Data, SocketBuffer.Count, Len);
|
|
|
|
|
SocketBuffer.Clear();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ErrorFollow.TraceWrite(ex.TargetSite.Name, ex.StackTrace, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 新增缓存
|
|
|
|
|
public void AddBuff(byte[] SrcArray, int offset)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
for (int i = offset; i < ReceiveLen; i++)
|
|
|
|
|
{
|
|
|
|
|
SocketBuffer.Add(SrcArray[i]);
|
|
|
|
|
}
|
|
|
|
|
CacheDataTime = DateTime.Now;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ErrorFollow.TraceWrite(ex.TargetSite.Name, ex.StackTrace, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 清理内存
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (SocketBuffer != null) SocketBuffer.Clear();
|
|
|
|
|
SocketBuffer = null;
|
|
|
|
|
if (ListMessage!=null) ListMessage.Clear();
|
|
|
|
|
ListMessage = null;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
#region 打包服务器数据
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 打包服务器数据
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="message">数据</param>
|
|
|
|
|
/// <returns>数据包</returns>
|
|
|
|
|
public static byte[] PackData(string message)
|
|
|
|
|
{
|
|
|
|
|
byte[] contentBytes = null;
|
|
|
|
|
byte[] temp = Encoding.UTF8.GetBytes(message);
|
|
|
|
|
|
|
|
|
|
if (temp.Length < 126)
|
|
|
|
|
{
|
|
|
|
|
contentBytes = new byte[temp.Length + 2];
|
|
|
|
|
contentBytes[0] = 0x81;
|
|
|
|
|
contentBytes[1] = (byte)temp.Length;
|
|
|
|
|
Array.Copy(temp, 0, contentBytes, 2, temp.Length);
|
|
|
|
|
}
|
|
|
|
|
else if (temp.Length < 0xFFFF)
|
|
|
|
|
{
|
|
|
|
|
contentBytes = new byte[temp.Length + 4];
|
|
|
|
|
contentBytes[0] = 0x81;
|
|
|
|
|
contentBytes[1] = 126;
|
|
|
|
|
contentBytes[2] = (byte)(temp.Length >> 8 & 0xFF);
|
|
|
|
|
contentBytes[3] = (byte)(temp.Length & 0xFF);
|
|
|
|
|
Array.Copy(temp, 0, contentBytes, 4, temp.Length);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 暂不处理超长内容
|
|
|
|
|
}
|
|
|
|
|
return contentBytes;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Public Methods
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Disconnects the client.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Disconnect()
|
|
|
|
|
{
|
|
|
|
|
lock (this)
|
|
|
|
|
{
|
|
|
|
|
CloseConnection(eventArgs);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Sends data to the client.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data">The data to send.</param>
|
|
|
|
|
/// <param name="offset">The offset into the data.</param>
|
|
|
|
|
/// <param name="count">The ammount of data to send.</param>
|
|
|
|
|
public bool SendData(Byte[] data, Int32 offset, Int32 count)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
lock (this)
|
|
|
|
|
{
|
|
|
|
|
if (socket.Connected == false) return false;
|
|
|
|
|
//bool bk=socket.Poll(-1, SelectMode.SelectWrite);
|
|
|
|
|
this.PrevTime = DateTime.Now;
|
|
|
|
|
int i = socket.Send(data, offset, count, SocketFlags.None);
|
|
|
|
|
if (i <= 0) return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#region WebSocket专用
|
|
|
|
|
public bool SendData(string data)
|
|
|
|
|
{
|
|
|
|
|
lock (this)
|
|
|
|
|
{
|
|
|
|
|
if (socket.Connected == false) return false;
|
|
|
|
|
//bool bk=socket.Poll(-1, SelectMode.SelectWrite);
|
|
|
|
|
this.PrevTime = DateTime.Now;
|
|
|
|
|
byte[] bytes = PackData(data);
|
|
|
|
|
int i = socket.Send(bytes, 0, bytes.Length, SocketFlags.None);
|
|
|
|
|
if (i <= 0) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
#region 获取Socket
|
|
|
|
|
public Socket GetSocket()
|
|
|
|
|
{
|
|
|
|
|
lock (this)
|
|
|
|
|
{
|
|
|
|
|
return socket;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Private Methods
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Starts and asynchronous recieve.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="args">The SocketAsyncEventArgs to use.</param>
|
|
|
|
|
private void ListenForData(SocketAsyncEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
lock (this)
|
|
|
|
|
{
|
|
|
|
|
if (socket == null) return;
|
|
|
|
|
if (socket.Connected)
|
|
|
|
|
{
|
|
|
|
|
socket.InvokeAsyncMethod(socket.ReceiveAsync,
|
|
|
|
|
ReceivedCompleted, args);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Called when an asynchronous receive has completed.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="sender">The sender.</param>
|
|
|
|
|
/// <param name="args">The SocketAsyncEventArgs for the operation.</param>
|
|
|
|
|
private void ReceivedCompleted(Object sender, SocketAsyncEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (args.BytesTransferred == 0)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(this.DeviceNo)==false)Disconnect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (args.SocketError != SocketError.Success)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(this.DeviceNo) == false) Disconnect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.Data = new byte[args.BytesTransferred];
|
|
|
|
|
this.ReceiveLen = this.Data.Length;
|
|
|
|
|
Buffer.BlockCopy(args.Buffer, args.Offset, this.Data, args.Offset, args.BytesTransferred);
|
|
|
|
|
if (this.DataReceived!=null)this.DataReceived(this, new DataEventArgs() { Data = this.Data, Length = this.ReceiveLen });
|
|
|
|
|
ListenForData(args);
|
|
|
|
|
//if (state.socket.Available == 0)
|
|
|
|
|
//{
|
|
|
|
|
// ListenForData(args);
|
|
|
|
|
//}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ErrorFollow.TraceWrite(ex.TargetSite.Name, ex.StackTrace, ex.Message);
|
|
|
|
|
Disconnect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region 输出数据日志
|
|
|
|
|
string WaitData(byte[] bytes, int index, int Length)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return Encoding.UTF8.GetString(bytes, index, Length);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
string a = "";
|
|
|
|
|
foreach (var item in bytes)
|
|
|
|
|
{
|
|
|
|
|
a += item.ToString("X");
|
|
|
|
|
}
|
|
|
|
|
return a;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Closes the connection.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="args">The SocketAsyncEventArgs for the connection.</param>
|
|
|
|
|
private void CloseConnection(SocketAsyncEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
if (this.Enabled == false)return;
|
|
|
|
|
this.Enabled = false;
|
|
|
|
|
this.WaitMsgNumber = 0;
|
|
|
|
|
if (socket == null) return;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
socket.Shutdown(SocketShutdown.Both);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{ }
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
socket.Close();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{ }
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Dispose();
|
|
|
|
|
args.Completed -= ReceivedCompleted; //MUST Remember This!
|
|
|
|
|
socket = null;
|
|
|
|
|
this.Data = null;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ErrorFollow.TraceWrite(ex.TargetSite.Name, ex.StackTrace, ex.Message);
|
|
|
|
|
}
|
|
|
|
|
if (this.CloseSocketed != null) this.CloseSocketed(this, args);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Events
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fires the DataReceivedCallback.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data">The data which was received.</param>
|
|
|
|
|
/// <param name="remoteEndPoint">The address the data came from.</param>
|
|
|
|
|
/// <param name="callback">The callback.</param>
|
|
|
|
|
private void OnDataReceived(DataReceivedCallback callback)
|
|
|
|
|
{
|
|
|
|
|
callback(this, new DataEventArgs() { Data = this.Data, Length = this.ReceiveLen });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fires the DisconnectedCallback.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="args">The SocketAsyncEventArgs for this connection.</param>
|
|
|
|
|
/// <param name="callback">The callback.</param>
|
|
|
|
|
private void OnDisconnected(SocketAsyncEventArgs args, DisconnectedCallback callback)
|
|
|
|
|
{
|
|
|
|
|
callback(this, args);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|