Scott Semyan
Microsoft Corporation
2000年7月
摘要:本文说明如何使用 ASP、Internet Information Services 和 Microsoft SQL Server 建立日历。
目录
简介
在本文中我将示范如何建立基于 Web 的日历。本文为不熟悉 Active Server Pages (ASP)、Structured Query Language (SQL) 和 ActiveX® Data Objects (ADO) 的开发者提供了对数据驱动的 Web 站点的很好介绍。同时也为很有经验的开发者提供了可伸缩性方面的技巧。
图 1. 基于 Web 的事件日历
什么是基于 Web 的日历?
基于 Web 的日历最近已变得十分流行。起初人们将其日程表保存在个人信息管理器,如 Microsoft® Outlook® 中。简单地说,这些日历的问题在于它们是属于个人的,并且很难共享日历供其他人查看或使用。这样就需要有一种方法,要么使您的个人日历可供其他人查看,要么有一个小组日历可供许多人同时使用。随着 Internet 的兴起 — 任何时间、任何地方均可用 — 基于 Web 的日历已变得可行。
用基于 Web 的日历您可以做什么?
基于 Web 的日历允许您与其他人共享您的日程表或管理一组人员的日历。如果您希望人们可以访问您的日程表,则共享您的日程表是有用的。另一方面,小组日历对于显示诸如最后期限或里程碑之类的主要事件,或显示谁在什么时候休假都是有用的。
在这里提供了什么?
在我的公司我们有一个 intranet 站点,在此站点中我们将所有与我们小组有关的信息,包括预定的休假、里程碑和假日,都集中在一起。为了做到这一点,我建立了一个简单的基于 Web 的日历,来供任何人查看、添加和/或删除事件(请参见图 1)。
我在本文中描述了如何使用 Microsoft Internet Information Services (IIS) 和 Microsoft SQL Server™ 内的 ASP 页建立一个非常简单的基于 Web 的日历。在本文的结论部分,我将讨论当扩展此应用程序以支持许多用户时所必须考虑的性能方面牵涉的问题。对于不熟悉 IIS 和 ASP 的人,本文可视为建立数据驱动 Web 站点的一个好的教程。
SQL后端
要保存什么数据?
第一个问题是您希望每个日期保存何种数据?就我的日历而言,我仅需保存表明事件性质的一个文本字符串,最长 100 个字符。可以容易地扩展此数据模型以包括很多内容,而不是仅仅包括一个文本字符串。
架构
Calendar.sql
SQL 代码包含在以下代码片断中:
--------------------------------------------------
-- 表
--------------------------------------------------
create table Schedule
(
idSchedule smallint identity primary key,
dtDate smalldatetime not null,
vcEvent varchar(100) not null
)
go
--------------------------------------------------
-- 存储过程
--------------------------------------------------
create procedure GetSchedule (@nMonth tinyint, @nYear smallint)
as
select idSchedule, convert(varchar, datepart(dd, dtDate)) 'nDay', vcEvent
from Schedule
where datepart(yy, dtDate) = @nYear and datepart(mm, dtDate) = @nMonth
order by datepart(dd, dtDate)
go
create procedure AddEvent (@vcDate varchar(20), @vcEvent varchar(100))
as
insert Schedule
select @vcDate, @vcEvent
go
create procedure DeleteEvent (@idSchedule smallint)
as
delete Schedule where idSchedule = @idSchedule
go
对于我的 schedule 表,我使用一个标识键(自身递增)为主键,因此我自己无需创建一个唯一键。我为 idSchedule 选择 smallint,这是因为根据我的需要,我不会有 32,000 多种不同的事件。而对于大型应用程序,则很可能希望使用一个长整数来获得多达 20 亿个记录。因为我无需按秒记录时间,所以我将日期保存为 smalldatetime 以节省空间。通常的日期时间会精确到毫秒,占用的空间是精确到分钟的 smalldatetime 的两倍。大型应用程序可能需要选用其它占用更少空间的记录日期的方法(例如,用一个整数来表示特定日期,如 1970 年 1 月 1 日之后的天数)。
对于我的数据,我选择变长字符串 (varchar),最长为 100 个字符。在 SQL Server 7.0 中您可以使用 nvarchar,它可以容纳 Unicode 字符,从而使您的应用程序可以在全球使用。然而,创建全球可以使用的 Web 页还有其它问题,但这超出了本文讨论的范围。
存储过程
有关 SQL 中的存储过程的简短注释:存储过程优于 ASP 中的硬编码的 SQL 有两个主要原因。第一个原因是,将数据访问代码与表示代码保持分离可确保数据的独立性。这允许您更改数据结构(及其伴随的存储过程),而无需修改 ASP 页。这是经典的 Microsoft Windows® DNA n 层体系结构。首选存储过程的第二个原因是它们比文本查询运行得更快,因为存储过程第一次被调用时就经过分析。因此,随后的调用不必再分析此过程。
我需要以下三个存储过程来访问和操纵此表中的数据:GetSchedule、AddEvent 和 DeleteEvent。
对于 GetSchedule 过程,我将希望返回的数据的月份和年份传递给它。下一步,我使用 Datepart() 函数来获得事件的数字日期。然后在我建立日历的同时使用此数字来与该月的这一天相匹配。按天对结果进行排序是很重要的,稍后我们就可以看到这一点。
AddEvent 过程仅向某个给定日期添加文本事件。由于所有的事件都标记有唯一的日程表 ID,因此我不担心添加重复事件。
从此日程表删除事件仅需唯一日程表 ID。将此 ID 传递给 DeleteEvent 过程即会删除此事件。
ASP 前端
基本 UI
图 1 显示主要的用户界面 (UI)。设置的 UI 象挂式日历。我的一周从星期天开始,包含 7 天。为了简易起见,我没有进行繁琐的格式化,但使用样式表可容易地使此日历看起来更专业。由此主页,用户可以看到哪些事件是已安排的。另外,使用底部的链接可以在日历中按月前后翻动,或者使用此页左上角的下拉式列表框可以选择特定的月份和年份。到“添加/删除事件”页的链接就在日历的下方。
ASP 代码基础
header.asp
<%@ LANGUAGE="VBSCRIPT"
ENABLESESSIONSTATE = False %>
<%
'********************************************************************
' 名称:header.asp
'
' 目的:标头包括用来启动所有页的文件,
' 还包括全局函数
'
'********************************************************************
Option Explicit
Response.Buffer = True
Response.Expires = 0
sub Doheader(strTitle)
%>
<html>
<head>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=gb2312">
<title>Event Calendar - <%= strTitle %></title>
</head>
<body bgcolor="white" link="blue" alink="blue" vlink="blue">
<basefont face="Verdana, Arial">
<center><h1>Event Calendar</h1>
<h3><%= strTitle %></h3>
<%
end sub
sub DoFooter(strTitle)
%>
</center>
</body></html>
<%
end sub
function GetDataConnection()
dim oConn, strConn
Set oConn = Server.CreateObject("ADODB.Connection")
strConn = "Provider=SQLOLEDB; Data Source=adspm; Initial Catalog=TeamWeb; "
strConn = strConn & "User Id=TeamWeb; Password=x"
oConn.Open strConn
set GetDataConnection = oConn
end function
%>
在我用 ASP 对 Web 站点进行编码时,我喜欢整个站点使用单个标头页。这使我可以将常用的函数、样式、Microsoft JScript® 等置于一页上。对于此站点,我已经建立了一个标头页(请参阅前面的代码示例),它为整个站点设定了一些 ASP 设置并包含了我在所有其他页中使用的三个函数:一个 HTML 标头和注脚以及我的数据连接代码。
我做的第一件事情是将语言设置为 Microsoft Visual Basic® Scripting Edition (VBScript) 并禁用会话状态。设置语言不是必需的,但这是一个好主意。另外,我显式告知 IIS 此站点不使用 ASP 会话状态,这样可以稍微提高性能。我的其他编码约定是设置 Option Explicit(意思是我必须在使用变量之前 dim 它们)、缓冲的输出(IIS 将保留输出,然后立即将其全部吐出 — 这可以提高性能),以及将此页设置为立即到期(以便用户始终可以获得刷新的页面)。
我使用处理所有我的 HTML 标头代码的 Doheader 函数。我传入页名,它同时显示在 HTML 标题中和页顶部的 H3 标记中。这给予我一个单独的位置来编辑 HTML BODY 属性、样式等。与 Doheader 函数类似,我喜欢在需要添加一个普通的注脚(包括联系信息等)时有一个注脚函数。
ADO 使我可以很容易地将我的 ASP 页与我的 SQL 数据库相连接。首先我创建一个到数据库的连接。为了获得记录集,我调用 Connection 对象的 Execute 方法,将希望执行的命令的文本字符串传入。一旦有了记录集,就可以在其中循环。header.asp 包含获得我的数据连接的函数。这意味着如果我的数据源有变化我就只有一个位置需要编辑连接信息(服务器、用户、口令)。请注意,作为结果我必须在函数的末尾使用 set 命令传出新连接。
工作原理
Calendar.asp
<!--#includes file="header.asp"-->
<%
'********************************************************************
' 名称:Calendar.asp
'
' 目的:主日历页以日历格式显示事件
'
'********************************************************************
dim dbConn, rs, nDex, nMonth, nYear, dtDate
' 获取当前日期
dtDate = Now()
' 设置月份和年份
nMonth = Request.QueryString("nMonth")
nYear = Request.QueryString("nYear")
if nMonth = "" then nMonth = Month(dtDate)
if nYear = "" then nYear = Year(dtDate)
' 将日期设置为当前月份的第一天
dtDate = DateSerial(nYear, nMonth, 1)
Set dbConn = GetDataConnection
Set rs = dbConn.Execute ("GetSchedule " & nMonth & ", " & nYear)
Doheader(MonthName(Month(dtDate)) & " " & nYear)
%>
<form method="get" name="DateSelect" action="Calendar.asp">
<table width=700>
<tr><td colspan=2><select name="nMonth" onChange="DateSelect.submit();"><%
for nDex = 1 to 12
Response.Write "<option value=""" & nDex & """"
if MonthName(nDex) = MonthName(nMonth) then
Response.Write " selected"
end if
Response.Write ">" & MonthName(nDex)
next %></select>
<select name="nYear" onChange="DateSelect.submit();"><%
' 注:已将年份设置为 1999 年和 2000 年之间
for nDex = 1999 to 2002
Response.Write "<option value=""" & nDex & """"
if nDex = CInt(nYear) then Response.Write " selected"
Response.Write ">" & nDex
next %></select></td></tr>
<tr><td colspan=2>
<table border=1 bgcolor="gray" cellpadding=3>
<tr bgcolor="Blue"><td width=90><font color="white"><b>Sunday</b></font></td>
<td width=90><font color="white"><b>Monday</b></font></td>
<td width=90><font color="white"><b>Tuesday</b></font></td>
<td width=90><font color="white"><b>Wednesday</b></font></td>
<td width=90><font color="white"><b>Thursday</b></font></td>
<td width=90><font color="white"><b>Friday</b></font></td>
<td width=90><font color="white"><b>Saturday</b></font></td></tr>
<tr bgcolor="#ffffc0">
<%
' 添加空单元格,直至到达正确日期为止
for nDex = 1 to Weekday(dtDate) - 1
Response.Write "<td bgcolor=""#c0c0c0""> </td>"
next
do
Response.Write "<td valign=""top"">" & Day(dtDate) & "<br> <br>"
if not rs.EOF then
do
if CInt(rs("nDay")) <> CInt(Day(dtDate)) then exit do
Response.Write "<font size=""-1""><b>" & rs("vcEvent")
Response.Write "</b></font><br>"
rs.MoveNext
if rs.EOF then exit do
loop
end if
Response.Write "</td>"
if WeekDay(dtDate) = 7 then
Response.Write "</tr>" & vbCrLf & "<tr bgcolor=""#ffffc0"">"
end if
dtDate = DateAdd("d", 1, dtDate)
loop until (Month(dtDate) <> CInt(nMonth))
' 如果需要添加空白单元格填充剩余的月份
if Weekday(dtDate) <> 1 then
for nDex = Weekday(dtDate) to 7
Response.Write "<td bgcolor=""#C0C0C0""> </td>"
next
end if
%>
</tr>
</table></td></tr>
<tr><td colspan=2 align="center"><b><a
href="Events.asp?nMonth=<%= nMonth %>&nYear=<%= nYear %>"
>Add/Remove Events</a></td></tr>
<tr><td><a href="Calendar.asp?nMonth=<%
if nMonth = 1 then
Response.Write "12&nYear=" & nYear - 1
else
Response.Write nMonth - 1 & "&nYear=" & nYear
end if %>"><b><- Previous Month</b></a></td>
<td align=right><a href="Calendar.asp?nMonth=<%
if nMonth = 12 then
Response.Write "1&nYear=" & nYear + 1
else
Response.Write nMonth + 1 & "&nYear=" & nYear
end if %>"><b>Next Month - ></b></a>
</td>
</tr>
</table>
</form>
<%
DoFooter("Home")
%>
此项目最具挑战性的部分是建立一个精确的 HTML 日历。有多个算法要确定。例如,每月有多少天,是否为闰年等。我不做所有这些工作,而是使用 Visual Basic 内的 date 对象来建立我的日历。
为了设计某个给定月份的日历,我首先为该年该月的第一天创建 date 对象。然后我使用 DateAdd() 函数只是将日期向前递增,一次一天。接着我可以使用内建方法来显示它是一周的星期几以及此月何时结束。我还知道这是此月的哪一天,并利用这一点来与我的日程表信息同步。这使我可以建立显示日历的表。
Event Calendar 页是通过使用在查询字符串中传递的日期和年份动态创建的。如果未传入月份和年份,则使用当前的月份和年份。
这简化了建立可在月份之间移动的 UI。向后移动一个月的链接是通过调用 Calendar.asp 和提供表明上一月的查询字符串(如果在一月份则递减年份)创建的。向前移动一个月与此一样容易(首先检查一下看看是否在十二月份)。移动到特定的月份和年份就更容易了。所有要做的工作是用 Calendar.asp 建立一个表格作为目标,用我们希望包含的月份和年份填充下拉式列表框,然后将提交事件附加到下拉式列表框的 onChange() 事件中。这使得页在用户选择新的月份或年份时自动更新。请注意我是如何使用 MonthName() 函数来列出月份的。
显示事件是十分容易的。首先我使用 ADO 连接到我的 SQL 数据库来抽出给定月份的事件。我调用我的 GetSchedule 存储过程并传入正在讨论的月份和年份。这提供给我一个包含事件列表以及它们发生的日子(按天排序)的记录集。
现在,在我建立日历的同时,我仅在其发生的日子显示事件。因为我知道我的事件是按天排序的,所以我可以做到这一点。
图 2. “添加/删除事件”页
“添加/删除事件”页使用户可以添加新事件或删除现有的事件。首先将日历移动到适当的月份,然后单击“添加/删除事件”链接可访问此页。此链接是为包括表明添加/删除事件的月份和年份的查询字符串而创建的。
此月的当前事件列表是通过使用与 Calendar.asp 相同的 GetSchedule 存储过程生成的。这一次所有我们要做的工作只是在记录集中循环以显示此月的所有事件。
Events.asp
<!--#includes file="header.asp"-->
<%
'********************************************************************
' 名称:Events.asp
'
' 目的:用于向事件数据库中添加事件以及从中删除事件。
'
'********************************************************************
dim dbConn, rs, nDex, nMonth, nYear, dtDate, dtTemp
' 获取当前日期
dtDate = Now()
' 设置月份和年份
nMonth = Request("nMonth")
nYear = Request("nYear")
if nMonth = "" then nMonth = Month(dtDate)
if nYear = "" then nYear = Year(dtDate)
Set dbConn = GetDataConnection
Set rs = dbConn.Execute ("GetSchedule " & nMonth & ", " & nYear)
Doheader("Add/Remove Events")
%>
<form method="get" action="AddEvent.asp">
<br>Currently Scheduled Events for <b><%
Response.Write MonthName(nMonth) & " " & nYear
%></b>:<p>
<table bgcolor="gray" border=1 cellpadding=3>
<tr bgcolor="Blue"><td><font color="white"><b>Day</b></font></td>
<td colspan=2><font color="white"><b>Event</b></font></td></tr>
<% if rs.EOF then
Response.Write "<tr><td colspan=3 bgcolor=""#ffffc0"">No events listed</td></tr>"
end if
while not rs.EOF
Response.Write "<tr bgcolor=""#ffffc0""><td>" & rs("nDay") & "</td><td>"
Response.Write rs("vcEvent") & "</td><td><input type=""button"" value=""Remove"""
Response.Write " onClick=""window.location.href='RemoveEvent.asp?nMonth=" & nMonth
Response.Write "&nYear=" & nYear & "&idSchedule=" & rs("idSchedule") & "'""></td></tr>"
rs.MoveNext
wend
%></table>
<p><br>
<table bgcolor="gray" border=1 cellpadding=3>
<tr bgcolor="Blue"><td><font color="white"><b>Add New Event:</b></font></td></tr>
<tr bgcolor="#ffffc0"><td>Event:
<input type="text" size=30 maxlength=100 name="Event"> Day: <select name="nDay"><%
' 将日期设置为当前月份的第一天
dtDate = DateSerial(nYear, nMonth, 1)
dtTemp = dtDate
do
Response.Write "<option value=""" & Day(dtTemp) & """>" & Day(dtTemp)
dtTemp = DateAdd("d", 1, dtTemp)
loop until (Month(dtTemp) <> CInt(nMonth)) %></select>
<input type="hidden" name="nMonth" value="<%= nMonth %>">
<input type="hidden" name="nYear" value="<%= nYear %>">
<input type="Submit" value="Add Event"></td></tr>
</table>
</form>
<p><a href="Calendar.asp?nMonth=<%= nMonth %>&nYear=<%= nYear %>">Back to Calendar</a>
<%
DoFooter("Home")
%>
要添加新事件,用户只是将其事件键入窗体,然后单击提交按钮。通过使用 maxlength 属性将窗体元素长度限制为 100 个字符,以匹配 Schedule 表中的 event 字段。
要删除一个事件,用户单击希望删除的事件旁边的按钮。此按钮调用 RemoveEvent.asp,传入此事件的 idSchedule 以及当前月份和年份,以便可以建立回到 Events.asp 链接。
AddEvent.asp
<!--#includes file="header.asp"-->
<%
'********************************************************************
' 名称:AddEvent.asp
'
' 目的:用于向事件数据库中添加事件。
'
'********************************************************************
dim dbConn, strSQL
Set dbConn = GetDataConnection
strSQL = "AddEvent '" & Request("nMonth") & "/" & Request("nDay")
strSQL = strSQL & "/" & Request("nYear") & "', '"
strSQL = strSQL & Replace(Request("Event"), "'", "''") & "'"
dbConn.Execute (strSQL)
Response.Redirect("Events.asp?nMonth=" & Request("nMonth") & "&nYear=" & Request("nYear"))
%>
AddEvent.asp 页接收由窗体传入的信息并使用 AddEvent 存储过程将其插入到数据库中。然后此页重定向回到“添加/删除事件”页,因此用户可立即看到他们的新事件。请注意我是如何使用 Replace() 函数来将单引号 (') 更改为一对单引号 (' ') 的,SQL 将后者解释为单引号。如果您不这样做,在用户试图输入类似“All Saint's Day”的事件时您会得到一个错误。
RemoveEvent.asp
<!--#includes file="header.asp"-->
<%
'********************************************************************
' 名称:RemoveEvent.asp
'
' 目的:用于从事件数据库中删除事件。
'
'********************************************************************
dim dbConn
Set dbConn = GetDataConnection
dbConn.Execute ("DeleteEvent " & Request("idSchedule") )
Response.Redirect("Events.asp?nMonth=" & Request("nMonth") & "&nYear=" & Request("nYear"))
%>
RemoveEvent.asp 页使用 DeleteEvent 存储过程来删除给定的事件 ID,然后立即向用户发送回“事件”页。
性能问题
伸展
ASP 的美妙之处在于它使建立 Web 页变得十分容易。然而,如果您希望建立一个可以适应大量用户的站点,则您需要仔细编码。在本节中我将讨论增强我在这里所提供的基于 Web 的日历的可伸缩性的各种方法。虽然我说明的这些方法是针对此应用程序的,但是它们可用于提高任何 ASP Web 站点上的性能。
SQL 优化
提高站点性能的一个简单方法是给 Schedule 表的 date 字段添加一个索引。这将加速 GetEvents 存储过程,其原因是它在给定日期之间查找事件。
对于小站点,可以将 SQL 与 IIS 安装在同一服务器上。一旦站点开始增长,您就会希望将 SQL 移动到其自身的服务器上。当站点进一步增长时,您可能希望添加均指向同一 SQL 服务器的多个 IIS 服务器。如果到 SQL 服务器的通信量过度增长,您就可以将数据分割到不同的服务器上。您可能想将奇数月份分配到一台服务器,将偶数月份分配到另一台服务器上。当然,您将希望修改 header.asp 中的 GetDataConnection,以便它为您提供基于此月份的正确连接。
ASP 优化
ASP 解释的主要优化将是高速缓存页面,以便无需每次都对它们进行解释。做到这一点的最简易的方法是借助 ASP Application 对象。要做到这一点,您只是将页 HTML 保存到含有月份和年份名称的应用程序变量(例如 Calendar07-2000)中。然后,当显示 Event Calendar 页时,您首先检查一下看看日历是否已经保存在应用程序变量中,如果是这样,只需检索它。这省去了到 SQL 框的过程。下面的一些伪代码显示这是如何工作的:
<<do header>>
ShowCalendar(nMonth, nYear)
<<do Footer>>
sub ShowCalendar(nMonth, nYear)
if Application("Calendar" & nMonth & "-" & nYear) = "" then
<<Build Calendar>>
Application("Calendar" & nMonth & "-" & nYear) = <<Calendar>>
End if
Response.Write Application("Calendar" & nMonth & "-" & nYear)
End sub
当然,在 Events.asp 页更改某个月份的事件时,您将需要清空该月份的应用程序变量,以便反映这些更改。
安全性
有几种方法可实现此站点上的安全性。对于 intranet 站点,基于 Microsoft Windows NT® 的验证是最容易设置的,其原因是您的用户将很可能已经登录到网络。您可能希望让所有用户查看 Event Calendar 页,但是只有管理员能访问 Add/Remove Events 页。
如果关心审计,您可以容易地修改 AddEvent 和 DeleteEvent 过程将其信息保存到审计表中。您还可以确保 IIS 为每个页命中的查询字符串和用户进行日志记录。然后逐个分析日志以确定何人于何时做了什么,这是很简单的。
结论
在本文中我演示了如何建立基于 Web 的日历。我的基于 Web 的日历是很简单的,但是可以容易地将其扩展为功能完备的站点。我试图细致地说明创建基于 Web 的日历所遵循的步骤,以便入门用户可以看到在工作的 ASP、SQL 和 ADO。最后,我提供了一些有关如何尽可能大地扩展此站点的几点技巧。如果您发现任何错误或者可能有什么想法能够改进此代码,请给我发送电子邮件,地址为 semyan@hotmail.com。