WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

DataObjectTypeName と Delete 操作

by WebSurfer 2013年5月24日 17:12

ObjectDataSource コントロールの DataObjectTypeName プロパティを利用する場合、それがバインドされるデータバウンドコントロール(GridView, ListView など)の DataKeyNames プロパティを適切に設定しないと、Delete 操作に失敗するという話です。

DataObjectTypeName を使った場合、ウィザードベースでは DataKeyNames プロパティは自動的に設定されないので注意が必要です。DataKeyNames プロパティを設定しなくても Insert, Update は成功するので、何が原因なのか見当がつかず結構ハマりました。

再びハマって時間を無駄にすることがないように、何故 Update と Insert は問題なく Delete だけがダメなのか、何故 DataKeyNames を設定すると問題が解決するのかを備忘録として書いておきます。

先の記事「SqlDataSource などのパラメータ」に書きましたように、ListView などのデータバウンドコントロールは、DataKeyNames プロパティ、子コントロール、ビューステートなどからパラメータ名と値を取得し、Keys, Values, OldValues, NewValues という IDictionary コレクションを作成します。

Update, Insert, Delete 操作を行うためにはパラメータが必要ですが、それらの値は Keys, Values, OldValues, NewValues コレクションから取得され、ObjectDataSource コントロールの UpdateMethod, InsertMethod, DeleteMethod プロパティに指定されている各メソッドの引数として渡されます。

普通、各メソッドの引数には、ObjectDataSource コントロールのパラメータコレクションから複数のパラメータを渡すことが多いですが、代わりに DataObjectTypeName に指定したオブジェクトを 1 つ生成して、それをメソッドの引数として渡すこともできます。

その具体例を以下のコードに示します。

以下のクラスファイルには、ObjectDataSource と Forms 認証用のデータベースの間に位置して Select, Update, Insert, Delete 操作を行う AggregateData クラスというビジネスロジックと、ObjectDataSource の DataObjectTypeName プロパティに設定される UserInfo クラスの定義が含まれています。

using System.Collections.Generic;
using System.ComponentModel;
using System.Web.Security;

public class UserInfo
{
    public string UserName { get; set; }
    public string Email { get; set; }
}


public class AggregateData
{
    public AggregateData()
    {

    }

    [DataObjectMethod(DataObjectMethodType.Select, true)]
    public List<UserInfo> GetAllUserData()
    {
        List<UserInfo> Data = new List<UserInfo>();
        MembershipUserCollection users = Membership.GetAllUsers();

        foreach (MembershipUser user in users)
        {
            UserInfo info = new UserInfo();

            info.UserName = user.UserName;
            info.Email = user.Email;
            Data.Add(info);
        }
        return Data;
    }

    [DataObjectMethod(DataObjectMethodType.Update, true)]
    public void UpdateUserData(UserInfo user)
    {
        // 省略
    }

    [DataObjectMethod(DataObjectMethodType.Delete, true)]
    public void DeleteUserData(UserInfo user)
    {
        // 省略
    }

    [DataObjectMethod(DataObjectMethodType.Insert, true)]
    public void InsertUserData(UserInfo user)
    {
        // 省略
    }
}

前のコード例で使用されている 2 つのクラスを使用する aspx ページを次のコード例を以下に示します。上のクラスファイルをベースにして、ほとんどのコードを Visual Studio のウィザードで自動生成できますが、ListView の DataKeyNames プロパティは自動生成されたコードには含まれないことに注意してください。

<%@ Page Language="C#" %>

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

<script runat="server">

