原E文地址: Pushing Data to a Silverlight Client with a WCF Duplex Service - Part I Part II 实例代码
正好准备做一个聊天模块,看到这篇文章, 做个学习的翻译笔记. 我的标注颜色是紫红的. 后面我将在这个基础上,参考这个例子, 建立一个基于Silverlight的聊天程序, 和以前的javascript聊天室程序对应一下.
Silverlight 提供了几个远程数据访问的方法. 数据可以从服务器上的WebService和REST Service上拉下来, 也可以通过Socket从服务器”推送”数据到客户端(here, here and here).Silverlight2 提供了一个种新的方式-WCF 和 HTTP, 从服务器上”推送”数据到客户端. WCF 支持的duplex服务契约为Silverlight客户端打开了一个唯一的数据通道. 第一部分演示WCF”推送”服务是怎么创建的, 以及让这个简单服务运行起来的整个过程; 第二部分集中介绍客户端, 以及使用WCF duplex 通讯,监听发送的数据.
很多WCF Services通过request-response这种简单机械的方式为很多应用程序提供服务,能工作的很好.然而, 除了标准的HTTP绑定外, WCF也支持其它的方式,包括可以轮训双向绑定, 使服务器可以向Silverlight客户端”推送”数据,而完成数据交互.因为Silverlight 客户端轮询是否有消息到达, 这种绑定不是象sockets的”推送”模式, 但是他提供了一种没有端口范围的限制的好方法”推送”数据到客户端. 一旦通讯通道打开, 消息可以互相发送, Silverlight SDK注明了Silverlight客户端和duplex Service怎么通讯和工作的:
"Silverlight 客户端在网络层周期性的轮询客户端, 检查是否有新数据发送回来, 在客户端回调通道中, 服务队列中有所有返回的数据, 当客户端轮询时服务将转交所有的对列数据."
不知道polling duplex services怎么翻译, 是不是轮询式的双向通讯服务?
我现在有点搞不懂, 这个回调的双向通讯是否是真的双向了,还是只是以前ajax无刷新的简单轮询方式, 和我那个聊天程序一样,不知道底部通讯机制是不是一样的; 是真的实现了服务端的push,还是只是采用客户端的drag方式封装模拟了服务端的push, 期待高手解答一下. 查了资料没结果. 如果是从客户端定期轮询服务器,那么效率也不敢恭维, 只是包装了一下而已, 没有实现真正的双向通讯. 引用一下HTTP Polling Duplex 内存泄漏问题和建议.
在 .NET Framework 3.5 SP1 或 .NET Framework 3.0 SP2 上使用 HTTP Polling Duplex 通道的服务器端部件 (System.ServiceModel.PollingDuplex.dll) 时,可能会遇到线程与内存泄漏问题。问题是服务器逐渐使用越来越多的线程与内存,而从不释放这些资源,最后导致不稳定状态。在 .NET Framework 3.5(不带 Service Pack)或 .NET Framework 3.0 SP1 上不会发生此问题。
在许多情况下,解决方法是以某个规则间隔(例如每 12 个小时)重新启动承载 Polling Duplex 服务的进程。(所使用的实际间隔取决于服务负载。)如果在 IIS 中承载 Polling Duplex 服务,则可能可以解决此问题,方法是在单独的应用程序池中隔离该服务,然后正确配置应用程序池循环设置。
在将来的版本中会解决此问题。如果此问题阻止您在生产应用程序中使用 HTTP Polling Duplex 通道,请联系 Microsoft 产品支持服务讨论您的意见,其中可以包括可能请求早期的修复程序。
当为Silverlight创建了duplex服务, 服务器就会创建一个标准的操作接口. 同时, 服务必须定义一个客户端回调的接口, 因为服务器需要和客户端通讯. 下面的例子定义了一个IGameStreamService接口, 包含一个操作函数:
[ServiceContract(Namespace = "Silverlight", CallbackContract = typeof(IGameStreamClient))] public interface IGameStreamService { [OperationContract(IsOneWay = true)] void GetGameData(Message receivedMessage); }
[ServiceContract]
public interface IGameStreamClient
{
[OperationContract(IsOneWay = true )]
void ReceiveGameData(Message returnMessage);
}
一旦服务器与客户端的契约被定义, 一个实现了IGameStreamService 接口的服务类就可以创建起来了. 接下来的代码创建了一个服务, 类似于一个篮球游戏, 将游戏更新信息定时发送到silverlight客户端.
using System; using System.ServiceModel; using System.ServiceModel.Channels; using System.Threading; namespace WCFPushService { public class GameStreamService : IGameStreamService { IGameStreamClient _Client; Game _Game = null; Timer _Timer = null; Random _Random = new Random(); public GameStreamService() { _Game = new Game(); } public void GetGameData(Message receivedMessage) { //Get client callback channel _Client = OperationContext.Current.GetCallbackChannel<IGameStreamClient>(); SendData(_Game.GetTeamData()); //Start timer which when fired sends updated score information to client _Timer = new Timer(new TimerCallback(_Timer_Elapsed), null, 5000, Timeout.Infinite); } private void _Timer_Elapsed(object data) { SendData(_Game.GetScoreData()); int interval = _Random.Next(3000, 7000); _Timer.Change(interval, Timeout.Infinite); } private void SendData(object data) { Message gameDataMsg = Message.CreateMessage( MessageVersion.Soap11, "Silverlight/IGameStreamService/ReceiveGameData", data); //Send data to the client _Client.ReceiveGameData(gameDataMsg); } } }
服务在构造时首先创建了一个Game类的实例, Game类中创建了可以发送到客户端的新数据. 当客户端调用服务的GetGameData() 方法(IsOneWay的). 通过OperationContext 检索到了客户端的回调接口, 然后调用GetCallbackChannel() 方法. 那些参与到游戏的Teams都在服务器上创建,并且这些数据都通过SendData()”推送”到客户端, SendData() 方法又调用Game的GetTeamData()方法, GetTeamData() 生成一个XML字符串数据返回, SendData() 创建一个WCF 消息对象发送到客户端, 客户端ReceiveGameData() 被触发,从而实现了消息传输. 具体数据调用参考实例代码. 我还是将过程试着画个不规范的时序图, 老外好险都喜欢写字啊,这种流程说着都晕了:
using System; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Channels; using System.ServiceModel.Configuration; namespace WCFPushService { public class PollingDuplexServiceHostFactory : ServiceHostFactoryBase { public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses) { return new PollingDuplexServiceHost(baseAddresses); } } class PollingDuplexServiceHost : ServiceHost { public PollingDuplexServiceHost(params System.Uri[] addresses) { base.InitializeDescription(typeof(GameStreamService), new UriSchemeKeyedCollection(addresses)); } protected override void InitializeRuntime() { // Define the binding and set time-outs PollingDuplexBindingElement bindingElement = new PollingDuplexBindingElement() { ServerPollTimeout = TimeSpan.FromSeconds(10),//轮询超时时间,以前版本可能是PollTimeout,msdn也还是这么描述的,但是MSND上没有ServerPollTimeout的描述,只有ClientPollTimeout InactivityTimeout = TimeSpan.FromMinutes(1) }; // Add an endpoint for the given service contract this.AddServiceEndpoint( typeof(IGameStreamService), new CustomBinding( bindingElement, new TextMessageEncodingBindingElement( MessageVersion.Soap11, System.Text.Encoding.UTF8), new HttpTransportBindingElement()), ""); base.InitializeRuntime(); } } }
这些代码都是直接从Silverlight SDK 例子上摘录下来的, 是一个很好的学习创建WCF/Silverlight polling duplex services的例子.服务工厂PollingDuplexServiceHostFactory使用CreateServiceHost() 方法创建了服务宿主PollingDuplexServiceHost的新实例; 服务宿主类重载了InitializeRuntime() 方法,并且创建了一个PollingDuplexBindingElement 实例,同时定义了客户端轮询的无活动状态(InactivityTimeout)超时的时间 和 轮询超时时间. MSDN的描述为:
在绑定使用的 PollingDuplexBindingElement 上有两个超时值,被设为默认值。PollTimeout 用于指定轮询请求在超时前必须完成的时间间隔,在默认情况下,设为 5 分钟。InactivityTimeout 指定在通道进入出错状态前允许通道上无活动的最大时间间隔,默认情况下,设置为 10 分钟。InactivityTimeout 属性可以用于更改直接在绑定上的默认值。但 PollTimeout 的值只能在为 CustomBinding 构造绑定元素堆栈时才能在 PollingDuplexBindingElement 上更改, 如果通道从远程终结点接收到 CloseSession 消息,则该通道处于半关闭状态,此时它仍然可以发送消息。因此,如果 Send 调用是唯一的剩余活动,则最后一个 Send 调用并经过 InactivityTimeout 时间间隔后通道将出错。
PollingDuplexBindingElement 类位于名称系统程序集中.ServiceModel.PollingDuplex.dll是Silverlight SDK的一部分. 使用PollingDuplexBindingElement 类,你必须在WCF 项目中引用这个程序集和名称空间System.ServiceModel.Channels. PollingDuplexBindingElement 被创建后, 调用宿主对象PollingDuplexServiceHost的AddServiceEndPoint() ,AddServiceEndPoint() 引用PollingDuplexBindingElement 对象和IGameStreamService接口 来创建了一个自定义的HTTPbinding进行消息交换. 在工厂类和服务类后,工厂实例可以在服务实例中使用,如下方法:
<%@ ServiceHost Language="C#" Factory="WCFPushService.PollingDuplexServiceHostFactory" %>
"duplex提供在后台轮询来实现消息机制---但是他不同于自定义轮询机制. 他初始化一个网络请求, 这个请求是休眠的,等待客户端相应(请求后不会立即返回--参考IsOneWay). 服务端保持连接打开但休眠着, 直到有消息发回客户端(或者连接超时--那么客户端需要再次连接并且再休眠等待服务器发送返回). 这种方式避免了重复连接服务器, 如果有数据发送,服务端会马上返回数据."
当客户端在后台轮询,他发送如下消息到服务器:<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <wsmc:MakeConnection xmlns:wsmc="http://docs.oasis-open.org/ws-rx/wsmc/200702"> <wsmc:Address> http://docs.oasis-open.org/ws-rx/wsmc/200702/anoynmous?id=7f64eefe-9328-4168-8175-1d4b82bef9c3 </wsmc:Address> </wsmc:MakeConnection> </s:Body> </s:Envelope>
后面,我将演示在Silverlight中怎么调用WCF的polling duplex service和监听数据. Silverlight界面如下:
组织成中文好吃力啊, 我e文太烂了. 不过这是一个好的学习方式, 推荐一下这种方式, 因为你必须了解文中的知识点和相关知识, 主要知识点还不能是一知半解. 通过这个笔记, 发现对WCF相关东东有了更深的了解.
前面我演示了在服务器上定义服务契约和操作, 使用WCF polling duplex service ”推送”数据到Silverlight客户端. WCF为duplex communication提供了内置的支持(服务和客户端通讯有两种方式), 在Silverlight中必须引用由SilverlightSdk提供的程序集System.ServiceModel.PollingDuplex.dll ,it is currently in “evaluation” mode (the Silverlight go-live license doesn’t apply to it) 什么意思, 可以用啊?. 通过polling duplex方式, Silverlight客户端轮询Serviec检查是否有消息在队列中, 他不像socket那样自由交换数据. 然而和socket比较, 他提供了很多好的解决办法, 并且通过http工作他没有端口的限制.
我们看一下Silverlight客户端怎么从polling duplex WCF service 收发消息的, 以及发送什么类型的消息.
<xs:schema elementFormDefault="qualified" targetNamespace="http://schemas.microsoft.com/Message" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://schemas.microsoft.com/Message"> <xs:complexType name="MessageBody"> <xs:sequence> <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##any"/> </xs:sequence> </xs:complexType> </xs:schema>
using System; using System.ServiceModel; using System.ServiceModel.Channels; using System.Threading; namespace WCFPushService { public class GameStreamService : IGameStreamService { IGameStreamClient _Client; Game _Game = null; Timer _Timer = null; Random _Random = new Random(); public GameStreamService() { _Game = new Game(); } public void GetGameData(Message receivedMessage) { //Get client callback channel _Client = OperationContext.Current.GetCallbackChannel<IGameStreamClient>(); SendData(_Game.GetTeamData()); //Start timer which when fired sends updated score information to client _Timer = new Timer(new TimerCallback(_Timer_Elapsed), null, 5000, Timeout.Infinite); } private void _Timer_Elapsed(object data) { SendData(_Game.GetScoreData()); int interval = _Random.Next(3000, 7000); _Timer.Change(interval, Timeout.Infinite); } private void SendData(object data) { Message gameDataMsg = Message.CreateMessage( MessageVersion.Soap11, "Silverlight/IGameStreamService/ReceiveGameData", data); //Send data to the client _Client.ReceiveGameData(gameDataMsg); } } }
在Silverlight中调用和接受数据,需要不少代码量. 在显示与polling duplex service配合的代码前, 理解Silverlight中收发数据的一些相关步骤是很重要的.
引用程序集和名称空间
创建Channel(通道)对象
收发信息
现在你已经知道了这些基础步骤, 我看来看一下处理这些流程的代码. 下面代码定义了一个PushDataReceiver 类, 封装了factory和channel类和处理所有异步操作. PushDataReceiver 允许一个实现了IProcessor 的类对象通过他, 将除了Service url, Service action以及初始化数据之外的发送到Service. 在这个例子中,IProcessor对象代表了真实的Silverlight页面类, 用于更新数据到用户界面上. 当接受到数据后, 页面类的ProcessData() 方法将被调用.
using System; using System.Net; using System.ServiceModel; using System.ServiceModel.Channels; using System.Threading; using System.IO; using System.Xml.Serialization; namespace SilverlightPushClient { public interface IProcessor { void ProcessData(object receivedData); } public class PushDataReceiver { SynchronizationContext _UiThread = null; public IProcessor Client { get; set; } public string ServiceUrl { get; set; } public string Action { get; set; } public string ActionData { get; set; } public PushDataReceiver(IProcessor client, string url, string action, string actionData) { Client = client; ServiceUrl = url; Action = action; ActionData = actionData; _UiThread = SynchronizationContext.Current; } public void Start() { // Instantiate the binding and set the time-outs PollingDuplexHttpBinding binding = new PollingDuplexHttpBinding() { PollTimeout = TimeSpan.FromSeconds(10), InactivityTimeout = TimeSpan.FromMinutes(1) }; // Instantiate and open channel factory from binding IChannelFactory<IDuplexSessionChannel> factory = binding.BuildChannelFactory<IDuplexSessionChannel>(new BindingParameterCollection()); IAsyncResult factoryOpenResult = factory.BeginOpen(new AsyncCallback(OnOpenCompleteFactory), factory); if (factoryOpenResult.CompletedSynchronously) { CompleteOpenFactory(factoryOpenResult); } } void OnOpenCompleteFactory(IAsyncResult result) { if (result.CompletedSynchronously) return; else CompleteOpenFactory(result); } void CompleteOpenFactory(IAsyncResult result) { IChannelFactory<IDuplexSessionChannel> factory = (IChannelFactory<IDuplexSessionChannel>)result.AsyncState; factory.EndOpen(result); // The factory is now open. Create and open a channel from the channel factory. IDuplexSessionChannel channel = factory.CreateChannel(new EndpointAddress(ServiceUrl)); IAsyncResult channelOpenResult = channel.BeginOpen(new AsyncCallback(OnOpenCompleteChannel), channel); if (channelOpenResult.CompletedSynchronously) { CompleteOpenChannel(channelOpenResult); } } void OnOpenCompleteChannel(IAsyncResult result) { if (result.CompletedSynchronously) return; else CompleteOpenChannel(result); } void CompleteOpenChannel(IAsyncResult result) { IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState; channel.EndOpen(result); // Channel is now open. Send message Message message = Message.CreateMessage(channel.GetProperty<MessageVersion>(), Action , ActionData); IAsyncResult resultChannel = channel.BeginSend(message, new AsyncCallback(OnSend), channel); if (resultChannel.CompletedSynchronously) { CompleteOnSend(resultChannel); } //Start listening for callbacks from the service ReceiveLoop(channel); } void OnSend(IAsyncResult result) { if (result.CompletedSynchronously) return; else CompleteOnSend(result); } void CompleteOnSend(IAsyncResult result) { IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState; channel.EndSend(result); } void ReceiveLoop(IDuplexSessionChannel channel) { // Start listening for callbacks. IAsyncResult result = channel.BeginReceive(new AsyncCallback(OnReceiveComplete), channel); if (result.CompletedSynchronously) CompleteReceive(result); } void OnReceiveComplete(IAsyncResult result) { if (result.CompletedSynchronously) return; else CompleteReceive(result); } void CompleteReceive(IAsyncResult result) { //A callback was received so process data IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState; try { Message receivedMessage = channel.EndReceive(result); // Show the service response in the UI. if (receivedMessage != null) { string text = receivedMessage.GetBody<string>(); _UiThread.Post(Client.ProcessData, text); } ReceiveLoop(channel); } catch (CommunicationObjectFaultedException exp) { _UiThread.Post(delegate(object msg) { System.Windows.Browser.HtmlPage.Window.Alert(msg.ToString()); }, exp.Message); } } void OnCloseChannel(IAsyncResult result) { if (result.CompletedSynchronously) return; else CompleteCloseChannel(result); } void CompleteCloseChannel(IAsyncResult result) { IDuplexSessionChannel channel = (IDuplexSessionChannel)result.AsyncState; channel.EndClose(result); } } }
当PushDataReceiver的Start()的方法被Silverlight调用后, 他将创建一个通道工厂类实例(channel factory), 这个工厂类将创建通道实例. CompleteOpenChannel()回调方法首先调用, 发送初始化消息到服务endpoint, 并且在一个WCF消息对象中封装了发送的数据. 然后除了特定的service action,消息数据在服务器上被调用(The CompleteOpenChannel() callback method shown previously then sends an initial message to the service endpoint and encapsulates the data to be sent in a WCF Message object. The message data is then sent along with the proper service action to call on the server). 当初始化消息被发送了, 接受循环启动了(参考ReceiveLoop()), 接受循环监听所有从服务器发出来的消息并且作适当的处理. 一旦消息被接受到了,CompleteReceive() 方法将被调用, 并且消息数据被路由送回Silverlight页面类.
PushDataReceiver类分发从服务器上传来的数据到Silverlight页面类处理. 从服务器来的数据都是xml格斯, 在Silverlight中处理这个数据有很多技术方式(XmlReader,LinQ,XMLDocument,XmlSerializer等). 我这里选择了XmlSerializer处理这个数据, 只需要少量的代码就可以简单地将 xml数据到CLR类型数据. 虽然可以手工对应到CLR数据映射, 我选择了XSD Schema方式, 并且使用.net的xsd.exe工具通过Schema来生成代码, 如下:
xsd.exe /c /namespace:SomeNamespace Teams.xsd
/c 参数让工具生成类, /namespace 参数表示生成代码的名称空间. 其它参数你可以参考 这里. 如下schema:
<?xml version="1.0" encoding="utf-16"?> <xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Teams"> <xs:complexType> <xs:sequence> <xs:element maxOccurs="unbounded" name="Team"> <xs:complexType> <xs:sequence> <xs:element maxOccurs="unbounded" name="Player"> <xs:complexType> <xs:attribute name="ID" type="xs:string" use="required" /> <xs:attribute name="Name" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="Name" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
Note: 如果使用 xsd.exe生成类,用于Silverlight客户端, 你必须删除一些不需要的代码.
当来自WCF polling duplex service 的数据被Silverlight客户端接受, 将被例子程序中的ProcessData() (被PushDataReceiver调用)方法处理, ProcessData() 使用XmlSerializer 解析出 XML data到自定义的Teams和ScoreData对象中. 这些对象类都是来自XSD Schema.
public void ProcessData(object receivedData) { StringReader sr = null; try { string data = (string)receivedData; sr = new StringReader(data); //Get initial team data if (_Teams == null && data.Contains("Teams")) { XmlSerializer xs = new XmlSerializer(typeof(Teams)); _Teams = (Teams)xs.Deserialize(sr); UpdateBoard(); } //Get updated score data if (data.Contains("ScoreData")) { XmlSerializer xs = new XmlSerializer(typeof(ScoreData)); ScoreData scoreData = (ScoreData)xs.Deserialize(sr); //ScoreDataHandler handler = new ScoreDataHandler(UpdateScoreData); //this.Dispatcher.BeginInvoke(handler, new object[] { scoreData }); UpdateScoreData(scoreData); } } catch { } finally { if (sr != null) sr.Close(); } }
public class Game { Dictionary> _Teams = null; ScoreData _ScoreData = null; public Game() { _Teams = new Dictionary>(); Dictionary _TeamPlayers1 = new Dictionary(); _TeamPlayers1.Add(Guid.NewGuid(), "K. Johnson"); _TeamPlayers1.Add(Guid.NewGuid(), "B. Stoudemaire"); _TeamPlayers1.Add(Guid.NewGuid(), "O. Neal"); _TeamPlayers1.Add(Guid.NewGuid(), "B. Thomas"); _TeamPlayers1.Add(Guid.NewGuid(), "T. Baker"); Dictionary _TeamPlayers2 = new Dictionary(); _TeamPlayers2.Add(Guid.NewGuid(), "S. Davidson"); _TeamPlayers2.Add(Guid.NewGuid(), "C. Jamison"); _TeamPlayers2.Add(Guid.NewGuid(), "M. Bryant"); _TeamPlayers2.Add(Guid.NewGuid(), "A. Nash"); _TeamPlayers2.Add(Guid.NewGuid(), "J. Doe"); _Teams.Add("Bug Slayers", _TeamPlayers1); _Teams.Add("Code Warriors", _TeamPlayers2); _ScoreData = new ScoreData(); _ScoreData.TeamOnOffense = "Bug Slayers"; } public string GetTeamData() { StringWriter sw = new StringWriter(); using (XmlWriter writer = XmlWriter.Create(sw)) { writer.WriteStartElement("Teams"); foreach (string key in _Teams.Keys) { writer.WriteStartElement("Team"); writer.WriteAttributeString("Name", key); Dictionary players = _Teams[key]; foreach (Guid playerKey in players.Keys) { writer.WriteStartElement("Player"); writer.WriteAttributeString("ID", playerKey.ToString()); writer.WriteAttributeString("Name", players[playerKey]); writer.WriteEndElement(); } writer.WriteEndElement(); } writer.WriteEndElement(); } return sw.ToString(); } public string GetScoreData() { UpdateScoreData(); Console.WriteLine("Sending score data..."); StringWriter sw = new StringWriter(); XmlSerializer xm = new XmlSerializer(typeof(ScoreData)); xm.Serialize(sw, _ScoreData); return sw.ToString(); } private void UpdateScoreData() { Random r = new Random(); //Get last action ActionsEnum action = (ActionsEnum)r.Next(0, Enum.GetNames(typeof(ActionsEnum)).Length); //Get player that performed action KeyValuePair player = GetActionPlayer(action); string defensiveTeam = _Teams.Keys.Where(key => key != _ScoreData.TeamOnOffense).First(); string message = _ScoreData.TeamOnOffense + ": " + player.Value; switch (action) { case ActionsEnum.Foul: message = defensiveTeam + ": " + player.Value + " committed a foul."; break; case ActionsEnum.Made2Pointer: message += " scored 2 points."; break; case ActionsEnum.Made3Pointer: message += " scored 3 points."; break; case ActionsEnum.MadeFoulShot: message += " made a foul shot after foul."; break; case ActionsEnum.Miss: message += " missed."; break; case ActionsEnum.Turnover: message += " turned it over"; break; } int teamPos = 1; int points = ((int)action > 3) ? 0 : (int)action; foreach (string name in _Teams.Keys) { if (teamPos == 1 && name == _ScoreData.TeamOnOffense) { _ScoreData.Team1Score += points; } if (teamPos == 2 && name == _ScoreData.TeamOnOffense) { _ScoreData.Team2Score += points; } teamPos++; } _ScoreData.Action = action; _ScoreData.LastActionPlayerID = player.Key; _ScoreData.LastAction = message; _ScoreData.LastActionPoints = points; //Change teams if a foul wasn't committed if (action != ActionsEnum.Foul) { _ScoreData.TeamOnOffense = defensiveTeam; } } private KeyValuePair GetActionPlayer(ActionsEnum action) { Dictionary players = null; if (action == ActionsEnum.Foul) { //Get defensive team players players = _Teams[_Teams.Keys.Where(name => name != _ScoreData.TeamOnOffense).First()]; } else { players = _Teams[_Teams.Keys.Where(name => name == _ScoreData.TeamOnOffense).First()]; } Random r = new Random(); return players.ElementAt(r.Next(0, players.Count)); } private void LogError(Exception exp) { string appFullPath = Assembly.GetCallingAssembly().Location; string logPath = appFullPath.Substring(0, appFullPath.LastIndexOf("\\")) + ".log"; StreamWriter writer = new StreamWriter(logPath, true); try { writer.WriteLine(logPath, String.Format("Error in ScoreSocketServer: {0} \r\n StackTrace: {1}", exp.Message, exp.StackTrace)); } catch { } finally { writer.Close(); } } }