發(fā)布日期: 1/13/2005 | 更新日期: 1/13/2005
Marco Bellinaso本文假設您熟悉 Visual Basic .NET 和 JavaScript
下載本文的代碼:
Blogging.exe (151KB)
摘要
多數情況下,ASP.NET 高級模板化控件(如 DataList 和 DataGrid)是用于數據表示的最佳選擇。但是,當需要靈活地進(jìn)行各種各樣的布局時(shí),Repeater 控件就是您所需要的。在本文中,作者將構建一個(gè)功能齊備的網(wǎng)絡(luò )日記應用程序,以舉例說(shuō)明使用 Repeater 和 DataList 控件來(lái)呈現主從關(guān)系中嵌套數據的方法。然后,作者將介紹如何通過(guò)添加一些使網(wǎng)絡(luò )日記反應更迅速且可用性更高的客戶(hù)端 JavaScript 代碼,來(lái)替代這些控件的默認實(shí)現。
如今,似乎每個(gè)人都需要網(wǎng)絡(luò )日記,我知道我自己就是這樣的。但是我找不到具有我想要的功能的預建 ASP.NET 網(wǎng)絡(luò )日記代碼,所以我構建了自己的代碼。在構建自己的網(wǎng)絡(luò )應用程序時(shí),最重要的一點(diǎn)是,要大量用到 ASP.NET 服務(wù)器控件,例如 Repeater、DataList 和 Calendar。網(wǎng)絡(luò )日記應用程序乍看上去似乎就是一個(gè)簡(jiǎn)單的練習,但是實(shí)際上,它要求您在一個(gè)典型的報告應用程序中實(shí)現很多需要的功能,如構建并呈現主從關(guān)系或編輯和刪除記錄,隱藏或顯示登錄用戶(hù)的內容和控件,以及管理在同一頁(yè)面上多個(gè)虛擬窗體的輸入驗證。本文將介紹網(wǎng)絡(luò )日記的設計和實(shí)現細節,并對可輕松應用到各種 ASP.NET 項目的技術(shù)進(jìn)行闡述,而暫且不考慮構建這些網(wǎng)絡(luò )日記的目的是出于業(yè)務(wù)需要還是為了娛樂(lè )。
在開(kāi)始編碼工作之前,您應該確定想要構建的網(wǎng)絡(luò )日記的類(lèi)型、它應具有的功能以及數據存儲的設計方式。有效的網(wǎng)絡(luò )日記包括許多功能。網(wǎng)絡(luò )日記的消息應按照從新到舊的順序進(jìn)行顯示。在同一天內可以張貼多條消息,這些消息應直觀(guān)地分組顯示于表格或框中,但是仍然可以按照張貼的時(shí)間順序對其進(jìn)行識別。同時(shí),用戶(hù)應當能夠為她希望閱讀的條目選擇時(shí)間間隔。這一點(diǎn)非常重要,因為您并不希望檢索用戶(hù)已經(jīng)看過(guò)的舊內容。
用戶(hù)應該能夠對任意一條消息進(jìn)行評注,并且張貼的評注應該能夠直接在其父消息之下進(jìn)行顯示,從而條理清晰。此外,網(wǎng)絡(luò )日記的所有者應該能夠張貼、編輯并刪除消息和評注,而用戶(hù)應該只能閱讀消息和張貼評注。要根據用戶(hù)身份來(lái)決定允許或禁止其進(jìn)行張貼或編輯操作,需要顯示或隱藏某些控件,并且還需要進(jìn)行某種形式的身份驗證。
數據庫設計
接下來(lái),必須您必須確定消息和評注的存儲方式。在本項目中,我使用了 SQL Server™ 數據庫,但我的示例代碼還包含一個(gè) Microsoft® Access 數據庫,以防您選擇這種數據儲存方式。該數據庫只包含兩個(gè)表:一個(gè)是消息表,另一個(gè)是評注表。消息表儲存唯一的 ID、可選標題或消息摘要、消息文本以及張貼消息的日期和時(shí)間。評注表儲存唯一的 ID、評注所對應消息的 ID、作者的名稱(chēng)、作者的電子郵件地址、評注文本以及張貼消息的日期和時(shí)間。圖 1 顯示這兩個(gè)表的設計。
圖 1 父表/子表
您可以看到,兩表之間是一對多的關(guān)系,并通過(guò) MessageID 字段進(jìn)行鏈接,這樣,通過(guò)級聯(lián)更新和刪除也加強了引用完整性。請注意,兩表的名稱(chēng)都以前綴“Blog_”開(kāi)頭。我一直在自己的 SQL Server 表中使用前綴,這是因為當將這些表按字母順序列出時(shí),它們在 Enterprise Manager 中將分組在一起。此外,Web 宿主計劃通常只提供一個(gè) SQL Server 數據庫,您必須在使用的所有 Web 模塊間共享該數據庫。如果不使用表前綴,而且某個(gè)模塊可能已經(jīng)使用一個(gè)名為 Message 的表,那么在部署時(shí)無(wú)法立即解決此種名稱(chēng)沖突問(wèn)題。
另一個(gè)要切記的重要細節是,任何字段都不能是空值,即使用戶(hù)將該字段保留為空白。為了防止有未處理的異常出現,您應該將空字段設置為空字符串,而不是允許出現空值。如果使用 Web 窗體進(jìn)行數據輸入,則不會(huì )有任何問(wèn)題,這是因為如果沒(méi)有內容輸入,控件將返回一個(gè)空字符串。
業(yè)務(wù)層
在典型的以數據庫為中心的應用程序中,具有數據層、業(yè)務(wù)層和表示層。數據層可能由一組存儲過(guò)程組成,而業(yè)務(wù)層由一組類(lèi)(在其各自的單獨程序集中進(jìn)行選擇性地編譯)組成,這些類(lèi)包裝存儲過(guò)程以確保數據的完整性、執行驗證以及對其他的業(yè)務(wù)規則進(jìn)行強制。但是,在本項目中,我決定不使用存儲過(guò)程;我直接將 SQL 查詢(xún)和命令硬編碼到主程序集中,以便能更容易地通過(guò) Access 來(lái)使用網(wǎng)絡(luò )日記。我也沒(méi)有將數據層和業(yè)務(wù)層使用的類(lèi)進(jìn)行分離,但是,由于只有兩個(gè)表和一些簡(jiǎn)單的業(yè)務(wù)規則,因此我創(chuàng )建了一個(gè)負責驗證輸入并執行正確的 SQL 語(yǔ)句的單個(gè)的類(lèi)。所有代碼以及表示層都將位于同一個(gè)程序集內,因為我不希望用使用業(yè)務(wù)類(lèi)的程序來(lái)更新該類(lèi),也不希望用其他類(lèi)型的應用程序或者在多個(gè)客戶(hù)端之間分布該業(yè)務(wù)類(lèi)。因此,利用主 ASP.NET 應用程序(稱(chēng)為 WebLogger)對其進(jìn)行編譯即可。該業(yè)務(wù)類(lèi)被命名為 Blog,位于名稱(chēng)為“Business”的命名空間下,所以它不會(huì )與代碼隱藏類(lèi)發(fā)生沖突。以下就是添加一條新消息的方法的實(shí)現方式:
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 = titlecmd.Parameters.Add("Message", OleDbType.LongVarChar).Value = _message.Replace(m_brChar, "<br>")ExecuteCommand(cmd)End Sub
該方法接受添加新記錄所需要的所有值。不需要在消息 ID 中傳值,因為它在數據庫表中是自動(dòng)進(jìn)行遞增的,并且 AddedDate 將當前日期作為默認值。該方法定義一個(gè) SQL INSERT 命令,并用 ExecuteCommand Helper 方法執行該命令。請注意,Message 參數的值就是作為輸入進(jìn)行傳遞的消息文本,但是,換行符將被替換為 HTML 的
標記。網(wǎng)絡(luò )管理員可以在進(jìn)行張貼時(shí)使用 HTML 格式,但是,這是因為經(jīng)常會(huì )出現新行,所以張貼的內容將自動(dòng)轉換為 HTML 格式,從而無(wú)需進(jìn)行鍵入操作。您可能已經(jīng)猜測到了,ExecuteCommand Helper 方法只執行作為輸入而接受的命令對象:
Private Sub ExecuteCommand(ByVal cmd As OleDbCommand)cmd.Connection = m_ConnectionTrym_Connection.Open()cmd.ExecuteNonQuery()Finallym_Connection.Close()End TryEnd Sub
ExecuteCommand 方法設置命令的連接;通過(guò)從 web.config 的 appSettings 部分定義的自定義項中檢索連接字符串,在類(lèi)的構造函數方法中可創(chuàng )建該連接;然后打開(kāi)這個(gè)連接,在 Try 塊中執行該命令,并在對應的 Finally 塊中關(guān)閉該連接。即使 ExecuteNonQuery 方法引發(fā)異常,該連接也將關(guān)閉??墒菦](méi)有 Catch 塊,因為我不必執行任何特定的業(yè)務(wù)操作,如返回交易或記錄一個(gè)錯誤。例如,我只想直接在調用頁(yè)中捕獲并處理這樣的異常以顯示一個(gè)用戶(hù)友好的錯誤消息。因為其他的 Insertxxx、Updatexxx 和 Deletexxx 方法操作類(lèi)似,所以我將不逐一介紹。但是,研究一下 InsertComment 方法還是很有價(jià)值的,因為除了在 Blog_Comments 表中添加新的記錄外,它還發(fā)送一封通知電子郵件,該郵件包含評注文本和一些涉及網(wǎng)絡(luò )日記所有者的消息。
有了這些功能,網(wǎng)絡(luò )日記所有者不必頻繁地加載網(wǎng)絡(luò )日記以及檢查新的電子郵件。當然,如果該網(wǎng)絡(luò )日記非常受歡迎,并獲得很多評注,您就有可能收到過(guò)多的電子郵件。因此,應用程序的 web.config 應該具備一個(gè)自定義項,它使管理員能夠決定她是否想要有關(guān)任何新評注的電子郵件通知。然后,需要另一個(gè)自定義項來(lái)儲存電子郵件的目標地址。這兩項設置分別命名為 Blog_SendNotifications 和 Blog_AdminEmail,它們在 web.config 的 appSettings 部分(與連接字符串的自定義項一起)內的聲明如下:
<add key="Blog_SendNotifications" value="1" /><add key="Blog_AdminEmail" value="mbellinaso@vb2themax.com" />
Blog_SendNotifications 的值為 1 表示通知功能被激活,其他值或該項空則表示該功能沒(méi)有被激活。
插入新評注并檢查評注通知是否打開(kāi)之后,您必須構建電子郵件的主體。除了評注文本之外,您還希望包含張貼的父消息的標題、日期和時(shí)間,這樣在原始消息和評注間就有了明確的連接。通過(guò)執行
圖 2 中的代碼來(lái)檢索父消息的數據,即利用一條命令從正在討論的單條消息記錄中檢索適當字段。
一旦具備了所有必需的數據,您就可以構建并發(fā)送電子郵件消息了。通過(guò)調用 String.Format 方法來(lái)構建電子郵件文本,該方法包括一個(gè)帶數字占位符的模板字符串(形式是 {n}),并將模板中的各種占位符用值代替:
‘ build the msg‘s contentDim 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 mailSystem.Web.Mail.SmtpMail.Send("WebLogger", _ConfigurationSettings.AppSettings("Blog_AdminEmail"), _"New comment notification", msg)End If
業(yè)務(wù)類(lèi)的最重要的組成元素是 GetData 方法,該方法對指定時(shí)間間隔內張貼的消息和各自的評注進(jìn)行檢索。這里您必須選擇是使用 DataReader 還是使用 DataAdapter 以及 DataSet 來(lái)檢索和讀取數據。在 Web 應用程序中,DataReader 通常是最好的選擇,特別是不需要進(jìn)行本地編輯和緩存數據副本時(shí)。但是,在這種特定情況下,我選擇 DataSet 是因為它允許存儲多個(gè)表并在這些表之間創(chuàng )建父子關(guān)系。這樣一次就可以輕松地檢索所有的父記錄和子記錄,并且無(wú)需執行單獨的查詢(xún)就可從父記錄定位到它們的子數據,而如果我使用 DataReader 方法則需要執行單獨的查詢(xún)。這種方法對于程序員來(lái)說(shuō)更加簡(jiǎn)單,并節省了數據庫資源和網(wǎng)絡(luò )通信量,這是因為發(fā)送到數據庫的 SQL 語(yǔ)句的數量更少了。
能夠在表間創(chuàng )建關(guān)聯(lián)是另一個(gè)絕妙的功能,因為您可以向表中添加計算列,這些列利用關(guān)聯(lián)中對其他表的引用計算表達式的值。我通過(guò) MessageID 字段在 Blog_Messages 和 Blog_Comments 表間創(chuàng )建了關(guān)聯(lián),正如我在圖 1 中為物理數據庫創(chuàng )建的關(guān)聯(lián)一樣。我希望其數據是來(lái)自 Blog_Messages 的 DataTable 中有一個(gè)計算列,該列返回對應消息記錄的子評注的數量。如果我使用 DataReader,這將需要單獨的查詢(xún)或至少一個(gè)子查詢(xún)(這在 Access 中是不可能的)。對于 DataSet,可以利用無(wú)連接的數據副本來(lái)實(shí)現計數子記錄的數量,而無(wú)需要求數據庫來(lái)完成。
我們來(lái)看看該方法是如何工作的。它將兩個(gè)日期作為輸入,利用 DataAdapter 來(lái)檢索在該時(shí)間間隔中張貼的消息,然后把它們儲存在名稱(chēng)為 Messages 的 DataSet 表中。如下所示:
Public Function GetData(ByVal fromDate As Date, ByVal toDate As Date) _As DataSetDim ds As New DataSet()intervalDim 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 = _fromDateda.SelectCommand.Parameters.Add("ToDate", OleDbType.Date).Value = _toDate.AddDays(1)m_Connection.Open()da.Fill(ds, "Messages")•••
請注意,必須將終止日期增加一天,以便將傳入終止日期那天張貼的消息包括在 resultset 中。如果您只想獲得返回消息的評注,那么可以在 SELECT 語(yǔ)句中使用 IN 篩選器,該篩選器包含由逗號分隔的記錄的所有消息 ID 列表,這些記錄由我剛剛列示的查詢(xún) 1 返回(參見(jiàn)
圖 3)。
然后,在兩表間創(chuàng )建剛剛介紹過(guò)的關(guān)聯(lián):
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) 返回指定關(guān)聯(lián)的所有子數據行,Count 函數與其在 SQL 中的工作方式相同。
要注意的最后一個(gè)細節是,在返回填充的 DataSet 并終止該函數之前,如果 Comments 表是空的,則該計算列將表達式計算為 NULL(而不是 0),而以后在 ASP.NET 頁(yè)中顯示該值或在其他表達式中使用該值時(shí),將會(huì )出現問(wèn)題。要解決該問(wèn)題,可以添加不涉及任何消息的假評注:
If ds.Tables("Comments").Rows.Count = 0 ThenDim dr As DataRow = ds.Tables("Comments").NewRow()dr("CommentID") = -1dr("Author") = "none"dr("Email") = "none"dr("Comment") = "none"dr("AddedDate") = Date.Todayds.Tables("Comments").Rows.Add(dr)dr.AcceptChanges()End IfReturn dsEnd Function
這樣,如果消息沒(méi)有任何子評注,則表達式計算為 0。
顯示消息和子評注
現在業(yè)務(wù)層已經(jīng)完成了,下一步就是編寫(xiě)表示層,在該層上可以很好地發(fā)揮 ASP.NET 的功能。大多數工作是在單個(gè)頁(yè)(即 Default.aspx)上進(jìn)行的。該頁(yè)將顯示消息和評注,并允許經(jīng)過(guò)身份驗證的管理員對評注進(jìn)行適度調整并編輯她自己的消息(參見(jiàn)圖 4 中的底部窗格)。
圖 4 管理員模式下的網(wǎng)絡(luò )日記
Default.aspx 顯示了一個(gè)按日期分組的消息列表,每條消息可以沒(méi)有評注或者有多條評注。最初,評注是隱藏的,但當用戶(hù)單擊 View 鏈接時(shí),評注的內部列表就會(huì )動(dòng)態(tài)地展開(kāi)或者折疊。多條消息可以同時(shí)展開(kāi)它們的評注列表,就像一個(gè)只有單級子節點(diǎn)的 TreeView 控件。在開(kāi)發(fā)該頁(yè)時(shí),會(huì )面臨一些挑戰,包括如何根據日期排序消息、如何在單獨的 HTML 表中對其進(jìn)行分組以及如何顯示或隱藏子記錄的內部列表。
讓我們從第一個(gè)問(wèn)題開(kāi)始。假設您希望在該頁(yè)面中顯示 5 條消息,前三條消息在同一天張貼,而其他兩條在另一天張貼。同時(shí),假設您希望將每一條消息最初都隱藏在自己的 HTML 表中。如果您希望在兩個(gè)單獨表格中將前三條消息分為一組,而將剩余的兩條分為一組,則您應當從該表中間的消息(不是當天第一條或最后一條消息)中刪除表打開(kāi)和關(guān)閉標記,從第一個(gè)消息的表中刪除關(guān)閉標記,從最后一條消息的表中刪除打開(kāi)標記。因為我想使用一個(gè)模板化數據綁定的控件來(lái)表示該視圖,并且由于必須要完全控制所有生成的 HTML,因此我選擇使用 Repeater 控件。除了您在 Repeater 的模板中指定,該控件沒(méi)有預定義任何輸出或者布局。
圖 5 中的代碼是 Repeater 模板的部分定義。
HeaderTemplate 包括表的打開(kāi)標記,而 FooterTemplate 用于關(guān)閉該表。打開(kāi)和關(guān)閉各種按日期分組消息的表的標記的定義,位于 ItemTemplate 部分內的 Literal 控件中。通過(guò)切換 Literal 控件的可視性,您可以決定是否輸出這些標記,從而決定何時(shí)關(guān)閉當前的表并打開(kāi)一個(gè)新表。問(wèn)題在于,何時(shí)隱藏 Literal 以便保持當前表為打開(kāi)狀態(tài)并且對消息進(jìn)行分組,以及何時(shí)顯示 Literal 從而關(guān)閉該表并打開(kāi)一個(gè)新表。
答案很簡(jiǎn)單:只要正在處理中的消息的日期與前一條消息的日期相同,就將這些消息分為一組。在日期更改時(shí),會(huì )顯示 Literal 控件并啟動(dòng)一個(gè)新組。另一個(gè)問(wèn)題是,應該在何時(shí)、何地顯示控件的內容。當在 Repeater的 ItemDataBound 事件中處理數據項(網(wǎng)絡(luò )日記的消息記錄)并將其綁定到 Repeater 的模板中時(shí),必須對此做出決定。這里您可以讀取當前數據項的所有值,并將這些值與前一條消息的數據進(jìn)行比較,這些數據已存儲在一個(gè)靜態(tài)變量中以便在方法調用間保留該值。獲得對模板的 Literal 控件的引用后,就可以設置其相應的可視性了。代碼如下:
Private Sub Blog_ItemDataBound(...) Handles Blog.ItemDataBoundStatic prevDayDate As DateIf e.Item.ItemType <> ListItemType.Item AndAlso _e.Item.ItemType <> ListItemType.AlternatingItem Then ReturnDim dayDate As Date = e.Item.DataItem("AddedDate")Dim isNewDay As Boolean = (dayDate.ToShortDateString() <> _prevDayDate.ToShortDateString())prevDayDate = dayDateCType(e.Item.FindControl("DayTitle"), Panel).Visible = isNewDayCType(e.Item.FindControl("DayBox"), Literal).Visible = ( _isNewDay AndAlso e.Item.ItemIndex > 0)End Sub
如您所見(jiàn),我為 Panel 控件設置了 Visible 屬性(在同一個(gè)模板中也有聲明),該屬性顯示當前表的日期。如果當前消息的日期和前一條消息的日期不同,則顯示該面板;或者如果是綁定的第一條消息,則顯示默認的日期。將 Literal 的可視性設置為 True 要受另一個(gè)條件約束:被綁定的消息必須不是第一條消息,因為在此種情況下,利用在 Repeater 的 HeaderTemplate 部分中聲明的標記可以打開(kāi)當天的表。要動(dòng)態(tài)地折疊和展開(kāi)評注列表而無(wú)需回發(fā)給服務(wù)器并且不重新處理頁(yè)面,請將評注放置到標記內,該標記的顯示樣式可以設置為“none”,或者設置為一個(gè)空字符串來(lái)分別隱藏或顯示它。將 DIV 聲明如下,根據綁定的數據項給其分配一個(gè) ID:
<div style="display:‘none‘; margin-left:2.0em; margin-top:.8em; "ID=‘<%# "div" & Container.DataItem("MessageID") %>‘><!-- put here the Comments DataList... --></div>
我這樣進(jìn)行分配,以便對于每個(gè) DIV 都有唯一的 ID(切記每條消息都有一個(gè))。為了展開(kāi)或者折疊 DIV,我使用了超級鏈接,該鏈接調用以 DIV 的 ID 作為輸入的自定義 JavaScript 代碼:
<asp:HyperLink Runat="server"Visible=‘<%# Container.DataItem("CommentsCount") > 0 %>‘NavigateUrl=‘<%# "javascript:ToggleDivState(div" & _Container.DataItem("MessageID") & ");" %>‘>View</asp:HyperLink>
同時(shí)還要注意,Visible 屬性與一個(gè)表達式綁定,只有在消息有評注顯示(如果其 CommentsCount 計算列的值大于 0)時(shí)該表達式才返回 True。ToggleDivState 只是將 DIV 的顯示樣式值取反,從而使其可見(jiàn)或隱藏:
ffunction ToggleDivState(ctrl){div = eval(ctrl);if (div.style.display == "none")div.style.display = "";elsediv.style.display = "none";}
現在我們來(lái)看一下評注功能。這次由 DataList 來(lái)完成這項工作,因為它的表布局正是我所需的。一般情況下,模板控件或者任何其他數據綁定列表控件的 DataSource 屬性都可以通過(guò)代碼隱藏(或者服務(wù)器端腳本)以編程方式進(jìn)行指派。但是,在這種情況下我沒(méi)有直接引用 DataList,因為它是由父 Repeater 動(dòng)態(tài)創(chuàng )建的。雖然和大多數屬性一樣,它可以有一個(gè)在運行時(shí)設置其值的數據綁定表達式。如果您使用 DataReader 方法來(lái)檢索數據,可以將 DataSource 屬性綁定到一個(gè)自定義方法,該方法接受消息的 ID 并返回 DataTable、DataReader 或者任何其他實(shí)現 IEnumerable 接口的數據類(lèi)型的子評注。在這里無(wú)需這樣做,因為您需要的所有數據都已存儲在包含消息的同一 DataSet 中。由于已經(jīng)在消息表和評注表之間創(chuàng )建了關(guān)聯(lián),所以您就可以輕松地利用當前數據項的 DataRow 的 GetChildRows 方法來(lái)檢索子評注數組。表達式聲明如下:
<asp:DataList Runat="server" DataSource=‘<%# Container.DataItem.Row.GetChildRows("MsgComments") %>‘>
利用綁定表達式完成 DataList 的 ItemTemplate,以顯示作者名稱(chēng)和電子郵件地址、消息文本、消息日期以及完整輸出該網(wǎng)絡(luò )日記內容的代碼。
圖 6 顯示了輸出模塊的完成代碼。
選擇時(shí)間間隔并加載網(wǎng)絡(luò )日記
至此我已經(jīng)介紹了 ASPX 文件中頁(yè)面內容的定義,但是還沒(méi)有介紹實(shí)際加載網(wǎng)絡(luò )日記內容的代碼。為了讓用戶(hù)選擇一個(gè)時(shí)間間隔,我使用了 Calendar 控件,將它的 SelectionMode 屬性設置為 DayWeekMonth,這樣用戶(hù)就可以選擇一天、一周或者整月。例如,如果用戶(hù)想選擇最后兩周或者最后的 45 天,提供文本框讓用戶(hù)來(lái)填寫(xiě)希望的起始和終止日期是個(gè)不錯的主意。圖 7 顯示了向頁(yè)面添加的新控件,在 Calendar 中選定了整周。
圖 7 時(shí)間間隔
由于回發(fā)而沒(méi)有加載該頁(yè)面時(shí),必須選擇一個(gè)默認的時(shí)間間隔,例如上一周。但是,對于頻繁更新的網(wǎng)絡(luò )日記,最好只加載少數幾天的數據,對于很少更新的網(wǎng)絡(luò )日記,最好加載上個(gè)月整月的數據。至于評注通知功能,最好留給網(wǎng)絡(luò )日記管理員用 web.congfig 文件中的自定義項進(jìn)行選擇,該項允許管理員指定默認的時(shí)間間隔天數。下面的代碼說(shuō)明了如何從文件中讀取該自定義項、如何將其分析為整數、如何用它來(lái)計算最近 n 天的時(shí)間間隔以及如何在日歷中突出顯示該時(shí)間間隔:
Private Sub Page_Load(...) Handles MyBase.LoadIf Not IsPostBack ThenDim 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 IfEnd Sub
通過(guò)從今天的日期中減去 n-1 天,可以計算出起始日期。調用 BindData 可以加載選定時(shí)間間隔的數據,并將該數據綁定到 Repeater 及其內部控件。該方法調用以前開(kāi)發(fā)的網(wǎng)絡(luò )日記業(yè)務(wù)類(lèi)的 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").DefaultViewBlog.DataBind()End Sub
如果選擇一天,SelectedDates 將只有一個(gè)條目,而且起始和終止日期相同。當用戶(hù)單擊日歷時(shí),該頁(yè)面被回發(fā),并自動(dòng)選擇新的時(shí)間間隔,處理日歷的 SelectionChanged 事件從而為新的時(shí)間間隔再次調用 BindDatahe。最后,您必須處理 Load 按鈕的 Click 事件以在日歷中選擇指定的自定義時(shí)間間隔并加載網(wǎng)絡(luò )日記數據。在該事件過(guò)程中,我對兩個(gè)輸入控件的內容進(jìn)行了分析并獲得了兩個(gè)日期。雖然我最終會(huì )添加驗證程序來(lái)確保在提交窗體前數據格式是正確的,但如果數據格式無(wú)效,這種分析仍會(huì )引發(fā)異常。在這種情況下,我采用了今天的日期(參見(jiàn)
圖 8)。
請注意,使用日歷的 VisibleDate 來(lái)確保終止日期在日歷中可見(jiàn)。這是必要的,因為如果用戶(hù)選擇過(guò)去的兩個(gè)月,則這種選擇在日歷中將無(wú)法顯示。日歷將顯示當前月,多數人對這并不十分清楚。
彈出式日歷
按照當前的情況,如果用戶(hù)想選擇的時(shí)間間隔不是一天、一周或者一月,那么他們就必須在兩個(gè)文本框中手工輸入起始和終止日期,并引用一個(gè)外部日歷。另外,他們輸入的日期格式也可能無(wú)效。由于這些原因,提供一個(gè)彈出式日歷是一個(gè)不錯的做法。當單擊某個(gè)日期時(shí),該日歷應該關(guān)閉并且日期應該會(huì )出現在主窗口的文本框控件中。使用 Calendar 控件和幾個(gè)客戶(hù)端 JavaScript,就可在 ASP.NET 中輕松地重現該功能。
我們首先關(guān)注父窗口的 ASPX 代碼。通過(guò)調用下面的 JavaScript 函數,我添加了一個(gè)打開(kāi)彈出式窗口的圖像鏈接:
<a href="javascript:PopupPicker(‘IntervalFrom‘, 200, 200);" ><img src="images/calendar.gif" border="0" ></a>
JavaScript 過(guò)程希望接收文本框控件的名稱(chēng)(該文本框由選定的日期來(lái)填充)以及要打開(kāi)的彈出式日歷窗口的寬度和高度。下面是 JavaScript 代碼,這些代碼位于已在該頁(yè)面頂部定義的 <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();}
該過(guò)程利用 window.open 打開(kāi)一個(gè)指定大小的且不能更改的彈出式窗口,該窗口沒(méi)有滾動(dòng)條、菜單、工具欄或狀態(tài)欄。第一個(gè)參數是要加載到新窗口中的頁(yè)的 URL,在剛才顯示的代碼中,它利用查詢(xún)字符串中的 ctl 參數來(lái)加載 DatePicker.aspx 頁(yè),該參數的值作為輸入傳入 PopupPicker 過(guò)程。
現在完成了主頁(yè),我必需編寫(xiě)呈現日歷的 DatePicker.aspx 頁(yè)。該頁(yè)有一個(gè) Calendar 控件,其 Width 和 Height 屬性設置為 100%,這樣它可以覆蓋整個(gè)頁(yè)。ASPX 文件中需要注意的其他重要事項是,客戶(hù)端 JavaScript 過(guò)程將字符串作為輸入并將它用作父窗體的輸入控件的值,該控件的名稱(chēng)用查詢(xún)字符串進(jìn)行傳遞。最后,它關(guān)閉彈出式窗體自身。JavaScript 代碼如
圖 9 所示。
當用戶(hù)單擊 Calendar 控件中的鏈接時(shí),我希望調用自定義的 JavaScript 過(guò)程,而不是正常的處理方法,即提交窗體并選擇單擊的日期。默認情況下,所有 Calendar 控件提供的鏈接都會(huì )向服務(wù)器產(chǎn)生一個(gè)回發(fā)。而我所希望的是讓它們指向自定義的 SetDate 過(guò)程。由于 Calendar 的 DayRender 事件,使得更改包含日期鏈接的表單元格的默認輸出非常簡(jiǎn)單,該事件在每次呈現日期時(shí)發(fā)生并提供對要創(chuàng )建的表單元格的引用。下面的代碼片段利用我自己的超級鏈接控件來(lái)替換默認的單元格內容,它們具有相同的文本,只是我的控件指向了 JavaScript 過(guò)程:
Private Sub DatePicker_DayRender(...) Handles DatePicker.DayRenderDim hl As New HyperLink()hl.Text = CType(e.Cell.Controls(0), LiteralControl).Texthl.NavigateUrl = "javascript:SetDate(‘" & _e.Day.Date.ToShortDateString() & "‘);"e.Cell.Controls.Clear()e.Cell.Controls.Add(hl)End Sub
傳遞給 JavaScript 過(guò)程的值是以短格式(一般是 mm/dd/yy)表示的單擊日的日期。該值將用于父窗體上的輸入控件。圖 10 所示為彈出的窗口。
圖 10 彈出式日歷
正如您所見(jiàn),可以對 ASP.NET 服務(wù)器控件進(jìn)行極其靈活的自定義。希望以這種方式使用 DayRender 事件的另一種情況是,您需要重新定向到另一個(gè)網(wǎng)頁(yè)并以查詢(xún)字符串傳遞日期時(shí),而不是在該日期回發(fā)后從服務(wù)器重新定向到第二個(gè)網(wǎng)頁(yè)。為此,只要用類(lèi)似下面的代碼來(lái)替換設置超級鏈接的 NavigateUrl 屬性的那一行代碼即可:
hl.NavigateUrl = "SecondPage.aspx?Date=" & e.Day.Date.ToShortDateString()
張貼評注
在圖 4 中,您可以看到每條消息的下面都有一個(gè) "Post your own comment" 的鏈接。當用戶(hù)單擊它時(shí),就會(huì )出現一個(gè)帶輸入控件的框用于張貼評注。您可以猜出它是如何工作的,因為我在構建可折疊評注列表時(shí)使用了同樣的技術(shù)。該帶輸入控件的 Comments 框在 DIV 中進(jìn)行聲明,它的顯示樣式最初設置為 "none",因而它是不可見(jiàn)的。當單擊該鏈接時(shí),顯示樣式被更改,頁(yè)面滾動(dòng)到底部從而使它可見(jiàn)。我在頁(yè)面的底部定義了一個(gè)單個(gè)的評注框(不是一條消息一個(gè))以避免發(fā)送不必要的會(huì )使網(wǎng)頁(yè)速度變慢的 HTML 代碼。圖 11 顯示了評注框和所需的輸入控件。
圖 11 張貼評注
如何指定是為哪一條消息張貼評注?一個(gè)不錯的解決方案是當單擊 "Post your own comment" 鏈接時(shí),將父消息的 ID 存儲到一個(gè)隱藏的 ASP.NET 文本框中。隨后,在單擊 Post 按鈕時(shí),可以從 codebehind 中檢索該值。請注意,不能使用 Visible 屬性來(lái)隱藏該控件,因為當將 Visible 設置為 False 時(shí),會(huì )隱藏該控件,并且根本不會(huì )將 HTML 代碼發(fā)送給客戶(hù)端。必須使用與 DIV 所用相同的顯示樣式。DIV 和文本框的聲明如下:
<div id="CommentBox" style="DISPLAY: none"><a name="CommentBoxAnchor"></a><asp:textbox id="ParentMessageID" style="DISPLAY: none"runat="server" />
請注意,我還使用了一個(gè)定位點(diǎn),用它來(lái)確保在頁(yè)面很長(cháng)并且用戶(hù)想要評注的消息位于頁(yè)面頂端時(shí),評注框確實(shí)可見(jiàn)。該鏈接聲明如下:
<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();}
真正使評注框可見(jiàn)并且向下滾動(dòng)頁(yè)面的代碼是一個(gè)獨立的例程(ShowCommentBox2 過(guò)程)。當希望顯示該評注框而不設置隱藏文本框控件的值屬性時(shí),我將再次調用該過(guò)程:
function ShowCommentBox2(){CommentBox.style.display = "";window.location.href = ‘#CommentBoxAnchor‘;}
剩下的所有工作就是處理 Post 按鈕上的單擊,以調用 Business.Blog 實(shí)例的 InsertComment 并再次將更新的數據綁定到 Repeater:
Private Sub PostComment_Click(...) Handles PostComment.Clickm_BlogManager.InsertComment(Integer.Parse(ParentMessageID.Text), _Author.Text, Email.Text, Comment.Text)BindData()‘ reset the value of the input controlsParentMessageID.Text = ""‘ reset the other visible textboxes...End Sub
管理網(wǎng)絡(luò )日記
現在,實(shí)現用戶(hù)任務(wù)的代碼基本上全部完成。用戶(hù)可以讀取選擇時(shí)間間隔的消息并張貼評注。另一方面,網(wǎng)絡(luò )日記的擁有者必須直接在數據存儲上添加、插入和刪除消息與評注。為了訪(fǎng)問(wèn)該表,下一步的主要工作就是開(kāi)發(fā)一個(gè)登錄頁(yè)并修改網(wǎng)絡(luò )日記的主頁(yè),這樣當管理員登錄后,該頁(yè)面將顯示用于管理操作的其他控件。登錄頁(yè)由用戶(hù)名稱(chēng)與密碼的文本框、“persistent login”選項的復選框以及一個(gè)提交按鈕組成。由于將來(lái)只有一個(gè)管理員并且不需要采用基于角色的安全策略,將憑據存儲在 web.config 文件中就足夠了。
圖 12 所示為代碼隱藏類(lèi)。如果指定的憑據有效,它將對用戶(hù)進(jìn)行身份驗證并重新定向到 Default.aspx 頁(yè)。
在 Default.aspx 頁(yè)中,我將添加編輯控件,只有用戶(hù)經(jīng)過(guò)身份驗證時(shí)該控件才可見(jiàn)。在該頁(yè)的頂部,我聲明了一個(gè)帶 "logout" 鏈接的面板,該鏈接利用查詢(xún)字符串中的 "action=logout" 參數指向 Login.aspx,同時(shí)還聲明了一個(gè)帶文本框的表以指定消息的標題和內容。該面板在 Page_Load 中顯示或者隱藏,如下所示:
MessageBox.Visible = User.Identity.IsAuthenticated
當管理員填充文本框并單擊 Post 按鈕時(shí),客戶(hù)端就會(huì )發(fā)生 Click 事件,在其事件處理程序中,我用 InsertMessage 函數來(lái)向數據庫添加新消息,并在 Repeater 中調用 BlindData 來(lái)加載它。
現在是該添加編輯功能的時(shí)候了??梢酝ㄟ^(guò)將 LinkButton 控件添加到 Repeater 的 ItemTemplate 來(lái)實(shí)現此目的:
<asp:LinkButton CausesValidation="False" runat="server" Text="Edit"CommandName="Edit" CommandArgument=‘<%# Container.DataItem("MessageID")%>‘Visible=‘<%# User.Identity.IsAuthenticated %>‘/>
只有在用戶(hù)經(jīng)過(guò)身份驗證后該鏈接才可見(jiàn)(它的工作方式和在 MessageBox 面板中的一樣,但此處通過(guò) ASPX 文件中的數據綁定表達式來(lái)顯示或隱藏該鏈接)。CommandArgument 屬性包含要編輯消息的 ID,但是還必須將 CommandName 指定為“Edit”,因為需要另一個(gè) LinkButton 來(lái)刪除消息,而且您想明確知道單擊的是兩個(gè)按鈕中的哪一個(gè)。Repeater 的 ItemCommand 事件處理程序的代碼如
圖 13 所示。
該代碼首先檢索消息的 ID,該 ID 作為單擊按鈕的 CommandArgument 屬性進(jìn)行傳遞。然后,管理員根據按鈕的 CommandName 來(lái)決定是否刪除或編輯該消息。當它等于 "Edit" 時(shí),檢索指定消息的當前數據,并用來(lái)填充 MessageBox 的文本框以便管理員會(huì )看到當前文本并可以對其進(jìn)行編輯。當單擊 Post 按鈕時(shí),如果 MessageID 文本框為空,則這意味著(zhù)管理員正在發(fā)送新消息;否則,該文本框中將包含要編輯消息的 ID。以下是部分 Click 事件處理程序:
Private Sub PostMessage_Click(...) Handles PostMessage.ClickIf Not User.Identity.IsAuthenticated Then _Response.Redirect("Login.aspx", True)If MessageID.Text.Trim().Length > 0 Thenm_BlogManager.UpdateMessage(Integer.Parse(MessageID.Text), _Title.Text, Message.Text)Elsem_BlogManager.InsertMessage(Title.Text, Message.Text)End If‘ reset the textboxes to an empty string, and call BindData...End IfEnd Sub
Add New 和 Edit 功能已經(jīng)全部實(shí)現(圖 4 顯示管理員模式下的頁(yè)面外觀(guān))。還有一件值得做的事情就是添加 Delete 功能。按照當前的情況,如果管理員誤點(diǎn)了 Delete 鏈接,由于不需要確認,消息就會(huì )被立即刪除。增加彈出式確認對話(huà)框非常簡(jiǎn)單,只需要一些 JavaScript 來(lái)響應超級鏈接的 onClick 事件即可。這可以通過(guò)在創(chuàng )建鏈接(即,當創(chuàng )建 Repeater 的項目時(shí))時(shí)向控件的屬性集合添加條目來(lái)完成。我只需要為奇、偶項處理 Repeater 的 ItemCreated 事件,獲得對 Delete LinkButton 的引用,并添加 JavaScript 彈出式確認對話(huà)框:
Private Sub Blog_ItemCreated(...) Handles Blog.ItemCreatedIf e.Item.ItemType <> ListItemType.AlternatingItem AndAlso _e.Item.ItemType <> ListItemType.Item Then Exit SubDim 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 返回“假”,不提交該頁(yè)面并且不刪除該消息。
編輯和刪除評注的實(shí)現方法相同,因此我就不再詳細介紹。但是,您可以在本文的下載代碼中找到完整的實(shí)現。有幾個(gè)細節值得在這里介紹一下。在每次必須處理 Repeater 的事件時(shí),我都使用 Visual Basic®.NET Handle 關(guān)鍵字,該關(guān)鍵字使您能夠將方法與由 WithEvents 聲明的控件實(shí)例的事件相關(guān)聯(lián)。評注的內部 DataList 不能這樣做,因為它是在運行時(shí)動(dòng)態(tài)創(chuàng )建的并且它沒(méi)有 WithEvents 控件變量。但是,您可在控件聲明中直接指定事件處理程序,如下所示:
<asp:DataList Runat="server" OnDeleteCommand="Comments_DeleteCommand"OnItemCreated="Comments_ItemCreated"OnEditCommand="Comments_EditCommand" ...>
其他的小細節是,當管理員單擊某個(gè)評注的 Edit 鏈接時(shí),必須顯示評注框并將頁(yè)面滾動(dòng)到底部以確保其可見(jiàn)。之前我針對“Post your own comment”鏈接這樣做過(guò),但在那樣的情況下,完成這樣操作的 JavaScript 例序直接與鏈接相關(guān)聯(lián),不用和服務(wù)器來(lái)回聯(lián)系。這里該頁(yè)面首先會(huì )回發(fā)給預先用評注填充的編輯文本框,當將該頁(yè)面再次發(fā)送給客戶(hù)端瀏覽器時(shí),將調用 JavaScript 例程。為此,我將一些 JavaScript 發(fā)送給剛好調用以前編寫(xiě)的 ShowCommentBox2 例程的客戶(hù)端,如下所示:
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
在關(guān)閉頁(yè)面的服務(wù)器端 <form> 標記之前,RegisterStartupScript 發(fā)出指定的 JavaScript 塊,確保已經(jīng)創(chuàng )建了 CommentBox (否則,在未找到 CommentBox 容器時(shí)會(huì )出現引用不正確的錯誤)。
驗證多個(gè)虛擬窗體
當有文本框或者其他的輸入控件時(shí),通過(guò)添加驗證程序控件來(lái)確保提供值并且值的格式和范圍正確,這通常是個(gè)不錯的主意。在本應用程序中,您必須根據用戶(hù)想要采取的操作來(lái)實(shí)施不同的驗證。如果用戶(hù)按下 Post 按鈕提交評注時(shí),則必須確保他提供了其姓名和評注文本。如果單擊了 Load Blog 按鈕,則必須要檢查起止日期的格式是否有效。不過(guò)我沒(méi)有添加驗證程序,因為還有另一個(gè)問(wèn)題需要解決。
我有三個(gè)帶有輸入控件的“虛擬窗體”:評注框、新消息框,和時(shí)間間隔選擇框。在 ASP.NET 頁(yè)中,可以只有一個(gè)服務(wù)器端窗體。這意味著(zhù)所有的輸入控件、驗證程序和提交按鈕都位于同一窗體中。一旦用戶(hù)正確填充了時(shí)間間隔文本框并單擊了提交按鈕,文本框驗證程序就會(huì )驗證該輸入。評注框和消息框的驗證程序將在其文本框沒(méi)有值或者值的格式不正確的情況下阻止該窗體回發(fā)。
為了解決這個(gè)問(wèn)題,我用一個(gè)由 James M. Venglarik 開(kāi)發(fā)的第二版自定義控件替換了標準的 ASP.NET 按鈕,該控件是他為 MSDN® Magazine 文章“
Selectively Enable Form Validation When Using ASP.NET Web Controls”而開(kāi)發(fā)的。該控件將創(chuàng )建使用某些客戶(hù)端 JavaScript 來(lái)禁用指定的驗證程序列表的按鈕,從而有可能獲得在提交頁(yè)面之前驗證某些輸入控件而不驗證其他控件的按鈕。一旦頁(yè)面引用了該控件,就會(huì )按照如下聲明 Load Blog 按鈕:
<nfvc:NoFormValButton ID="LoadBlog" Runat=Server Text="Load"NoFormValList="RequireAuthor,RequireComment,ValidateEmailFormat" />
NoFormValList 屬性指定單擊它時(shí)禁用的由逗號隔開(kāi)的驗證程序列表,在此指的是評注框和消息框中的所有文本框驗證程序。
小結
本文構建的應用程序至此就算是功能齊全了。您可以將它上載到自己的服務(wù)器上來(lái)編寫(xiě)網(wǎng)絡(luò )日記,或者可以在 http://www.bytecommerce.com/blog 上在線(xiàn)查看。DataSet 的關(guān)聯(lián)功能和創(chuàng )建嵌套 DataLists 和 DataGrids 的能力使得提供主從關(guān)系報告變得非常簡(jiǎn)單,而這正是創(chuàng )建該網(wǎng)絡(luò )日記應用程序的目的。模板控件的靈活性意味著(zhù)您幾乎可以創(chuàng )建任何形式的布局。您可以對表中的多個(gè)數據項進(jìn)行分組并自定義默認實(shí)現的行為,就像在給 Delete 按鈕添加確認彈出框時(shí)所看到的那樣。這些控件結合少許客戶(hù)端 JavaScript(用來(lái)對這些控件的行為進(jìn)行腳本編寫(xiě)并將它們結合在一起),造就了一個(gè)完整且功能豐富的 ASP.NET 報告應用程序。