玄铁剑

成功的途径:抄,创造,研究,发明...
posts - 128, comments - 42, trackbacks - 0, articles - 174

Multi User Chat Room Using ASP.NET 2.0 and AJAX

Posted on 2007-03-26 22:46 玄铁剑 阅读(459) 评论(0)  编辑 收藏 引用 所属分类: ASP.NET相关


<UL class=download>
<LI><A href="ASPNetChat/ASPNETCHAT.zip">Download source files - 10.4 KB</A>
</LI></UL><IMG height=276 alt="Screenshot - chat.jpg" src="ASPNetChat/chat.jpg"
width=582>
<H2>Introduction</H2>
<P nd="1">Building an interactive chat room requires keeping the users up to
date with the messages and status of other users. Using ASP.NET AJAX to
implement the chat room will remove the unnecessary post backs and will provide
a seamless chat experience to the user. </P>
<H2>Background</H2>
<P nd="2">You can read <A href="AliAspNetChat.asp">my previous article</A> on
how to build a one to one chat room, this article was built using ASP.NET 1.1
and a third party AJAX library. Most of the Chatting Logic (rooms, users,
messages sending, etc..) is used here. </P>
<H2>Code Walkthrough</H2>
<P nd="3">The App_Code folder contains all the classes that will be used to
create our chat application. Let us start with the <CODE nd="4">ChatUser</CODE>
</P>
<DIV class=precollapse id=premain0 style="WIDTH: 100%"><IMG id=preimg0
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="0"> Collapse</DIV><PRE lang=cs id=pre0 style="MARGIN-TOP: 0px" nd="7">//ChatUser.cs
public class ChatUser:IDisposable
{
    #region Members
    public string UserID;
    public string UserName;
    public bool IsActive;
    public DateTime LastSeen;
    public int LastMessageReceived;
    #endregion

    #region Constructors
    public ChatUser(string id,string userName)
    {
        this.UserID=id;
        this.IsActive=false;
        this.LastSeen=DateTime.MinValue ;
        this.UserName=userName;
        this.LastMessageReceived=0;
    }
    #endregion

    #region IDisposable Members
    public void Dispose()
    {
        this.UserID="";
        this.IsActive=false;
        this.LastSeen=DateTime.MinValue ;
        this.UserName="";
        this.LastMessageReceived=0;
    }
    #endregion
}
</PRE>
<P nd="16">This class represents the user that will join the chat room. The
properties we are interested in are <CODE nd="17">IsActive</CODE>, <CODE
nd="18">LastSeen </CODE>and <CODE nd="19">LastMessageReceived</CODE>. Note that
in your application you may be using the ASP.NET membership providers. You can
easily use this with our chat application. You will find that the <CODE
nd="20">MembershipUser</CODE> class contains two properties <CODE
nd="21">IsOnline</CODE> and <CODE nd="22">LastActivityDate</CODE> that can be
used to replace the properties <CODE nd="23">IsActive</CODE> and <CODE
nd="24">LastSeen</CODE> respectively. </P>
<P nd="25">Each message that appears in the chat room is represented by the
following class:</P>
<DIV class=precollapse id=premain1 style="WIDTH: 100%"><IMG id=preimg1
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="1"> Collapse</DIV><PRE lang=cs id=pre1 style="MARGIN-TOP: 0px" nd="28">//ChatMessage.cs
public class Message
{
    #region Members
    public string user;
    public string msg;
    public MsgType type;
    #endregion

    #region Constructors
    public Message(string _user, string _msg, MsgType _type)
    {
        user = _user;
        msg = _msg;
        type = _type;
    }
    public Message(string _user, MsgType _type) :
                this(_user, "", _type) { }
    public Message(MsgType _type) : this("", "", _type) { }
    #endregion

    #region Methods
    public override string ToString()
    {
        switch(this.type)
        {
            case MsgType.Msg:
            return this.user+" says: "+this.msg;
            case MsgType.Join :
            return this.user + " has joined the room";
            case MsgType.Left :
            return this.user + " has left the room";
        }
        return "";
    }
    #endregion
}

public enum MsgType { Msg, Start, Join, Left, Action}
</PRE>
<P nd="42">Each chat room contains a hashtable of users and a list of
messages.</P>
<DIV class=precollapse id=premain2 style="WIDTH: 100%"><IMG id=preimg2
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="2"> Collapse</DIV><PRE lang=cs id=pre2 style="MARGIN-TOP: 0px" nd="45">//ChatRoom.cs
public class ChatRoom : IDisposable
{
    #region Members
   
    public List<message /> messages = null;
    public string RoomID;
    private Dictionary<string,chatuser /> RoomUsers;
    private int userChatRoomSessionTimeout;
       
    #endregion
    #region IDisposable Members
    public void Dispose()
    {
        this.messages.Clear();
        this.RoomID="";
        foreach(object key in RoomUsers.Keys)
        {
            this.RoomUsers[key.ToString()].Dispose ();
        }
    }
    #endregion
       
    #region Constructors
    public ChatRoom(string roomID)
    {
        this.messages = new List<message />();
        this.RoomID=roomID;
    userChatRoomSessionTimeout = Int32.Parse
    (System.Configuration.ConfigurationManager.AppSettings
            ["UserChatRoomSessionTimeout"]);
        RoomUsers = new Dictionary<string,chatuser />
    (Int32.Parse(System.Configuration.ConfigurationManager.AppSettings
                    ["ChatRoomMaxUsers"]));
    }
    #endregion
    .
    .
    .
    }
