Note: This article is submitted by Thomas Holloway for Silverlight Contest: Write and Win.Thanks a lot, Thomas! Hello All, Please drop a comment if you like it.
This article is obsolete and is valid only for Silverlight 2 and previous versions.
Overview
Alright, so now that we know what a simple messenger looks like with WCF callbacks, what does it take to put one together in Silverlight? For starters, Silverlight does not have the same capabilities as the full WCF framework. WCF callbacks in Silverlight must be done through HTTP Polling and therefore we don’t really have a truly bi-directional environment. Instead, we must manually setup the message asynchronous message architecture with a few special classes in a new System.ServiceModel.PollingDuplex library. It’s really not as scary as it seems, but it is somewhat confusing. Nevertheless, this article takes you through all that, plus how to work with Json serialization and JQuery to create an instant messenger for the web in Silverlight.
Download the Solution
The GUI
I thought as a change in pace we will be working with XHTML and JQuery for our user interface. For this GUI, I thought it would be nice to have a similar chat bar to that of Facebook. We know that the bar will be docked to the bottom, stretch the width of the browser and contain the important chat boxes docked to the right of the bar. Before the chat is available, it should require us to log in with a nickname.
This “plexbar” which thus far includes the title, the nickname textbox, connect button and welcome text, in HTML looks like this:
<div id=”plexbar”>
<div id=”container”>
<div id=”title”>
<img alt=”" src=”Images/Universe.png” height=”20″ width=”20″
style=”float: left; padding-right: 4px;” />Post Galaxy
</div>
<input id=”nicknameTextBox” type=”text” class=”nickname” value=”Type a nickname” />
<input id=”connectButton” type=”button” value=”connect” class=”connect” />
— chat boxes go here –
<div id=”login”>Welcome</div>
</div>
</div>
A few CSS rules will dock the bar to the bottom, stretch it to the width of the browser and give it style:
body { overflow: hidden; width: 100%; margin: 0; background-color: #131313; }
#plexbar {
width: 100%; position: fixed;
bottom: 0px; text-align: right;
min-width: 450px;
}
#container {
height: 26px; margin-left: 15px;
margin-right: 15px; border-top: solid 1px #424242;
background-color: #323232; border-right: solid 1px #424242;
border-left: solid 1px #424242;
}
#title {
float: left; color: #AFAFAF;
padding-top: 2px; padding-left: 5px;
font-weight: bold; font-family: Trebuchet MS;
font-size: 10pt; cursor: default;
}
.nickname {
float: left; margin: 0px 0px 0px 20px;
font-size: 9pt; height: 23px;
padding: 3px 4px 0px 6px; width: 110px;
text-align: left; font-family: Trebuchet MS;
background-color: #424242; color: #AFAFAF;
border-left: solid 1px #878787; border-right: solid 1px #878787;
border-bottom-style: none; border-top-style: none;
font-style: italic;
}
.connect {
border: solid 1px #878787; background-color: #424242;
height: 100%; padding: 3px 10px 3px 10px;
color: #AFAFAF; margin-left: 10px;
outline-style: none; float: left;
}
#login { color: #AFAFAF; margin-right: 10px; font-style: italic; }
The — chat boxes — section will act as a button, initially it will be hidden. The html looks as follows:
<div id=”chatbar”>
<div id=”usersButton” class=”button”>
<div class=”usersTitle”>
<img alt=”" src=”Images/Dot.png” height=”12″ width=”12″ class=”titleImage” />
Online Friends <strong id=”numberOfOnline”>(loading)</strong>
</div>
</div>
</div>
The CSS that defines this HTML is show below:
#chatbar { height: 26px; float: right; margin-right: 10px; }
.button {
width: auto; height: 100%;
cursor: pointer; text-align: center;
margin: 0 2px 0 2px; display: inline-block;
border-left: solid 1px #565656; border-right: solid 1px #565656;
color: #AFAFAF; font-size: 9pt;
padding-top: 3px; padding-right: 13px;
padding-left: 12px; font-family: Trebuchet MS;
background-color: #323232;
}
.button:hover { background-color: #444444; color: #FFFFFF; }
.usersTitle strong { font-weight: bolder; font-size: 1.01em; }
.titleImage { position: relative; top: 1px; }
Of course, we will need a place for showing a list of users, I have setup a simple container. In order to get the vertical stack that we want, I am using a table where all the user items will be the subsequent rows.
<div id=”box”>
<div id=”plexbox”>
<table id=”listOUsers” style=”margin:0;padding:0;”>
</table>
</div>
</div>
JQuery
JQuery is a javascript library that handles all the nitty gritty parts of animation, DOM manipulation, JSON, AJAX and all sort of neat effects. The first thing to be aware of with JQuery is that the $ (dollar sign) is what you base almost all of your code from. The $ acts as a selector just like in scripting languages like PHP.
$(“#box”).fadeIn(“slow”)
There is a ton of really helpful documentation on JQuery’s site and I recommend looking through it, try a few of your own solutions out. Moving on, you can add the following scripts to the headers to use JQuery. The second script is where all of our JQuery code will go.
<script type=”text/javascript” src=”http://code.jquery.com/jquery-latest.js”></script>
<script type=”text/javascript” src=”DefaultBehavior.js”></script>
When the document first loads we need to hide a few visible items and show a few invisible ones.
$(document).ready(function() {
$(“#box”).hide();
$(“#chatbar”).hide();
$(“#login”).show();
…
…
});
In addition to the initial display changes, we need to wire to the textbox’s focus and blur events (when the textbox is focused and unfocused). Here I am simply changing the text based on whether or not there is any typed content. By doing this, it adds a watermark like effect to the textbox.
$(“#nicknameTextBox”).blur(function() {
if($(“#nicknameTextBox”).val() == “”) {
$(“#nicknameTextBox”).val(“Type a nickname”);
}
});
$(“#nicknameTextBox”).focus(function() {
if($(“#nicknameTextBox”).val() == “Type a nickname”) {
$(“#nicknameTextBox”).val(“”);
}
});
However, the two most important events to watch are a) when the user clicks the usersButton (that way we can show/hide the online friends) and b) when the user clicks the connect button.
$(“#usersButton”).click(function() {
$(“#box”).slideToggle(“fast”);
});
$(“#connectButton”).click(function() {
if($(“#connectButton”).val() == “connect”) {
if($(“#nicknameTextBox”).val() != “Type a nickname”) {
connect();
}
}
else {
leave();
}
});
Before, I move into the code for connect and leave, we are going to first get established with making javascript calls to Silverlight C# code and vice-versa. Add a Silverlight application and place a Silverlight control in the default.aspx page (along with the other html content).
<asp:ScriptManager ID=”scriptManager” runat=”server”></asp:ScriptManager>
<asp:Silverlight ID=”Xaml1″ runat=”server” Source=”~/ClientBin/Plex.xap”
MinimumVersion=”2.0.30523″ Width=”0″ Height=”0″ />
You will need to add the register prefix at the top of the page just like this:
<%@ Register Assembly=”System.Web.Silverlight”
Namespace=”System.Web.UI.SilverlightControls” TagPrefix=”asp” %>
Now that we have that setup, we will move into setting up the link between the Javascript and Silverlight code.
Silverlight Scriptables
Setting up scriptable content is as easy as its name implies. Simply create a class, and add the ScriptableType class attribute. Then, any method that you want to be callable from javascript should have a ScriptableMember method attribute. That’s it!
[ScriptableType]
public class ScriptCode
{
[ScriptableMember]
public void Connect(string name)
{
// connect code
}
}
The only thing left to let our javascript be able to see it is using the HtmlPage.RegisterScriptableObject method when the App loads.
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new Page();
code = new ScriptCode();
HtmlPage.RegisterScriptableObject(“PlexScriptApp”, code);
}
Now we can call this code from our clientside javascript functions.
function connect() {
$(“#login”).show();
$(“#login”).val(“Logging in…”);
$(“#connectButton”).fadeOut(“slow”);
var slCtrl = document.getElementById(“Xaml1″);
slCtrl.Content.PlexScriptApp.Connect($(“#nicknameTextBox”).val());
}
function leave() {
$(“#chatbar”).fadeOut(“fast”);
$(“#login”).fadeIn(“slow”);
$(“#connectButton”).val(“connect”);
var slCtrl = document.getElementById(“Xaml1″);
slCtrl.Content.PlexScriptApp.Leave();
}
Before we get too crazy, we need to actually have some sort of server communication setup to do all this.
WCF Duplex Communication
Setting up a Duplex WCF service is as simple as adding a CallbackContract to the the ServiceContract of interest. We have this same capability with Silverlight 2 Beta 2, however it is somewhat limited in power. In Silverlight 2 Beta 2, we have to setup a polling binding where the client will using HttpPolling to see if there are any messages that are waiting. In light of this, much (if not all) of the setup is manual and without the traditional proxy layer that we’re used to.
Because of this manual process, we have to send Messages through a lower SOAP11 based class, Message, in the System.ServiceModel.Channels namespace. The Message class specifies the action, the MessageVersion and the content being sent with every request or response.
[ServiceContract(Namespace = "Silverlight", CallbackContract = typeof(IPlexChatCallback))]
public interface IPlexChatService
{
[OperationContract(IsOneWay = true)]
void Join(Message message);
[OperationContract(IsOneWay = true)]
void Leave(Message message);
[OperationContract(IsOneWay = true)]
void PostMessage(Message message);
}
[ServiceContract]
public interface IPlexChatCallback
{
[OperationContract(IsOneWay = true)]
void UserJoined(Message message);
[OperationContract(IsOneWay = true)]
void UserLeft(Message message);
[OperationContract(IsOneWay = true)]
void Users(Message message);
[OperationContract(IsOneWay = true)]
void NewMessage(Message message);
}
Of course, this doesn’t look it provides a whole lot of sustenance. That’s why we need to define a few data contracts.
[DataContract]
public class User
{
[DataMember]
public string ID { get; set; }
[DataMember]
public string Name { get; set; }
}
[DataContract]
public class ChatMessage
{
[DataMember]
public string FromID { get; set; }
[DataMember]
public string ToID { get; set; }
[DataMember]
public string Body { get; set; }
}
To use these types, we need to use a work with the Message object and choose a Serialization or Deserialization scheme to write and read data. For this article, since we’re working directly with the DOM using JQuery, we might as well work directly with Json. Json serialization is done via the DataContractJsonSerializer and the JsonReaderWriterFactory located in the System.Runtime.Serialization.Json namespace. You can obtain this by adding the System.ServiceModel.Web library to your project.
Client Callbacks, Message and Json Serialization
Now that the service is defined, we can implement it by first setting up a dictionary to hold all of the IPlexChatCallback clients.
public class PlexChatService : IPlexChatService
{
private static Dictionary<IPlexChatCallback, User> clientCallbacks =
new Dictionary<IPlexChatCallback, User>();
private const string Action = “Silverlight/IPlexChatService/”;
The Action is defined for brevity since we are going to be using it each time we need to create a Message.
Remember that connect method? When a user clicks the connect button the client will make a call to the Join method passing in their desired nickname. This method should add the client to the clientCallbacks dictionary and notify any existing clients that a new user has joined.
void IPlexChatService.Join(Message message)
{
OperationContext context = OperationContext.Current;
if (context.InstanceContext.State == CommunicationState.Opened)
{
IPlexChatCallback callback = context.GetCallbackChannel<IPlexChatCallback>();
if (!clientCallbacks.ContainsKey(callback))
{
User user = new User();
user.ID = Guid.NewGuid().ToString();
user.Name = message.GetBody<string>();
The first part of the method is fairly straight-forward. Just like in the previous article we must obtain the client callback channel and determine if the client is in an opened state. Since the user is connected the method must create a new User object and set its properties, including the name via the Message content. Since the client is simply passing in the name, a call to GetBody<T> will suite our needs since we don’t need to deserialize any wrapped objects.
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(User));
MemoryStream ms = new MemoryStream();
XmlWriter writer = JsonReaderWriterFactory.CreateJsonWriter(ms);
serializer.WriteObject(ms, user);
ms.Flush();
string xml = Encoding.Default.GetString(ms.GetBuffer());
xml = xml.Substring(0, xml.LastIndexOf(‘}’) + 1);
Here we are using the DataContractJsonSerializer to serialize the User object into its Json format. The string is then trimmed of the excess bytes located at the end of the ‘}’ delimiter of the Json object. The last part is constructing the Message itself and notifying everyone else.
Message newUM = Message.CreateMessage(
MessageVersion.Soap11, Action + “UserJoined”, xml);
newUM.Headers.Insert(0, MessageHeader.CreateHeader(“Action”, “”, “UserJoined”));
foreach (var item in clientCallbacks.Keys)
item.UserJoined(newUM);
clientCallbacks.Add(callback, user);
}
Before the method ends, we are going to make a call to the client to fill in all the current users in the room. We use the same concepts we used above to construct the Message and send it to the user.
DataContractJsonSerializer newJson = new DataContractJsonSerializer(typeof(User[]));
MemoryStream newMemoryStream = new MemoryStream();
XmlWriter newWriter = JsonReaderWriterFactory.CreateJsonWriter(newMemoryStream);
newJson.WriteObject(newMemoryStream, clientCallbacks.Values.ToArray());
newMemoryStream.Flush();
string usersXML = Encoding.Default.GetString(newMemoryStream.GetBuffer());
usersXML = usersXML.Substring(0, usersXML.LastIndexOf(‘]’) + 1);
Message newMessage = Message.CreateMessage(
MessageVersion.Soap11, Action + “Users”, usersXML);
newMessage.Headers.Insert(0, MessageHeader.CreateHeader(“Action”, “”, “Users”));
callback.Users(newMessage);
}}
That’s all it takes really. Notice here that I’m trimming based on the ‘]’ delimiter. This is because Json arrays are always defined as beginning with a [ and ending with a ], while single Json objects start with { and end with }.
Now, when the client calls the leave operation virtually nothing will be passed in; so we simply must create a Message to notify everyone else that the user has left the room. We also don’t mind if the operation context is connected anymore since we need to simply notify in any case.
void IPlexChatService.Leave(Message incoming)
{
OperationContext context = OperationContext.Current;
IPlexChatCallback callback = context.GetCallbackChannel<IPlexChatCallback>();
if (clientCallbacks.ContainsKey(callback))
{
Message message = Message.CreateMessage(
MessageVersion.Soap11, Action + “UserLeft”,
clientCallbacks[callback].ID);
message.Headers.Insert(0, MessageHeader.CreateHeader(“Action”, “”, “UserLeft”));
foreach (var item in clientCallbacks.Keys)
if (!item.Equals(callback))
item.UserLeft(message);
clientCallbacks.Remove(callback);
}
}
The last method, PostMessage, takes upon a trickier concept that we have to work around with serialization.
XmlSerialization and Library Reference Limitations
Have you ever tried to add a Silverlight class library as a reference to a non-Silverlight project? Doesn’t work, right? The same occurs when you try to add a non-Silverlight class library to a Silverlight project. Unfortunately since Silverlight is using an entirely different CLR, creating an in-between library to store common types used by both our WCF service and Silverlight is simply not possible. In our last operation the Silverlight application will be sending a serialized version of the ChatMessage object.
void IPlexChatService.PostMessage(Message message)
{
OperationContext context = OperationContext.Current;
if (context.InstanceContext.State == CommunicationState.Opened)
{
IPlexChatCallback callback = context.GetCallbackChannel<IPlexChatCallback>();
if (clientCallbacks.ContainsKey(callback))
{
XmlSerializer serializer = new XmlSerializer(typeof(ChatMessage));
ChatMessage chatMessage = (ChatMessage)serializer.Deserialize(
new StringReader(message.GetBody<string>()));
chatMessage.FromID = clientCallbacks[callback].ID;
DataContractJsonSerializer json = new
DataContractJsonSerializer(typeof(ChatMessage));
MemoryStream ms = new MemoryStream();
XmlWriter writer = JsonReaderWriterFactory.CreateJsonWriter(ms);
json.WriteObject(ms, chatMessage);
ms.Flush();
string xml = Encoding.Default.GetString(ms.GetBuffer());
xml = xml.Substring(0, xml.LastIndexOf(‘}’) + 1);
Message newMessage = Message.CreateMessage(
MessageVersion.Soap11, Action + “NewMessage”, xml);
newMessage.Headers.Insert(0, MessageHeader.CreateHeader(
“Action”, “”, “NewMessage”));
var client = clientCallbacks.Where((kvp) => {
return kvp.Value.ID == chatMessage.ToID;
}).FirstOrDefault();
if (client.Key != null)
client.Key.NewMessage(newMessage);
}
}
}
We are simply deserializing the ChatMessage, setting the FromID according to the current operation context and using Json serialization to send it to the appropriate receiving user. By doing this we get our fully functional private chat.
Message Headers
If you notice, in every message I am inserting a header called Action. This is because with every Message sent in this manner, it tends to lose some context. When you take a look at the actual message being sent, this is what you get originally:
<?xml version=”1.0″ encoding=”utf-16″?>
<s:Envelope xmlns:s=”http://schemas.xmlsoap.org/soap/envelope/”>
<s:Header> <Action>Users</Action>
<netdx:Duplex xmlns:netdx=”http://schemas.microsoft.com/2008/04/netduplex”>
<netdx:Address>http://docs.oasis-open.org/ws-rx/wsmc/200702/anoynmous?
id=4cf560de-babe-46da-a6a9-a3878e7d5172</netdx:Address>
<netdx:SessionId>6ee557c6-2a1b-46b9-8339-73c0df1bc49e</netdx:SessionId>
</netdx:Duplex>
</s:Header>
<s:Body>
<string xmlns=”http://schemas.microsoft.com/2003/10/Serialization/”>
[{"ID":"1bf8b9a2-6d13-4893-b1a6-8d941d8d523a","Name":"nyxtom"}]</string>
</s:Body>
</s:Envelope>
Notice that without the Action this Message would otherwise have no meaning to us! We’re just about done with the Service. We need one additional component that will allow this service to be Duplex.
DuplexPollingFactory
If you’ve never used one before, a ServiceHostFactory is what you can create to essentially replace what the Web.Config would do to setup a web service, it’s bindings and endpoint addresses. Since DuplexCommunication is brand new, we need to setup such a component. Add a class to your Service project and take a look at the following code.
public class PlexServiceHostFactory : ServiceHostFactoryBase
{
public override ServiceHostBase CreateServiceHost(string constructorString,
Uri[] baseAddresses)
{
return new PlexChatServiceHost(baseAddresses);
}
}
The first class, PlexServiceHostFactory does not do anything special that any normal ServiceHostFactory would do except create the ServiceHost. In order to use the next piece of code, though, you need to add a reference to System.ServiceModel.PollingDuplex. This library is located in your %Program Files%\Microsoft SDKs\Silverlight\v2.0\Libraries\Server\Evaluation\ directory. When we code the Silverlight application we will need to use the other System.ServiceModel.PollingDuplex library located in the Client directory.
class PlexChatServiceHost : ServiceHost
{
public PlexChatServiceHost(params System.Uri[] addresses)
{
base.InitializeDescription(typeof(PlexChatService),
new UriSchemeKeyedCollection(addresses));
}
protected override void InitializeRuntime()
{
string pollTimeout = ConfigurationSettings.AppSettings["PollTimeout"];
string inactivityTimeout = ConfigurationSettings.AppSettings["InactivityTimeout"];
// Define the binding and set time-outs.
PollingDuplexBindingElement pdbe = new PollingDuplexBindingElement()
{
PollTimeout = TimeSpan.Parse(pollTimeout),
InactivityTimeout = TimeSpan.Parse(inactivityTimeout)
};
Here we are simply setting up a PollingDuplexBinding for the Service. Remember that PollingDuplex has to has its timeouts of sorts. This is why there is a PollTimeout and InactivityTimeout. I have added these to the AppSettings in the Web.Config file for brevity. Next, all we need to do is add the ServiceEndpoint with out custom binding and we’re good to go!
// Add an endpoint for the given service contract.
this.AddServiceEndpoint(
typeof(IPlexChatService),
new CustomBinding(
pdbe,
new TextMessageEncodingBindingElement(
MessageVersion.Soap11,
System.Text.Encoding.UTF8),
new HttpTransportBindingElement()),
“”);
base.InitializeRuntime();
}
}
In your Web Project you just need the PlexChatService.svc to contain the following with the Factory attribute.
<%@ ServiceHost Factory=”MyService.PlexServiceHostFactory” %>
<%@ Assembly Name=”MyService” %>
ClientAccessPolicy.xml
Silverlight 2 Beta 2 (and future releases) check for the ClientAccessPolicy.xml file. This file is essentially our security configuration that either grants or denys cross domain access and local domain access to certain ports and services. Whenever Silverlight first loads it checks for this file to see if it can access certain socket ports and resources. Here is what ours looks like:
<?xml version=”1.0″ encoding=”utf-8″ ?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers=”*”>
<domain uri=”*” />
</allow-from>
<grant-to>
<resource path=”/” include-subpaths=”true” />
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
ScriptCode and DuplexFactory
Remember the ScriptCode class that handles all the Javascript calls to and fro? That class will need to work with our web service using a class I put together called DuplexFactory. (Remember to use this part, you need to add a reference to System.ServiceModel.PollingDuplex in the directory located at %ProgramFiles%\Microsoft SDKs\Silverlight\v2.0\Libraries\Client\).
This class handles all the nitty gritty stuff that you have to do to actually work with a DuplexService (the manual part of this article essentially). So, for brevity of this article, check out the source code in the download to take a look at what this class is doing.
I’ve managed to make this part of Duplex Services as simple as possible. All you need to do is create an instance of this class and pass in two parameters: PollTimeout and InactivityTimeout timespans. Then you must set the ServiceEndpointAddress before you can call the OpenChannel method to actually initiate communication. Finally, wire to the events of interest (specifically the MessageReceived and OpenCompleted events) and use the SendMessage method to send the SOAP11 Message we’ve been using throughout the article.
[ScriptableType]
public class ScriptCode
{
private DuplexFactory factory;
private const string Action = “Silverlight/IPlexChatService/”;
private string name;
[ScriptableMember]
public void Connect(string name)
{
this.name = name;
if (factory == null) {
factory = new DuplexFactory(TimeSpan.FromSeconds(40), TimeSpan.FromMinutes(1));
factory.OpenCompleted += new EventHandler(factory_OpenCompleted);
factory.MessageReceived += new
EventHandler<Common.MessageEventArgs>(factory_MessageReceived);
factory.ServiceEndpointAddress = new
EndpointAddress(“http://localhost:4895/PlexChatService.svc”);
factory.OpenChannel();
}
}
Be sure to check what port your service is running on, or manually set it by right-clicking on your Web project, click properties, click the Web tab and instead of Auto-assign Port, choose Specific Port.
Invoking Javascript Methods from C#
Now, remember we still need to make a call to the Join operation as soon as we are connected. In the factory_OpenCompleted we will notify the user (via Javascript) that the service is connected. Then we will create a Message, just like we’ve doing before, and send it using the SendMessage method in the DuplexFactory object.
void factory_OpenCompleted(object sender, EventArgs e)
{
HtmlPage.Window.Invoke(“connected”);
Message joinMessage = Message.CreateMessage(
MessageVersion.Soap11, Action + “Join”, name);
factory.SendMessage(joinMessage);
}
All you need to do to call Javascript methods from C# is use the HtmlPage.Window.Invoke method and pass in the name of the function and any parameters you have. In this case we are just calling it without parameters. Let’s take a look at the connected code in our Javascript file.
function connected() {
$(“#login”).hide();
$(“#chatbar”).show();
$(“#chatbar”).fadeIn(“fast”);
}
Javascript Functions for each Message Received
Just a few things to hide and a few things to show and animate in. Now, for the receiving part of our client, we need to use that Action header we inserted from the Service to figure out what Javascript function to pass the Json data to.
void factory_MessageReceived(object sender, Common.MessageEventArgs e)
{
Message message = e.Message;
string method = message.Headers.GetHeader<string>(0);
string content = message.GetBody<string>();
switch (method)
{
case “UserJoined”:
HtmlPage.Window.Invoke(“userJoined”, content);
break;
case “UserLeft”:
HtmlPage.Window.Invoke(“userLeft”, content);
break;
case “Users”:
HtmlPage.Window.Invoke(“getUsers”, content);
break;
case “NewMessage”:
HtmlPage.Window.Invoke(“newMessage”, content);
break;
}
}
The method is obtained by calling the Message.Headers.GetHeader<string>(0); since the Action header is always the first header in the Message. Next, we do a switch on that Method and invoke the appropriate Javascript function, passing in the data provided by the body. Going through each function, starting with the getUsers, we need to evaluate the Json data and loop through each User.
var allUsers = null;
function getUsers(users) {
allUsers = eval(users);
for(i=0; i<allUsers.length; i=i+1) {
var u = allUsers[i];
if(u.Name == $(“#nicknameTextBox”).val())
$(“#listOUsers”).prepend(“<tr id=\”" + u.ID +
“\”><td class=\”me\”>” + u.Name + “</td></tr>”);
else
$(“#listOUsers”).append(“<tr id=\”" + u.ID +
“\”><td class=\”userOnline\”>” + u.Name + “</td></tr>”);
}
$(“#numberOfOnline”).html(“(” + allUsers.length + “)”);
$(“#connectButton”).val(“disconnect”);
$(“#connectButton”).fadeIn(“slow”);
register($(“.userOnline”));
}
The function checks to see if one of the users is our user, that way we can style it different from the rest. At each iteration, using JQuery, we insert a row for each User. When its done, the function sets the numberOfOnline to the right number and shows the disconnect button. The last part of the function needs to wire to each of the row’s click event. Since we’ve added more than one, we must select all of these rows at once with JQuery. And since each of our cells applies the userOnline style, JQuery can find all elements that apply this style using the $(”.userOnline”) selector. The function register must wire to each element’s click function to open up a new chatbox (if one is not already open).
function register(obj) {
$(obj).click(function() {
var name = $(this).text();
var id = “#” + name + “chat”;
if($(id).length == 0) {
var newContent = “<div id=\”" + name +
“chat\” class=\”button\”>” + name + “</div>”;
$(“#usersButton”).before(newContent);
}
});
}
This function must also be used when new users join in the userJoined function below. The only difference below is that instead of evaluating an array of Users, there is only one User being passed in.
function userJoined(userJson) {
var user = eval(userJson);
$(“#listOUsers”).append(“<tr id=\”" + user.ID +
“\”><td class=\”userOnline\”>” + user.Name + “</td></tr>”);
allUsers.push(user);
$(“#numberOfOnline”).html(“(” + allUsers.length + “)”);
register($(“#” + user.ID));
}
Thus far, without the chat part, we already have a semi-usable chat bar. Here you can see the online users container in action.
The Chat
If you notice, we haven’t filled out the whole point of this program! Adding chat is going to modify our register a bit, since we need to insert the chat body as soon as its available. The above code will register when a user joins, but we also need to register when we receive a new message. When we receive a new message we need to iterate through the users to check who the chat message is coming from. Then, a call to the register function will wire the events for the chatbox, and lastly we need to insert the message into the chat body.
function newMessage(message) {
var chatMessage = eval(“(” + message + “)”);
for(i=0;i<allUsers.length;i+=1) {
var user = allUsers[i];
if(user.ID == chatMessage.FromID) {
var name = user.Name;
// Make sure it exists
chatboxRegister(name);
// Insert into the text body
var chatBodyID = “#” + name + “chatBody”;
insertToChatBody(name, “<b>” + name + “: </b>” + chatMessage.Body);
break;
}
}
}
Since we’re using the chatBoxRegister function to wire the events and insert the content, the new register function looks like this:
var currentChatID = null;
var currentChatName = null;
function register(obj) {
$(obj).click(function() { chatboxRegister($(this).text()); });
}
The chatBoxRegister function will need to do the same things the old register function did, in addition to inserting the chat box itself.
function chatboxRegister(name) {
var id = “#” + name + “chat”;
if($(id).length == 0) {
var chatContent = “<div id=’” + name + “chatbox’ class=’chatbox’>” +
“<div class=’titleBar’>Chatting with: “ + name + “</div>” +
“<div id=’” + name + “chatBody’ class=’chatbody’></div>” +
“<input type=’text’ id=’” + name + “chatInput’ class=’chatInput’ />” +
“</div>”;
var newContent = “<div id=\”" + name + “chat\” class=\”button removepad\”>” +
“<div id=’” + name + “button’ class=’buttonName’>” + name +
“</div>” +
chatContent +
“</div>”;
$(“#usersButton”).before(newContent);
Next, we need to hide the chat body initially and then wire to the chat button’s click event. This function should show or hide the chat box depending on whether or not it is displayed. We are also only going to allow one chat box open at a time. To do that, we will keep track of the currently open chat box and hide it if the user switches to a different chat.
var chatBoxID = “#” + name + “chatbox”;
// Hide the chatbox
$(chatBoxID).hide();
// Show the chatbox when the button is pressed
$(“#” + name + “button”).click(function() {
if(currentChatID != null && currentChatID != chatBoxID) {
$(currentChatID).hide();
$(“#” + currentChatName + “button”).removeClass(“selected”);
}
if($(chatBoxID).css(“display”) == “none”) {
$(chatBoxID).show();
$(“#” + name + “button”).addClass(“selected”);
}
else {
$(chatBoxID).hide();
$(“#” + name + “button”).removeClass(“selected”);
}
currentChatID = chatBoxID;
currentChatName = name;
});
The last part of this function needs to wire to the keypress event of the textbox in the chat box. We want to see when the user hits ‘enter’ that way we can send the message off and finally insert the text into the current chat.
var chatInputID = “#” + name + “chatInput”;
// Wire to the chatbox’s typing a message enter
$(chatInputID).keypress(function(e) {
if(e.which == 13) {
// Send the message
var text = $(chatInputID).val();
var userID = null;
for(i=0;i<allUsers.length;i+=1) {
var u = allUsers[i];
if(u.Name == name) {
userID = u.ID;
break;
}
}
send(userID, text);
// Insert into the text body
var chatBodyID = “#” + name + “chatBody”;
var values = $(chatBodyID).val();
$(chatInputID).val(“”);
var myName = $(“#nicknameTextBox”).val();
insertToChatBody(name, “<b>” + myName + “: </b>” + text);
}
});
}
}
The send method simply uses the same concept we’ve been using to call the C# methods from Javascript. Here we need to make a call to the PostMessage method and pass in the user id and text to send.
function send(user, text) {
var slCtrl = document.getElementById(“Xaml1″);
slCtrl.Content.PlexScriptApp.PostMessage(user, text);
}
And as for adding the text to the body, all we need to do is insert a div container into the fixed height chat body. We also need to scroll to the bottom of the container should the body begin to overflow.
function insertToChatBody(username, text) {
var newContent = “<div class=’chattext’>” + text + “</div>”;
var bodyID = username + “chatBody”;
$(“#” + bodyID).append(newContent);
var bodyDiv = document.getElementById(bodyID);
bodyDiv.scrollTop = bodyDiv.scrollHeight;
}
The last thing we need to do is essentially clean-up. In the userLeft method we are going to modify it to insert the text “username has left” into an open chat box that matches the user who left (should one exist). As well, we are going to disable the textbox to prevent the user from sending a message to the user who has left. The new userLeft method looks like this:
function userLeft(user) {
index = -1;
for(i=0; i<allUsers.length; i=i+1) {
var u = allUsers[i];
if(u.ID == user) {
$(“#” + u.ID).remove();
index = i;
var chatID = “#” + u.Name + “chat”;
if($(chatID).length > 0) {
insertToChatBody(u.Name, “<b><i>” + u.Name + ” has left</i></b>”);
$(“#” + u.Name + “chatInput”).attr(“disabled”, “disabled”);
}
break;
}
}
if(index > -1) {
allUsers.splice(index, 1);
}
$(“#numberOfOnline”).html(“(” + allUsers.length + “)”);
}
As well, at the bottom of the userJoined method we are going to modify it to enable any previously disabled textbox in a chat box that matches the user who has joined (should one exist). The new userJoined method looks like this:
function userJoined(userJson) {
var user = eval(“(” + userJson + “)”);
$(“#listOUsers”).append(“<tr id=\”" + user.ID +
“\”><td class=\”userOnline\”>” + user.Name + “</td></tr>”);
allUsers.push(user);
$(“#numberOfOnline”).html(“(” + allUsers.length + “)”);
register($(“#” + user.ID));
if($(“#” + user.Name + “chat”).length > 0) {
$(“#” + user.Name + “chatInput”).removeAttr(“disabled”);
}
}
Now, we can’t go without good styles of course! Add these styles to the bottom of the css stylesheet:
.chatbox {
width: 170px; height: 155px; text-align: center;
background-color: #444444; color: #AFAFAF;
display: table; position: relative;
bottom: 185px; border: solid 3px #242424;
border-bottom-style: none; border-right-width: 1px;
border-left-width: 1px; padding: 2px 5px 0px 5px;
float: right;
}
.chatbody {
text-align: left; font-family: Trebuchet MS;
font-size: 9pt; height: 110px; overflow: auto;
}
.titleBar {
font-style: italic; font-weight: bold;
font-size: 10pt; font-family: Trebuchet MS;
}
.removepad {
padding-left: 0px; padding-right: 0px; width: 80px;
cursor: default;
}
.chatInput { width: 170px; }
.chattext {
text-align: left; width: 100%;
height: auto; padding-top: 1px;
}
.buttonName { width: 100%; height: 100%; }
.selected {
border: solid 3px #242424; border-top-width: 0px;
border-left-width: 1px; border-right-width: 1px;
background-color: #444444; color: #FFFFFF;
}
This takes care of the chat box, the chat body, the selected style on the button, the title bar, and a modification to the button for padding.
Voila!
If you’ve managed to get this far, you should absolutely give yourself a pat on the back. Running the solution several times over you’ll get something like below; if not, then feel free to download the solution right below that!
Download the Solution