</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" 
            DataObjectTypeName="UserInfo" 
            DeleteMethod="DeleteUserData" 
            InsertMethod="InsertUserData" 
            SelectMethod="GetAllUserData" 
            TypeName="AggregateData" 
            UpdateMethod="UpdateUserData">
        </asp:ObjectDataSource>

        <asp:ListView ID="ListView1" 
            runat="server" 
            DataSourceID="ObjectDataSource1" 
            InsertItemPosition="LastItem" 
            DataKeyNames="UserName">
            <EditItemTemplate>
                <tr style="">
                    <td>
                        <asp:Button ID="UpdateButton" 
                            runat="server" 
                            CommandName="Update" 
                            Text="更新" />
                        <asp:Button ID="CancelButton" 
                            runat="server" 
                            CommandName="Cancel" 
                            Text="キャンセル" />
                    </td>
                    <td>
                        <asp:Label ID="UserNameLabel" 
                            runat="server" 
                            Text='<%# Bind("UserName") %>' />
                    </td>
                    <td>
                        <asp:TextBox ID="EmailTextBox" 
                            runat="server" 
                            Text='<%# Bind("Email") %>' />
                    </td>
                </tr>
            </EditItemTemplate>
            <InsertItemTemplate>
                <tr style="">
                    <td>
                        <asp:Button ID="InsertButton" 
                            runat="server" 
                            CommandName="Insert" 
                            Text="挿入" />
                        <asp:Button ID="CancelButton" 
                            runat="server" 
                            CommandName="Cancel" 
                            Text="クリア" />
                    </td>
                    <td>
                        <asp:TextBox ID="UserNameTextBox" 
                            runat="server" 
                            Text='<%# Bind("UserName") %>' />
                    </td>
                    <td>
                        <asp:TextBox ID="EmailTextBox" 
                            runat="server" 
                            Text='<%# Bind("Email") %>' />
                    </td>
                </tr>
            </InsertItemTemplate>
            <ItemTemplate>
                <tr style="">
                    <td>
                        <asp:Button ID="DeleteButton" 
                            runat="server" 
                            CommandName="Delete" 
                            Text="削除" />
                        <asp:Button ID="EditButton" 
                            runat="server" 
                            CommandName="Edit" 
                            Text="編集" />
                    </td>
                    <td>
                        <asp:Label ID="UserNameLabel" 
                            runat="server" 
                            Text='<%# Eval("UserName") %>' />
                    </td>
                    <td>
                        <asp:Label 
                            ID="EmailLabel" 
                            runat="server" 
                            Text='<%# Eval("Email") %>' />
                    </td>
                </tr>
            </ItemTemplate>
            <LayoutTemplate>
                <table runat="server">
                    <tr runat="server">
                        <td runat="server">
                            <table ID="itemPlaceholderContainer" 
                                runat="server" 
                                border="0" 
                                style="">
                                <tr runat="server" style="">
                                    <th runat="server">
                                    </th>
                                    <th runat="server">
                                        UserName</th>
                                    <th runat="server">
                                        Email</th>
                                </tr>
                                <tr ID="itemPlaceholder" 
                                    runat="server">
                                </tr>
                            </table>
                        </td>
                    </tr>
                    <tr runat="server">
                        <td runat="server" style="">
                        </td>
                    </tr>
                </table>
            </LayoutTemplate>
        </asp:ListView>
    </div>
    </form>
</body>
</html>

上記のコードのように、ObjectDataSource の DataObjectTypeName プロパティに UserInfo クラスを指定することによって、UserInfo クラスから生成されたオブジェクトが AggregateData クラスの UpdateUserData, InsertUserData, DeleteUserData メソッドの引数として渡されるようになります。

そのオブジェクトを生成する際に、ListView の Keys, Values, OldValues, NewValues コレクションからデータを取得します。Keys, Values, OldValues, NewValues コレクションのどれからデータを得るかは、Update、Insert、Delete 操作によって異なります。具体的には以下のとおりです。

  • Update: NewValues
  • Insert: Values
  • Delete: Keys

Keys コレクションには ListView の DataKeyNames プロパティに指定した項目(上のコードの例では UserName )の値が入ります。DataKeyNames プロパティを指定しないと Keys コレクションは空になります。Keys コレクションが空でも、DeleteUserData メソッドの引数 user に渡す UserInfo オブジェクト自体は生成されますが、Keys が空なのでオブジェクト中のフィールドは空になり(user.UserName は null になり)Delete 操作に失敗します。

一方、UpdateUserData, InsertUserData メソッドに渡すオブジェクトには、それぞれ NewValues, Values コレクションから値が渡されます。それらには、DataKeyNames プロパティを指定しなくても、UserName 他すべての新しい値が含まれているので、Update, Delete 操作は問題なく完了するというわけです。

なお、Delete 操作の場合、UserName の値は Label からでなく DataKeyNames から取得するので、ItemTemplate の中の Text='<%# Eval("UserName") %>' は Eval のままで(Bind にしなくても)問題ありません。

Tags: ,

ASP.NET

DataGrid, GridView に動的に列を追加

by WebSurfer 2013年5月17日 16:41

あまり使い道はないと思いますが、DataGrid と GridView に動的に列を追加する方法を備忘録として書いておきます。 (【2021/9/21 追記】もっと簡単かつスマートにできる方法がありました。詳しくは「DataGrid, GridView に動的に列を追加 (2)」を見てください)

DataGrid に動的に列を追加

DataGrid, GridView ともヘッダ、フッタを含め各行は TableCellCollection で構成されているので、そのコレクションの適切な位置に TableCell を追加することが基本です。

追加したセルの中に文字列や CheckBox などを配置したい場合は、当該セルの ControlCollection に LiteralControl や CheckBox を初期化して追加します。

追加するタイミングは、DataGrid なら ItemCreated イベント、GridView なら RowCreated イベントがよさそうです。

その例を以下のコードに示します。上の画像を表示したものです。実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。

<%@ 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">

  // 表示用のデータソース (DataView) を生成
  ICollection CreateDataSource()
  {
    DataTable dt = new DataTable();
    DataRow dr;
       
    dt.Columns.Add(new DataColumn("Item", typeof(Int32)));
    dt.Columns.Add(new DataColumn("Name", typeof(string)));
    dt.Columns.Add(new DataColumn("Price", typeof(decimal)));

    for (int i = 0; i < 5; i++)
    {
      dr = dt.NewRow();

      dr["Item"] = i;
      dr["Name"] = "Name-" + i.ToString();
      dr["Price"] = 1.23m * (i + 1);

      dt.Rows.Add(dr);
    }

    DataView dv = new DataView(dt);
    return dv;
  }

  // 初回のみデータソースを生成してバインド(ポストバック
  // 時は ViewState から自動的にデータを取得するので不要)
  protected void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack)
    {
      ICollection dv = CreateDataSource();
      DataGrid1.DataSource = dv;
      DataGrid1.DataBind();
      GridView1.DataSource = dv;
      GridView1.DataBind();
    }
  }
    
  // GataGrid.ItemCreated イベントで列を動的に追加する。
  // ItemDataBound イベントで行うと以下の問題があるので
  // 注意:
  // (1) ポストバック時にはイベントが発生しないので追加
  //     行を再生できず、結果消えてしまう。
  // (2) 追加コントロールの LoadViewState, LoadPostData 
  //     の呼び出しがうまくいかない。結果、以下の例では 
  //     CheckBox の CheckedChanged イベントの発生が期
  //     待通りにならない。
  protected void DataGrid1_ItemCreated(object sender, 
    DataGridItemEventArgs e)
  {
    if (e.Item.ItemType == ListItemType.Header)
    {
      // ヘッダ行に列(セル)を追加。セルの中にリテ
      // ラルを配置。
      TableCell cell = new TableCell();
      cell.Controls.Add(new LiteralControl("追加列"));
      e.Item.Cells.Add(cell);
    }
    else if (e.Item.ItemType == ListItemType.Item ||
      e.Item.ItemType == ListItemType.AlternatingItem)
    {
      // データ行に列(セル)を追加。セルの中に
      // CheckBox を配置。
      TableCell cell = new TableCell();
      CheckBox cb1 = new CheckBox();
      cb1.AutoPostBack = true;
      cb1.ID = "CheckBoxInItemIndex-" + 
        e.Item.ItemIndex.ToString();
      cb1.CheckedChanged += 
        new EventHandler(cb1_CheckedChanged);
      cell.Controls.Add(cb1);
      e.Item.Cells.Add(cell);
    }
    else if (e.Item.ItemType == ListItemType.Footer)
    {
      // フッタ行に列(セル)を追加。セルの中にリテ
      // ラルを配置。
      TableCell cell = new TableCell();
      cell.Controls.Add(new LiteralControl("追加列"));
      e.Item.Cells.Add(cell);
    }
  }

  protected void cb1_CheckedChanged(object sender, 
    EventArgs e)
  {
    CheckBox cb = (CheckBox)sender;
    Label1.Text = cb.ID + " clicked to " + 
      cb.Checked.ToString();
  }

  // DataGrid の場合と同様な理由で RowDataBound イベ
  // ントではなく RowCreated イベントで動的に列を追
  // 加する。
  protected void GridView1_RowCreated(object sender, 
    GridViewRowEventArgs e)
  {
    if (e.Row.RowType == DataControlRowType.Header)
    {
      // ヘッダ行に列(セル)を追加。セルの中にリテ
      // ラルを配置。
      // GridView のヘッダ行は th 要素が使われるので、
      // TableCell でなく TableHeaderCell を用いる。
      TableHeaderCell hc = new TableHeaderCell();
      hc.Controls.Add(new LiteralControl("追加列"));
      e.Row.Cells.Add(hc);
    }
    else if (e.Row.RowType == DataControlRowType.DataRow)
    {
      // データ行に列(セル)を追加。セルの中に
      // CheckBox を配置。
      TableCell cell = new TableCell();
      CheckBox cb2 = new CheckBox();
      cb2.AutoPostBack = true;
      cb2.ID = "CheckBoxInRowIndex-" + 
        e.Row.RowIndex.ToString();
      cb2.CheckedChanged += 
        new EventHandler(cb2_CheckedChanged);
      cell.Controls.Add(cb2);
      e.Row.Cells.Add(cell);
    }
    else if (e.Row.RowType == DataControlRowType.Footer)
    {
      // フッタ行に列(セル)を追加。セルの中にリテ
      // ラルを配置。
      TableCell cell = new TableCell();
      cell.Controls.Add(new LiteralControl("追加列"));
      e.Row.Cells.Add(cell);
    }
  }

  protected void cb2_CheckedChanged(object sender, 
    EventArgs e)
  {
    CheckBox cb = (CheckBox)sender;
    Label2.Text = cb.ID + " clicked to " + 
      cb.Checked.ToString();
  }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>WebSurfer's Page - 実験室</title>
</head>
<body>
  <form id="form1" runat="server">

    <h2>DataGrid</h2>
    <asp:DataGrid ID="DataGrid1"
      runat="server" 
      ShowFooter="True" 
      OnItemCreated="DataGrid1_ItemCreated">
    </asp:DataGrid>
    <asp:Label ID="Label1" runat="server" />

    <h2>GridView</h2>
    <asp:GridView ID="GridView1" 
      runat="server" 
      ShowFooter="True" 
      OnRowCreated="GridView1_RowCreated">
    </asp:GridView>
    <asp:Label ID="Label2" runat="server" />

  </form>
</body>
</html>

Tags: ,

ASP.NET

SqlCommand の Dispose は呼ぶべきか?

by WebSurfer 2013年4月23日 13:58

結論は呼ぶべきなのですが、そのあたりのことについて調べて新発見があったので、備忘録として書いておきます。

SqlConnection クラスの Close メソッド(Dispose と機能的に同じ)を明示的に呼び出して接続を閉じることは、リソースリーク防止のため重要であることはよく知られていると思います。

詳しくは、.NETの例外処理 Part.2 を見てください。特に、そのページに書かれている、"GC の仕組みで防がれるリークはメモリリークである、という点です。実は、アプリケーションにおけるリークには大別してメモリリークとリソースリークがあり、リソースリークは GC のみで防止することができません。" という点に注目です。

では、SqlCommand クラスや SqlDataReader クラスの場合はどうでしょうか? MSDN ライブラリのサンプルコードでも Dispose メソッドを呼び出してないケースが多々見られます。SqlConnection クラスと違ってリソースリークの問題はなさそうですが、メモリリーク防止のため Dispose メソッドを呼び出す必要はないのでしょうか?

