WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

2 つのテーブルの同時更新

by WebSurfer 21. August 2010 15:21

SQL Server DB の 2 つのテーブルから、INNER JOIN 句を用いてデータを抽出して GridView に表示し、それを編集・更新 (UPDATE) する Web アプリを考えます。

テーブルが 1 つなら、コードは一行も書かずに SqlDataSource を利用してウィザードベースでアプリを作成できますが、2 つのテーブルを一括更新する場合はそう簡単にはいきません。

それでも、できるだけ自力でコードを書かないで、ObjectDataSource と型付 DataSet + TableAdapter をウィザードベースで作って実現できないか考えて見ました。

ベースは MSDN ライブラリのチュートリアル「Walkthrough: Performing Bulk Updates to Rows Bound to a GridView Web Server Control 」で、そのテーブルを以下の 2 つのテーブルに変更することにします。(チュートリアルには日本語版もあったのですが、リンク切れになってしまいました)

CREATE TABLE [dbo].[table-1](
    [no] int NOT NULL,
    [name] nvarchar(50) NOT NULL,
 CONSTRAINT [PK_table-1] PRIMARY KEY CLUSTERED ([no] ASC)
)

CREATE TABLE [dbo].[table-2](
	[no] int NOT NULL,
	[tel] nvarchar(50) NOT NULL,
	[ban] nvarchar(50) NOT NULL,
 CONSTRAINT [PK_table-2] PRIMARY KEY CLUSTERED ([no] ASC)
)

まず、Visual Studio のウィザードで、型付 DataSet + TableAdapter(xsd ファイル)をつくります。以下のように、クエリビルダを使えば SELECT クエリは簡単に作成でき、それを基にした型付 DataSet も自動生成されます。

クエリビルダ

ただし、ここまでで自動生成された TableAdapter のコードには、更新に必要なメソッドは含まれていません。この先、以下のような方法で、2 つのテーブルを同時に UPDATE するコードを実装します。できるだけ自力でコードを書かないというのが条件です。

xsd ファイルを開いて、その TableAdapter にツールボックスから Query をドラッグ&ドロップ。クエリビルダで tablle-1 を UPDATE するクエリを作り、適当なメソッド名(例: UpdateQuery1)をつけて保存します。

同様に、table-2 を UPDATE するクエリを作り、適当なメソッド名(例:UpdateQuery2)をつけて保存します。結果は、以下のようになるはずです。

xsd ファイル

App_Code フォルダにクラスファイルを追加。UpdateQuery1 と UpdateQuery2 を使って2つのテーブルを UPDATE するメソッドを TableAdapter の partial class として実装します。ここは自力でコードを書く必要があります。以下のようになります。

using System;
using System.Data;
using System.Collections.Generic;
using System.Web;
using System.Transactions;
using System.ComponentModel;

namespace TwoTableDataSetTableAdapters
{
  public partial class DataTable1TableAdapter
  {
    [DataObjectMethod(DataObjectMethodType.Update)]
    public int UpdateTwoTables(int original_no, string name, string tel)
    {
      int returnValue;

      using (TransactionScope scope = 
        new TransactionScope(TransactionScopeOption.RequiresNew))
      {
        returnValue = this.UpdateQuery1(name, original_no);
        returnValue += this.UpdateQuery2(tel, original_no);
        scope.Complete();
      }
      return returnValue;
    }
  }
}

ObjectDataSource の「データソースの構成」で UPDATE メソッドに上記の partial class に作ったメソッドを選択します。

