Wednesday, April 23, 2008

A SilverLight-WCF Chat



kick it on DotNetKicks.com

Introduction



This is very simple and basic SilverLight-WCF chat application that uses basicHttpBinding and a timer to call the
WCF service in order to refresh the SilverLight client each certain amount of time.





Technique


The solution consists of:




  • ASP.NET Web Project


    • ASP.NET page to host the SilverLight application

    • WCF Service which contains two generic lists to hold online chatters and messages history



  • SilverLight Project (Client)



SilverLight client calls the Join() method and starts the timer, the timer ticks every two seconds
and calls GetChatters() and GetMessages() methods, client now is free to call
Say() method to send message to other clients or call Leave() method to disconnect
from the service and stop the timer.



WCF service responds to client calls to add or remove Client or Message from
the generic lists, or send these lists back to the client.



The Code



WCF Service


This is the service contract



[ServiceContract]
public interface IbasicChatService
{
[OperationContract(IsOneWay = false)]
bool Join(Chatter _chatter);

[OperationContract(IsOneWay = true)]
void Say(Message _msg);

[OperationContract(IsOneWay = false)]
List< Chatter> GetChatters();

[OperationContract(IsOneWay = false)]
List< Message> GetMessages();

[OperationContract(IsOneWay = true)]
void Leave(Chatter _chatter);
}


This is the client and message data contracts



[DataContract]
public class Chatter
{
private string _name;
private DateTime _time;

[DataMember]
public string Name
{
get { return _name; }
set { _name = value; }
}

[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}

[DataContract]
public class Message
{
private string _sender;
private string _content;
private DateTime _time;

[DataMember]
public string Sender
{
get { return _sender; }
set { _sender = value; }
}

[DataMember]
public string Content
{
get { return _content; }
set { _content = value; }
}

[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}



This is the service implementation



[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,
ConcurrencyMode=ConcurrencyMode.Multiple)]
public class basicChatService : IbasicChatService
{

private List< Chatter> chatters = new List< Chatter>();
private List< Message> messages = new List< Message>();

private object syncObj = new object();


#region IbasicChatService Members

public bool Join(Chatter _chatter)
{
foreach (Chatter chtr in this.chatters)
{
if (chtr.Name == _chatter.Name)
{
return false;
}
}

lock (syncObj)
{
this.chatters.Add(_chatter);

Message msg = new Message();
msg.Sender = "Service";
msg.Content = "---- " + _chatter.Name + " joined chat ----";
msg.Time = DateTime.Now;

this.messages.Add(msg);
}

return true;
}

public void Say(Message _msg)
{
lock (syncObj)
{
this.messages.Add(_msg);
}
}

public List< Chatter> GetChatters()
{
return this.chatters;
}

public List< Message> GetMessages()
{
return this.messages;
}

public void Leave(Chatter _chatter)
{

foreach (Chatter chtr in this.chatters)
{
if (chtr.Name == _chatter.Name)
{
this.chatters.Remove(chtr);

if (this.chatters.Count < 1)
{
this.messages.Clear();
return;
}
Message msg = new Message();
msg.Sender = "Server";
msg.Content = "---- " + _chatter.Name + " leftt chat ----";
msg.Time = DateTime.Now;

this.messages.Add(msg);

return;
}
}
}

#endregion
}


To integrate SilverLight application with WCF service you have to use basicHttpBinding as followed in the
service configuarion file



< system.serviceModel>
< serviceHostingEnvironment aspNetCompatibilityEnabled="true">
< /serviceHostingEnvironment>
< services>
< service behaviorConfiguration="SilverlightApp_Host.basicChatServiceBehavior"
name="SilverlightApp_Host.basicChatService">
< host>
< baseAddresses>
< add baseAddress="http://localhost:6464/localsystem"/>
< /baseAddresses>
< /host>
< endpoint address=""
binding="basicHttpBinding"
contract="SilverlightApp_Host.IbasicChatService">
< identity>
< dns value="localhost"/>
< /identity>
< /endpoint>
< endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
< /service>
< /services>
< behaviors>
< serviceBehaviors>
< behavior name="SilverlightApp_Host.basicChatServiceBehavior">
< serviceMetadata httpGetEnabled="true"/>
< serviceDebug includeExceptionDetailInFaults="true"/>
< /behavior>
< /serviceBehaviors>
< /behaviors>
< bindings>
< basicHttpBinding>
< binding name="basicBinding"
closeTimeout="00:00:20"
maxBufferPoolSize="1048576"
maxBufferSize="1048576"
maxReceivedMessageSize="1048576"
openTimeout="00:00:20"
receiveTimeout="01:00:00"
sendTimeout="00:01:00"
transferMode="Buffered">
< readerQuotas maxArrayLength="1048576" maxBytesPerRead="1048576"
maxStringContentLength="1048576"/>
< security mode="None">
< transport clientCredentialType="Windows"/>
< /security>
< /binding>
< /basicHttpBinding>
< /bindings>
< /system.serviceModel>



SilverLight Client



I'm a very bad designer so I'll let you imagine any design for this application which of course will be better than
this, this is just two list boxes to hold the online chatters and messages history coming from the service, other
controls are three buttons to join, leave the chat or send a message, and two textboxes for chatter name, and message.



This is the application xaml code



< UserControl x:Class="SilverlightApp.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
< Grid x:Name="LayoutRoot" >

< Grid.Background>
< LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
< GradientStop Color="Black" Offset="0"/>
< GradientStop Color="LavenderBlush" Offset="1"/>
< /LinearGradientBrush>
< /Grid.Background>

< Grid.RowDefinitions>
< RowDefinition Height="50"/>
< RowDefinition Height="25"/>
< RowDefinition Height="25"/>
< RowDefinition Height="200"/>
< RowDefinition Height="30"/>
< RowDefinition Height="50"/>
< /Grid.RowDefinitions>
< Grid.ColumnDefinitions>
< ColumnDefinition Width="70"/>
< ColumnDefinition Width="360"/>
< ColumnDefinition Width="120"/>
< ColumnDefinition Width="70"/>
< /Grid.ColumnDefinitions>

< TextBlock x:Name="labelStatus"
Margin="8, 5, 2, 2"
Grid.Row="1"
Grid.Column="2"
FontFamily="Consolas"
FontSize="13"
Foreground="White">Offline< /TextBlock>

< Grid x:Name="layoutLogin" Grid.Row="2" Grid.Column="1">
< Grid.RowDefinitions>
< RowDefinition Height="25" />
< /Grid.RowDefinitions>
< Grid.ColumnDefinitions>
< ColumnDefinition Width="80"/>
< ColumnDefinition Width="160"/>
< ColumnDefinition Width="*"/>
< /Grid.ColumnDefinitions>

< TextBlock FontFamily="Consolas"
Margin="4, 2, 2, 0"
Grid.Row="0"
Grid.Column="0"
FontSize="12"
Foreground="White">User Name:< /TextBlock>

< TextBox x:Name="textboxName"
Margin="2, 2, 2, 2"
Grid.Row="0"
Grid.Column="1">< /TextBox>

< Button x:Name="buttonJoin"
Background="Transparent"
Margin="2, 2, 2, 2"
Grid.Row="0"
Grid.Column="2"
Click="buttonJoin_Click"
Content="Join">< /Button>

< /Grid>

< Button x:Name="buttonLeave"
Background="Transparent"
Margin="2, 2, 2, 2"
Click="buttonLeave_Click"
Grid.Row="2"
Grid.Column="2"
Content="Leave">< /Button>

< ListBox x:Name="listBoxMsgs"
LayoutUpdated="listBoxMsgs_LayoutUpdated"
Margin="2, 2, 2, 2"
Grid.Row="3"
Grid.Column="1">< /ListBox>

< ListBox x:Name="listBoxNames"
Margin="2, 2, 2, 2"
Grid.Row="3"
Grid.Column="2">< /ListBox>

< TextBox x:Name="textboxMsg"
Margin="2, 2, 2, 2"
Grid.Row="4"
Grid.Column="1">< /TextBox>

< Button x:Name="buttonSend"
Background="Transparent"
Margin="2, 2, 2, 2"
Grid.Row="4"
Grid.Column="2"
Click="buttonSend_Click"
Content="Send">< /Button>

< /Grid>
< /UserControl>



SilverLight client implementation consists of some feilds, constructor, connection event handlers, private methods, and UI event handlers


Feilds



namespace SilverlightApp
{
public partial class Page : UserControl
{

#region Feilds

//TIMER
DispatcherTimer _timer = null;

SVC.IbasicChatServiceClient proxy = null;
SVC.Chatter localChatter = null;

//List to hold online chatters
List< SVC.Chatter> chatters = new List< SilverlightApp.SVC.Chatter>();

//List to hold messages history
List< SVC.Message> messages = new List< SilverlightApp.SVC.Message>();

//To enable listbox auto scroll
bool flag = false;

#endregion

...



Page constructor



public Page()
{
InitializeComponent();
buttonLeave.IsEnabled = false;
buttonSend.IsEnabled = false;
textboxMsg.KeyDown += new KeyEventHandler(textboxMsg_KeyDown);

//Create Timer and set interval
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(2);
_timer.Tick += new EventHandler(_timer_Tick);
}



Connection event handlers




#region Connection Event Handlers

void proxy_JoinCompleted(object sender, SilverlightApp.SVC.JoinCompletedEventArgs e)
{
if (e.Result)
{
labelStatus.Text = "Online";
this._timer.Start();
}
else
{
proxy.Close();
labelStatus.Text = "Name Found";
}
}

void proxy_LeaveCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
proxy.Close();
}

void proxy_GetMessagesCompleted(object sender, SilverlightApp.SVC.GetMessagesCompletedEventArgs e)
{
listBoxMsgs.Items.Clear();
foreach (SVC.Message msg in e.Result)
{
//This will not scroll the listbox
//listBoxMsgs.Items.Add(msg.Sender + " : " + msg.Content);

//Auto scroll, AddItem() is a private method
AddItem(msg.Sender + " : " + msg.Content);
flag = true;
}

}

void proxy_GetChattersCompleted(object sender, SilverlightApp.SVC.GetChattersCompletedEventArgs e)
{
listBoxNames.Items.Clear();
foreach (SVC.Chatter chtr in e.Result)
{
listBoxNames.Items.Add(chtr.Name);
}
}

void InnerChannel_Opened(object sender, EventArgs e)
{
HandleProxy();
}

void InnerChannel_Faulted(object sender, EventArgs e)
{
HandleProxy();
}

void InnerChannel_Closed(object sender, EventArgs e)
{
HandleProxy();
}


#endregion



Private methods



#region Private Methods

public void AddItem(String messageText)
{
listBoxMsgs.Items.Add(new ListBoxItem { Content = messageText });
}

private void Join()
{
proxy = null;
//CREATE PROXY
proxy = new SilverlightApp.SVC.IbasicChatServiceClient();
proxy.InnerChannel.Closed += new EventHandler(InnerChannel_Closed);
proxy.InnerChannel.Faulted += new EventHandler(InnerChannel_Faulted);
proxy.InnerChannel.Opened += new EventHandler(InnerChannel_Opened);

//CREATE LOCAL CLIENT
this.localChatter = new SilverlightApp.SVC.Chatter();
this.localChatter.Name = textboxName.Text.ToString();
this.localChatter.Time = DateTime.Now;

//JOIN()
proxy.JoinAsync(this.localChatter);
proxy.JoinCompleted +=
new EventHandler< SilverlightApp.SVC.JoinCompletedEventArgs>(proxy_JoinCompleted);
}

private void Send()
{
if (proxy != null && proxy.State == CommunicationState.Opened)
{
SVC.Message msg = new SilverlightApp.SVC.Message();
msg.Sender = this.localChatter.Name;
msg.Content = textboxMsg.Text.ToString();
msg.Time = DateTime.Now;

proxy.SayAsync(msg);
textboxMsg.Text = "";
}
else
{
HandleProxy();
}
}

private void HandleProxy()
{
if (proxy != null)
{
switch (proxy.State)
{
case CommunicationState.Closed:
proxy = null;
labelStatus.Text = "Offline";
buttonJoin.IsEnabled = true;
buttonLeave.IsEnabled = false;
buttonSend.IsEnabled = false;
listBoxMsgs.Items.Clear();
listBoxNames.Items.Clear();
break;
case CommunicationState.Closing:
break;
case CommunicationState.Created:
break;
case CommunicationState.Faulted:
proxy.Abort();
proxy = null;
labelStatus.Text = "Offline";
buttonJoin.IsEnabled = true;
buttonLeave.IsEnabled = false;
buttonSend.IsEnabled = false;
listBoxMsgs.Items.Clear();
listBoxNames.Items.Clear();
break;
case CommunicationState.Opened:
buttonJoin.IsEnabled = false;
buttonLeave.IsEnabled = true;
buttonSend.IsEnabled = true;
labelStatus.Text = "Online";
break;
case CommunicationState.Opening:
break;
default:
break;
}
}
else
{
//Join();
}
}

#endregion


And finally the UI event handlers



#region UI Events

void _timer_Tick(object sender, EventArgs e)
{
//Refresh online chatters and messages by calling GetChattersAsync() and GetMessagesAsync()
proxy.GetChattersAsync();
proxy.GetChattersCompleted +=
new EventHandler< SilverlightApp.SVC.GetChattersCompletedEventArgs>(proxy_GetChattersCompleted);

proxy.GetMessagesAsync();
proxy.GetMessagesCompleted +=
new EventHandler< SilverlightApp.SVC.GetMessagesCompletedEventArgs>(proxy_GetMessagesCompleted);
}

void textboxMsg_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
Send();
}
}

private void buttonJoin_Click(object sender, RoutedEventArgs e)
{
Join();
}

private void buttonSend_Click(object sender, RoutedEventArgs e)
{
Send();
}

private void buttonLeave_Click(object sender, RoutedEventArgs e)
{
this._timer.Stop();
if (proxy != null && proxy.State == CommunicationState.Opened)
{
proxy.LeaveAsync(this.localChatter);
proxy.LeaveCompleted +=
new EventHandler< System.ComponentModel.AsyncCompletedEventArgs>(proxy_LeaveCompleted);
}
else
{
HandleProxy();
}
}

private void listBoxMsgs_LayoutUpdated(object sender, EventArgs e)
{
if (flag && listBoxMsgs.Items.Count > 1)
{
listBoxMsgs.ScrollIntoView(listBoxMsgs.Items[listBoxMsgs.Items.Count - 1]);
flag = false;
}
}

#endregion




Other Stuff


Enable cross domain calls for SilverLight application


In order to enable cross domain calls just copy this file clientaccesspolicy.xml to your host root, if you use IIS then
copy the file to WWWRoot folder and then restart IIS. (file is included in the source code).