WebSurfer's Home

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

反復子メソッドと using 句

by WebSurfer 2013年6月24日 14:55

このようなトラブルに悩む人はいないかもしれませんが、yield return ステートメント を使用した反復子のメソッドで using 句を使ったデータベースアクセスを行う場合の注意点を備忘録として書いておきます。

エラーメッセージ

下のコードを見てください(SqlDataSource コントロールを利用すれば自力でこのようなコードを書く必要はありませんが、あくまで例として書きました)。

GetData が反復子のメソッドです。ここで注意しなければならないことは、このメソッドの戻り値(この例では IEnumerable<Employee> 型として定義されている employees)が foreach ループなどで反復処置されるまで GetData メソッドは呼び出されないことです。

従って、using のスコープの中で、まだ SqlDataReader が閉じられてないうちに、反復処置を完了しなければなりません。この例では、反復処置は GridView1.DataBind メソッドで行われますので、using のスコープの中で GridView1.DataBind メソッドを実行しなければなりません。

using のスコープを抜けた後で GridView1.DataBind メソッドを実行すると(下のコードでコメントアウトしたように)、その時点では SqlDataReader は閉じられているので、上の画像のように reader.Read() のところで "リーダーが閉じている場合は、Read の呼び出しは無効です。" というエラーが出ます。

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

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

<script runat="server">

  public class Employee
  {
    public string ID { get; set; }
    public string Name { get; set; }
  }

  // yield return を使用した反復子のメソッド。戻り値が
  // foreach ループなどで反復処置されるまで呼び出されない。
  public IEnumerable<Employee> GetData(SqlDataReader reader)
  {
    while (reader.Read())
    {
      Employee employee = new Employee();
      employee.ID = reader["EmployeeID"].ToString();
      employee.Name = reader["LastName"].ToString();
      yield return employee;
    }
  }

  IEnumerable<Employee> employees = null;
    
  protected void Page_Load(object sender, EventArgs e)
  {
    if (!Page.IsPostBack)
    {
      string connString = 
        System.Web.Configuration.WebConfigurationManager.
        ConnectionStrings["Northwind"].ConnectionString;
      string query = 
        "SELECT EmployeeID, LastName FROM Employees";

      using (SqlConnection conn = new SqlConnection(connString))
      {
        conn.Open();
        using (SqlCommand cmd = new SqlCommand(query, conn))
        {
          using (SqlDataReader reader = cmd.ExecuteReader())
          {
            if (reader != null)
            {
              employees = GetData(reader);

              // GetData メソッドが実行されるのは、employees が
              // foreach などによって反復処置される時。ここで 
              // 以下のようにデータバインドすれば reader が開い
              // ているうちに反復処置が完了するので問題なし。
              GridView1.DataSource = employees;
              GridView1.DataBind();
            }
          }
        }
      }

      // ここでバインドしたのではダメ。GetData メソッドは
      // using 句を抜けてから実行され、reader.Read() で
      // "リーダーが閉じている場合は、Read の呼び出しは無
      // 効です。" というエラーが出る。
      //GridView1.DataSource = employees;
      //GridView1.DataBind();
    }        
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:GridView ID="GridView1" runat="server">
    </asp:GridView>
  </div>
  </form>
</body>
</html>

Tags:

ADO.NET

リソース埋込カスタムコントロールの継承

by WebSurfer 2013年6月20日 13:15

先の記事「リソース埋め込みカスタムコントロール」で、画像、html ファイル、スクリプトファイル、css ファイルなどのリソースをアセンブリに埋め込んだカスタムコントロールの作り方を説明しました。

このようなリソース埋め込みカスタムコントロールを継承したコントロールを作って使う場合の注意点を書きます。

アセンブリに埋め込んだリソースは、GetWebResourceUrl メソッド によって URL 参照を取得して使用しますが、注意すべきはこのメソッドの第一引数 type(リソースの型)です。

先の記事の MsButton.cs のコードでは this.GetType() でリソースの型を取得していますが、このコントロールを継承した場合 this は継承先になり、ASP.NET は継承先からリソースを探すので、リソースが見つからないという結果になります。

この問題を解決するには this.GetType() に代えて typeof 演算子を利用します。先の記事の MsButton.cs のコードでは以下のようにします。

protected override void CreateChildControls()
{
  ClientScriptManager cs = Page.ClientScript;

  // このコントロールを継承する場合 this.GetType() ではダメ
  // this は継承先になり、結果、リソースが見つからない。
  //Type rsType = this.GetType();

  Type rsType = typeof(MsButton);

  // css への参照を <head></head> に配置
  HtmlLink css = new HtmlLink();
  css.Href = 
    cs.GetWebResourceUrl(rsType, "SimpleControl.MyStylesheet.css");

  // ・・・中略・・・
}

上に紹介した MSDN ライブラリのサンプルコードでも typeof 演算子を使っていますね。

Tags:

Web Custom Control

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

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar