如何利用ASP.NET、JavaScript和OLE DB从头设计网络日记应用程序?

如何利用ASP.NET、JavaScript和OLE DB从头设计网络日记应用程序?

多数情况下,ASP.NET 高级模板化控件(如 DataList 和 DataGrid)是用于数据表示的最佳选择。但是,当需要灵活地进行各种各样的布局时,Repeater 控件就是您所需要的。在本文中,作者将构建一个功能齐备的网络日记应用程序,以举例说明使用 Repeater 和 DataList 控件来呈现主从关系中嵌套数据的方法。然后,作者将介绍如何通过添加一些使网络日记反应更迅速且可用性更高的客户端 JavaScript 代码,来替代这些控件的默认实现。

如今,似乎每个人都需要网络日记,我知道我自己就是这样的。但是我找不到具有我想要的功能的预建 ASP.NET 网络日记代码,所以我构建了自己的代码。在构建自己的网络应用程序时,最重要的一点是,要大量用到 ASP.NET 服务器控件,例如 Repeater、DataList 和 Calendar。网络日记应用程序乍看上去似乎就是一个简单的练习,但是实际上,它要求您在一个典型的报告应用程序中实现很多需要的功能,如构建并呈现主从关系或编辑和删除记录,隐藏或显示登录用户的内容和控件,以及管理在同一页面上多个虚拟窗体的输入验证。本文将介绍网络日记的设计和实现细节,并对可轻松应用到各种 ASP.NET 项目的技术进行阐述,而暂且不考虑构建这些网络日记的目的是出于业务需要还是为了娱乐。

在开始编码工作之前,您应该确定想要构建的网络日记的类型、它应具有的功能以及数据存储的设计方式。有效的网络日记包括许多功能。网络日记的消息应按照从新到旧的顺序进行显示。在同一天内可以张贴多条消息,这些消息应直观地分组显示于表格或框中,但是仍然可以按照张贴的时间顺序对其进行识别。同时,用户应当能够为她希望阅读的条目选择时间间隔。这一点非常重要,因为您并不希望检索用户已经看过的旧内容。

用户应该能够对任意一条消息进行评注,并且张贴的评注应该能够直接在其父消息之下进行显示,从而条理清晰。此外,网络日记的所有者应该能够张贴、编辑并删除消息和评注,而用户应该只能阅读消息和张贴评注。要根据用户身份来决定允许或禁止其进行张贴或编辑操作,需要显示或隐藏某些控件,并且还需要进行某种形式的身份验证。

数据库设计

接下来,必须您必须确定消息和评注的存储方式。在本项目中,我使用了 SQL Server™ 数据库,但我的示例代码还包含一个 Microsoft® Access 数据库,以防您选择这种数据储存方式。该数据库只包含两个表:一个是消息表,另一个是评注表。消息表储存唯一的 ID、可选标题或消息摘要、消息文本以及张贴消息的日期和时间。评注表储存唯一的 ID、评注所对应消息的 ID、作者的名称、作者的电子邮件地址、评注文本以及张贴消息的日期和时间。图 1 显示这两个表的设计。

如何利用ASP.NET、JavaScript和OLE DB从头设计网络日记应用程序?

图 1 父表/子表

 

您可以看到,两表之间是一对多的关系,并通过 MessageID 字段进行链接,这样,通过级联更新和删除也加强了引用完整性。请注意,两表的名称都以前缀“Blog_”开头。我一直在自己的 SQL Server 表中使用前缀,这是因为当将这些表按字母顺序列出时,它们在 Enterprise Manager 中将分组在一起。此外,Web 宿主计划通常只提供一个 SQL Server 数据库,您必须在使用的所有 Web 模块间共享该数据库。如果不使用表前缀,而且某个模块可能已经使用一个名为 Message 的表,那么在部署时无法立即解决此种名称冲突问题。

另一个要切记的重要细节是,任何字段都不能是空值,即使用户将该字段保留为空白。为了防止有未处理的异常出现,您应该将空字段设置为空字符串,而不是允许出现空值。如果使用 Web 窗体进行数据输入,则不会有任何问题,这是因为如果没有内容输入,控件将返回一个空字符串。

业务层

在典型的以数据库为中心的应用程序中,具有数据层、业务层和表示层。数据层可能由一组存储过程组成,而业务层由一组类(在其各自的单独程序集中进行选择性地编译)组成,这些类包装存储过程以确保数据的完整性、执行验证以及对其他的业务规则进行强制。但是,在本项目中,我决定不使用存储过程;我直接将 SQL 查询和命令硬编码到主程序集中,以便能更容易地通过 Access 来使用网络日记。我也没有将数据层和业务层使用的类进行分离,但是,由于只有两个表和一些简单的业务规则,因此我创建了一个负责验证输入并执行正确的 SQL 语句的单个的类。所有代码以及表示层都将位于同一个程序集内,因为我不希望用使用业务类的程序来更新该类,也不希望用其他类型的应用程序或者在多个客户端之间分布该业务类。因此,利用主 ASP.NET 应用程序(称为 WebLogger)对其进行编译即可。该业务类被命名为 Blog,位于名称为“Business”的命名空间下,所以它不会与代码隐藏类发生冲突。以下就是添加一条新消息的方法的实现方式:

Public Sub InsertMessage(ByVal title As String, ByVal message As String)
Dim cmd As New OleDbCommand("INSERT INTO Blog_Messages (Title, _
Message) VALUES (?, ?)")
cmd.Parameters.Add("Title", OleDbType.VarChar).Value = title
cmd.Parameters.Add("Message", OleDbType.LongVarChar).Value = _
message.Replace(m_brChar, "<br>")
ExecuteCommand(cmd)
End Sub

该方法接受添加新记录所需要的所有值。不需要在消息 ID 中传值,因为它在数据库表中是自动进行递增的,并且 AddedDate 将当前日期作为默认值。该方法定义一个 SQL INSERT 命令,并用 ExecuteCommand Helper 方法执行该命令。请注意,Message 参数的值就是作为输入进行传递的消息文本,但是,换行符将被替换为 HTML 的
标记。网络管理员可以在进行张贴时使用 HTML 格式,但是,这是因为经常会出现新行,所以张贴的内容将自动转换为 HTML 格式,从而无需进行键入操作。您可能已经猜测到了,ExecuteCommand Helper 方法只执行作为输入而接受的命令对象:

Private Sub ExecuteCommand(ByVal cmd As OleDbCommand)
cmd.Connection = m_Connection
Try
m_Connection.Open()
cmd.ExecuteNonQuery()
Finally
m_Connection.Close()
End Try
End Sub

ExecuteCommand 方法设置命令的连接;通过从 web.config 的 appSettings 部分定义的自定义项中检索连接字符串,在类的构造函数方法中可创建该连接;然后打开这个连接,在 Try 块中执行该命令,并在对应的 Finally 块中关闭该连接。即使 ExecuteNonQuery 方法引发异常,该连接也将关闭。可是没有 Catch 块,因为我不必执行任何特定的业务操作,如返回交易或记录一个错误。例如,我只想直接在调用页中捕获并处理这样的异常以显示一个用户友好的错误消息。因为其他的 Insertxxx、Updatexxx 和 Deletexxx 方法操作类似,所以我将不逐一介绍。但是,研究一下 InsertComment 方法还是很有价值的,因为除了在 Blog_Comments 表中添加新的记录外,它还发送一封通知电子邮件,该邮件包含评注文本和一些涉及网络日记所有者的消息。

有了这些功能,网络日记所有者不必频繁地加载网络日记以及检查新的电子邮件。当然,如果该网络日记非常受欢迎,并获得很多评注,您就有可能收到过多的电子邮件。因此,应用程序的 web.config 应该具备一个自定义项,它使管理员能够决定她是否想要有关任何新评注的电子邮件通知。然后,需要另一个自定义项来储存电子邮件的目标地址。这两项设置分别命名为 Blog_SendNotifications 和 Blog_AdminEmail,它们在 web.config 的 appSettings 部分(与连接字符串的自定义项一起)内的声明如下:

<add key="Blog_SendNotifications" value="1" />
<add key="Blog_AdminEmail" value="mbellinaso@vb2themax.com" />

Blog_SendNotifications 的值为 1 表示通知功能被激活,其他值或该项空则表示该功能没有被激活。

插入新评注并检查评注通知是否打开之后,您必须构建电子邮件的主体。除了评注文本之外,您还希望包含张贴的父消息的标题、日期和时间,这样在原始消息和评注间就有了明确的连接。通过执行图 2 中的代码来检索父消息的数据,即利用一条命令从正在讨论的单条消息记录中检索适当字段。

一旦具备了所有必需的数据,您就可以构建并发送电子邮件消息了。通过调用 String.Format 方法来构建电子邮件文本,该方法包括一个带数字占位符的模板字符串(形式是 {n}),并将模板中的各种占位符用值代替:

' build the msg's content
Dim msg As String = String.Format( _
"{0} (email: {1}) has just posted a comment the message ""{2}"" " & _
"of your BLOG that you posted at {3}. Here's the comment:{4}{4}{5}", _
author, email, msgTitle, msgDate, Environment.NewLine, comment)
' send the mail
System.Web.Mail.SmtpMail.Send("WebLogger", _
ConfigurationSettings.AppSettings("Blog_AdminEmail"), _
"New comment notification", msg)
End If

业务类的最重要的组成元素是 GetData 方法,该方法对指定时间间隔内张贴的消息和各自的评注进行检索。这里您必须选择是使用 DataReader 还是使用 DataAdapter 以及 DataSet 来检索和读取数据。在 Web 应用程序中,DataReader 通常是最好的选择,特别是不需要进行本地编辑和缓存数据副本时。但是,在这种特定情况下,我选择 DataSet 是因为它允许存储多个表并在这些表之间创建父子关系。这样一次就可以轻松地检索所有的父记录和子记录,并且无需执行单独的查询就可从父记录定位到它们的子数据,而如果我使用 DataReader 方法则需要执行单独的查询。这种方法对于程序员来说更加简单,并节省了数据库资源和网络通信量,这是因为发送到数据库的 SQL 语句的数量更少了。

能够在表间创建关联是另一个绝妙的功能,因为您可以向表中添加计算列,这些列利用关联中对其他表的引用计算表达式的值。我通过 MessageID 字段在 Blog_Messages 和 Blog_Comments 表间创建了关联,正如我在图 1 中为物理数据库创建的关联一样。我希望其数据是来自 Blog_Messages 的 DataTable 中有一个计算列,该列返回对应消息记录的子评注的数量。如果我使用 DataReader,这将需要单独的查询或至少一个子查询(这在 Access 中是不可能的)。对于 DataSet,可以利用无连接的数据副本来实现计数子记录的数量,而无需要求数据库来完成。

我们来看看该方法是如何工作的。它将两个日期作为输入,利用 DataAdapter 来检索在该时间间隔中张贴的消息,然后把它们储存在名称为 Messages 的 DataSet 表中。如下所示:

Public Function GetData(ByVal fromDate As Date, ByVal toDate As Date) _
As DataSet
Dim ds As New DataSet()
interval
Dim da As New OleDbDataAdapter("SELECT * FROM Blog_Messages WHERE" & _
"AddedDate BETWEEN ? AND ?" & _
"ORDER BY AddedDate DESC", m_Connection)
da.SelectCommand.Parameters.Add("FromDate", OleDbType.Date).Value = _
fromDate
da.SelectCommand.Parameters.Add("ToDate", OleDbType.Date).Value = _
toDate.AddDays(1)
m_Connection.Open()
da.Fill(ds, "Messages")
•••

请注意,必须将终止日期增加一天,以便将传入终止日期那天张贴的消息包括在 resultset 中。如果您只想获得返回消息的评注,那么可以在 SELECT 语句中使用 IN 筛选器,该筛选器包含由逗号分隔的记录的所有消息 ID 列表,这些记录由我刚刚列示的查询 1 返回(参见图 3)。

然后,在两表间创建刚刚介绍过的关联:

ds.Relations.Add(New DataRelation("MsgComments", _
ds.Tables("Messages").Columns("MessageID"), _
ds.Tables("Comments").Columns("MessageID")))

下一步,向 Messages 表中添加计算列,该列返回子评注的数量:

ds.Tables("Messages").Columns.Add("CommentsCount", _
GetType(Integer), "Count(Child(MsgComments).CommentID)")

函数 Child(MsgComments) 返回指定关联的所有子数据行,Count 函数与其在 SQL 中的工作方式相同。

要注意的最后一个细节是,在返回填充的 DataSet 并终止该函数之前,如果 Comments 表是空的,则该计算列将表达式计算为 NULL(而不是 0),而以后在 ASP.NET 页中显示该值或在其他表达式中使用该值时,将会出现问题。要解决该问题,可以添加不涉及任何消息的假评注:

If ds.Tables("Comments").Rows.Count = 0 Then
Dim dr As DataRow = ds.Tables("Comments").NewRow()
dr("CommentID") = -1
dr("Author") = "none"
dr("Email") = "none"
dr("Comment") = "none"
dr("AddedDate") = Date.Today
ds.Tables("Comments").Rows.Add(dr)
dr.AcceptChanges()
End If
Return ds
End Function

这样,如果消息没有任何子评注,则表达式计算为 0。

显示消息和子评注

现在业务层已经完成了,下一步就是编写表示层,在该层上可以很好地发挥 ASP.NET 的功能。大多数工作是在单个页(即 Default.aspx)上进行的。该页将显示消息和评注,并允许经过身份验证的管理员对评注进行适度调整并编辑她自己的消息(参见图 4 中的底部窗格)。

如何利用ASP.NET、JavaScript和OLE DB从头设计网络日记应用程序?

图 4 管理员模式下的网络日记

 

Default.aspx 显示了一个按日期分组的消息列表,每条消息可以没有评注或者有多条评注。最初,评注是隐藏的,但当用户单击 View 链接时,评注的内部列表就会动态地展开或者折叠。多条消息可以同时展开它们的评注列表,就像一个只有单级子节点的 TreeView 控件。在开发该页时,会面临一些挑战,包括如何根据日期排序消息、如何在单独的 HTML 表中对其进行分组以及如何显示或隐藏子记录的内部列表。

让我们从第一个问题开始。假设您希望在该页面中显示 5 条消息,前三条消息在同一天张贴,而其他两条在另一天张贴。同时,假设您希望将每一条消息最初都隐藏在自己的 HTML 表中。如果您希望在两个单独表格中将前三条消息分为一组,而将剩余的两条分为一组,则您应当从该表中间的消息(不是当天第一条或最后一条消息)中删除表打开和关闭标记,从第一个消息的表中删除关闭标记,从最后一条消息的表中删除打开标记。因为我想使用一个模板化数据绑定的控件来表示该视图,并且由于必须要完全控制所有生成的 HTML,因此我选择使用 Repeater 控件。除了您在 Repeater 的模板中指定,该控件没有预定义任何输出或者布局。图 5 中的代码是 Repeater 模板的部分定义。

HeaderTemplate 包括表的打开标记,而 FooterTemplate 用于关闭该表。打开和关闭各种按日期分组消息的表的标记的定义,位于 ItemTemplate 部分内的 Literal 控件中。通过切换 Literal 控件的可视性,您可以决定是否输出这些标记,从而决定何时关闭当前的表并打开一个新表。问题在于,何时隐藏 Literal 以便保持当前表为打开状态并且对消息进行分组,以及何时显示 Literal 从而关闭该表并打开一个新表。

答案很简单:只要正在处理中的消息的日期与前一条消息的日期相同,就将这些消息分为一组。在日期更改时,会显示 Literal 控件并启动一个新组。另一个问题是,应该在何时、何地显示控件的内容。当在 Repeater的 ItemDataBound 事件中处理数据项(网络日记的消息记录)并将其绑定到 Repeater 的模板中时,必须对此做出决定。这里您可以读取当前数据项的所有值,并将这些值与前一条消息的数据进行比较,这些数据已存储在一个静态变量中以便在方法调用间保留该值。获得对模板的 Literal 控件的引用后,就可以设置其相应的可视性了。代码如下:

Private Sub Blog_ItemDataBound(...) Handles Blog.ItemDataBound
Static prevDayDate As Date
If e.Item.ItemType <> ListItemType.Item AndAlso _
e.Item.ItemType <> ListItemType.AlternatingItem Then Return
Dim dayDate As Date = e.Item.DataItem("AddedDate")
Dim isNewDay As Boolean = (dayDate.ToShortDateString() <> _
prevDayDate.ToShortDateString())
prevDayDate = dayDate
CType(e.Item.FindControl("DayTitle"), Panel).Visible = isNewDay
CType(e.Item.FindControl("DayBox"), Literal).Visible = ( _
isNewDay AndAlso e.Item.ItemIndex > 0)
End Sub

如您所见,我为 Panel 控件设置了 Visible 属性(在同一个模板中也有声明),该属性显示当前表的日期。如果当前消息的日期和前一条消息的日期不同,则显示该面板;或者如果是绑定的第一条消息,则显示默认的日期。将 Literal 的可视性设置为 True 要受另一个条件约束:被绑定的消息必须不是第一条消息,因为在此种情况下,利用在 Repeater 的 HeaderTemplate 部分中声明的标记可以打开当天的表。要动态地折叠和展开评注列表而无需回发给服务器并且不重新处理页面,请将评注放置到标记内,该标记的显示样式可以设置为“none”,或者设置为一个空字符串来分别隐藏或显示它。将 DIV 声明如下,根据绑定的数据项给其分配一个 ID:

<div style="display:'none'; margin-left:2.0em; margin-top:.8em; "
ID='<%# "div" & Container.DataItem("MessageID") %>'>
<!-- put here the Comments DataList... -->
</div>

我这样进行分配,以便对于每个 DIV 都有唯一的 ID(切记每条消息都有一个)。为了展开或者折叠 DIV,我使用了超级链接,该链接调用以 DIV 的 ID 作为输入的自定义 JavaScript 代码:

<asp:HyperLink Runat="server"
Visible='<%# Container.DataItem("CommentsCount") > 0 %>'
NavigateUrl='<%# "javascript:ToggleDivState(div" & _
Container.DataItem("MessageID") & ");" %>'>
View
</asp:HyperLink>

同时还要注意,Visible 属性与一个表达式绑定,只有在消息有评注显示(如果其 CommentsCount 计算列的值大于 0)时该表达式才返回 True。ToggleDivState 只是将 DIV 的显示样式值取反,从而使其可见或隐藏:

ffunction ToggleDivState(ctrl)
{
div = eval(ctrl);
if (div.style.display == "none")
div.style.display = "";
else
div.style.display = "none";
}

现在我们来看一下评注功能。这次由 DataList 来完成这项工作,因为它的表布局正是我所需的。一般情况下,模板控件或者任何其他数据绑定列表控件的 DataSource 属性都可以通过代码隐藏(或者服务器端脚本)以编程方式进行指派。但是,在这种情况下我没有直接引用 DataList,因为它是由父 Repeater 动态创建的。虽然和大多数属性一样,它可以有一个在运行时设置其值的数据绑定表达式。如果您使用 DataReader 方法来检索数据,可以将 DataSource 属性绑定到一个自定义方法,该方法接受消息的 ID 并返回 DataTable、DataReader 或者任何其他实现 IEnumerable 接口的数据类型的子评注。在这里无需这样做,因为您需要的所有数据都已存储在包含消息的同一 DataSet 中。由于已经在消息表和评注表之间创建了关联,所以您就可以轻松地利用当前数据项的 DataRow 的 GetChildRows 方法来检索子评注数组。表达式声明如下:

<asp:DataList Runat="server" DataSource=
'<%# Container.DataItem.Row.GetChildRows("MsgComments") %>'>

利用绑定表达式完成 DataList 的 ItemTemplate,以显示作者名称和电子邮件地址、消息文本、消息日期以及完整输出该网络日记内容的代码。图 6 显示了输出模块的完成代码。

选择时间间隔并加载网络日记

至此我已经介绍了 ASPX 文件中页面内容的定义,但是还没有介绍实际加载网络日记内容的代码。为了让用户选择一个时间间隔,我使用了 Calendar 控件,将它的 SelectionMode 属性设置为 DayWeekMonth,这样用户就可以选择一天、一周或者整月。例如,如果用户想选择最后两周或者最后的 45 天,提供文本框让用户来填写希望的起始和终止日期是个不错的主意。图 7 显示了向页面添加的新控件,在 Calendar 中选定了整周。

如何利用ASP.NET、JavaScript和OLE DB从头设计网络日记应用程序?

图 7 时间间隔

 

由于回发而没有加载该页面时,必须选择一个默认的时间间隔,例如上一周。但是,对于频繁更新的网络日记,最好只加载少数几天的数据,对于很少更新的网络日记,最好加载上个月整月的数据。至于评注通知功能,最好留给网络日记管理员用 web.congfig 文件中的自定义项进行选择,该项允许管理员指定默认的时间间隔天数。下面的代码说明了如何从文件中读取该自定义项、如何将其分析为整数、如何用它来计算最近 n 天的时间间隔以及如何在日历中突出显示该时间间隔:

Private Sub Page_Load(...) Handles MyBase.Load
If Not IsPostBack Then
Dim defPeriod As Integer = Integer.Parse( _
ConfigurationSettings.AppSettings("Blog_DefaultPeriod"))
Dim fromDate = Date.Today.Subtract(New TimeSpan(defPeriod -_
1,0,0,0))
BlogCalendar.SelectedDates.SelectRange(fromDate, Date.Today)
BindData()
End If
End Sub

通过从今天的日期中减去 n-1 天,可以计算出起始日期。调用 BindData 可以加载选定时间间隔的数据,并将该数据绑定到 Repeater 及其内部控件。该方法调用以前开发的网络日记业务类的 GetData 方法,并传入在日历上选定的起始和终止日期,从 SelectedDates 集合中读取这些日期:

Private Sub BindData()
Dim ds As DataSet = m_BlogManager.GetData( _
BlogCalendar.SelectedDates(0), _
BlogCalendar.SelectedDates(BlogCalendar.SelectedDates.Count - 1))
Blog.DataSource = ds.Tables("Messages").DefaultView
Blog.DataBind()
End Sub

如果选择一天,SelectedDates 将只有一个条目,而且起始和终止日期相同。当用户单击日历时,该页面被回发,并自动选择新的时间间隔,处理日历的 SelectionChanged 事件从而为新的时间间隔再次调用 BindDatahe。最后,您必须处理 Load 按钮的 Click 事件以在日历中选择指定的自定义时间间隔并加载网络日记数据。在该事件过程中,我对两个输入控件的内容进行了分析并获得了两个日期。虽然我最终会添加验证程序来确保在提交窗体前数据格式是正确的,但如果数据格式无效,这种分析仍会引发异常。在这种情况下,我采用了今天的日期(参见图 8)。

请注意,使用日历的 VisibleDate 来确保终止日期在日历中可见。这是必要的,因为如果用户选择过去的两个月,则这种选择在日历中将无法显示。日历将显示当前月,多数人对这并不十分清楚。

弹出式日历

按照当前的情况,如果用户想选择的时间间隔不是一天、一周或者一月,那么他们就必须在两个文本框中手工输入起始和终止日期,并引用一个外部日历。另外,他们输入的日期格式也可能无效。由于这些原因,提供一个弹出式日历是一个不错的做法。当单击某个日期时,该日历应该关闭并且日期应该会出现在主窗口的文本框控件中。使用 Calendar 控件和几个客户端 JavaScript,就可在 ASP.NET 中轻松地重现该功能。

我们首先关注父窗口的 ASPX 代码。通过调用下面的 JavaScript 函数,我添加了一个打开弹出式窗口的图像链接:

<a href="javascript:PopupPicker('IntervalFrom', 200, 200);" >
<img src="images/calendar.gif" border="0" >
</a>

JavaScript 过程希望接收文本框控件的名称(该文本框由选定的日期来填充)以及要打开的弹出式日历窗口的宽度和高度。下面是 JavaScript 代码,这些代码位于已在该页面顶部定义的 <script> 部分中:

function PopupPicker(ctl,w,h)
{
var PopupWindow=null;
settings='width='+ w + ',height='+ h;
PopupWindow=window.open('DatePicker.aspx?Ctl=' +
ctl,'DatePicker',settings);
PopupWindow.focus();
}

该过程利用 window.open 打开一个指定大小的且不能更改的弹出式窗口,该窗口没有滚动条、菜单、工具栏或状态栏。第一个参数是要加载到新窗口中的页的 URL,在刚才显示的代码中,它利用查询字符串中的 ctl 参数来加载 DatePicker.aspx 页,该参数的值作为输入传入 PopupPicker 过程。

现在完成了主页,我必需编写呈现日历的 DatePicker.aspx 页。该页有一个 Calendar 控件,其 Width 和 Height 属性设置为 100%,这样它可以覆盖整个页。ASPX 文件中需要注意的其他重要事项是,客户端 JavaScript 过程将字符串作为输入并将它用作父窗体的输入控件的值,该控件的名称用查询字符串进行传递。最后,它关闭弹出式窗体自身。JavaScript 代码如图 9 所示。

当用户单击 Calendar 控件中的链接时,我希望调用自定义的 JavaScript 过程,而不是正常的处理方法,即提交窗体并选择单击的日期。默认情况下,所有 Calendar 控件提供的链接都会向服务器产生一个回发。而我所希望的是让它们指向自定义的 SetDate 过程。由于 Calendar 的 DayRender 事件,使得更改包含日期链接的表单元格的默认输出非常简单,该事件在每次呈现日期时发生并提供对要创建的表单元格的引用。下面的代码片段利用我自己的超级链接控件来替换默认的单元格内容,它们具有相同的文本,只是我的控件指向了 JavaScript 过程:

Private Sub DatePicker_DayRender(...) Handles DatePicker.DayRender
Dim hl As New HyperLink()
hl.Text = CType(e.Cell.Controls(0), LiteralControl).Text
hl.NavigateUrl = "javascript:SetDate('" & _
e.Day.Date.ToShortDateString() & "');"
e.Cell.Controls.Clear()
e.Cell.Controls.Add(hl)
End Sub

传递给 JavaScript 过程的值是以短格式(一般是 mm/dd/yy)表示的单击日的日期。该值将用于父窗体上的输入控件。图 10 所示为弹出的窗口。

如何利用ASP.NET、JavaScript和OLE DB从头设计网络日记应用程序?

图 10 弹出式日历

 

正如您所见,可以对 ASP.NET 服务器控件进行极其灵活的自定义。希望以这种方式使用 DayRender 事件的另一种情况是,您需要重新定向到另一个网页并以查询字符串传递日期时,而不是在该日期回发后从服务器重新定向到第二个网页。为此,只要用类似下面的代码来替换设置超级链接的 NavigateUrl 属性的那一行代码即可:

hl.NavigateUrl = "SecondPage.aspx?Date=" & e.Day.Date.ToShortDateString()

张贴评注

图 4 中,您可以看到每条消息的下面都有一个 "Post your own comment" 的链接。当用户单击它时,就会出现一个带输入控件的框用于张贴评注。您可以猜出它是如何工作的,因为我在构建可折叠评注列表时使用了同样的技术。该带输入控件的 Comments 框在 DIV 中进行声明,它的显示样式最初设置为 "none",因而它是不可见的。当单击该链接时,显示样式被更改,页面滚动到底部从而使它可见。我在页面的底部定义了一个单个的评注框(不是一条消息一个)以避免发送不必要的会使网页速度变慢的 HTML 代码。图 11 显示了评注框和所需的输入控件。

如何利用ASP.NET、JavaScript和OLE DB从头设计网络日记应用程序?

图 11 张贴评注

 

如何指定是为哪一条消息张贴评注?一个不错的解决方案是当单击 "Post your own comment" 链接时,将父消息的 ID 存储到一个隐藏的 ASP.NET 文本框中。随后,在单击 Post 按钮时,可以从 codebehind 中检索该值。请注意,不能使用 Visible 属性来隐藏该控件,因为当将 Visible 设置为 False 时,会隐藏该控件,并且根本不会将 HTML 代码发送给客户端。必须使用与 DIV 所用相同的显示样式。DIV 和文本框的声明如下:

<div id="CommentBox" style="DISPLAY: none">
<a name="CommentBoxAnchor"></a>
<asp:textbox id="ParentMessageID" style="DISPLAY: none"
runat="server" />

请注意,我还使用了一个定位点,用它来确保在页面很长并且用户想要评注的消息位于页面顶端时,评注框确实可见。该链接声明如下:

<a href='<%# "javascript:ShowCommentBox(" &
Container.DataItem("MessageID") & ");" %>'>Post your own comment</a>

ShowCommentBox JavaScript 例程将接受要评注的消息的 ID,并将它用作刚刚声明的隐藏文本框控件的值:

function ShowCommentBox(msgID)
{
document.forms[0].ParentMessageID.value = msgID;
ShowCommentBox2();
}

真正使评注框可见并且向下滚动页面的代码是一个独立的例程(ShowCommentBox2 过程)。当希望显示该评注框而不设置隐藏文本框控件的值属性时,我将再次调用该过程:

function ShowCommentBox2()
{
CommentBox.style.display = "";
window.location.href = '#CommentBoxAnchor';
}

剩下的所有工作就是处理 Post 按钮上的单击,以调用 Business.Blog 实例的 InsertComment 并再次将更新的数据绑定到 Repeater:

Private Sub PostComment_Click(...) Handles PostComment.Click
m_BlogManager.InsertComment(Integer.Parse(ParentMessageID.Text), _
Author.Text, Email.Text, Comment.Text)
BindData()
' reset the value of the input controls
ParentMessageID.Text = ""
' reset the other visible textboxes...
End Sub

管理网络日记

现在,实现用户任务的代码基本上全部完成。用户可以读取选择时间间隔的消息并张贴评注。另一方面,网络日记的拥有者必须直接在数据存储上添加、插入和删除消息与评注。为了访问该表,下一步的主要工作就是开发一个登录页并修改网络日记的主页,这样当管理员登录后,该页面将显示用于管理操作的其他控件。登录页由用户名称与密码的文本框、“persistent login”选项的复选框以及一个提交按钮组成。由于将来只有一个管理员并且不需要采用基于角色的安全策略,将凭据存储在 web.config 文件中就足够了。图 12 所示为代码隐藏类。如果指定的凭据有效,它将对用户进行身份验证并重新定向到 Default.aspx 页。

在 Default.aspx 页中,我将添加编辑控件,只有用户经过身份验证时该控件才可见。在该页的顶部,我声明了一个带 "logout" 链接的面板,该链接利用查询字符串中的 "action=logout" 参数指向 Login.aspx,同时还声明了一个带文本框的表以指定消息的标题和内容。该面板在 Page_Load 中显示或者隐藏,如下所示:

MessageBox.Visible = User.Identity.IsAuthenticated

当管理员填充文本框并单击 Post 按钮时,客户端就会发生 Click 事件,在其事件处理程序中,我用 InsertMessage 函数来向数据库添加新消息,并在 Repeater 中调用 BlindData 来加载它。

现在是该添加编辑功能的时候了。可以通过将 LinkButton 控件添加到 Repeater 的 ItemTemplate 来实现此目的:

<asp:LinkButton CausesValidation="False" runat="server" Text="Edit"
CommandName="Edit" CommandArgument='<%# Container.DataItem("MessageID")
%>'
Visible='<%# User.Identity.IsAuthenticated %>'/>

只有在用户经过身份验证后该链接才可见(它的工作方式和在 MessageBox 面板中的一样,但此处通过 ASPX 文件中的数据绑定表达式来显示或隐藏该链接)。CommandArgument 属性包含要编辑消息的 ID,但是还必须将 CommandName 指定为“Edit”,因为需要另一个 LinkButton 来删除消息,而且您想明确知道单击的是两个按钮中的哪一个。Repeater 的 ItemCommand 事件处理程序的代码如图 13 所示。

该代码首先检索消息的 ID,该 ID 作为单击按钮的 CommandArgument 属性进行传递。然后,管理员根据按钮的 CommandName 来决定是否删除或编辑该消息。当它等于 "Edit" 时,检索指定消息的当前数据,并用来填充 MessageBox 的文本框以便管理员会看到当前文本并可以对其进行编辑。当单击 Post 按钮时,如果 MessageID 文本框为空,则这意味着管理员正在发送新消息;否则,该文本框中将包含要编辑消息的 ID。以下是部分 Click 事件处理程序:

Private Sub PostMessage_Click(...) Handles PostMessage.Click
If Not User.Identity.IsAuthenticated Then _
Response.Redirect("Login.aspx", True)
If MessageID.Text.Trim().Length > 0 Then
m_BlogManager.UpdateMessage(Integer.Parse(MessageID.Text), _
Title.Text, Message.Text)
Else
m_BlogManager.InsertMessage(Title.Text, Message.Text)
End If
' reset the textboxes to an empty string, and call BindData...
End If
End Sub

Add New 和 Edit 功能已经全部实现(图 4 显示管理员模式下的页面外观)。还有一件值得做的事情就是添加 Delete 功能。按照当前的情况,如果管理员误点了 Delete 链接,由于不需要确认,消息就会被立即删除。增加弹出式确认对话框非常简单,只需要一些 JavaScript 来响应超级链接的 onClick 事件即可。这可以通过在创建链接(即,当创建 Repeater 的项目时)时向控件的属性集合添加条目来完成。我只需要为奇、偶项处理 Repeater 的 ItemCreated 事件,获得对 Delete LinkButton 的引用,并添加 JavaScript 弹出式确认对话框:

Private Sub Blog_ItemCreated(...) Handles Blog.ItemCreated
If e.Item.ItemType <> ListItemType.AlternatingItem AndAlso _
e.Item.ItemType <> ListItemType.Item Then Exit Sub
Dim lnkDelete As LinkButton = CType( _
e.Item.FindControl("DeleteMessage"), LinkButton)
lnkDelete.Attributes.Add("onclick", _
"return confirm('Are you sure you want to delete this" & _
"message?');")
End Sub

如果管理员单击 Cancel,则 JavaScript 返回“假”,不提交该页面并且不删除该消息。

编辑和删除评注的实现方法相同,因此我就不再详细介绍。但是,您可以在本文的下载代码中找到完整的实现。有几个细节值得在这里介绍一下。在每次必须处理 Repeater 的事件时,我都使用 Visual Basic®.NET Handle 关键字,该关键字使您能够将方法与由 WithEvents 声明的控件实例的事件相关联。评注的内部 DataList 不能这样做,因为它是在运行时动态创建的并且它没有 WithEvents 控件变量。但是,您可在控件声明中直接指定事件处理程序,如下所示:

<asp:DataList Runat="server" OnDeleteCommand="Comments_DeleteCommand"
OnItemCreated="Comments_ItemCreated"
OnEditCommand="Comments_EditCommand" ...>

其他的小细节是,当管理员单击某个评注的 Edit 链接时,必须显示评注框并将页面滚动到底部以确保其可见。之前我针对“Post your own comment”链接这样做过,但在那样的情况下,完成这样操作的 JavaScript 例序直接与链接相关联,不用和服务器来回联系。这里该页面首先会回发给预先用评注填充的编辑文本框,当将该页面再次发送给客户端浏览器时,将调用 JavaScript 例程。为此,我将一些 JavaScript 发送给刚好调用以前编写的 ShowCommentBox2 例程的客户端,如下所示:

Sub Comments_EditCommand(...)
' fill the textboxes with the current data for the clicked comment
•••
Dim script As String = _
"<script language=""JavaScript"">ShowCommentBox2();</script>"
Me.RegisterStartupScript("ShowEditCommentBox", script)
End Sub

在关闭页面的服务器端 <form> 标记之前,RegisterStartupScript 发出指定的 JavaScript 块,确保已经创建了 CommentBox (否则,在未找到 CommentBox 容器时会出现引用不正确的错误)。

验证多个虚拟窗体

当有文本框或者其他的输入控件时,通过添加验证程序控件来确保提供值并且值的格式和范围正确,这通常是个不错的主意。在本应用程序中,您必须根据用户想要采取的操作来实施不同的验证。如果用户按下 Post 按钮提交评注时,则必须确保他提供了其姓名和评注文本。如果单击了 Load Blog 按钮,则必须要检查起止日期的格式是否有效。不过我没有添加验证程序,因为还有另一个问题需要解决。

我有三个带有输入控件的“虚拟窗体”:评注框、新消息框,和时间间隔选择框。在 ASP.NET 页中,可以只有一个服务器端窗体。这意味着所有的输入控件、验证程序和提交按钮都位于同一窗体中。一旦用户正确填充了时间间隔文本框并单击了提交按钮,文本框验证程序就会验证该输入。评注框和消息框的验证程序将在其文本框没有值或者值的格式不正确的情况下阻止该窗体回发。

为了解决这个问题,我用一个由 James M. Venglarik 开发的第二版自定义控件替换了标准的 ASP.NET 按钮,该控件是他为 MSDN® Magazine 文章“Selectively Enable Form Validation When Using ASP.NET Web Controls”而开发的。该控件将创建使用某些客户端 JavaScript 来禁用指定的验证程序列表的按钮,从而有可能获得在提交页面之前验证某些输入控件而不验证其他控件的按钮。一旦页面引用了该控件,就会按照如下声明 Load Blog 按钮:

<nfvc:NoFormValButton ID="LoadBlog" Runat=Server Text="Load"
NoFormValList="RequireAuthor,RequireComment,ValidateEmailFormat" />

NoFormValList 属性指定单击它时禁用的由逗号隔开的验证程序列表,在此指的是评注框和消息框中的所有文本框验证程序。

小结

本文构建的应用程序至此就算是功能齐全了。您可以将它上载到自己的服务器上来编写网络日记,或者可以在 http://www.bytecommerce.com/blog 上在线查看。DataSet 的关联功能和创建嵌套 DataLists 和 DataGrids 的能力使得提供主从关系报告变得非常简单,而这正是创建该网络日记应用程序的目的。模板控件的灵活性意味着您几乎可以创建任何形式的布局。您可以对表中的多个数据项进行分组并自定义默认实现的行为,就像在给 Delete 按钮添加确认弹出框时所看到的那样。这些控件结合少许客户端 JavaScript(用来对这些控件的行为进行脚本编写并将它们结合在一起),造就了一个完整且功能丰富的 ASP.NET 报告应用程序。