Tuesday, April 28, 2009

Ajax jQuery Chat Demo

** Update **

Here is a simpler version with source code designed for Webmatrix/Razor/MVC3
[HERE]


I wanted to do a quick demo for a chat client similar to the one I see on gTalk and Facebook. Note in this demo I am polling, I use javascript to poll a server page for any new chat comments. This technique will work ok for small to mid sized trafficed web sites. For heavy trafficed sites, you probably need to do some sort of subscript/event model, maybe using a comet server (google). That technique is beyond me at this point, but here is a demo of the more basic polling technique that you can expand on.

The concept is pretty simple. You need three things.

1) The chat page (HTML and jQuery...or in my case an aspx and jQuery page).
2) A push page with which will accept your chat form post and save them to a database (for this example I'm using SQL Server 2008.
3) a pull page that will send new chat messages to the chat pages as they become available (actually not really sending, the chat script will continuously poll the page about every 2 seconds looking for new data to read).

Ok, our chat page will look like this- (remember to include a reference to your jQuery script somewhere, not shown).

<form id="form1" runat="server">
  <div>
    <div style="width: 200px; height: 400px; border: solid 1px blue; overflow: auto"
      id="Chat_window">
    </div>
    <br />
    ID:
<input type="text" style="width: 80px" id="ChatID" maxlength="10" /><br />
    <input type="text" id="ChatMsg" style="width: 200px" maxlength="200" />
    <a href="javascript:postit()">Post</a>
    <script type="text/javascript">
      function postit() {
clearInterval(ptime);
var zpost = { "ChatID": $("#ChatID").val(), "ChatMsg": $("#ChatMsg").val() };
$("#Chat_window").prepend("<span>" + zpost.ChatID + ": " + zpost.ChatMsg + "<br /></span>");
$.post("push.aspx"
       , zpost
, function(data) {
pullit();
ptime = setInterval(pullit, 1500);
});
}
function pullit() {
var url = "pull.aspx?t=" + eval(Math.floor(Math.random() * 90000));
$.post(url, function(data) {
if (data == "") return;
$("#Chat_window").prepend(data);
while ($("#Chat_window").children().size() > 29) {
$("#Chat_window").find("span:last-child").remove();
}
});
}
var ptime = setInterval(pullit, 1500);
</script>
  </div>
  </form>



Ok, I know setting the ptime inline is bad form, but heh, it is just a demo. Some highlights. You have a postit and pullit method. The postit sends the ChatID and ChatMsg to the server as a form request. I'm adding some random query string var to the end of the pull.aspx page call to help prevent caching, this worked for me but you might have to tweak things a little bit more in your environment. The pullit method polls the the pull page about every 1.5 seconds for changes.

Now for the push it page. I'm using Asp.net, but you can accomplish the same thing in PHP or whatever.

 public partial class push : System.Web.UI.Page
  {
protected void Page_Load(object sender, EventArgs e)
{
SaveChatPost();
}
void SaveChatPost()
{
using (SqlConnection oConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["myconnectionstringname"].ToString()))
{
using (SqlCommand oCmd = new SqlCommand())
{
oCmd.CommandType = CommandType.Text;
oCmd.CommandText = "declare @ts as datetime;set @ts = current_timestamp;Insert into HonoviMarketing.dbo.tChat (ChatTime, ChatId, ChatMsg) values (@ts,@p_ChatID, @p_ChatMsg);Select @ts as CurrentChatTime";
oCmd.Parameters.Add(new SqlParameter("@p_ChatID", Server.UrlDecode(Request.Form["ChatID"])));
oCmd.Parameters.Add(new SqlParameter("@p_ChatMsg", Server.UrlDecode(Request.Form["ChatMsg"])));
oConn.Open();
oCmd.Connection = oConn;
using (SqlDataReader dr = oCmd.ExecuteReader())
{
if (dr != null && dr.HasRows)
{
dr.Read();
Session["CurrentChatTime"] = dr["CurrentChatTime"].ToString();
Session["ChatID"] = Request.Form["ChatID"];
}
}
oConn.Close();
}
}
}
}