MSDN ライブラリの Dispose メソッドの実装 を見ると、"マネージリソースのみを使用する型は、ガベージコレクターによって自動的にクリアされるため、このような型で Dispose メソッドを実装しても、パフォーマンス上の利点はありません。" とのことです。(.NET 4.6 / 4.5 の記事にはその説明はありませんが同じことかと思います)

ということは、アンマネージリソースを使用していない SqlCommand などでは、Dispose メソッドを呼び出してもパフォーマンス上の利点はないということになってしまいます。

しかし、実は、Dispose メソッドには、メモリ開放の機能以外に、GC.SuppressFinalize メソッド を実装することにより、冗長なファイナライザーの呼び出しを防ぐことができるという利点があるそうです。

そのあたりは、MSDN フォーラムの Should I call Dispose on a SQLCommand object? というページで、Microsoft (MSFT) の方が "To prevent a finalizer from running, most well written Dispose implementations call a special method called GC.SuppressFinalize, which indicates to the GC that its finalizer shouldn't be run when it falls out of scope (as the Dispose method did the clean up). The component class (which remember the SqlCommand indirectly inherits from), implements such a Dispose method. Therefore to prevent the finalizer from running (and even though it does nothing in the case of SqlCommand), you should always call Dispose." と述べてられている通りだと思います。

また、Dispose メソッドの実装 の説明にも、"Dispose メソッドは、破棄するオブジェクトの SuppressFinalize メソッドを呼び出す必要があります。 SuppressFinalize を呼び出すと、オブジェクトが終了キューに置かれている場合は、そのオブジェクトの Finalize メソッドの呼び出しは行われません。Finalize メソッドの実行は、パフォーマンスに影響を与えることを覚えておいてください。" と書かれています。(.NET 4.6 / 4.5 の記事にはその説明はありませんが同じことかと思います)

という訳で、結論は、IDisposable インターフェイス を継承して Dispose メソッドを実装しているクラスは、そのオブジェクトが使用されなくなった時点で Dispose メソッドを呼び出すべきということのようです。

そのために良く使われるのが、上に紹介した .NETの例外処理 Part.2 にある try/finally パターンや using ステートメントですね。

using ステートメントを使用すると、以下の例のようになると思います。

using(SqlConnection conn = new SqlConnection("接続文字列"))
{

  conn.Open();

  using(SqlCommand cmd = new SqlCommand("クエリ", conn))
  {
    using (SqlDataReader reader = cmd.ExecuteReader())
    {
      if (reader != null)
      {
        while (reader.Read())
        {
          // 何らかの処置
        }
      }
    } // SqlDataReader の Dispose(注1)

  } // SqlCommand の Dispose

} // SqlConnection の Dispose(注2)

注1:Dispose メソッドは、SqlDataReader によって使用されているリソースを解放し、Close メソッドを呼び出します。

注2:SqlConnection クラスの Close メソッドと Dispose メソッドは、機能的に同じです。

------ 2014/9/25 追記 ------

クラスによってはコンストラクタに GC.SuppressFinalize メソッドが実装されており、冗長な Finalize メソッドの呼び出しを防ぐという意味では Dispose() メソッドを呼ぶ必要はないものもあります。

実は、SqlCommand クラスの場合も、現時点のソースコードを見る限り、コンストラクタに GC.SuppressFinalize メソッドが実装されています。

ただし、コンストラクタでの GC.SuppressFinalize の実装は MSDN ライブラリなどにはドキュメント化されてない(ソースコードを見ないと分からない)、ソースコードは変更される可能性がある、将来ネイティブリソースが含まれる可能性はゼロではない(ゼロに近いとは思いますが)・・・ということを考えるべきです。

なので、IDisposable を継承するクラスは Dispose() を呼ぶべきというのが基本ルールであると思っています。

------ 2015/9/11 追記 ------

リンク先の MSDN ライブラリの記事を .NET Framework 4 のものに固定しました。URL にバージョンを指定しないと .NET 4.5 / 4.6 の MSDN ライブラリにリンクされますが、その記述が .NET 4 のものかなり異なっていて、.NET 4 をベースに書いた上の記事とミスマッチが生じましたので。

Tags: ,

ADO.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar