使用 ASP.Net 3.5、LINQ 和 AJAX(C# 3.5 或 VB 9.0)构建 Web 聊天应用程序
我们将从头开始使用最新的 ASP.Net 3.5 技术创建一个非常简单的 Web 聊天应用程序。
使用的技术
ASP.Net 3.5, AJAX, JavaScript, C# 3.5 或 Visual Basic (VB) 9.0, LINQ-to-SQL, MS SQL Server 2000/2005
您也可以 在此处 找到 VB 9.0 文章。
引言
我的一位 Java 程序员朋友曾经问我是否构建过 Web 聊天应用程序。我说“没有”。然后他问我,如果我要构建一个非常简单、受监控、最基本、可用的 Web 聊天应用程序,我需要多长时间(大概的意思)。我说“我不知道”。几天过去了,在一个懒洋洋的晚上,我上网搜索 ASP.Net Web 聊天应用程序。似乎找不到一个足够简单的应用程序可以实现。所以我想我还是从头开始构建一些简单的东西。我说简单的意思就是:我会尽量快速地构建这个 Web 聊天应用程序,几乎不做任何计划或架构。所以下面你将读到的是我在完成 Web 聊天应用程序构建后的笔记。下面是一个聊天室页面外观的示例。
背景
我们将从头开始使用最新的 ASP.Net 3.5 技术,仅仅为了好玩而创建一个非常简单的 Web 聊天应用程序。这个聊天应用程序将包含 2 个网页,登录页面和聊天室页面。大部分教程将侧重于聊天室页面。我想要实现的一些功能如下:
- 必须可以在任何地方访问,并且无需下载和安装任何组件。这就是为什么我们要创建一个 Web 聊天。
- Web 聊天必须“无闪烁”。这就是 AJAX 非常有用之处。
- 我们希望能够使用数据库监控聊天对话。我们将使用 MS SQL Server 存储对话和用户信息。
- 不使用 frames。我在网上读到过 frames 是邪恶的,我有点同意。
- 使用动态 SQL,LINQ-to-SQL 代替存储过程,实现超快的编码。请记住,我们这样做只是为了好玩。
- 不使用 Application Variables。我在网上看到过一些使用 Application Variables 的例子,我就是不喜欢这种想法。
乐趣开始
1. 首先,我们需要使用 MS SQL Server 2000/2005 构建我们的数据库。请注意,我们将只创建最基本的部分,以使 Web 聊天应用程序能够运行。下面列出了表名及其在我们的聊天应用程序中的主要用途。
- User:包含用户信息。您可以随意添加自己的字段,如地址、城市等。
- Message:将保存用户在聊天时发送的消息。
- Room:包含有关不同房间的信息。这意味着您可以拥有多个房间。但出于本教程的目的,我们现在将只使用一个房间。
- LoggedInUsers:将保存登录/聊天中的用户。简而言之,如果用户进入一个房间,我们将在其中保存他们的信息,这样我们就可以显示特定房间中正在聊天的用户列表。
2. 使用 Visual Studio 2008 创建一个 ASP.Net 3.5 网站。目前,让我们使用 C# 3.5 创建。我应该在一周内提供 VB 9.0 版本(请稍后回来查看 VB 版本)。
3. 创建一个登录页面。我们的聊天应用程序要求所有用户都登录。本教程不讨论注册新用户的页面的创建。虽然您可以看到用户名和密码是明文的,但在现实世界中,我建议使用哈希值。登录页面非常简单;如果您通过身份验证,您将被重定向到房间 1。同样,您可以自定义此设置,以便用户可以从多个房间中进行选择。
4. 创建聊天室页面。
首先,让我们谈谈 GUI(图形用户界面)的主要结构。如下图所示,我们有以下元素:
- 消息的 Div 标签:这将容纳/显示/包含聊天者发送的消息。这里没有 frames。
- 文本框:聊天者将在其中输入他们的消息。
- 发送按钮:点击时发送消息。
- 聊天者的 Div 标签:显示登录到当前房间的聊天者。
- 注销按钮:注销用户退出当前房间。
- 更新面板 (Update Panel):这不是 GUI 的可见部分,但它是聊天应用程序最重要的部分之一。此面板将确保我们看到的消息是“无闪烁”的。
- 计时器控件 (Timer Control):这在 GUI 中也看不见。计时器控件的功能是每 7 秒刷新我们的页面。
现在让我们看看 Web 应用程序的流程。
- 所有聊天者都必须经过身份验证。简而言之,每个想聊天的人都必须登录。Default.aspx 是我们的登录页面,所有未经身份验证的用户都将被重定向到这里。这在我们的 web.config 文件中很容易配置。请看下面的代码。
50 <authentication mode="Forms"> 51 <forms name=".ASPXAUTH" loginUrl="Default.aspx"/> 52 </authentication> 53 54 <authorization> 55 <deny users="?" /> 56 </authorization>
- 在将用户重定向到聊天室页面之前,将 UserID 保存到 Session 中。为了简单起见,用户在通过身份验证后会自动重定向到“房间 1”。注意:您可以将用户重定向到一个包含聊天室列表的页面,让用户在重定向到聊天室页面之前从中选择。只需向 Room 表添加房间,并在页面中列出它们。
13 protected void Login1_Authenticate(object sender, AuthenticateEventArgs e) 14 { 15 LinqChatDataContext db = new LinqChatDataContext(); 16 17 var user = (from u in db.Users 18 where u.Username == Login1.UserName 19 && u.Password == Login1.Password 20 select u).SingleOrDefault(); 21 22 if (user != null) 23 { 24 e.Authenticated = true; 25 Session["ChatUserID"] = user.UserID; 26 Session["ChatUsername"] = user.Username; 27 } 28 else 29 { 30 e.Authenticated = false; 31 } 32 } 33 34 protected void Login1_LoggedIn(object sender, EventArgs e) 35 { 36 Response.Redirect("Chatroom.aspx?roomId=1"); 37 }
- 然后用户到达聊天室页面。我们首先检查查询字符串中的 roomID。同样,为了简单起见,下面的代码假定您传递了一个整数值作为 roomID。但是,您应该确保 roomID 不为 null 且可以转换为整数。下面的代码还显示 roomID 值被分配给一个名为 lblRoomID 的不可见 Label 控件。我们也可以将此值保存在 Session 变量中,但这只是我的偏好,因为除了当前页面之外,我们并不真正需要知道 roomID。我们需要在整个页面中知道 roomID 的值,例如在检索此房间的消息、插入此房间的消息、检索此房间的用户、注销此房间的用户时等等,您明白了,对吧?
16 // for simplity's sake we're going to assume that a 17 // roomId was passed in the query string and that 18 // it is an integer 19 // note: in reality you would check if the roomId is empty 20 // and is an integer 21 string roomId = (string)Request["roomId"]; 22 lblRoomId.Text = roomId;
- 我们想通知所有聊天者,这位用户刚刚登录。因此,我们向特定于此房间的 LoggedInUser 表添加/插入此用户。然后我们检索并显示此房间的所有用户。
插入消息的调用
26 this.InsertMessage(ConfigurationManager.AppSettings["ChatLoggedInText"] + " " + DateTime.Now.ToString());
插入消息方法:请注意第 84 行,我删除了小于号“<”,因为这会破坏应用程序,而且我没有心情通过将 ValidateRequest 设置为 false 来使我的 Web 应用程序稍微不安全。我写这个 Web 聊天的时候很着急,现在我倒觉得我应该使用 AJAX Control Toolkit 的 Filtered Text Box 来代替普通的文本框来输入消息。哦,好吧,也许在第二部分。
74 private void InsertMessage(string text) 75 { 76 LinqChatDataContext db = new LinqChatDataContext(); 77 78 Message message = new Message(); 79 message.RoomID = Convert.ToInt32(lblRoomId.Text); 80 message.UserID= Convert.ToInt32(Session["ChatUserID"]); 81 82 if (String.IsNullOrEmpty(text)) 83 { 84 message.Text = txtMessage.Text.Replace("<", ""); 85 message.Color = ddlColor.SelectedValue; 86 } 87 else 88 { 89 message.Text = text; 90 message.Color = "gray"; 91 } 92 92 // in the future, we will use this value for private messages message.ToUserID = null; 94 message.TimeStamp = DateTime.Now; 95 db.Messages.InsertOnSubmit(message); 96 db.SubmitChanges(); 97 }
在 web.config 文件中
24 <appSettings> 25 <add key="ChatLoggedInText" value="Just logged in!"/> 26 </appSettings>
然后我们在 Message 表中添加/插入一条消息,说明此用户刚刚登录。我们还需要检索消息并显示更新的消息在消息框中。是的,我们添加或检索特定于此房间的数据。我不想一直说“特定于此房间”,我想说的是,在这个聊天室页面中所做的一切都特定于此房间,这意味着我们需要传递 roomID。
148 /// <summary> 149 /// Get the last 20 messages for this room 150 /// </summary> 151 private void GetMessages() 152 { 153 LinqChatDataContext db = new LinqChatDataContext(); 154 155 var messages = (from m in db.Messages 156 where m.RoomID == Convert.ToInt32(lblRoomId.Text) 157 orderby m.TimeStamp descending 158 select m).Take(20).OrderBy(m => m.TimeStamp); 159 160 if (messages != null) 161 { 162 StringBuilder sb = new StringBuilder(); 163 int ctr = 0; // toggle counter for alternating color 164 165 foreach (var message in messages) 166 { 167 // alternate background color on messages 168 if (ctr == 0) 169 { 170 sb.Append("<div style='padding: 10px;'>"); 171 ctr = 1; 172 } 173 else 174 { 175 sb.Append("<div style='background-color: #EFEFEF; padding: 10px;'>"); 176 ctr = 0; 177 } 178 179 if (message.User.Sex.ToString().ToLower() == "m") 180 sb.Append("<img src='Images/manIcon.gif' style='vertical-align:middle' alt=''> " + message.Text + "</div>"); 181 else 182 sb.Append("<img src='Images/womanIcon.gif' style='vertical-align:middle' alt=''> " + message.Text + "</div>"); 183 } 184 185 litMessages.Text = sb.ToString(); 186 } 187 }
- 我们还需要检索登录到此房间的所有用户,以便将其列在用户 div 标签中。请注意,除了您自己以外的用户都是可点击的,因为在第二部分,我将向您展示如何向另一个聊天者发送私人消息。
99 private void GetLoggedInUsers() 100 { 101 LinqChatDataContext db = new LinqChatDataContext(); 102 103 // let's check if this authenticated user exist in the 104 // LoggedInUser table (means user is logged-in to this room) 105 var user = (from u in db.LoggedInUsers 106 where u.UserID == Convert.ToInt32(Session["ChatUserID"]) 107 && u.RoomID == Convert.ToInt32(lblRoomId.Text) 108 select u).SingleOrDefault(); 109 110 // if user does not exist in the LoggedInUser table 111 // then let's add/insert the user to the table 112 if (user == null) 113 { 114 LoggedInUser loggedInUser = new LoggedInUser(); 115 loggedInUser.UserID = Convert.ToInt32(Session["ChatUserID"]); 116 loggedInUser.RoomID = Convert.ToInt32(lblRoomId.Text); 117 db.LoggedInUsers.InsertOnSubmit(loggedInUser); 118 db.SubmitChanges(); 119 } 120 121 string userIcon; 122 StringBuilder sb = new StringBuilder(); 123 124 // get all logged in users to this room 125 var loggedInUsers = from l in db.LoggedInUsers 126 where l.RoomID == Convert.ToInt32(lblRoomId.Text) 127 select l; 128 129 // list all logged in chat users in the user list 130 foreach (var loggedInUser in loggedInUsers) 131 { 132 // show user icon based on sex 133 if (loggedInUser.User.Sex.ToString().ToLower() == "m") 134 userIcon = "<img src='Images/manIcon.gif' style='vertical-align:middle' alt=''> "; 135 else 136 userIcon = "<img src='Images/womanIcon.gif' style='vertical-align:middle' alt=''> "; 137 138 if (loggedInUser.User.Username != (string)Session["ChatUsername"]) 139 sb.Append(userIcon + "<a href=#>" + loggedInUser.User.Username + "</a><br>"); 140 else 141 sb.Append(userIcon + "<b>" + loggedInUser.User.Username + "</b><br>"); 142 } 143 144 // holds the names of the users shown in the chatroom 145 litUsers.Text = sb.ToString(); 146 }
- 所以简单来说,这就是发生的事情。您登录,通过身份验证后,您会被重定向到聊天室页面,当您到达聊天室页面时,屏幕上会显示一条消息,通知其他聊天者您已登录。
发送消息
- 您会注意到,您输入消息的文本框控件在您按下“回车键”或直接单击发送按钮后仍然保持焦点。这在两个地方完成:
在 BtnSend_Click 事件中
58 ScriptManager1.SetFocus(txtMessage.ClientID);
以及在 Timer1_OnTick 事件中
68 ScriptManager1.SetFocus(txtMessage);
- 您还会注意到,您不必直接单击“发送按钮”来发送消息,您只需按键盘上的“回车键”,其效果与直接单击发送按钮相同。如下所示的代码。
<form id="form1" defaultbutton="btnSend" defaultfocus="txtMessage" runat="server" >
- 当您发送消息时,会发生几件事。它们按顺序排列:
- 消息被插入到 Message 表中。
- 消息从 Message 表中检索。
- 然后消息显示在消息框中。根据聊天者的性别,会显示男性或女性图标以及消息。
- 用户从 LoggedInUser 表中检索。
- 然后用户被重建并显示在聊天者列表框中。您会注意到,除了您自己以外的所有其他用户都显示为链接。这是因为在本次教程的下一部分,我将对此代码进行扩展,使其他用户可点击,以便您可以私聊。目前,它只是未来代码的一个占位符。
- 您还会注意到,消息的 div 标签上的滚动条始终设置为 div 的底部,这是因为我们调用了一个客户端函数,每次页面重新加载时都会将 div 滚动条的位置设置为底部。
来自 body 标签
<body style="background-color: gainsboro;" onload="SetScrollPosition()" onunload="LogMeOut()">
客户端函数
function SetScrollPosition()
{
var div = document.getElementById('divMessages');
div.scrollTop = 100000000000;
}
有两种方法可以注销。您可以单击注销按钮或关闭浏览器。
- 单击注销按钮:当用户单击注销按钮时,该用户将被从 LoggedInUser 表中删除。一条消息将被插入到 Message 表中,说明该用户已注销。
189 protected void BtnLogOut_Click(object sender, EventArgs e) 190 { 191 // log out the user by deleting from the LoggedInUser table 192 LinqChatDataContext db = new LinqChatDataContext(); 193 194 var loggedInUser = (from l in db.LoggedInUsers 195 where l.UserID == Convert.ToInt32(Session["ChatUserID"]) 196 && l.RoomID == Convert.ToInt32(lblRoomId.Text) 197 select l).SingleOrDefault(); 198 199 db.LoggedInUsers.DeleteOnSubmit(loggedInUser); 200 db.SubmitChanges(); 201 202 // insert a message that this user has logged out 203 this.InsertMessage("Just logged out! " + DateTime.Now.ToString()); 204 205 // clean the session 206 Session.RemoveAll(); 207 Session.Abandon(); 208 209 // redirect the user to the login page 210 Response.Redirect("Default.aspx"); 211 }
- 关闭浏览器:我猜大多数用户可能不会单击注销按钮,而是会在离开聊天室时直接关闭浏览器。我们唯一能捕获这一点的地方是客户端。当然,我最喜欢的客户端脚本是 JavaScript。我们可以使用“onunload”函数捕获这一点,如以下代码所示,将其放在 body 标签中。
<body style="background-color: gainsboro;" onload="SetScrollPosition()" onunload="LogMeOut()">
调用服务器端方法的客户端 JavaScript
function LogMeOut()
{
LogOutUserCallBack();
}
我们还需要调用一个服务器端方法,从客户端异步删除该用户。当然,有很多方法可以做到这一点。我使用了 ASP.Net 的 CallBack 功能。要做到这一点,我们只需要在代码隐藏文件中显式实现“ICallbackEventHandler”,如下所示。
8 public partial class Chatroom : System.Web.UI.Page, System.Web.UI.ICallbackEventHandler
ICallbackEventHandler 有 2 个成员需要您显式实现。“RaiseCallbackEvent”是我们从客户端到服务器端代码进行回调的事件。这就是我们从 LoggedInUser 表中删除用户的地方,如下所示。
220 void System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) 221 { 222 _callBackStatus = "failed"; 223 224 // log out the user by deleting from the LoggedInUser table 225 LinqChatDataContext db = new LinqChatDataContext(); 226 227 var loggedInUser = (from l in db.LoggedInUsers 228 where l.UserID == Convert.ToInt32(Session["ChatUserID"]) 229 && l.RoomID == Convert.ToInt32(lblRoomId.Text) 230 select l).SingleOrDefault(); 231 232 db.LoggedInUsers.DeleteOnSubmit(loggedInUser); 233 db.SubmitChanges(); 234 235 // insert a message that this user has logged out 236 this.InsertMessage("Just logged out! " + DateTime.Now.ToString()); 237 238 _callBackStatus = "success"; 239 }
“GetCallbackResult”是我们返回一个字符串到 JavaScript(客户端)函数的方法。因此,从 RaiseCallbackEvent,您可以为某个变量分配一个值,该值从 GetCallbackResult 方法返回。就我们而言,我们可以分别返回“success”或“failed”值。代码如下。
215 string System.Web.UI.ICallbackEventHandler.GetCallbackResult() 216 { 217 return _callBackStatus; 218 }
这种异步/回调处理的美妙之处在于,在 Page_Load 部分的代码中,我们注册了回调脚本。
29 // create a call back reference so we can log-out user when user closes the browser 30 string callBackReference = Page.ClientScript.GetCallbackEventReference(this, "arg", "LogOutUser", ""); 31 string logOutUserCallBackScript = "function LogOutUserCallBack(arg, context) { " + callBackReference + "; }"; 32 Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "LogOutUserCallBack", logOutUserCallBackScript, true);
我将谈谈我们如何从数据库中检索消息。有两种主要方法可以从数据库中检索消息以显示给聊天者。
- 当我们发送消息时:每次您按下发送按钮或回车键,都会向 Message 表插入一条消息。在您的聊天窗口中,您还将自动从 Message 表检索消息。因此,例如,您正在与其他人聊天,而另一位聊天者在刚才发送了一条消息,该消息将出现在您刚发送的消息之上,看起来就像另一位用户刚刚给您发送了一条消息。如果另一位用户不断发送消息,那么对于该用户来说,他/她将好像立即收到回复,并且在聊天时没有空闲时间。
- 每隔 7 秒在 Timer Tick 事件中:如果出于某种原因您从未发送过消息,备份就是 Timer Control。计时器控件将每 7 秒自动刷新您的窗口以显示更新的聊天消息。您可能首先认为这太长了。实际上不是,因为只有在您 7 秒内不发送任何消息时,我们才会真正检索消息。最棒的是:每次您按下发送按钮或回车键时,计时器计数器都会重置为零(0)。这使得您的 Web 应用程序性能稍快。
- 最后,上述两种消息检索方式的组合比每秒刷新一次浏览器能提供更好的性能。它还将为用户提供无缝且更快的消息检索。
构建这个 Web 聊天应用程序对我来说确实很有趣,最重要的是,由于使用了 LINQ,它非常快速(大约 2 小时)。事实上,我写这篇文章花费的时间比在 Visual Studio 2008 中实际构建聊天应用程序花费的时间要长得多,远远得多。当然,您可以通过添加其他功能来改进此聊天应用程序,本文档仅用于让您体验使用 LINQ 和 AJAX 构建聊天应用程序的乐趣和速度。我计划写第二部分,展示如何轻松地添加与其他聊天者进行私人通信的方式。我将在下周左右提供 Visual Basic (VB 9.0) 的代码。
一如既往,代码和文章按“原样”提供,绝对没有任何保证。请自行承担使用风险。