No biggie here (and if the code runs off the page, just highlight and copy it, you'll get it all, stupid blogger :)). I save the post to the database, get a current_timestamp, and set that as well as the chosen ChatID into session variables that help with pulling the right data on the pull.aspx page, which is next. Note on both pages I put this logic in the code behind of the aspx page, and removed all the HTML from the front page except for the top asp.net directives line. And again, I'm just doing a simple example here, not worried to much about query security and the like, so don't get anal.

 public partial class server : System.Web.UI.Page
  {
protected void Page_Load(object sender, EventArgs e)
{
PullData();
}
string XMLSafe(string s)
{
return s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
}
void PullData()
{
using (SqlConnection oConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["myconnectionstringname"].ToString()))
{
using (SqlCommand oCmd = new SqlCommand())
{
oCmd.CommandType = CommandType.Text;
oCmd.Connection = oConn;
oConn.Open();
if (Session["CurrentChatTime"] == null)
{
oCmd.CommandText = "Select Current_Timestamp as CurrentChatTime";
using (SqlDataReader dr = oCmd.ExecuteReader())
{
if (dr != null && dr.HasRows)
{
dr.Read();
Session["CurrentChatTime"] = dr["CurrentChatTime"].ToString();
}
}
}
else
          {
string sDate = Session["CurrentChatTime"].ToString();
SqlParameter p = new SqlParameter();
p.Direction = ParameterDirection.Input;
p.Value = sDate;
p.SqlDbType = SqlDbType.DateTime;
p.ParameterName = "@p_CurrentTime";
oCmd.Parameters.Add(p);
oCmd.CommandText = "select chatTime, ChatID, ChatMsg from HonoviMarketing.dbo.tChat where ChatTime > @p_CurrentTime and ChatID <> '" + Session["ChatID"] + "' order by ChatTime; SELECT CONVERT(varchar, current_timestamp, 21) as CurrentChatTime";
using (SqlDataReader dr = oCmd.ExecuteReader())
{
if (dr.HasRows)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
while (dr.Read())
{
sb.Append("<span>" + XMLSafe(dr["ChatID"].ToString()) + ": " + XMLSafe(dr["ChatMsg"].ToString()) + "<br /></span>");
}
Response.Write(sb.ToString());
dr.NextResult();
if (dr.HasRows)
{
dr.Read();
Session["CurrentChatTime"] = dr["CurrentChatTime"].ToString();
}
}
}
}
oConn.Close();
}
}
}
}



Again of note is that I'm using session vars to help pull just the latest message that is needed. One gotcha that I encountered with SQL server to be aware of is that you need to format the current_timestamp else you loose the milliseconds, which was causing the chat page to sometime receive double messages. You need to format in your query like so - SELECT CONVERT(varchar, current_timestamp, 21) as CurrentChatTime for setting an accurate time for a session var.

Also remember to put a reference on both pages to the System.Data.SqlClient if you are copying my code. Here is a pic of the table layout I used.




Happy coding!

[Update]

I noticed I didn't do the SELECT CONVERT(varchar, current_timestamp, 21) as CurrentChatTime formating on the if session["CurrentTimeChat"] == null clause for the first time the pull page is hit, you can add that if you want, though things seem to work without it. Also the XMLSafe method isn't really necessary nor fully implimented, so you can ditch that method if you want or fully build it out if you are concerned about illegal HTML and XML chars. I'd fix this all in code but reposting in blogger is a pain when dealing with code, so I'll leave it as is, which works fine.

7 comments:

Anonymous said...

can you pls send me all files for this work.

Thanks in advance

Rgds

Murdock said...

Did you guys get a copy of files?

Amr said...

excellent tutorial. If you don't mind, I'd like a copy of the files.

Thanks

James said...
This comment has been removed by the author.
James said...
This comment has been removed by the author.
prashant said...
This comment has been removed by a blog administrator.
Sarkaut Mahmood said...
This comment has been removed by a blog administrator.