GridView を上記の ObjectDataSource に接続し、参考にした MSDN ライブラリのチュートリアルに従って、Template を編集し、コードを実装します。以下のようになります。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  // http://msdn.microsoft.com/ja-jp/library/aa992036(VS.80).aspx を参照
    
  private bool tableCopied = false;
  private DataTable originalDataTable;

  // 最初の行のバインディング中に、元のデータベース値のコピーが 
  // DataTable オブジェクトに格納され、さらにこのオブジェクトが 
  // ViewState に格納されます。
  protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
  {
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
      if (!tableCopied)
      {
        originalDataTable = 
          ((DataRowView)e.Row.DataItem).Row.Table.Copy();
        ViewState["originalValuesDataTable"] = originalDataTable;
        tableCopied = true;
      }
    }
  }

  protected void UpdateButton_Click(object sender, EventArgs e)
  {
    originalDataTable = (DataTable)ViewState["originalValuesDataTable"];

    // GridView コントロールの行を反復処理し、各行に対してカスタムの 
    // IsRowModified 関数を呼び出します。
    foreach (GridViewRow r in GridView1.Rows)
    {
      if (IsRowModified(r))
      {
        GridView1.UpdateRow(r.RowIndex, false);
      }
    }

    // Rebind the Grid to repopulate the original values table.
    tableCopied = false;
    GridView1.DataBind();
  }

  // このプロシージャは、編集可能な各 TextBox コントロールの値と、
  // キャッシュされた DataTable オブジェクトに格納された値の文字列
  // 比較を実行します。行が変更されている場合は true を返します。
  protected bool IsRowModified(GridViewRow r)
  {
    int currentNo = Convert.ToInt32(GridView1.DataKeys[r.RowIndex].Value);
    string currentName = ((TextBox)r.FindControl("nameTextBox")).Text;
    string currentTel = ((TextBox)r.FindControl("telTextBox")).Text;
    // フィルタ基準と一致するすべての DataRow オブジェクトを主
    // キーの順に(主キーがない場合は追加された順に) 配列として
    // 取得します。
    DataRow row = 
      originalDataTable.Select(String.Format("no = {0}", currentNo))[0];

    if (!currentName.Equals(row["name"].ToString()))
    {
      return true;
    }
    if (!currentTel.Equals(row["tel"].ToString()))
    {
      return true;
    }

    return false;
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>無題のページ</title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:ObjectDataSource ID="ObjectDataSource1" 
      runat="server" 
      OldValuesParameterFormatString="original_{0}" 
      SelectMethod="GetData" 
      TypeName="TwoTableDataSetTableAdapters.DataTable1TableAdapter" 
      UpdateMethod="UpdateTwoTables">
      <UpdateParameters>
        <asp:Parameter Name="original_no" Type="Int32" />
        <asp:Parameter Name="name" Type="String" />
        <asp:Parameter Name="tel" Type="String" />
      </UpdateParameters>
    </asp:ObjectDataSource>
    <asp:GridView ID="GridView1" 
      runat="server" 
      AutoGenerateColumns="False" 
      DataKeyNames="no" 
      DataSourceID="ObjectDataSource1" 
      onrowdatabound="GridView1_RowDataBound">
      <Columns>
        <asp:BoundField 
          DataField="no" 
          HeaderText="no" 
          ReadOnly="True" 
          SortExpression="no" />
        <asp:TemplateField 
          HeaderText="name" 
          SortExpression="name">
          <ItemTemplate>
            <asp:TextBox ID="nameTextBox" 
              runat="server" 
              Text='<%# Bind("name") %>'>
            </asp:TextBox>
          </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField 
          HeaderText="tel" 
          SortExpression="tel">
          <ItemTemplate>
            <asp:TextBox ID="telTextBox" 
              runat="server" 
              Text='<%# Bind("tel") %>'>
            </asp:TextBox>
          </ItemTemplate>
        </asp:TemplateField>
      </Columns>
    </asp:GridView>    
    <asp:Button ID="UpdateButton" 
      runat="server" 
      onclick="UpdateButton_Click" 
      Text="Update" />    
  </div>
  </form>
</body>
</html>

上記のコードおよび TableAdapter の partial class は自力でコードを書く必要がありますが、その他はすべて ウィザードベースでコードを書かずに実装できるはずです。

実行結果は、以下の画像にようになります。

実行時の画面

Tags: ,

ASP.NET

Eval の引数

by WebSurfer 20. August 2010 22:02

Eval の引数の文字列として使える文字に制限があることもついでに書いておきます。

Eval("xxxxx") の xxxxx は、先の記事で書きましたように、プロパティ名またはインデクサ名の文字列でなければならないそうです。

SQL Server の DB のデータを GridView に表示する場合、通常、xxxxx には DB のフィールド名が使用されます。

そこで、DB のフィールド名に "Name(ABC)" のように半角のカッコが入っていた場合どうなるでしょう?

Visual Studio のウィザードはなかなか賢くて、「Select ステートメントの構成」メニューで「テーブルまたはビューから列を指定します(T)」を選んで SELECT クエリを作ると、自動的に [NAME(ABC)] AS column1 というような別名を定義してくれるので問題ないです。(ただし、クエリビルダでクエリを組み立てるとダメです)

自分でクエリを組み立てる場合は要注意です。ちなみに、DB のフィールド名に "Name(ABC)" という文字列を使った場合、カッコが全角か半角かによって、結果は以下のように異なります。

SELECT クエリ Eval の引数 結果
[Name(ABC)]
半角カッコ
"Name(ABC)"
半角カッコ
System.ArgumentException: DataBinding: 'System.String' はインデックス付のアクセスを許可しません。
[Name(ABC)]
半角カッコ
"[Name(ABC)]"
半角カッコ
System.ArgumentException: NAME(ABC は、テーブル DefaultView の DataColumn でも DataRelation でもありません。
[Name(ABC)]
半角カッコ
"Name(ABC)"
全角カッコ
System.Web.HttpException: DataBinding: 'System.Data.DataRowView' には NAME(ABC) という名前のプロパティは含まれません。
[NAME(ABC)]
半角カッコ
"[Name(ABC)]"
全角カッコ
問題なし。
[Name(ABC)]
全角カッコ
"Name(ABC)"
全角カッコ
問題なし。
[Name(ABC)]
全角カッコ
"[Name(ABC)]"
全角カッコ
問題なし。

SELECT [NAME(ABC)] のカッコを半角から全角に変更してもデータを抽出できるのは、SQL Server のデフォルトの照合順序が Japanese_CI_AS(全角半角を区別しない)だからです。

Eval メソッドの引数で [ ] は特別な意味を持つらしいです。[ ] を使うと、SQL Server のデフォルトの照合順序にあわせて、全角文字と半角文字の区別をしなくなるような感じです。

一体どのように解釈されているのか訳が分かりませんが、とにかく、Eval メソッドの引数の中の文字に半角の( ) を使ってはダメで、使うと予期しない結果になるようです。

Tags: ,

ASP.NET

Eval, Bind の正体

by WebSurfer 18. August 2010 16:17

データバインド式と Eval メソッドの話が出てきたので、ついでに Eval, Bind メソッドとは一体何かについて調べたことを書いておきます。

前に書いたことのおさらいですが、データバインド式は <%# デリミタと %> デリミタの間に記述され、Eval メソッドと Bind メソッドを使用します。

Eval メソッドは、一方向 (読み取り専用) バインディングを定義するために使用します。Bind メソッドは、双方向 (更新可能) バインディングに使用します。

Eval メソッド

以下は、MSDN ライブラリの「データ バインド式の概要」からの抜粋です。

"Eval メソッドは、名前付けコンテナの現在のデータ項目を参照する DataBinder オブジェクトの Eval メソッドを実行時に呼び出します。"

上記を、自分の理解(独断と偏見?)に基づき、具体的に書いてみます。

例えば以下のように、GirdView の ItemTemplate に配置した Label の Text プロパティにデータバインド式が設定してあったとします。

Text='<%# Eval("Price") %>'

これは DataBinder.Eval 静的メソッドを以下のように呼び出します。

Text='<%# DataBinder.Eval(Container.DataItem, "Price") %>'

第 1 引数に使われている Container は ASP.NET によって自動的に定義され初期化されます。使用されているデータバインドコントロールによって異なりますが、たとえば GridView を使用した場合、Container は GridViewRow になります(詳細後述)。

第 2 引数は、第 1 引数の Container.DataItem オブジェクトから値を取得するプロパティ名またはインデクサ名の文字列となります。例えば、DictionaryEntry.Key の Key などのプロパティ名、または DataRowView["Price"] の Price などのインデクサ名から成る文字列である必要があります。(2010/8/20 一部修正)

DataBinder.Eval メソッドは、第 1 引数で指定したオブジェクトに対して、第 2 引数で指定した名前を持つプロパティの値またはインデクサが指す値を取得して、Object 型として返します。先の DictionaryEntry および DataRowView を例にとると、下記のようになります。(2010/8/20 一部修正)

((DictionaryEntry)Container.DataItem).Key
((DataRowView)Container.DataItem)["Price"]

Eval メソッドで、プログラマが明示的にキャストを書かなくてもよいのはリフレクション機能を利用しているからだそうです。

Bind メソッド

DataBinder クラスには Bind というメソッドはありません。Bind メソッドの詳細が書いてあるドキュメントも見つかりませんでしたが、以下の点以外は Eval と同じと思われます。

GridView、DetailsView、FormView などのデータバインドコントロールのテンプレートに配置した TextBox や CheckBox で Bind を使用すると、それらへの入力値を抽出して、編集モードでは NewValues コレクション経由、挿入モードでは Values コレクション経由で、データソースコントロール(SqlDataSource など)に渡すことができます。

Container.DataItem

DataBinder.Evalメソッドの第 1 引数のオブジェクト Container.DataItem とは何でしょう?

例えばデータバインドコントロールに GridView を使用すると、ASP.NET が自動生成するコードで Container は以下のように定義され、自動的に初期化されるようになっています。

GridViewRow Container;

その他、Repeater, DataGrid, DataList, DetailsView, FormView, ListView などのデータバインドコントロールを用いると、適宜 RepeaterItem, DataGridItem, DataListItem, DetailsView, FormView, ListViewDataItem などが Container として定義され、初期化されるようになっています。

詳しくは、@IT のサイトの Container.DataItemの正体は? を参照してください。

------------ 2010/8/20 追記 ------------

DataBinder.Eval メソッドの第 2 引数は、MSDN ライブラリには "プロパティ名またはフィールド名から成る文字列" と書いてありましたが、インデクサの場合もありますので修正しました。

@IT のサイトの Webフォームにおけるデータ連結 を参照してください。

Tags: ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  August 2020  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar