ユーザー入力を使ってデータベースの更新を行うケースで、ユーザー入力の検証結果が NG の場合、先にデータバインドコントロール(ここでは DetailsView を例にします)にユーザーが入力したデータを維持したままユーザーに再入力を促すにはどうすればよいかという話です。
ASP.NET には、ユーザー入力を検証するために RegularExpressionValidator, CustomValidator 等の検証のためのコントロールが用意されており、通常はこれらを組み合わせて使えば簡単に要件は満たせ��す。
ただし、検証するタイミングが更新の直前でなければならず、かつ DB ベースから値を取得して検証し、結果 NG ならロールバックしなければならないような場合は、上記の検証コントロールを使って目的を果たすのは難しいです。
その場合は、ObjectDataSource と型付 DataSet + TableAdapter を DetailsView と組み合わせた 3 層構造とし、検証やロールバック等の処理は TableAdapter を拡張したメソッドを作成して行うのが良いと思いますが、検証結果が NG の時 DetailsView にユーザーが入力したデータを維持するには工夫が必要です。
例えば、拡張したメソッドの戻り値などで検証結果 NG を判断して、DetailsView を挿入モードに保ってユーザーに再入力を促すことはできます。しかしながら、その前に DetailsView に DataBind が起こって、せっかくユーザーが入力したデータがすべて消えてしまいます。
それを避けるためには、拡張したメソッドで検証結果 NG と判定された場合は、そこで例外をスローすることです。ただし、スローしっぱなしではサーバーエラーで終わってしまうので、スローされた例外を処置しなければなりません。
処置と言っても、拡張したメソッド内で try - catch で行うのではありません。拡張したメソッドは、検証結果 NG の場合に特定の例外をスローするだけです。
処置は ItemInserted イベントのイベントハンドラに渡された DetailsViewInsertedEventArgs オブジェクトを使用して行います。
検証結果 NG でスローされた特定の例外を捕捉した場合のみ ExceptionHandled プロパティを true に設定することで例外は処置されたと見なされます。
以下に検証に使用した aspx ページのコードをアップしておきます。ArgumentNullException と NorthwindDataException のみを捕捉し、ExceptionHandled プロパティを true に設定しています。
<%@ Page Language="C#" %>
<%@ Import Namespace="ProductsDataSetTableAdapters" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
MessageLabel.Text = String.Empty;
}
protected void DetailsView1_ItemInserted(object sender,
DetailsViewInsertedEventArgs e)
{
if (e.Exception == null && e.AffectedRows == 1)
{
MessageLabel.Text = "Insert に成功";
}
else
{
if (e.Exception != null)
{
Exception innerEx = e.Exception.InnerException;
if (innerEx is ArgumentNullException ||
innerEx is ProductsTableAdapter.NorthwindDataException)
{
MessageLabel.Text = innerEx.Message;
e.ExceptionHandled = true;
}
}
else
{
MessageLabel.Text = "例外の発生なし。";
}
e.KeepInInsertMode = true;
}
}
protected void DetailsView1_ItemUpdated(object sender,
DetailsViewUpdatedEventArgs e)
{
if (e.Exception == null && e.AffectedRows == 1)
{
MessageLabel.Text = "Update に成功。";
}
else
{
if (e.Exception != null)
{
Exception innerEx = e.Exception.InnerException;
if (innerEx is ArgumentNullException ||
innerEx is ProductsTableAdapter.NorthwindDataException)
{
MessageLabel.Text = innerEx.Message;
e.ExceptionHandled = true;
}
}
else
{
MessageLabel.Text = "例外の発生なし。";
}
e.KeepInEditMode = true;
}
}
protected void DetailsView1_DataBinding(object sender,
EventArgs e)
{
// 例外が出ない場合は DataBind がかかる。
// 結果、ユーザー入力が消える。
MessageLabel.Text += " DataBinding イベントが発生";
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h2>挿入、更新操作中の例外の処置</h2>
<asp:ObjectDataSource ID="ObjectDataSource1"
runat="server"
SelectMethod="GetData"
DeleteMethod="Delete"
InsertMethod="ThrowExceptionBeforeInsert"
UpdateMethod="ThrowExceptionBeforeUpdate"
OldValuesParameterFormatString="original_{0}"
TypeName="ProductsDataSetTableAdapters.ProductsTableAdapter">
<DeleteParameters>
<asp:Parameter Name="Original_ProductID" Type="Int32" />
</DeleteParameters>
<InsertParameters>
<asp:Parameter Name="ProductName" Type="String" />
<asp:Parameter Name="SupplierID" Type="Int32" />
<asp:Parameter Name="CategoryID" Type="Int32" />
<asp:Parameter Name="QuantityPerUnit" Type="String" />
<asp:Parameter Name="UnitPrice" Type="Decimal" />
<asp:Parameter Name="UnitsInStock" Type="Int16" />
<asp:Parameter Name="UnitsOnOrder" Type="Int16" />
<asp:Parameter Name="ReorderLevel" Type="Int16" />
<asp:Parameter Name="Discontinued" Type="Boolean" />
</InsertParameters>
<UpdateParameters>
<asp:Parameter Name="ProductName" Type="String" />
<asp:Parameter Name="SupplierID" Type="Int32" />
<asp:Parameter Name="CategoryID" Type="Int32" />
<asp:Parameter Name="QuantityPerUnit" Type="String" />
<asp:Parameter Name="UnitPrice" Type="Decimal" />
<asp:Parameter Name="UnitsInStock" Type="Int16" />
<asp:Parameter Name="UnitsOnOrder" Type="Int16" />
<asp:Parameter Name="ReorderLevel" Type="Int16" />
<asp:Parameter Name="Discontinued" Type="Boolean" />
<asp:Parameter Name="Original_ProductID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
<asp:DetailsView ID="DetailsView1"
runat="server"
AllowPaging="True"
AutoGenerateRows="False"
DataKeyNames="ProductID"
DataSourceID="ObjectDataSource1"
OnItemInserted="DetailsView1_ItemInserted"
OnItemUpdated="DetailsView1_ItemUpdated"
OnDataBinding="DetailsView1_DataBinding">
<Fields>
<asp:BoundField DataField="ProductID"
HeaderText="ProductID"
InsertVisible="False"
ReadOnly="True"
SortExpression="ProductID" />
<asp:BoundField DataField="ProductName"
HeaderText="ProductName"
SortExpression="ProductName" />
<asp:BoundField DataField="SupplierID"
HeaderText="SupplierID"
SortExpression="SupplierID" />
<asp:BoundField DataField="CategoryID"
HeaderText="CategoryID"
SortExpression="CategoryID" />
<asp:BoundField DataField="QuantityPerUnit"
HeaderText="QuantityPerUnit"
SortExpression="QuantityPerUnit" />
<asp:BoundField DataField="UnitPrice"
HeaderText="UnitPrice"
SortExpression="UnitPrice" />
<asp:BoundField DataField="UnitsInStock"
HeaderText="UnitsInStock"
SortExpression="UnitsInStock" />
<asp:BoundField DataField="UnitsOnOrder"
HeaderText="UnitsOnOrder"
SortExpression="UnitsOnOrder" />
<asp:BoundField DataField="ReorderLevel"
HeaderText="ReorderLevel"
SortExpression="ReorderLevel" />
<asp:CheckBoxField DataField="Discontinued"
HeaderText="Discontinued"
SortExpression="Discontinued" />
<asp:CommandField ShowDeleteButton="True"
ShowEditButton="True"
ShowInsertButton="True" />
</Fields>
</asp:DetailsView>
<asp:label id="MessageLabel"
forecolor="Red"
runat="server"/>
</div>
</form>
</body>
</html>
TableAdapter を拡張したメソッドは以下の通りです。partial class として別ファイルにコードを書いて簡単に拡張できます。なお、以下のコードは検証用ですので、例外をスローするだけで、データーベースを更新するコードは含まれていません。
using System;
using System.Data;
using System.Configuration;
using System.Data.SqlClient;
using System.ComponentModel;
namespace ProductsDataSetTableAdapters
{
public partial class ProductsTableAdapter
{
public virtual int ThrowExceptionBeforeInsert(
string ProductName,
Nullable<int> SupplierID,
Nullable<int> CategoryID,
string QuantityPerUnit,
Nullable<decimal> UnitPrice,
Nullable<short> UnitsInStock,
Nullable<short> UnitsOnOrder,
Nullable<short> ReorderLevel,
bool Discontinued)
{
if (ProductName == null)
{
throw new ArgumentNullException("ProductName");
}
else if (ProductName == "xxx")
{
throw new ApplicationException("ハンドルされない例外");
}
else if (ProductName == "zzz")
{
return 0;
}
else
{
throw new NorthwindDataException("Insert で例外。");
}
}
public virtual int ThrowExceptionBeforeUpdate(
Nullable<int> Original_ProductID,
string ProductName,
Nullable<int> SupplierID,
Nullable<int> CategoryID,
string QuantityPerUnit,
Nullable<decimal> UnitPrice,
Nullable<short> UnitsInStock,
Nullable<short> UnitsOnOrder,
Nullable<short> ReorderLevel,
bool Discontinued)
{
if (ProductName == null)
{
throw new ArgumentNullException("ProductName");
}
else if (ProductName == "xxx")
{
throw new ApplicationException("ハンドルされない例外");
}
else if (ProductName == "zzz")
{
return 0;
}
else
{
throw new NorthwindDataException("Update で例外。");
}
}
public class NorthwindDataException : Exception
{
public NorthwindDataException(string msg) : base(msg) { }
}
}
}