</PRE>
<P nd="55">The main methods in the <CODE nd="56">ChatRoom</CODE> class are
sending a message, joining a room and leaving a room. </P>
<DIV class=precollapse id=premain3 style="WIDTH: 100%"><IMG id=preimg3
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="3"> Collapse</DIV><PRE lang=cs id=pre3 style="MARGIN-TOP: 0px" nd="61">//ChatRoom.cs
#region Operations Join,Send,Leave
    /// Marks the user as inactive
    public void LeaveRoom(string userID)
    {
        //deactivate user
        ChatUser user=this.GetUser(userID);
        if (user == null)
        return ;
        user.IsActive=false;
        user.LastSeen=DateTime.Now;
            this.RoomUsers.Remove(userID);

        //Add leaving message
        Message msg = new Message(user.UserName ,"",MsgType.Left);
        this.AddMsg(msg);
           
    if (IsEmpty())
    ChatEngine.DeleteRoom(this.RoomID);
    }

    /// Activates the user and adds a join message to the room
    /// <returns />All the messages sent in the room</returns />
    public string JoinRoom(string userID,string userName)
    {
        //activate user
        ChatUser user=new ChatUser(userID,userName);
        user.IsActive=true;
        user.UserName=userName;
        user.LastSeen=DateTime.Now;
        if (!this.RoomUsers.ContainsKey(userID))
        {
            //Add join message
        Message msg=new Message(user.UserName ,"",MsgType.Join);
            this.AddMsg(msg);
            //Get all the messages to the user
            int lastMsgID;
            List<message /> previousMessages=
                this.GetMessagesSince(-1,out lastMsgID);
            user.LastMessageReceived=lastMsgID;
            //return the messages to the user
            string str=GenerateMessagesString(previousMessages);
            this.RoomUsers.Add(userID,user);
            return str;
        }
        return "";
    }

    /// Adds a message in the room
    /// <returns />All the messages sent from the other user from the last time
    ///the user sent a message</returns />
    public string SendMessage(string strMsg,string senderID)
    {
        ChatUser user=this.GetUser(senderID);
        Message msg=new Message(user.UserName ,strMsg,MsgType.Msg);
        user.LastSeen=DateTime.Now;
        this.ExpireUsers(userChatRoomSessionTimeout);
        this.AddMsg(msg);
        int lastMsgID;
        List<message /> previousMsgs= this.GetMessagesSince
            ( user.LastMessageReceived,out lastMsgID);
        if (lastMsgID!=-1)
            user.LastMessageReceived=lastMsgID;
        string res=this.GenerateMessagesString(previousMsgs);
        return res;
    }

    /// Removes the users that hasn't sent any message during the
    /// last window seconds
    /// <PARAM name="window" />time in secondes</PARAM />
    public void ExpireUsers(int window)
    {
        lock(this)
        {
    foreach (object key in RoomUsers.Keys)
    {
            ChatUser usr = this.RoomUsers[key.ToString()];
                lock (usr)
                {
                  if (usr.LastSeen != System.DateTime.MinValue)
                  {
                    TimeSpan span = DateTime.Now - usr.LastSeen;
                    if (span.TotalSeconds > window && usr.IsActive != false)
                        {
                          this.LeaveRoom(usr.UserID);
                        }
                   }
                 }
                }
    }
    #endregion
 </PRE>
<P nd="80">To keep the user updated, the following function retrieves all the
messages sent in the room up to the last message that was received by the
user.</P>
<DIV class=precollapse id=premain4 style="WIDTH: 100%"><IMG id=preimg4
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="4"> Collapse</DIV><PRE lang=cs id=pre4 style="MARGIN-TOP: 0px" nd="83">//ChatRoom.cs
<summary />    /// Returns all the messages sent since the last message
    /// the user received
    public string UpdateUser(string userID)
    {
        ChatUser user=this.GetUser(userID);
        user.LastSeen=DateTime.Now;
        this.ExpireUsers(userChatRoomSessionTimeout);
        int lastMsgID;
        List<message /> previousMsgs= this.GetMessagesSince
            ( user.LastMessageReceived,out lastMsgID);
        if (lastMsgID!=-1)
            user.LastMessageReceived=lastMsgID;
        string res=this.GenerateMessagesString(previousMsgs);
        return res;
    }
    /// Returns a list that contains all messages sent after the
    ///             message with id=msgid
    /// <PARAM name="msgid" />The id of the message after which all
    ///            the message will be retuned </PARAM />
    /// <PARAM name="lastMsgID" />the id of the last message returned</PARAM />
    public List<message /> GetMessagesSince(int msgid,out int lastMsgID)
    {
        lock(messages)
        {
        if ((messages.Count) <= (msgid+1))
            lastMsgID=-1;
        else
            lastMsgID=messages.Count-1;
        return messages.GetRange(msgid+1 , messages.Count - (msgid+1));
        }
    }
   
</PRE>
<P nd="91">The <CODE nd="92">ChatEngine</CODE> class acts as the container of
the chat rooms.</P>
<DIV class=precollapse id=premain5 style="WIDTH: 100%"><IMG id=preimg5
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="5"> Collapse</DIV><PRE lang=cs id=pre5 style="MARGIN-TOP: 0px" nd="95">//ChatEngine.cs
    public static class ChatEngine
    {
        #region Members
        private static Dictionary<string, /> Rooms =
                new Dictionary<string, />
        (Int32.Parse(System.Configuration.ConfigurationManager.AppSettings
                            ["MaxChatRooms"]));
        private static int userChatRoomSessionTimeout =
        Int32.Parse(System.Configuration.ConfigurationManager.AppSettings
                    ["UserChatRoomSessionTimeout"]);
        #endregion
               
        #region Methods
    /// Cleans all the chat roomsDeletes the empty chat rooms
    public static void CleanChatRooms(object state)
        {
            lock (Rooms)
            {
                foreach (object key in Rooms.Keys)
                {
                    ChatRoom room = Rooms[key.ToString()];
                    room.ExpireUsers(userChatRoomSessionTimeout);
                    if (room.IsEmpty())
                    {
                        room.Dispose();
                        Rooms.Remove(key.ToString());
                    }
                }
            }
        }

    /// Returns the chat room for this two users or create a new one
    /// if nothing exists
    public static ChatRoom GetRoom(string roomID)
    {
        ChatRoom room=null;
            lock (Rooms)
            {
                if (Rooms.ContainsKey (roomID))
                    room = Rooms[roomID];
                else
                {
                    room = new ChatRoom(roomID);
                    Rooms.Add(roomID, room);
                }
            }
            return room;
        }

    /// Deletes the specified room
    public static void DeleteRoom(string roomID)
    {
            if (!Rooms.ContainsKey(roomID))
                return;
            lock (Rooms)
            {
                ChatRoom room = Rooms[roomID];
                room.Dispose();
                Rooms.Remove(roomID);
            }
        }
        #endregion
    }
</PRE>
<P nd="106">An application level timer is set when the application starts to
periodically clean the empty chat rooms.</P><PRE lang=cs nd="108">//Global.asax
void Application_Start(object sender, EventArgs e)
    {
        System.Threading.Timer ChatRoomsCleanerTimer =
        new System.Threading.Timer(new TimerCallback
        (ChatEngine.CleanChatRooms), null, 1200000, 1200000);
    }
</PRE>
<P nd="109">When you run the application, it will take you to the default.aspx
page, where you are prompted to enter the user name that you will use in the
chat room. After that the user chooses the chat room that he will join.</P>
<P nd="110">The chatting page consists of a text area that displays all the
messages in the chat room and a list box that shows all the online users in the
room. The user writes his message in the text box and presses enter or clicks
the send button to send the message. A text box looks like this:</P>
<P><IMG height=276 alt="Chat Room" src="ASPNetChat/chat.jpg" width=582
border=0></P>
<H2>Client Side AJAX </H2>
<P nd="111">The Chat.aspx page contains javascript that calls methods on the <A
class=iAs
style="FONT-WEIGHT: normal; FONT-SIZE: 100%; PADDING-BOTTOM: 1px; COLOR: darkgreen; BORDER-BOTTOM: darkgreen 0.07em solid; BACKGROUND-COLOR: transparent; TEXT-DECORATION: underline"
href="#" target=_blank itxtdid="3442207">server</A> using AJAX. To call a method
on the server asynchoronously in AJAX you can: </P>
<UL>
<LI nd="112">Either place this method inside a web service and apply the <CODE
nd="113">ScriptService</CODE> attribute to this web service
<LI nd="114">Or place this method in the aspx page as a static public method and
apply the <CODE nd="115">WebMethod</CODE> attribute to it </LI></UL>
<P nd="116">I used the second approach in calling the server side methods.</P>
<P nd="117">All the javascript code is placed in the scripts.js file, there are
four asynchronous requests that are made from the javascript to the server, the
first two are sent periodically using a javascript timer, the <CODE
nd="118">updateUser</CODE> request is used to get all the messages that were
sent by the other users and updates the text area with these messages. The <CODE
nd="119">updateMembers</CODE> request is used to get all the online members in
the room and updates the members list box such that the members that left are
removed from the list and the newly joined members are added to the list. The
javascript code for these two requests is shown below:</P>
<DIV class=precollapse id=premain7 style="WIDTH: 100%"><IMG id=preimg7
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="7"> Collapse</DIV><PRE lang=jscript id=pre7 style="MARGIN-TOP: 0px" nd="121">//scripts.js
        var msgTimer = "";
           var membersTimer = "";
       
        startTimers();
     
        function startTimers()
        {
            msgTimer = window.setInterval("updateUser()",3000);
            membersTimer = window.setInterval("updateMembers()",10000);
        }
        function updateUser()
        {
            PageMethods.UpdateUser($get("hdnRoomID").value, UpdateMessages);
        }
        function updateMembers()
        {
            PageMethods.UpdateRoomMembers($get("hdnRoomID").value,
                            UpdateMembersList);
        }
        function UpdateMessages(result)
        {
            $get("txt").value=$get("txt").value+result;
            $get("txt").doScroll();
        }
        function UpdateMembersList(result)
        {
           // alert(result);
            var users=result.split(",");
           // alert(users.length);
            var i=0;
                               
                $get("lstMembers").options.length=0;
                 var i=0;
                while (i < users.length)
                {
                    if (users[i]!="");
                    {
                        var op=new Option(users[i],users[i]);
                        $get("lstMembers").options[$get("lstMembers").
                    options.length]= op;
                    }
                    i+=1;
                }
               }
</PRE>
<P nd="136">The <CODE nd="137">PageMethods</CODE> class is generated by the AJAX
script manager control and provides a proxy for all your server methods so that
you can call the method by passing it your parameters and passing the name of
the script callback function (the function that will be called when the result
of your method arrives from the server).</P>
<P nd="138">The corresponding server side methods for these two requests are
shown below:</P>
<DIV class=precollapse id=premain8 style="WIDTH: 100%"><IMG id=preimg8
style="CURSOR: hand" height=9 src="http://www.codeproject.com/images/minus.gif"
width=9 preid="8"> Collapse</DIV><PRE lang=cs id=pre8 style="MARGIN-TOP: 0px" nd="143">//Chat.aspx.cs
 
    /// This function is called peridically called from the user to update
    /// the messages
    [WebMethod]
    static public string UpdateUser(string roomID)
    {
        try
        {
            ChatRoom room = ChatEngine.GetRoom(roomID);
            if (room != null)
            {
                string res = "";
                if (room != null)
                {
                    res = room.UpdateUser(HttpContext.Current.Session
                    ["UserName"].ToString());
                }
                return res;
            }
        }
        catch (Exception ex)
        {

        }
        return "";
    }
    /// Returns a comma separated string containing the names of the users
    /// currently online
    [WebMethod]
    static public string UpdateRoomMembers(string roomID)
    {
        try
        {
            ChatRoom room = ChatEngine.GetRoom(roomID);
            if (room != null)
            {
                IEnumerable<string /> users=room.GetRoomUsersNames ();
                string res="";

                foreach (string  s in users)
                {
                    res+=s+",";
                }
                return res;
            }
        }
        catch (Exception ex)
        {
        }
        return "";
    }
</PRE>
<P nd="152">The third request is sent when the user presses the send button.
This request sends the user message to the room.</P><PRE lang=jscript nd="153">//scripts.js
    function button_clicked()
    {
        PageMethods.SendMessage($get("txtMsg").value,$get
        ("hdnRoomID").value, UpdateMessages, errorCallback);
        $get("txtMsg").value="";
        $get("txt").scrollIntoView("true");
    }
   
    function errorCallback(result)
    {
        alert("An error occurred while invoking the remote method: "
        + result);
    }
</PRE>
<P nd="161">The corresponding server side method for this request is:</P><PRE lang=cs>//Chat.aspx.cs
<summary />    /// This function is called from the client script
    [WebMethod]
    static public string SendMessage(string msg, string roomID)
    {
        try
        {
            ChatRoom room = ChatEngine.GetRoom(roomID);
            string res = "";
            if (room != null)
            {
                res = room.SendMessage
         (msg, HttpContext.Current.Session["UserName"].ToString());
            }
            return res;
        }
        catch (Exception ex)
        {

        }
        return "";
    }
</PRE>
<P>The last request is called during the <CODE>onunload</CODE> event of the
<body> element. This method notifies the server that the user left the
room. </P><PRE lang=jscript>//scripts.js
    function Leave()
    {
        stopTimers();
        PageMethods.LeaveRoom($get("hdnRoomID").value);
    }
</PRE>
<P>The corresponding server side method for this request is:</P><PRE lang=cs>//Chat.aspx.cs
<summary />    /// This function is called from the client when the user is about to
    /// leave the room
    [WebMethod]
    static public string LeaveRoom(string roomID)
    {
        try
        {
            ChatRoom room = ChatEngine.GetRoom(roomID);
            if (room != null)
                room.LeaveRoom(HttpContext.Current.Session["UserName"].
                            ToString());
        }
        catch (Exception ex)
        {

        }
        return "";
    }
</PRE>

只有注册用户登录后才能发表评论。