共用方式為


處理 ASP.NET 頁面中 BLL 和 DAL 層級的例外狀況 (C#)

斯科特·米切爾

下載 PDF

在這個教學中,我們將學習如何在 ASP.NET 數據 Web 控件的插入、更新或刪除操作期間,如果發生異常,顯示一個友好且資訊豐富的錯誤訊息。

簡介

使用階層式應用程式架構處理來自 ASP.NET Web 應用程式的數據牽涉到下列三個一般步驟:

  1. 判斷需要叫用哪些商業規則層的方法,以及要傳遞哪些參數值。 參數值可以是硬式編碼、以程式設計方式指派或使用者輸入的輸入。
  2. 叫用 方法。
  3. 處理結果。 呼叫傳回數據的 BLL 方法時,這可能牽涉到將數據系結至數據 Web 控制件。 對於修改數據的 BLL 方法,這可能包括根據傳回值執行某些動作,或正常處理步驟 2 中出現的任何例外狀況。

上一個教學課程中所見,ObjectDataSource 和數據 Web 控件都提供步驟 1 和 3 的擴充點。 例如,GridView 會在將域值指派給 ObjectDataSource 的RowUpdating集合之前引發其UpdateParameters事件;其RowUpdated事件會在 ObjectDataSource 完成作業之後引發。

我們已檢視步驟 1 期間引發的事件,並已看到如何用來自定義輸入參數或取消操作。 在本教學課程中,我們會將注意力轉向作業完成之後引發的事件。 透過這些後置事件處理程式,我們可以判斷作業期間是否發生例外狀況並正常處理,並在畫面上顯示易記、資訊豐富的錯誤訊息,而不是預設為標準 ASP.NET 例外狀況頁面。

為了說明使用這些後置事件,讓我們建立一個頁面,列出可編輯 GridView 中的產品。 更新產品時,如果引發例外狀況,我們的 ASP.NET 頁面會顯示 GridView 上方的簡短訊息,說明發生問題。 現在就開始吧!

步驟 1:建立產品的可編輯 GridView

在上一個教學課程中,我們建立了一個包含兩個字段,ProductNameUnitPrice 的可編輯 GridView。 這需要為 ProductsBLL 類別的 UpdateProduct 方法建立額外的多載,其中一個只接受三個輸入參數(產品名稱、單價和標識符),而不是每個產品字段的參數。 在本教學課程中,讓我們再次練習這項技術,建立可編輯的 GridView,以顯示產品名稱、每單位數量、單價和庫存單位,但只允許編輯庫存中的名稱、單價和單位。

為了適應此情況,我們需要UpdateProduct方法的另一種重載,這種重載需要接受四個參數:產品名稱、單價、庫存數量和編號。 將下列方法新增至 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 頁面。 ErrorHandling.aspx開啟資料夾中的頁面EditInsertDelete,並透過設計工具將 GridView 新增至頁面。 將 GridView 系結至新的 ObjectDataSource,將 Select() 方法對應至 ProductsBLL 類別的 GetProducts() 方法,並將 Update() 方法對應至剛建立的多 UpdateProduct 載。

使用接受四個輸入參數的 UpdateProduct 方法重載

圖 1:使用接受四個輸入參數的多載的方法 (UpdateProduct

這將建立一個具有四個 UpdateParameters 參數的 ObjectDataSource,並且生成的 GridView 會包含每個產品欄位對應的欄位。 ObjectDataSource 的宣告式標記會指派 OldValuesParameterFormatStringoriginal_{0},這會導致例外狀況,因為我們的 BLL 類別不希望傳入名為 original_productID 的輸入參數。 別忘了從宣告式語法移除此設定 (或將它設定為預設值 , {0}

接下來,將 GridView 精簡至只限於包含 ProductNameQuantityPerUnitUnitPriceUnitsInStock BoundFields。 您也可以隨意套用您認為必要的任何欄位層級格式設定(例如變更 HeaderText 屬性)。

在上一個教學課程中,我們探討如何以只讀模式和編輯模式將 BoundField 格式化 UnitPrice 為貨幣。 讓我們在這裡執行相同的動作。 請記得,這需要將 BoundField 的 DataFormatString 屬性設定為 {0:c},將其 HtmlEncode 屬性設定為 false,並將其 ApplyFormatInEditMode 設定為 true,如圖 2 所示。

將 UnitPrice BoundField 設定為貨幣

圖 2:將 UnitPrice BoundField 設定為貨幣顯示 (按兩下以檢視完整大小的影像

在編輯介面中,將 UnitPrice 格式化為貨幣需要建立 GridView 的 RowUpdating 事件處理程式,以便將貨幣格式的字串轉換為 decimal 值。 回想一下,上次教學課程中的 RowUpdating 事件監聽器也會檢查使用者是否提供了 UnitPrice 值。 不過,在本教學課程中,讓我們允許使用者省略價格。

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 BoundField,但此 BoundField 應該僅供顯示之用,且使用者不應編輯。 若要進行此排列,只要將 BoundFields 的 ReadOnly 屬性設定為 true

將 QuantityPerUnit BoundField 設為唯讀

圖 3:使 QuantityPerUnit BoundField Read-Only (按兩下以檢視完整大小的影像

最後,從 GridView 的智慧標記中核取 [啟用編輯] 複選框。 完成這些步驟之後, ErrorHandling.aspx 頁面的設計工具看起來應該類似圖 4。

移除所有不需要的 BoundFields,並核取 [啟用編輯] 複選框

圖 4:移除除必要項目以外的所有 BoundFields 並勾選 [啟用編輯] 核取方塊 (點擊以檢視完整大小的影像

此時,我們擁有所有產品的 ProductNameQuantityPerUnitUnitPriceUnitsInStock 欄位清單,不過,只有 ProductNameUnitPriceUnitsInStock 欄位可以編輯。

用戶現在可以輕鬆地編輯產品的名稱、價格和庫存欄位中的單位

圖 5:使用者現在可以輕鬆地編輯產品的名稱、價格和庫存欄位中的單位(單擊即可檢視完整大小的影像

步驟 2:正常處理 DAL-Level 例外狀況

雖然我們的可編輯 GridView 在使用者輸入已編輯產品名稱、價格和庫存單位的法律值時效果非常出色,但輸入非法值會導致例外狀況。 例如,省略 ProductName 值會引發 NoNullAllowedException,因為 ProductName 屬性在 ProductsRow 類別中已設定為 AllowDBNull;如果資料庫關閉,則當 TableAdapter 嘗試連接到資料庫時會引發 false。 不採取任何動作時,這些例外狀況會從資料存取層向上傳遞到商務邏輯層,然後到 ASP.NET 頁面,最後到 ASP.NET 執行環境。

根據 Web 應用程式的設定方式,以及您是否從 localhost瀏覽應用程式,未處理的例外狀況可能會導致一般伺服器錯誤頁面、詳細的錯誤報告或使用者易記的網頁。 如需 ASP.NET 運行時間如何回應未攔截例外狀況的詳細資訊,請參閱 ASP.NET 中的 Web 應用程式錯誤處理customErrors 元素

圖 6 顯示嘗試更新產品而不指定 ProductName 值時遇到的畫面。 這是傳入 localhost時顯示的預設詳細錯誤報告。

省略產品名稱會顯示例外狀況詳細數據

圖 6:省略產品名稱將會顯示例外狀況詳細資料(按兩下以檢視完整大小的影像

雖然這類例外狀況詳細資訊在測試應用程式時很有幫助,但在例外狀況時向終端使用者呈現這樣的畫面並不是理想的做法。 使用者可能不知道NoNullAllowedException是什麼,也不清楚其成因。 更好的方法是向用戶呈現更方便使用的訊息,說明嘗試更新產品時發生問題。

如果執行作業時發生例外狀況,ObjectDataSource 和資料 Web 控制中的後級事件提供了捕捉它並避免例外狀況冒泡至 ASP.NET 運行時的方法。 在我們的範例中,讓我們建立 GridView RowUpdated 事件的事件處理程式,以判斷是否引發例外狀況,如果是的話,則會在標籤 Web 控制件中顯示例外狀況詳細數據。

首先,將標籤新增至 ASP.NET 頁面,將其 ID 屬性設定為 ExceptionDetails ,並清除其 Text 屬性。 若要吸引使用者的眼球至此訊息,請將其 CssClass 屬性設定為 Warning,這是我們在上一個教學課程中新增至 Styles.css 檔案的 CSS 類別。 回想一下,這個 CSS 類別會導致標籤的文字以紅色、斜體、粗體、超大型字型顯示。

將標籤 Web 控制項新增至頁面

圖 7:將標籤 Web 控制項新增至頁面 (按兩下以檢視完整大小的影像

由於我們希望此標籤 Web 控制件只在發生例外狀況之後立即顯示,因此在事件處理程式中Visible將其 Page_Load 屬性設定為 false:

protected void Page_Load(object sender, EventArgs e)
{
    ExceptionDetails.Visible = false;
}

使用此程式代碼時,在第一次頁面瀏覽和後續回傳期間,會將 ExceptionDetails 控制的 Visible 屬性設定為 false。 面對 DAL 或 BLL 層級例外狀況,我們可以在 GridView 的RowUpdated事件處理程式中偵測到此例外狀況,我們將控件的 ExceptionDetails 屬性設定Visible為 true。 由於 Web 控制項的事件處理程式會在頁面生命週期中的 Page_Load 事件處理程式之後發生,因此標籤會被顯示。 不過,在下一次回傳時,Page_Load 事件處理程式會將 Visible 屬性還原為 false,並再次將其隱藏。

備註

或者,我們可以透過在宣告式語法中將控件的ExceptionDetails屬性指派為Visible,並停用其檢視狀態(將其Page_Load屬性設定為Visible),來移除設定false控件的EnableViewState屬性false的必要性。 我們將在未來的教學課程中使用這個替代方法。

新增標籤後,下一個步驟是建立 GridView RowUpdated 事件的事件處理程式。 選取設計師中的 GridView,移至 [屬性] 視窗,然後按兩下閃電圖示,列出 GridView 的事件。 因為我們稍早在本教學課程中為此事件建立了事件處理程式,因此 GridView 的 RowUpdating 事件應該已經有一個項目。 同時建立 RowUpdated 事件的事件處理程式。

為 GridView 的 RowUpdated 事件建立事件處理程式

圖 8:建立 GridView RowUpdated 事件的事件處理程式

備註

您也可以透過程式代碼後置類別檔案頂端的下拉式清單建立事件處理程式。 從左側的下拉式清單中選取 GridView,並從 RowUpdated 右側的下拉式清單中選取事件。

建立此事件處理程式會將下列程式代碼新增至 ASP.NET 頁面的程式代碼後置類別:

protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
}

此事件處理程式的第二個輸入參數是 GridViewUpdatedEventArgs 類型的物件,它有三個處理例外狀況感興趣的屬性:

  • Exception 擲回例外狀況的參考;如果未擲回例外狀況,則此屬性的值會是 null
  • ExceptionHandled 布爾值,指示例外狀況是否在 RowUpdated 事件處理程式中被處理;如果是 false(預設值),則例外狀況會重新擲回,並向上傳遞至 ASP.NET 執行階段
  • KeepInEditMode 如果設定為 true,已編輯的 GridView 資料列將保持在編輯模式中;如果設定為 false(預設值),GridView 資料列會還原為只讀模式。

然後,我們的程式代碼應該檢查是否 Exception 不是 null,這表示在執行作業時引發例外狀況。 如果是這種情況,我們想要:

  • ExceptionDetails標籤中顯示友好的使用者訊息
  • 表明例外狀況已被處理
  • 讓 GridView 資料列保持編輯模式

下列程式代碼會完成下列目標:

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.Exceptionnull。 如果不是, ExceptionDetails Label 的 Visible 屬性會設定為 true ,且其 Text 屬性會設定為「更新產品時發生問題」。擲回的實際例外狀況詳細數據位於 e.Exception 對象的 屬性中 InnerException 。 檢查此內部例外狀況,如果它屬於特定類型,則會將額外的實用訊息附加至 ExceptionDetails Label 的 Text 屬性。 最後, ExceptionHandledKeepInEditMode 屬性都設定為 true

圖 9 顯示這個頁面在省略產品名稱時的螢幕快照;圖 10 顯示輸入非法 UnitPrice 值時的結果(-50)。

ProductName BoundField 必須包含值

圖 9ProductName BoundField 必須包含值(按兩下即可檢視完整大小的影像

UnitPrice 值不得為負

圖 10:不允許負 UnitPrice 值(按兩下以檢視完整大小的影像

將屬性設定 e.ExceptionHandledtrueRowUpdated 事件處理程式表示它已處理例外狀況。 因此,例外狀況不會傳播到 ASP.NET 運行時間。

備註

圖 9 和 10 顯示因使用者輸入無效而引發例外狀況的優雅處理方式。 不過,在理想情況下,這類無效的輸入應該永遠都不會到達商業邏輯層,因為 ASP.NET 頁面應該在呼叫ProductsBLL類別的UpdateProduct方法之前,先確保使用者的輸入是有效的。 在下一個教學課程中,我們將瞭解如何將驗證控件新增至編輯和插入介面,以確保提交至商業規則層的數據符合商務規則。 驗證控制項不僅會防止調用UpdateProduct方法,直到使用者提供的資料有效為止,還提供更具資訊量的使用者體驗以識別資料輸入問題。

步驟 3:正常處理 BLL-Level 例外狀況

插入、更新或刪除資料時,數據存取層可能會在發生數據相關錯誤時擲回例外狀況。 資料庫可能離線、必要的資料庫資料表資料行可能未指定值,或違反資料表層級條件約束。 除了嚴格數據相關例外狀況之外,商業規則層還可以使用例外狀況來指出違反商務規則的時間。 例如,在 建立商業邏輯層教學課程 中,我們已將商業規則檢查新增至原始 UpdateProduct 多載。 具體來說,如果使用者將產品標示為停產,我們要求該產品不是其供應商唯一提供的產品。 如果違反此條件,則會拋出 ApplicationException

針對本教程中建立的 UpdateProduct 過載,讓我們新增一條業務規則,禁止將 UnitPrice 欄位設定為超過原始 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 引發的例外狀況一樣,可以在 GridView 的 ApplicationException 事件處理程式中偵測及處理這個由 BLL 引發的 RowUpdated。 事實上, RowUpdated 事件處理程式的程式代碼會正確偵測到這個例外狀況,並顯示 ApplicationExceptionMessage 屬性值。 圖 11 顯示使用者嘗試將 Chai 的價格更新為 $50.00 的螢幕快照,這個價格比其目前的 $19.95 超過兩倍。

商務規則不允許價格上漲超過產品價格的兩倍

圖 11:商務規則不允許價格上漲超過產品價格的兩倍 (按兩下以檢視全尺寸影像

備註

理想情況下,我們的業務邏輯規則應該從UpdateProduct方法多載中重構出來,並移至一個通用方法。 這留給讀者自己去練習。

總結

在插入、更新和刪除操作期間,資料 Web 控制項和 ObjectDataSource 會觸發前置和後置層級事件,以框住實際操作。 如我們在本教學課程和前一個教學課程中所見,當使用可編輯的 GridView 時,會觸發 GridView 的 RowUpdating 事件,接著激發 ObjectDataSource 的 Updating 事件,此時會將更新命令應用至 ObjectDataSource 的基礎物件。 作業完成後,ObjectDataSource 的 Updated 事件會先觸發,然後 GridView 的 RowUpdated 事件隨之而來。

我們可以為預先層級事件建立事件處理程式,以便自定義輸入參數或後置事件,以便檢查和響應作業的結果。 層級後事件處理程式最常用來偵測作業期間是否發生例外狀況。 面對例外狀況,這些後置事件處理程式可以選擇自行處理例外狀況。 在本教學課程中,我們已瞭解如何藉由顯示易記的錯誤訊息來處理這類例外狀況。

在下一個教學課程中,我們將瞭解如何降低數據格式問題所產生的例外狀況可能性(例如輸入負 UnitPrice數)。 具體而言,我們將探討如何將驗證控件新增至編輯和插入介面。

快樂的程序設計!

關於作者

斯科特·米切爾,七本 ASP/ASP.NET 書籍和 4GuysFromRolla.com 創始人的作者,自1998年以來一直與Microsoft Web 技術合作。 斯科特擔任獨立顧問、教練和作家。 他的最新書是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 可以透過 mitchell@4GuysFromRolla.com 聯絡他。

特別感謝

本教學系列已由許多熱心的評論者審閱。 本教學課程的主要檢閱者是 Liz Shulok。 有興趣檢閱我即將推出的 MSDN 文章嗎? 如果是,請在 mitchell@4GuysFromRolla.com給我留言。