データバウンドコントロール (GridView など) からデータソースコントロール (SqlDataSource など) へのパラメータの渡し方について調べたことを書いておきます。
(1) データバウンドコントロール
簡単に言えば、データバウンドコントロールは、DataKeyNames プロパティ、子コントロール、ビューステートなど(他に DataKey などもあるかも)からパラメータ名と値を取得し、Keys, Values, OldValues, NewValues という IDictionary コレクションを作成して、それをデータソースコントロールのパラメータに渡すということだそうです。
詳しくは、MSDN ライブラリ の「データソースコントロールがデータ連結フィールドのパラメーターを作成する方法」に書いてあります。
これによると、IDictionary コレクションは、以下のように各操作(のパラメータ)に渡されているそうです。
IDictionary
コレクション
|
内容
|
操作
|
Values
|
新しい値(主キー含む)
|
Insert
|
Keys
|
元の主キー値
|
Update, Delete
|
NewValues
|
新しい値(主キー含む)
|
Update
|
OldValues
|
元の値(主キー除く)
|
Update, Delete
|
主キーも Update する場合、新しい主キー値は NewValues に含まれるとイベントハンドラのドキュメントに書いてありました。
Insert の時の新しい主キー値がどれに含まれるか、はっきり書いてあるドキュメントは見つけられませんでしたが、Values 以外にはなさそうです。
Keys, Values, OldValues, NewValues には、データバインドコントロールの Deleting, Deleted, Inserting, Inserted, Updating, Updated イベントのハンドラの引数を通じてアクセスできます。
それを使って、データベースに書き込む前の値の検証や HTML エンコードを行ったり、新旧データをログに残したりできるということです。
イベントハンドラのドキュメントで、Deleting, Deleted イベントのハンドラの引数から IDictionary コレクションにアクセスするためのプロパティ名が Keys と Values になっていることがちょっと気になります。
Keys と OldValues のはずなんですが・・・
この Values というのは単なるプロパティ名であって、中身は OldValues であろうと勝手に解釈し、上にリンクを張った MSDN ライブラリの記述は正しいと信じることにしました。(笑)
(2) データソースコントロール
データソースコントロール (SqlDataSource, ObjectDataSource など) 側では、DeleteParameters, InsertParameters, UpdateParameters パラメータコレクションに必要なパラメータが設定され、それにデータバウンドコントロールからのデータが渡されるのは間違いないようです。
ただ、それらのパラメータがどのように作られるか、Keys, Values, OldValues, NewValues との対応がどうなっているかは正直言ってよく分かりません。
想像も含みますが、自分が調べた範囲で分かったことを書いておきます。
(2.1) SqlDataSource
SqlDataSource の場合は、SELECT クエリだけは自分で組み立てるものの、後はウィザード任せで自動生成することがほとんどなので、結果として出来上がったパラメータしか見えないです。
ですが、楽観的同時実行制御を行うか否かによって、ConflictDetection, OldValuesParameterFormatString の設定が変わってきて、それによってできるパラメータが変わってくるだけのようです。
例えば、ウィザードで作った SqlDataSource のパラメータコレクションにあるパラメータと、たぶん内部的に作られる ADO.NET の SqlCommand のパラメータの関係は以下ようになっていると思われます。
<asp:Parameter Name="xxx" Type="Int32" />
↓
SqlCommand.Parameters.Add(new SqlParameter("@xxx", SqlDbType.Int))
上記の SqlParameter の Value に、該当する Keys, Values, OldValues, NewValues から自動的に値が渡されるようです(このあたりは外から見えないので、確証はないのですが)。
楽観的同時実行制御なしの場合、デフォルトでは OldValuesParameterFormatString が設定されない(デフォルトで "{0}" になる)ので、パラメータ名が original_xxx にはなりません。この点ちょっと紛らわしいのですが、各パラメータには、Keys, Values, NewValues から正しく値が渡されるようです。例えば、主キーが id、その他のパラメータが name とすると、以下のようになるはずです。
パラメータコレクション
|
IDictionary コレクション
|
DeleteParameters
|
id ← Keys
|
InsertParameters
|
id ← Values name ← Values
|
UpdateParameters
|
id ← Keys name ← NewValues
|
楽観的同時実行制御オプションを有効にすると、ConflictDetection が "CompareAllValues" に、OldValuesParameterFormatString が "original_{0}" に自動的に設定されます。結果、DELETE, INSERT, UPDATE クエリの WHERE 句に、元の値との相違をチェックするための条件が追加され(例えば、WHERE [xxx] = @original_xxx ... のように)、それに応じてパラメータにも original_xxx が追加されます。この場合は、元の値は original_xxx に、新しい値は xxx に代入されるので分かりやすいです。例えば、主キーが id、その他のパラメータが name とすると、以下のようになるはずです。
2012/6/23 注記追加:下の表の DeleteParameters 列で original_name ← OldValues と書きましたが、Deleting, Deleted イベントのハンドラの引数から OldValues 相当の IDictionary コレクションにアクセスするためのプロパティ名は Values(OldValues ではなくて)になりますので注意してください。
パラメータコレクション
|
IDictionary コレクション
|
DeleteParameters
|
original_id ← Keys original_name ← OldValues
|
InsertParameters
|
id ← Values name ← Values
|
UpdateParameters
|
original_id ← Keys id ← NewValues original_name ← OldValues name ← NewValues
|
(2.2) ObjectDataSource
SQL Server のテーブルを基に、型付 DataSet + TableAdapter を作���た場合、その TableAdapter クラスの中に Select, Insert, Delete, Update メソッドが定義されます。それらのメソッドをベースに ObjectDataSource をウィザードベースで作った場合は、そのメソッドに定義されている引数に合わせてパラメータが作られます。
データバウンドコントロールの Keys, Values, OldValues, NewValues コレクションは、パラメータを通じて、該当する引数に渡されます。ウィザードに任せて TableAdapter と ObjectDataSource を作れば、たぶんほとんどのケースで、渡される Keys, Values, OldValues, NewValues コレクションと、それを受け取るパラメータの間に不整合を生ずることはないはずです。
ただし、自力でメソッドのコードを書く場合や、データソースに主キーがない場合は、以下の点に注意が必要です。
(2.2.1) OldValuesParameterFormatString の設定
ウィザードでデフォルト設定のまま ObjectDataSource を作っていくと、どういう条件になっているのか分かりませんが、楽観的同時実行制御なしの場合でも OldValuesParameterFormatString="original_{0}" の設定がされることがあります。
そうすると、更新もしくは削除操作の際、主キーについては xxx と original_xxx の両方を引数に加えて Update, Delete メソッドを呼び出すようです。
楽観的同時実行制御なしで自力でメソッドのコードを書いた場合、引数に新旧両方の値を定義する必要はなく、普通はたぶん xxx のみしか定義しないと思います。
それゆえ、更新もしくは削除操作の際、"... original_xxx を含む非ジェネリックメソッド 'Update/Delete' が見つかりませんでした。" という例外がスローされ、悩むことになります。
ちょっとくどいかもしれませんが、具体的に、Microsoft ASP.NET のチュートリアル "Creating a Business Logic Layer" のコードを例にとって説明します。
サンプルコードを見ると、DeleteProduct, UpdateProduct メソッドの引数には、主キーとしては productID のみしか定義されていません。
このサンプルコードをベースにして、ObjectDataSource をウィザードで作っていった場合、引数に original_pruductID は定義されていないにもかかわらず、 OldValuesParameterFormatString="original_{0}" が設定されます(自分で試した限りですが)。
そうすると、例えば更新操作を行った場合、"ObjectDataSource 'ObjectDataSource1' では、パラメータ ..., productID, original_productID を含む非ジェネリック メソッド 'UpdateProduct' が見つかりませんでした。" という InvalidOperationException 例外がスローされます。
対応策としては、OldValuesParameterFormatString="original_{0}" を削除するか、使わなくても、xxx と original_xxx の両方を Update, Delete の引数には、定義しておくことです。
(2.2.2) データソースに主キーがない場合
データソースが xml ファイルなどで、主キーが設定されておらず、しかも更新や削除操作のため自力でそのためのクラスを書くような場合は特に注意が必要です。
例としては MSDN ライブラリの「GridView で XML ファイルをデータ ソースとして使いレコードを編集する方法」のような場合です。(そもそも、そのサンプルコードは間違っているようですが)
上記のページの例のように、「XML ファイル操作用のクラス」を作って、それをベースに GridView と ObjectDataSource をウィザードベースで作成するとうまくいきません。
id と name の両方を更新する場合、以下の変更が必要です。(サンプルは id は更新しないという前提で書かれているようですが)
-
GridView に DataKeyNames="id" を追加。そもそも DataKeyNames を定義しないと、主キーが ObjectDataSource に渡されません。
-
ObjectDataSource に OldValuesParameterFormatString="original_{0}" を追加。これがないと id の新旧の区別ができません。(上に、「楽観的同時実行制御なしの場合でも、デフォルトで OldValuesParameterFormatString="original_{0}" の設定がされます。」と書きましたが、何故かこの場合は定義されません。主キーがないから?)
-
GridView で AutoGenerateColumns="False" とし、id と name の BoundField を定義。こうしないと、id 列が TextBox にならず、更新できません。
-
「XML ファイル操作用のクラス」の UpdateDataSet の引数に original_id を追加。original_id で DataSet の行を検索し、ヒットした行の当該項目を id に書き換えるよう修正する。
上記のリンク先の MSDN ライブラリのコードを修正し、Delete もできるようにしたサンプルを、後日、作成して書き込んでおくことにします。