LANGUAGES: VB.NET | C#
ASP.NET VERSIONS: 2.x
WebChat
A Fully Functional Chat Room - Free!
Nearly every Web site could benefit from a chat room to help users socialize or
sort out important issues. However, creating a chat room is usually more effort
than it’s worth — unless somebody’s already done the work for you. This article
presents a WebChat control that you can drop onto any ASP.NET Web page to get an
instant, fully functional chat room.
This article will teach you how the free WebChat control works and how to use it.
And during the process you just might learn some cutting-edge development techniques,
such as how to implement ASP.NET 2.0 client-side callbacks (AJAX),
how to use the new Visual Studio 2005 Resource Manager, and how to work with Generics.
You might also learn some valuable tips about how to create custom controls, how
to work with application state, and how to write server code that emits client-side
JavaScript.
The WebChat control works a lot like you might expect. It allows multiple people
to join a conversation and have a text conversation with each other. The control
indirectly supports emoticons and the ability to filter bad words (see Figure
1).
AJAX keeps the user interface running smoothly while conversation requests happen inconspicuously
in the background. The control consists of standard HTML and JavaScript on the client
to send and receive requests, with server-side code centrally coordinating conversations
among users.
Figure 1: The WebChat control allows
end users to interact with each other. It indirectly supports bad-word filtering,
emoticons, and other HTML-based special effects.
User Guide
To use this Firefox-compatible control, download the sample code (see end of article
for details) and add the included WebChat.DLL to your Visual Studio 2005 toolbox.
Then drag it onto any WebForm — that’s it. Run the project and the chat window should
be immediately functional. Of course, you can customize it with various properties,
such as those shown in Figure 2.
|
Unique WebChat Members
|
Description
|
|
Chatter event
|
This event is raised to the page anytime someone adds to the conversation.
This provides an opportunity to filter and/or alter the text.
|
|
CallBackInterval property
|
Sets or gets the frequency that the browser requests conversation updates from the
server. Default: 2 (seconds)
|
|
ChatTopic property
|
Sets or gets the the conversation with which this instance of the control is associated.
This provides the ability for a web site to have multiple chat rooms, each with
different topics. Default:
“general”
|
|
HistoryCapacity property
|
Sets or gets the number of chat messages will be cached in application state. Default: 10
|
|
InitialFocus property
|
A flag that specifies whether or not this control should recieve focus upon page
load. Default: True
|
|
UserName property
|
This runtime property sets or gets the name of the user that's chatting in this
instance of the control. Default: “Anonymous”
|
Figure 2:
The WebChat control provides several important members that allow versatile usage.
The ASPX declaration looks like this:
<cc1:WebChat ID="WebChat2"
runat="server"
ChatTopic="Cooking" HistoryCapacity="20"
/>
As illustrated in Figure 2, the WebChat control provides several unique properties,
and one event. The ChatTopic property permits multiple separate chat conversations
to occur on a Web site. Some users might be interested in talking about cooking;
others might be more interested in muscle cars. This allows the option to have the
WebChat control active on every cooking-related page, for example, so users can
keep up with the conversation as they travel from page to page.
The CallBackInterval property is more technical in nature, and is related to
AJAX
. As you might know,
AJAX provides the ability for a page to call back to the server to update its content
without having to refresh the entire page. This property specifies how many seconds
should elapse between each such request. This is useful for adjusting bandwidth
demands. Set it too low and your server is more likely to get bogged down when large
numbers of users are chatting. Set it too high and users could end up twiddling
their thumbs while needlessly waiting for new messages from other users. The default
of 2 seconds is usually a good starting point.
The HistoryCapacity property specifies how much of the conversation should be kept
in the server’s memory. Another effect of this property is that — if this property
is left at the default of 10 — when a user enters the chat room they’ll see the
last 10 messages of that conversation as they join the conversation. It also caches
the conversation between
AJAX requests; if the value is set too low, users might miss bits of the conversation.
(advertisement)
The InitialFocus property specifies whether the text entry area of this control
should attempt to grab focus immediately upon page load.
The Chatter event is raised to the page every time a user submits a new message,
but before the message is officially added to the conversation. Keep in mind that
this event is firing as the result of an
AJAX request, and so page rendering will not happen after this event. Therefore, attempting
to set properties of other controls in the page during this event will be futile.
Rather, this event is intended to let the application developer peek at the text
and make any desired changes before the text is added to the conversation. This
can be useful for filtering out undesirable words or replacing specific pieces of
text with images (like emoticons) or other interesting bits of HTML; again, see
Figure 1. Here’s a code sample:
Protected
Sub WebChat1_Chatter(ByRef
ChatText As String)
_
Handles WebChat1.Chatter
'You can replace
bad words...
ChatText = ChatText.Replace("hell", "h***")
'...or spruce
things up with emoticons
ChatText = ChatText.Replace(":)", _
"<img src='smiley.jpg' title=':)' />")
End Sub
Don’t forget to set the UserName property with the name (or nickname) of the user
that’s chatting in this instance of the control. For example, if you’ve already
got an authentication system in place, this line of code may do the trick:
WebChat1.UserName = User.Identity.Name
That’s all you really need to know to use the WebChat control. Feel free to stop
reading here, download it, and try it out. On the other hand, because you’re reading
this magazine, you’re probably the curious type and you want to know more about
how the control works from the inside out. In that case, read on...
Innards
A good deal of JavaScript is necessary for all this to work. The two primary client-side
functions are used to send the
AJAX
request back to the server, and to process the new messages that are retrieved from
the server. The JavaScript functions must be customized somewhat to reflect the
property values that have been set by the application developer. What’s the best
way to dynamically generate such JavaScript? There are a couple different techniques
used by the WebChat control; both are demonstrated in Figure 3.
Private
Sub WebChat_Load(ByVal
sender As Object,
_
ByVal e
As System.EventArgs) Handles
Me.Load
If
Me.Visible Then
'Retrieve the embedded
JavaScript functions
'that will recieve
the result
Dim sCallBack
As String = _
My.Resources.Callback.Replace("WebChat1", Me.ID)
If Me.CallBackInterval <> 2
Then
sCallBack = sCallBack.Replace("2000", _
(Me.CallBackInterval
* 1000).ToString())
End If
Page.ClientScript.RegisterClientScriptBlock(Me.GetType, _
"CalledBack",
sCallBack, True)
'Now generate the
function that initiates
'the client side callback
Dim sb As New StringBuilder()
sb.Append(System.Environment.NewLine)
sb.Append("function
DoChatCallBack(txt)")
sb.Append(System.Environment.NewLine)
sb.Append("{")
sb.Append("var msg
= ''; if (txt != null) msg=txt.value; ")
sb.Append(System.Environment.NewLine)
sb.Append(Page.ClientScript.GetCallbackEventReference(Me, _
"WebChatMaxMsgID
+ '||' + msg", _
"CalledBack",
"ErrCalledBack"))
sb.Append(";")
sb.Append(System.Environment.NewLine)
sb.Append("if (txt
!= null) txt.value='';")
sb.Append(System.Environment.NewLine)
sb.Append("}")
Dim sDoCallBack
As String = sb.ToString()
Page.ClientScript.RegisterClientScriptBlock(Me.GetType, _
"DoChatCallBack",
sDoCallBack, True)
'set initial focus
(if configured to do so)
If Me.InitialFocus AndAlso
Me.Enabled Then
Page.ClientScript.RegisterStartupScript(Me.GetType, _
"ChatFocus",
"document.getElementById('" _
& _txt.ClientID &
"').focus();", True)
End If
'emit JavaScript function
call that initializes
'the control and joins
the user into the conversation
Page.ClientScript.RegisterStartupScript(Me.GetType, _
"ChatEnter",
"ChatEnter();", True)
End
If
End Sub
Figure 3: The WebChat control’s Page_Load subroutine emits nearly
all of the necessary JavaScript to make the control work.
The first technique takes advantage of a compiled resource. In the project you’ll
find a file named Callback.js that contains most of the JavaScript to handle new
messages received from the server. This file also contains some other important
variables and initialization routines. In this case, the JavaScript file is added
as a resource. By right-clicking on the project in solution explorer, you can open
the properties dialog of the project. Visual Studio 2005’s new Resource Manager
(shown in
Figure 4) makes it obscenely easy to work with virtually
any kind of file and compile them directly into the assembly. VB.NET’s My.Resources
namespace takes advantage of the Resource Manager. With a single line of strongly
typed code the resource is retrieved and used, as shown in the first code block
of Figure 3. (In C# the syntax is similarly easy: Properties.Resources.) The few
bits of the JavaScript file that need to be dynamic are customized with a simple
String.Replace method call before the contents are rendered into the page.
Figure 4: Visual Studio 2005’s
new Resource Manager makes it mind-numbingly easy to work with resources in a strongly
typed manner.
The second JavaScript generation technique is a bit more standard, using a StringBuilder
object to concatenate the required JavaScript together, piece by piece. This is
demonstrated in the second code block of Figure 3. Within this code block, you might
want to take note of the GetCallbackEventReference method call, which tells ASP.NET
to emit the
AJAX
client-side callback code. By utilizing this method you can ensure that appropriate
callback JavaScript code will be generated for virtually every browser that supports
such functionality. This ASP.NET 2.0 feature shields you from messy issues, such
as browser sniffing and evolving Web 2.0 standards.
The WebChat control essentially consists of a table to position the sub-controls,
a panel for the display of the conversation, a textbox to allow message entry, and
a button to submit new messages. These sub-controls are configured from within the
RenderContents subroutine shown in Figure 5.
Protected
Overrides Sub
RenderContents(ByVal output _
ns="urn:schemas-microsoft-com:office:office"
As HtmlTextWriter)
'create the containing
table
Dim tbl
As Table = New
Table
tbl.Height = Me.Height
tbl.Width = Me.Width
tbl.ToolTip = Me.ToolTip
tbl.Style.Add("overflow",
"scroll")
tbl.Style.Add("position",
"absolute")
Dim td
As TableCell = New
TableCell
td.ColumnSpan = 2
td.Height = New
Unit(95, UnitType.Percentage)
Dim tr
As TableRow = New
TableRow
tr.Cells.Add(td)
tbl.Rows.Add(tr)
'create the message
display panel
_pnl.Width = New
Unit(99, UnitType.Percentage)
_pnl.Height = New
Unit(100, UnitType.Percentage)
_pnl.BorderStyle = WebControls.BorderStyle.Solid
_pnl.BorderWidth =
New Unit(1, UnitType.Pixel)
_pnl.Style.Add(HtmlTextWriterStyle.Overflow,
"scroll")
td.Controls.Add(_pnl)
'create a new table
row for textbox & button
tr = New
TableRow
tbl.Rows.Add(tr)
td = New
TableCell
td.Width = New
Unit(95, UnitType.Percentage)
tr.Cells.Add(td)
'create the button
Dim btn
As Button = New
Button
btn.ID = Me.ID
& "_button"
btn.UseSubmitBehavior =
False
btn.Text = "Send"
'create the textbox
_txt.Width = New
Unit(99, UnitType.Percentage)
_txt.BorderColor = Drawing.Color.Black
_txt.BorderWidth =
New Unit(1, UnitType.Pixel)
_txt.Attributes("autocomplete")
= "off"
_txt.Attributes("onkeydown")
= _
"if ((event.keyCode
== 13)) {document.getElementById('" _
& btn.ClientID & _
"').click();return
false;} else return true;"
td.Controls.Add(_txt)
'create the final
table cell
td = New
TableCell
td.Controls.Add(btn)
tr.Cells.Add(td)
'Attach button's client event to the JavaScript functions
btn.Attributes("onclick")
= _
"DoChatCallBack(document.getElementById('"
_
& _txt.ClientID &
"'));"
tbl.RenderControl(output)
End Sub
Figure 5: The RenderContents subroutine configures all the constituent
controls of which the WebChat control is comprised.
The bulk of this code is fairly boilerplate — instantiating controls, positioning
them in relation to each other, and configuring the appropriate properties. The
more interesting tidbits are near the end where client-side events are defined for
the controls. For example, the textbox will handle a client-side OnKeyDown event
that will automatically “click” the submit button when the user presses the Enter
key. The button’s OnClick event, in turn, will call the previously emitted DoChatCallback
JavaScript function to initiate the
AJAX call to the server. The DoChatCallback JavaScript function sends the new text message
to the server and requests any new messages that were entered by other users.
Server-side Callback Code
If you tinkered with the client-side callback functionality in beta 2 of the .NET
Framework version 2, you’ll notice a few things have changed in the final release.
The most noticeable difference is that there are now two subroutines (instead of
one) that must be implemented to support client-side callbacks. The RaiseCallback
subroutine shown in Figure 6 is called first, as soon as the request arrives at
the server. The new GetCallbackResult subroutine is subsequently called, just before
a response is sent back to the client.
Public
Sub RaiseCallbackEvent(ByVal
_
eventArgument As
String) Implements
_
System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent
'extract the last message ID that was received by the client
'so that we can pass back only the new messages
Dim
aryArgs As String()
= _
eventArgument.Split("||".ToCharArray())
_LastMsgID
= CType(aryArgs(0), Long)
eventArgument
= aryArgs(2).Trim
Dim
WebChatTopic As String
= _
"WebChat_Conversation_"
& Me.ChatTopic
'Raise Chatter event if new message was sent
If
eventArgument.Length > 0 Then
RaiseEvent
Chatter(eventArgument)
End
If
'Massage the message cosmetically
Dim
ModifiedMsgText As String
= "<b><i>" & _
Me.UserName
& "</i></b>: " & eventArgument
'Does the conversation need instantiation?
If
Context.Application(WebChatTopic) Is
Nothing Then
'no existing queue
found so create a new queue
_Messages = New
_
System.Collections.Generic.Queue(Of ChatMessage)
If eventArgument.Trim.Length
> 0 Then
Dim
Msg As New _
ChatMessage(1,
Date.Now, _
ModifiedMsgText,
Me.UserName)
_Messages.Enqueue(Msg)
End If
Context.Application.Lock()
Context.Application(WebChatTopic) = _Messages
Context.Application(WebChatTopic & "_MAX") = 1
Context.Application.UnLock()
Else
'existing Queue found, so use it
Context.Application.Lock()
_Messages = CType(Context.Application(WebChatTopic),
_
System.Collections.Generic.Queue(Of ChatMessage))
If eventArgument.Trim.Length > 0
Then
Dim
Max As Long =
_
CType(Context.Application(WebChatTopic
& _
"_MAX"),
Long)
Max += 1
Dim
Msg As New ChatMessage(Max,
_
Date.Now,
ModifiedMsgText, Me.UserName)
'keep track
of the max message id
Context.Application(WebChatTopic
_
& "_MAX") = Max
'place new
message on the stack
_Messages.Enqueue(Msg)
'purge stale
messages
If
_Messages.Count > _HistoryCapacity Then
Msg = _Messages.Dequeue()
End
If
End If
'put the queue back
into application state
Context.Application(WebChatTopic)
= _Messages
Context.Application.UnLock()
End
If
End Sub
Figure 6: The RaiseCallbackEvent method is called by ASP.NET when
the client makes an AJAX call to the server. The WebChat control accepts incoming
messages and adds them to the existing conversation that’s kept in a generic queue
that’s cached in application state.
When a control implements ICallbackEventHandler, the RaiseCallbackEvent is fired
whenever the client makes an
AJAX request to the server. The code in Figure 6 first checks to see if there is an incoming
message and, if so, parses it to retrieve the message and the ID of the last message
that the client received. (This is so the server knows to return all messages that
have been created since then.) If there is an incoming message, the Chatter event
is raised so the application developer may alter the incoming text.
The code then branches based on if the requested conversation already exists or
if it needs to be instantiated. If this is the first message of a conversation,
a queue is instantiated that will accept only ChatMessage objects. (ChatMessage
is a very simple custom class that contains information about each chat message.)
This specialized queue utilizes Generics (a new feature of .NET 2.0) to ensure it
only contains ChatMessage objects — and nothing else. This technique is more efficient
than using a standard Queue object because less casting is necessary. A new ChatMessage
object is then instantiated, and all relevant information about the message is passed
to its constructor before the ChatMessage is added to the queue. This generic queue
is then added to application state (along with a separate entry that contains the
maximum MessageID so far) so it will be saved between calls to the server.
(advertisement)
The Else block handles the case where the conversation already exists and doesn’t
need to be instantiated. In this situation, the generic queue of ChatMessage is
retrieved from application state, and the new ChatMessage (if any) will be added
to the queue. Stale messages are also removed from the queue at this time. Finally,
the updated queue is placed back into application state.
The GetCallbackResult function shown in Figure 7 loops through each ChatMessage
in the conversation. Any messages that are newer than the last message the client
received are packaged together into a string and returned to the client. Fields
are delimited with two pipe characters (||) and each row is delimited by two tilde
characters (~~). The custom JavaScript function CalledBack (contained within Callback.js)
splits that data apart again once it’s received at the client, and then displays
the new messages.
Figure 7: The GetCallbackResult function concatenates all new messages
together into a single string and returns it to the client for display.
The End Is Just the Beginning
Although the primary functionality has been explained, it’s impossible to cover
in a single article all the details of a control this complex. I encourage you to
download and explore the code more thoroughly. From it you may learn more about
how to create custom controls, how to utilize client-side callbacks (AJAX),
and how to use client-side JavaScript to improve the user experience and gain efficiencies
not otherwise possible.
Though this WebChat control covers most of the basics you’d expect from a chat room,
I can think of at least a dozen interesting ways to extend the control with cool
new features. Can you? Send feature requests my way, and maybe there will be an
enhanced version in the future. Meanwhile, feel free to enhance the control yourself
with new features, and let me know what you come up with — I’d love to hear about
them!
If you're looking for in-depth coverage of the free
ASP.NET AJAX framework
then I suggest you
read my book!
The sample code in this article is available for download.
This article was originally published in
ASP.NET Pro Magazine.