導言
在一個使用了分層體系架構的ASP.NET web應用系統裡處理數據,一般遵循以下幾步:
1.確定業務邏輯層需要調用哪個方法,並且需要出入哪些參數。這些參數可以通過硬編碼設置,程序自動設定,或者由用戶輸入。
2.調用此方法。
3.處理結果。當調用一個返回數據的BLL方法時,這包括綁定數據到Data Web服務器控件。而對於修改數據的BLL方法而言,這包括基於返回值的基礎上執行某些動作,或者適當地處理在第二步中引發的異常。
正如我們在前一節裡看到的,無論ObjectDataSource控件還是數據Web服務器控件,都為第1和第3步提供了可擴展性。例如GridView控件,觸發它的RowUpdating事件之前把它的字段的值賦值到ObjectDataSource的UpdateParameters集合;在ObjectDataSource完成它的操作之後觸發RowUpdated事件。
我們已經檢測到第1步中觸發的事件,並且看過了如何使用它們實現自定義出入參數或者取消操作。這一節我們將把我們的注意力轉到操作完成後所觸發的事件。通過這些post級的event handler和其它,可以判斷在操作過程中是否產生了一個異常,並且適當地處理它,在屏幕中顯示友好的錯誤信息要優於轉到ASP.NET的默認錯誤處理頁。
為了舉例說明這些post級事件的工作方式,讓我們創建一個頁面,它在一個可編輯的GridView中列出產品信息。當更新一個產品時,如果引發了一個異常,我們的ASP.NET頁面會在GridView控件的上方顯示一個簡短的信息,說明出現了一個問題。好吧,讓我們開始!
第一步: 為產品創建一個可編輯的GridView
這一節裡我們創建一個可編輯的GridView,它僅僅包含兩個的字段,ProductName和UnitPrice。這需要為ProductsBLL類的UpdateProduct方法增加一個額外的重載,它僅僅接受3個輸入參數(product's name,unit price,和ID),相對於接受每一個產品的字段的方法。在本節裡讓我們再一次練習一下這些技巧,創建一個可編輯的GridView,它顯示產品的name、quantity per unit、unit price、和units in stock,但僅僅允許name,unit price,和units in stock可編輯。
為了提供這個場景,我們需要對UpdateProduct方法的另一個重載,它接收4個參數: product's name,unit price,units in stock和ID。在ProductsBLL類中添加下面這個方法:
[System.ComponentModel.DataObjectMethodAttribute( System.ComponentModel.DataObjectMethodType.Update, false)] public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock, int productID) { Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID); if (products.Count == 0) // no matching record found, return false return false; Northwind.ProductsRow product = products[0]; product.ProductName = productName; if (unitPrice == null) product.SetUnitPriceNull(); else product.UnitPrice = unitPrice.Value; if (unitsInStock == null) product.SetUnitsInStockNull(); else product.UnitsInStock = unitsInStock.Value; // Update the product record int rowsAffected = Adapter.Update(product); // Return true if precisely one row was updated, otherwise false return rowsAffected == 1; }
完成了此方法後,我們可以創建一個ASP.NET頁面,它允許編輯這四個產品字段。打開EditInsertDelete文件夾裡的ErrorHandling.aspx頁面,並通過設計器添加一個GridView控件到頁面中。綁定這個GridView到一個新的ObjectDataSource控件,映射Select()方法到ProductsBLL類的GetProducts()方法,方法Update()映射到剛剛創建的UpdateProduct重載。
圖1: 使用UpdateProduct方法重載,它接受四個輸入參數
這將創建一個ObjectDataSource,它包含四個參數的UpdateParameters集合,還有一個一個GridView,它包含產品的每一個字段。ObjectDataSource的聲明標記給OldValuesParameterFormatString屬性賦值為original_{0},它將引發一個異常,因為我們的BLL類沒有一個名為original_productID的輸入參數需要傳入。別忘了從聲明語法裡把這些設置通通刪除(或者把它們設置為默認值:{0})。
然後,減少GridView的綁定列,僅包含ProductName,QuantityPerUnit,UnitPrice和UnitsInStock這幾列。隨意設置一些你認為必要的字段級的格式(例如更改HeaderText屬性)。
在之前的章節裡我們已經看過了如何在只讀和編輯兩種模式下格式化UnitPrice綁定列為貨幣格式。在這裡我們同樣這樣做。這需要設置綁定列的DataFormatString屬性為{0:c},它的HtmlEncode屬性為false,還有它的ApplyFormatInEditMode屬性為true,如圖2所示。
圖2: UnitPrice綁定列配置為顯示一個貨幣金額
要在編輯界面將UnitPrice格式化為貨幣,這需要為GridView的RowUpdating事件創建一個事件處理,它將一個貨幣格式的字符串轉換成decimal。回想上一節,RowUpdating事件處理也用來檢測並確保用戶輸入的是一個UnitPrice的值。不過,本節我們可以允許用戶忽略price列。
protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e) { if (e.NewValues["UnitPrice"] != null) e.NewValues["UnitPrice"] =decimal.Parse(e.NewValues["UnitPrice"].ToString(), System.Globalization.NumberStyles.Currency); }
我們的GridView包含一個QuantityPerUnit綁定列,但它僅僅用作顯示,不能被用戶編輯。為了實現這一點,只需要簡單地將該綁定列的ReadOnly屬性設置為true。
圖 3: 設置QuantityPerUnit綁定列為只讀
最後,從GridView的智能標記裡勾選上“啟用編輯”。完成了這些步驟後,ErrorHandling.aspx頁面在設計視圖裡將如圖4所示。
圖 4: 刪除除了必需的綁定列之外的其它列並啟用編輯
在這裡我們顯示產品的所有列,ProductName、QuantityPerUnit、UnitPrice和UnitsInStock;不過僅僅ProductName、UnitPrice和UnitsInStock這幾列可以編輯。
圖 5: 用戶現在可以很方便地編輯Products' Names、Prices和Units In Stock字段
第二步:適當地處理DAL層異常
這時我們的可編輯的GridView在用戶輸入合法的product's name、price和units in stock時表現極佳,輸入不合法的值時則導致一個異常。例如,遺漏了ProductName值則引發拋出一個NoNullAllowedException異常,因為ProdcutsRow類的ProductName屬性設置了它的AllowDBNull屬性為false;如果數據庫不正常運作,則在試圖連接數據庫時通過TableAdapter拋出一個SqlException異常。沒有任何的動作,這些異常都會從數據訪問層冒出到業務邏輯層,然後到ASP.NET頁面,最後到ASP.NET運行時。
取決於你的web應用程序如何配置以及是否從localhost訪問該應用,一個未經處理的異常會出現在一類服務器錯誤處理頁,一個詳細的錯誤報表,或者一個對用戶友好的web頁面。查看Web Application Error Handling in ASP.NET 和 customErrors Element 獲得更多的關於ASP.NET頁面如何響應一個未捕獲的異常的相關信息。
圖6展示的是試圖不指定ProductName的值更新一個產品時屏幕的狀況。這顯示的是通過localhost訪問時的默認詳細錯誤報表。
圖 6: 省略Product's Name將顯示異常明細
雖然這樣的異常明細在我們測試應用程序的時候是很有用的,然而當一個最終用戶面對這樣的異常呈現時卻是無所適從的。一個最終用戶很可能並不知道NoNullAllowedException是什麼,或者它是如何引起的。更好的方法是呈現給用戶一個更友好的信息說明試圖更新產品時出現了問題。
如果在執行這項操作時出現了一個異常,ObjectDataSource 和數據Web控件的post級事件都提供了發現並不讓它出現在ASP.NET運行時的方法。在我們的例子裡,讓我們為GridView的RowUpdated事件創建一個事件處理程序,它判斷是否激發了一個異常,如果是,則在一個Label服務器控件中顯示異常詳細信息。
首先,添加一個Label控件到ASP.NET頁面,設置它的ID屬性為ExceptionDetails並清空它的Text屬性。為了吸引用戶的實現到此信息,設置其CssClass為Warning,這是我們在之前的章節裡添加到Styles.css文件的一個CSS類別。記得這個CSS類別讓Label的text顯示為紅色、斜體、加粗的較大的字體。
圖 7: 添加一個Label服務器控件到頁面
因為我們希望這個Label控件僅在異常出現時顯示,在Page_Load事件處理中設置它的Visible屬性為false:
protected void Page_Load(object sender, EventArgs e) { ExceptionDetails.Visible = false; }
通過這些代碼,當第一次訪問頁面和隨後的回傳後,ExceptionDetails控件的Visible屬性都將被設置為false。當在GridView的RowUpdated事件處理程序中檢測到一個DAL/BLL層的異常時,我們將設置ExceptionDetails控件的Visible屬性為true。因為頁面生命周期裡Web服務器控件的事件處理出現在Page_Load事件處理之後,該Label將會顯示。不過,下一次回傳,Page_Load事件處理將重新將Visible屬性設置回false,再次隱藏它。
注意: 我們也可以不必在Page_Load裡設置ExceptionDetails控件的Visible屬性,作為另一種選擇,可以在聲明語法裡設置其Visible屬性為false並禁用視圖狀態(設置它的EnableViewState屬性為false)。我們將在以後的章節裡使用這種方法。
通過添加這個Label控件,我們下一步是為GridView的RowUpdated事件添加一個事件處理程序。在設計視圖中選中GridView控件,打開屬性窗口,點擊黃色閃電狀圖標,列出GridView的所有事件。在GridView的RowUpdating事件裡我們可以看到已經存在一個入口,因為我們在本節較早的時候已經為此事件創建了一個事件處理程序。為RowUpdated事件創建一個事件處理程序。
圖 8: 為GridView的事件創建一個事件處理
注意: 你也可以通過代碼隱藏文件頂處的下拉列表創建這個事件處理。從左邊的下拉列表中選擇這個GridView控件,並從右邊的下拉列表中選擇RowUpdated事件。
創建這個事件處理將添加下面這些代碼到ASP.NET頁面的代碼隱藏類中:
protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e) { }
這個事件處理程序的第二個輸入參數是一個GridViewUpdatedEventArgs類型的對象,它有三個關於處理異常的屬性:
·Exception –獲取更新操作過程中引發的異常;如果沒有拋出異常,該屬性的值為null
·ExceptionHandled –獲取或設置一個值,它指示在更新操作過程中所引發的異常是否已在RowUpdated事件處理程序中得到處理;如果設為false(默認值),該異常將被重新引發,漏出到ASP.NET運行時
·KeepInEditMode – 如果設置為true,GridView當前編輯行將維持在編輯模式;如果設置為false(默認值),當前行將恢復到只讀模式
那麼我們的代碼應該檢測Exception是否為null,不是null則意味著執行此操作時引發了一個異常。如果是這樣,我們則希望:
·在ExceptionDetails控件中顯示一個對用戶友好的提示信息
·指示異常已經被處理
·讓當前行保持編輯模式
下面的代碼實現了上述的目的:
protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e) { if (e.Exception != null) { // Display a user-friendly message ExceptionDetails.Visible = true; ExceptionDetails.Text = "There was a problem updating the product. "; if (e.Exception.InnerException != null) { Exception inner = e.Exception.InnerException; if (inner is System.Data.Common.DbException) ExceptionDetails.Text += "Our database is currently experiencing problems." + "Please try again later."; else if (inner is NoNullAllowedException) ExceptionDetails.Text += "There are one or more required fields that are missing."; else if (inner is ArgumentException) { string paramName = ((ArgumentException)inner).ParamName; ExceptionDetails.Text += string.Concat("The ", paramName, " value is illegal."); } else if (inner is ApplicationException) ExceptionDetails.Text += inner.Message; } // Indicate that the exception has been handled e.ExceptionHandled = true; // Keep the row in edit mode e.KeepInEditMode = true; } }
在這個事件處理程序中,首先檢測e.Exception是否為null。如果不是,設置ExceptionDetails控件的Visible屬性為true、設置它的Text屬性為“There was a problem updating the product.”。當前拋出的異常詳細信息則保存在e.Exception對象的InnerException屬性裡。檢查這個內部異常,如果它是特定的類型,則把一些額外的有用的信息附加到ExceptionDetails標簽的Text屬性。最後,ExceptionHandled和KeepInEditMode屬性都設置為true。
圖9展示的是遺漏了產品名稱時的頁面的截屏;圖10則顯示輸入一個不合法的UnitPrice值(-50)時的結果。
圖 9: ProductName綁定列必須包含一個值
圖 10: UnitPrice值不接受負數
通過設置屬性為,事件處理程序指示該異常已經被處理。因此,這個異常不會傳送到ASP.NET運行時。
注意: 圖9和圖10顯示了一種得體的方式處理不正確的用戶輸入所引發的異常。可是,更理想地,這些不正確的輸入不應該到達業務邏輯層,因為ASP.NET頁面應該在調用ProductsBLL類的UpdateProduct方法之前就確保用戶的輸入是有效的。我們在下一節裡將會看看如何添加validation控件到編輯和插入界面從而保證提交到業務邏輯層的數據遵循業務規則。validation控件不但可以阻止調用UpdateProduct方法直到用戶提供有效的數據,還可以為定位數據輸入問題提供一個更充滿提示性的用戶體驗。
第三步: 適當地處理BLL層異常
當插入、更新或刪除數據時,面對一個數據相關的錯誤時數據訪問層會拋出一個異常。數據庫可能未連線,一個必需的數據庫表字段可能未指定值,或者違反了某個表間約束。除了確定的數據相關的異常外,業務邏輯層也使用異常指示違反了業務邏輯。在創建一個業務邏輯層 這一節裡,作為例子,我們添加了一個業務規則檢查最初的UpdateProduct重載。特別地,如果用戶標記一個產品為停止供應,我們要求這個產品不能是該供應商唯一供應的產品。如果違反了這個條件,拋出一個ApplicationException異常。
在這一節裡,我們給UpdateProduct重載增加一個業務規則:禁止把UnitPrice字段的值設置為超過原來的兩倍。為了實現這一點,調整UpdateProduct重載以使它可以執行這個檢查並且在違反該規則時拋出一個ApplicationException異常。此更新方法如下:
public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock, int productID) { Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID); if (products.Count == 0) // no matching record found, return false return false; Northwind.ProductsRow product = products[0]; // Make sure the price has not more than doubled if (unitPrice != null && !product.IsUnitPriceNull()) if (unitPrice > product.UnitPrice * 2) throw new ApplicationException( "When updating a product price," + " the new price cannot exceed twice the original price."); product.ProductName = productName; if (unitPrice == null) product.SetUnitPriceNull(); else product.UnitPrice = unitPrice.Value; if (unitsInStock == null) product.SetUnitsInStockNull(); else product.UnitsInStock = unitsInStock.Value; // Update the product record int rowsAffected = Adapter.Update(product); // Return true if precisely one row was updated, otherwise false return rowsAffected == 1; }
通過這個修改,任何超過現有價格兩倍的價格更新都回引發一個ApplicationException異常被拋出。就像DAL中引發的異常一樣,這個BLL引發的ApplicationException異常可以在GridView的RowUpdated事件處理程序中被偵測並處理。實際上,我們已有的RowUpdated事件處理程序的代碼可以正確地發現到這個異常並顯示ApplicationException的Message屬性的值。圖11顯示的是當一個用戶試圖將產品“Chai”的價格更新為$50.00時的截屏,這超過了它原有價格$19.95的兩倍。
圖 11: 這個業務規則不接受價格增長超出產品現有價格的兩倍
注意: 理想化地我們的業務規則不應該在UpdateProduct方法重載裡而應該在一個公共的方法中。這留作讀者練習。
總結
在插入、更新或刪除操作的過程中,數據Web控件和ObjectDataSource控件都包含了pre- 和post-級的事件,它們記錄著當前的操作。正如我們在本節和前面的一節裡所看到的,當使用一個可編輯的GridView時,GridView的RowUpdating事件在ObjectDataSource的Updating事件之後觸發,此時update命令發送到ObjectDataSource的隱含對象。完成了此操作,在GridView的RowUpdated事件之後,觸發ObjectDataSource的Updated事件。
我們可以為這些發生在操作之前的事件創建事件處理程序,目的是自定義輸入參數;為發生在
操作之後的事件創建事件處理,目的是檢測和相應操作的結果。Post-level的事件處理程序通常用作偵測在操作過程中是否出現了一個異常。當面對一個異常時,這些post-level的事件處理程序可以隨意地處理該異常。在本節裡我們看過了如何處理這樣的一個異常,顯示一個友好的錯誤提示信息。
在下一節裡我們將看看如何降低因數據格式的問題引起異常的可能性(例如在UnitPrice輸入一個負數)。特別地,我們將看看如何添加validation控件到編輯和插入界面。
祝編程快樂!
作者簡介
Scott Mitchell,著有六本ASP/ASP.NET方面的書,是4GuysFromRolla.com的創始人,自1998年以來一直應用微軟Web技術。Scott是個獨立的技 術咨詢顧問,培訓師,作家,最近完成了將由Sams出版社出版的新作,24小時內精通ASP.NET 2.0。他的聯系電郵為[email protected],也可以通過他的博客http://ScottOnWriting.NET與他聯系。