ControlFreak
LANGUAGES: VB.NET | C#
ASP.NET VERSIONS: 2.x
WebChat
A Fully Functional Chat Room Free!
By Steve C. Orr
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 month we present 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 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 that will be cached in application state. Default: 10. |
InitialFocus property |
A flag that specifies whether or not this control should receive focus upon page load. Default: True. |
UserName property |
This run-time 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 usages.
The ASPX declaration looks like this:
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. 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(":)", _ "") 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... 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 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. 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 _ 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. 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 = ""
& _ Me.UserName & ":
" & 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. 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. Public Function GetCallbackResult() As String Implements _ System.Web.UI.ICallbackEventHandler.GetCallbackResult Dim retval As String =
"" Dim sb As New
StringBuilder For Each Msg As
ChatMessage In _Messages If Msg.MessageID >
_LastMsgID Then 'Build the return string 'containing
recent chat messages sb.Append(Msg.MessageID.ToString()) sb.Append("||") 'column delimiter sb.Append(Msg.MessageText) sb.Append("~~") 'row delimiter End If Next retval = sb.ToString() Return retval End Function Figure 7: The
GetCallbackResult function concatenates all new messages together into a single
string and returns it to the client for display. 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! The source code for
the WebChat control is available for download. Steve C. Orr is an
MCSD and a Microsoft MVP in ASP.NET. He s been developing software solutions
for leading companies in the Seattle
area for more than a decade. When he s not busy designing software systems or
writing about them, he can often be found loitering at local user groups and
habitually lurking in the ASP.NET newsgroup. Find out more about him at http://SteveOrr.net
or e-mail him at mailto:[email protected]. Innards
Figure 4: Visual Studio 2005 s new
Resource Manager makes it mind-numbingly easy to work with resources in a
strongly typed manner. Server-side Callback Code
The End Is Just the Beginning