このチュートリアルでは、ASP.NET データ Web コントロールの挿入、更新、または削除操作中に例外が発生した場合に、わかりやすい有益なエラー メッセージを表示する方法について説明します。
イントロダクション
階層化されたアプリケーション アーキテクチャを使用して ASP.NET Web アプリケーションのデータを操作するには、次の 3 つの一般的な手順が必要です。
- 呼び出す必要があるビジネス ロジック レイヤーのメソッドと、それを渡すパラメーター値を決定します。 パラメーター値は、ハード コーディング、プログラムによる割り当て、またはユーザーが入力した入力を行うことができます。
- メソッドを呼び出します。
- 結果を処理します。 データを返す BLL メソッドを呼び出すときに、データをデータ Web コントロールにバインドする必要がある場合があります。 データを変更する BLL メソッドの場合は、戻り値に基づいて何らかのアクションを実行したり、手順 2 で発生した例外を適切に処理したりできます。
前のチュートリアルで説明したように、ObjectDataSource コントロールとデータ Web コントロールの両方に、手順 1 と 3 の機能拡張ポイントが用意されています。 たとえば、GridView は、ObjectDataSource の RowUpdating
コレクションにフィールド値を割り当てる前に、UpdateParameters
イベントを発生させます。そのRowUpdated
イベントは、ObjectDataSource が操作を完了した後に発生します。
手順 1 で発生するイベントを既に調べ、入力パラメーターのカスタマイズや操作の取り消しにどのように使用できるかを確認しました。 このチュートリアルでは、操作が完了した後に発生するイベントに注目します。 これらの事後レベルのイベント ハンドラーを使用すると、特に、操作中に例外が発生したかどうかを判断し、正常に処理し、標準の ASP.NET 例外ページではなく、わかりやすい有益なエラー メッセージを画面に表示できます。
これらのポストレベル イベントの操作を説明するために、編集可能な GridView 内の製品を一覧表示するページを作成しましょう。 製品を更新するときに、例外が発生した場合、ASP.NET ページには、問題が発生したことを説明する短いメッセージが GridView の上に表示されます。 それでは始めましょう。
手順 1: 編集可能な製品の GridView を作成する
前のチュートリアルでは、 ProductName
と UnitPrice
の 2 つのフィールドのみを含む編集可能な GridView を作成しました。 これには、 ProductsBLL
クラスの UpdateProduct
メソッドに対して追加のオーバーロードを作成する必要がありました。1 つは、各製品フィールドのパラメーターではなく、3 つの入力パラメーター (製品名、単価、ID) のみを受け入れたものです。 このチュートリアルでは、この手法をもう一度練習して、製品の名前、単位あたりの数量、単価、在庫単位を表示する編集可能な GridView を作成しますが、編集できるのは、名前、単価、在庫単位のみです。
このシナリオに対応するには、 UpdateProduct
メソッドのもう 1 つのオーバーロードが必要です。1 つは、製品の名前、単価、在庫単位、ID の 4 つのパラメーターを受け取ります。 次のメソッドを ProductsBLL
クラスに追加します:
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct _
(ByVal productName As String, ByVal unitPrice As Nullable(Of Decimal), _
ByVal unitsInStock As Nullable(Of Short), ByVal productID As Integer) As Boolean
Dim products As Northwind.ProductsDataTable = _
Adapter.GetProductByProductID(productID)
If products.Count = 0 Then
Return False
End If
Dim product As Northwind.ProductsRow = products(0)
product.ProductName = productName
If Not unitPrice.HasValue Then
product.SetUnitPriceNull()
Else
product.UnitPrice = unitPrice.Value
End If
If Not unitsInStock.HasValue Then
product.SetUnitsInStockNull()
Else
product.UnitsInStock = unitsInStock.Value
End If
Dim rowsAffected As Integer = Adapter.Update(product)
Return rowsAffected = 1
End Function
このメソッドを完了すると、これら 4 つの特定の製品フィールドを編集できる ASP.NET ページを作成する準備ができました。
ErrorHandling.aspx
フォルダーのEditInsertDelete
ページを開き、デザイナーを使用してページに GridView を追加します。 GridView を新しい ObjectDataSource にバインドし、 Select()
メソッドを ProductsBLL
クラスの GetProducts()
メソッドにマッピングし、 Update()
メソッドを先ほど作成した UpdateProduct
オーバーロードにマッピングします。
図 1: 4 つの入力パラメーターを受け入れる UpdateProduct
メソッドのオーバーロードを使用する (フルサイズの画像を表示する をクリックします)。
これにより、4 つのパラメーターを持つ UpdateParameters
コレクションと、各製品フィールドのフィールドを持つ GridView を含む ObjectDataSource が作成されます。 ObjectDataSource の宣言型マークアップにより、 OldValuesParameterFormatString
プロパティに値 original_{0}
が割り当てられます。これにより、BLL クラスでは original_productID
という名前の入力パラメーターが渡されるとは想定されないため、例外が発生します。 宣言構文からこの設定を完全に削除することを忘れないでください (または既定値 {0}
に設定してください)。
次に、GridView を整理して ProductName
、QuantityPerUnit
、UnitPrice
、および UnitsInStock
の BoundFields のみを含めます。 また、必要と思われるフィールド レベルの書式設定 ( HeaderText
プロパティの変更など) も自由に適用できます。
前のチュートリアルでは、読み取り専用モードと編集モードの両方で、 UnitPrice
BoundField を通貨として書式設定する方法について説明しました。 ここで同じ操作を行いましょう。 図 2 に示すように、BoundField の DataFormatString
プロパティを {0:c}
に設定し、その HtmlEncode
プロパティを false
に設定し、その ApplyFormatInEditMode
を true
に設定する必要があることを思い出してください。
図 2: UnitPrice
BoundField を通貨として表示するように構成する (フルサイズの画像を表示する をクリックします)。
編集インターフェイスでUnitPrice
を通貨として書式設定するには、通貨形式の文字列をRowUpdating
値に解析する GridView のdecimal
イベントのイベント ハンドラーを作成する必要があります。 最後のチュートリアルの RowUpdating
イベント ハンドラーもチェックして、ユーザーが UnitPrice
値を指定したことを確認したことを思い出してください。 ただし、このチュートリアルでは、ユーザーが価格を省略できるようにしましょう。
Protected Sub GridView1_RowUpdating(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.GridViewUpdateEventArgs) _
Handles GridView1.RowUpdating
If e.NewValues("UnitPrice") IsNot Nothing Then
e.NewValues("UnitPrice") = _
Decimal.Parse(e.NewValues("UnitPrice").ToString(), _
System.Globalization.NumberStyles.Currency)
End If
GridView には BoundField QuantityPerUnit
が含まれていますが、この BoundField は表示目的でのみ使用し、ユーザーが編集することはできません。 これを配置するには、BoundFields の ReadOnly
プロパティを true
に設定するだけです。
図 3: QuantityPerUnit
BoundField Read-Only にする (フルサイズの画像を表示する をクリックします)
最後に、GridView のスマート タグの [編集を有効にする] チェック ボックスをオンにします。 これらの手順を完了すると、 ErrorHandling.aspx
ページのデザイナーは図 4 のようになります。
図 4: 必要な BoundField を除くすべてを削除し、[編集を有効にする] チェック ボックスをオンにします (フルサイズの画像を表示する をクリックします)。
この時点で、製品のすべての ProductName
、 QuantityPerUnit
、 UnitPrice
、および UnitsInStock
フィールドの一覧が表示されますが、編集できるのは ProductName
、 UnitPrice
、および UnitsInStock
フィールドのみです。
図 5: 在庫フィールドの製品の名前、価格、ユニットをユーザーが簡単に編集できるようになりました (フルサイズの画像を表示する をクリックします)。
手順 2: DAL-Level 例外を適切に処理する
編集可能な GridView は、ユーザーが編集した製品の名前、価格、および在庫単位の有効な値を入力するとうまく機能しますが、無効な値を入力すると例外が発生します。 たとえば、ProductName
値を省略すると、 クラスのProductName
プロパティが ProductsRow
AllowDBNull
に設定されているため、false
がスローされます。データベースがダウンしている場合、データベースに接続しようとすると TableAdapter によってSqlException
がスローされます。 これらの例外は何も行わずに、データ アクセス層からビジネス ロジック層にバブル アップし、次に ASP.NET ページに移動し、最後に ASP.NET ランタイムにバブル アップします。
Web アプリケーションの構成方法と、 localhost
からアプリケーションにアクセスするかどうかに応じて、ハンドルされない例外によって、一般的なサーバー エラー ページ、詳細なエラー レポート、またはユーザーフレンドリな Web ページが発生する可能性があります。 ASP.NET ランタイムがキャッチされない例外に応答する方法の詳細については、 ASP.NET の Web アプリケーション エラー処理 と customErrors 要素 に関するページを参照してください。
図 6 は、 ProductName
値を指定せずに製品を更新しようとしたときに発生した画面を示しています。 これは、 localhost
経由で表示される既定の詳細なエラー レポートです。
図 6: 製品の名前を省略すると例外の詳細が表示されます (フルサイズの画像を表示する をクリックします)。
このような例外の詳細はアプリケーションをテストするときに役立ちますが、例外が発生した場合にエンド ユーザーにこのような画面を表示することは理想的ではありません。 エンド ユーザーは、 NoNullAllowedException
が何であるか、その原因がわからない可能性があります。 より良い方法は、製品の更新を試みる際に問題が発生したことを説明するわかりやすいメッセージをユーザーに表示することです。
操作の実行時に例外が発生した場合、ObjectDataSource とデータ Web コントロールの両方のポストレベル イベントは、それを検出し、ASP.NET ランタイムにバブルアップから例外を取り消す手段を提供します。 この例では、GridView の RowUpdated
イベントのイベント ハンドラーを作成して、例外が発生したかどうかを判断し、発生した場合は、Label Web コントロールに例外の詳細を表示します。
まず、ASP.NET ページにラベルを追加し、その ID
プロパティを ExceptionDetails
に設定し、 Text
プロパティをクリアします。 このメッセージにユーザーの目を引くために、その CssClass
プロパティを Warning
に設定します。これは、前のチュートリアルで Styles.css
ファイルに追加した CSS クラスです。 この CSS クラスでは、ラベルのテキストが赤、斜体、太字、特大フォントで表示されることを思い出してください。
図 7: ラベル Web コントロールをページに追加する (フルサイズの画像を表示する をクリックします)
この Label Web コントロールは例外が発生した直後にのみ表示されるようにするため、Visible
イベント ハンドラーでそのPage_Load
プロパティを false に設定します。
Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
ExceptionDetails.Visible = False
End Sub
このコードを使用すると、最初のページの visit および後続のポストバックで、 ExceptionDetails
コントロールの Visible
プロパティが false
に設定されます。 GridView の RowUpdated
イベント ハンドラーで検出できる DAL レベルまたは BLL レベルの例外が発生した場合は、 ExceptionDetails
コントロールの Visible
プロパティを true に設定します。 Web コントロール イベント ハンドラーは、ページ ライフサイクルの Page_Load
イベント ハンドラーの後に発生するため、ラベルが表示されます。 ただし、次のポストバックでは、 Page_Load
イベント ハンドラーは、 Visible
プロパティを false
に戻し、再度非表示に戻します。
注
または、宣言構文でExceptionDetails
プロパティVisible
を割り当て、そのビューステートを無効にすることで、Page_Load
でVisible
コントロールのfalse
プロパティを設定する必要性を排除することもできます (EnableViewState
プロパティをfalse
に設定します)。 この代替方法は、今後のチュートリアルで使用します。
Label コントロールを追加したら、次の手順として GridView の RowUpdated
イベントのイベント ハンドラーを作成します。 デザイナーで GridView を選択し、[プロパティ] ウィンドウに移動し、稲妻アイコンをクリックして GridView のイベントを一覧表示します。 このチュートリアルで前にこのイベントのイベント ハンドラーを作成したので、GridView の RowUpdating
イベントのエントリが既に存在する必要があります。
RowUpdated
イベントのイベント ハンドラーも作成します。
図 8: GridView の RowUpdated
イベントのイベント ハンドラーを作成する
注
分離コード クラス ファイルの上部にあるドロップダウン リストを使用して、イベント ハンドラーを作成することもできます。 左側のドロップダウン リストから GridView を選択し、右側のイベントから RowUpdated
イベントを選択します。
このイベント ハンドラーを作成すると、ASP.NET ページの分離コード クラスに次のコードが追加されます。
Protected Sub GridView1_RowUpdated(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _
Handles GridView1.RowUpdated
End Sub
このイベント ハンドラーの 2 番目の入力パラメーターは、 GridViewUpdatedEventArgs 型のオブジェクトであり、例外を処理するための 3 つのプロパティがあります。
-
Exception
スローされた例外への参照。例外がスローされていない場合、このプロパティの値はnull
-
ExceptionHandled
RowUpdated
イベント ハンドラーで例外が処理されたかどうかを示すブール値。false
(既定値) の場合、例外は再スローされ、ASP.NET ランタイムまでパーコレートされます。 -
KeepInEditMode
編集した GridView 行true
に設定されている場合は編集モードのままです。false
(既定値) の場合、GridView 行は読み取り専用モードに戻ります
その後、コードは、 Exception
が null
されていないかどうかを確認する必要があります。つまり、操作の実行中に例外が発生しました。 その場合は、次の操作を行います。
-
ExceptionDetails
ラベルにわかりやすいメッセージを表示する - 例外が処理されたことを示す
- GridView 行を編集モードのままにする
次のコードは、これらの目標を達成します。
Protected Sub GridView1_RowUpdated(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _
Handles GridView1.RowUpdated
If e.Exception IsNot Nothing Then
ExceptionDetails.Visible = True
ExceptionDetails.Text = "There was a problem updating the product. "
If e.Exception.InnerException IsNot Nothing Then
Dim inner As Exception = e.Exception.InnerException
If TypeOf inner Is System.Data.Common.DbException Then
ExceptionDetails.Text &= _
"Our database is currently experiencing problems." & _
"Please try again later."
ElseIf TypeOf inner _
Is System.Data.NoNullAllowedException Then
ExceptionDetails.Text += _
"There are one or more required fields that are missing."
ElseIf TypeOf inner Is ArgumentException Then
Dim paramName As String = CType(inner, ArgumentException).ParamName
ExceptionDetails.Text &= _
String.Concat("The ", paramName, " value is illegal.")
ElseIf TypeOf inner Is ApplicationException Then
ExceptionDetails.Text += inner.Message
End If
End If
e.ExceptionHandled = True
e.KeepInEditMode = True
End If
End Sub
このイベント ハンドラーは、まず、 e.Exception
が null
されているかどうかを確認します。 そうでない場合、 ExceptionDetails
Label の Visible
プロパティは true
に設定され、その Text
プロパティは "製品の更新中に問題が発生しました" に設定されます。スローされた実際の例外の詳細は、 e.Exception
オブジェクトの InnerException
プロパティにあります。 この内部例外が調べられ、特定の種類の場合は、 ExceptionDetails
Label の Text
プロパティに追加の有用なメッセージが追加されます。 最後に、 ExceptionHandled
プロパティと KeepInEditMode
プロパティの両方が true
に設定されます。
図 9 は、製品の名前を省略したときのこのページのスクリーン ショットを示しています。図 10 は、無効な UnitPrice
値 (-50) を入力したときの結果を示しています。
図 9: ProductName
BoundField には値を含める必要があります (フルサイズの画像を表示する をクリックします)。
図 10: 負の UnitPrice
値は許可されません (フルサイズの画像を表示する をクリックします)
e.ExceptionHandled
プロパティを true
に設定することで、RowUpdated
イベント ハンドラーは例外を処理したことを示しています。 そのため、例外は ASP.NET ランタイムに反映されません。
注
図 9 と図 10 は、無効なユーザー入力によって発生した例外を適切に処理する方法を示しています。 理想的には、そのような無効な入力はそもそもビジネスロジック層に到達しないはずです。ASP.NET ページは、ProductsBLL
クラスの UpdateProduct
メソッドを呼び出す前に、ユーザーの入力が有効であることを確認する必要があります。 次のチュートリアルでは、ビジネス ロジック レイヤーに送信されたデータがビジネス ルールに準拠していることを確認するために、編集インターフェイスと挿入インターフェイスに検証コントロールを追加する方法について説明します。 検証コントロールは、ユーザーが指定したデータが有効になるまで、 UpdateProduct
メソッドの呼び出しを防ぐだけでなく、データ入力の問題を特定するためのより有益なユーザー エクスペリエンスを提供します。
手順 3: BLL-Level 例外を適切に処理する
データを挿入、更新、または削除する際に、データ関連のエラーが発生すると、データアクセス層は例外をスローします。 データベースがオフラインであるか、必要なデータベース テーブル列に値が指定されていないか、テーブル レベルの制約に違反している可能性があります。 ビジネス ロジック レイヤーでは、厳密にデータ関連の例外に加えて、例外を使用して、ビジネス ルールに違反したタイミングを示すことができます。 たとえば、 ビジネス ロジック レイヤーの作成 チュートリアルでは、元の UpdateProduct
オーバーロードにビジネス ルール チェックを追加しました。 具体的には、ユーザーが製品を廃止済みとしてマークしている場合は、その製品がサプライヤーによって提供される唯一の製品ではないことを要求しました。 この条件に違反した場合は、ApplicationException
がスローされる。
このチュートリアルで作成した UpdateProduct
オーバーロードについては、 UnitPrice
フィールドが元の UnitPrice
値の 2 倍以上の新しい値に設定されないようにするビジネス ルールを追加しましょう。 これを実現するには、このチェックを実行し、ルールに違反した場合にUpdateProduct
をスローするように、ApplicationException
オーバーロードを調整します。 更新されたメソッドは次のとおりです。
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(ByVal productName As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
ByVal productID As Integer) As Boolean
Dim products As Northwind.ProductsDataTable = Adapter.GetProductByProductID(productID)
If products.Count = 0 Then
Return False
End If
Dim product As Northwind.ProductsRow = products(0)
If unitPrice.HasValue AndAlso Not product.IsUnitPriceNull() Then
If unitPrice > product.UnitPrice * 2 Then
Throw New ApplicationException( _
"When updating a product price," & _
" the new price cannot exceed twice the original price.")
End If
End If
product.ProductName = productName
If Not unitPrice.HasValue Then
product.SetUnitPriceNull()
Else
product.UnitPrice = unitPrice.Value
End If
If Not unitsInStock.HasValue Then
product.SetUnitsInStockNull()
Else
product.UnitsInStock = unitsInStock.Value
End If
Dim rowsAffected As Integer = Adapter.Update(product)
Return rowsAffected = 1
End Function
この変更により、既存の価格の 2 倍以上の価格更新が発生すると、 ApplicationException
がスローされます。 DAL から発生した例外と同様に、この BLL で発生した ApplicationException
は、GridView の RowUpdated
イベント ハンドラーで検出および処理できます。 実際、 RowUpdated
イベント ハンドラーのコードは、記述されているように、この例外を正しく検出し、 ApplicationException
の Message
プロパティ値を表示します。 図 11 は、ユーザーが Chai の価格を $50.00 に更新しようとしたときのスクリーンショットを示しています。これは、現在の価格の 2 倍以上の $19.95 です。
図 11: ビジネス ルールは、製品の価格の 2 倍以上の値上げを禁止します (フルサイズの画像を表示する をクリックします)。
注
ビジネス ロジック ルールは、 UpdateProduct
メソッドのオーバーロードから一般的なメソッドにリファクタリングされるのが理想的です。 これは、読者の演習として残されています。
概要
挿入、更新、削除の操作中に、データ Web コントロールと ObjectDataSource の両方が、実際の操作を予約する事前および事後レベルのイベントを発生させます。 このチュートリアルと前のチュートリアルで説明したように、編集可能な GridView を操作すると、GridView の RowUpdating
イベントが発生し、その後に ObjectDataSource の Updating
イベントが発生し、その時点で ObjectDataSource の基になるオブジェクトに対して更新コマンドが実行されます。 操作が完了すると、ObjectDataSource の Updated
イベントが発生し、その後に GridView の RowUpdated
イベントが発生します。
入力パラメーターをカスタマイズするために、または操作の結果を検査して応答するために、事前レベルのイベントのイベント ハンドラーを作成できます。 ポストレベルのイベント ハンドラーは、操作中に例外が発生したかどうかを検出するために最も一般的に使用されます。 例外が発生した場合、これらの事後レベルのイベント ハンドラーは、必要に応じて独自に例外を処理できます。 このチュートリアルでは、わかりやすいエラー メッセージを表示して、このような例外を処理する方法について説明しました。
次のチュートリアルでは、データの書式設定の問題 (負の UnitPrice
の入力など) によって発生する例外の可能性を軽減する方法について説明します。 具体的には、編集インターフェイスと挿入インターフェイスに検証コントロールを追加する方法について説明します。
プログラミングに満足!
著者について
7 冊の ASP/ASP.NET 書籍の著者であり、4GuysFromRolla.com の創設者である Scott Mitchell は、1998 年から Microsoft Web テクノロジを使用しています。 Scott は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズ・ティーチ・セルフ ASP.NET 24時間で2.0です。 彼には mitchell@4GuysFromRolla.comで連絡できます。
特別な感謝
このチュートリアル シリーズは、多くの役に立つ校閲者によってレビューされました。 このチュートリアルのリード レビュー担当者は Liz Shulok でした。 今後の MSDN の記事を確認することに関心がありますか? その場合は、mitchell@4GuysFromRolla.comにメッセージを送ってください。