WebSurfer's Home

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

XML ファイルの更新操作

by WebSurfer 2010年9月27日 23:14

XML ファイルをデータソースに使った場合、表示するだけなら XmlDataSource を使えば、ほぼコーディングレスで Web アプリケーションを作成できます。ただし、XmlDataSource には更新機能がないので、必要な場合は自力でコードを書いて更新機能を実装する必要があります。

MSDN ライブラリの XmlDataSource の説明には、GetXmlDocument メソッドを使って XmlDataDocument オブジェクトを取得し、それに変更を加えてから Save するという方法が紹介されていますが、GridView や ListView 上で編集して更新するにはその方法は難しそうです。

それより、XML ファイル操作用のクラス(選択、削除、挿入、更新操作を行うメソッドを実装)を作り、そのクラスを ObjectDataSource を経由 GridView や ListView にバインドして操作するのが簡単そうです。

そのサンプルは MSDN ライブラリ の「GridView で XML ファイルをデータ ソースとして使いレコードを編集する方法」に紹介されています。そのサンプルに削除、挿入機能を加えて、さらに ID も更新できるように拡張したコードを書いておきます。なお、言語はサンプルの VB.NET を C# に変更しました。

まず、XML ファイル操作用クラス(UserInfoTable クラス)の UpdateDataSet メソッドを、ID も更新できるように変更します。具体的には、引数に original_id を追加し、original_id で DataSet の行を検索し、ヒットした行の当該項目を id に書き換えるよう修正します。次に、削除と挿入操作のためのメソッドを追加します。修正、追加後の UserInfoTable クラスは以下のようになります。

using System;
using System.Data;
using System.Web;
using System.ComponentModel;

public class XmlDataSet
{
  public class UserInfoTable : IDisposable
  {
    const string strXmlFile = "~/App_Data/UserInfo.xml";
    private DataSet myDataSet;

    public UserInfoTable()
    {
      myDataSet = new DataSet();
      myDataSet.Locale = 
        System.Globalization.CultureInfo.InvariantCulture;
      string filePath = 
        HttpContext.Current.Server.MapPath(strXmlFile);
      myDataSet.ReadXml(filePath);
    }

    public virtual void Dispose(bool disposing)
    {
      if (disposing)
      {
        myDataSet.Dispose();
      }
    }

    public void Dispose()
    {
      Dispose(true);
      System.GC.SuppressFinalize(this);
    }

    ~UserInfoTable()
    {
      Dispose(false);
    }

    [DataObjectMethod(DataObjectMethodType.Select, true)]
    public DataSet GetDataSet()
    {
      return myDataSet;
    }

    [DataObjectMethod(DataObjectMethodType.Update, true)]
    public void UpdateDataSet(string id, string name, string original_id)
    {
      string strFillter = "ID='" + original_id + "'";
      DataRow[] rows = myDataSet.Tables[0].Select(strFillter);
      if (rows.Length > 0)
      {
        rows[0]["ID"] = id;
        rows[0]["NAME"] = name;
        Save();
      }
    }

    [DataObjectMethod(DataObjectMethodType.Delete, true)]
    public void DeleteItem(string original_id)
    {
      string strFillter = "ID='" + original_id + "'";
      DataRow[] rows = myDataSet.Tables[0].Select(strFillter);
      if (rows.Length > 0)
      {
        rows[0].Delete();
        Save();
      }
    }

    [DataObjectMethod(DataObjectMethodType.Insert, true)]
    public void InsertItem(string id, string name)
    {
      DataRow row = myDataSet.Tables[0].NewRow();
      row["ID"] = id;
      row["NAME"] = name;
      myDataSet.Tables[0].Rows.Add(row);
      Save();
    }

    private void Save()
    {
      string filePath = 
        HttpContext.Current.Server.MapPath(strXmlFile);
      myDataSet.WriteXml(filePath, XmlWriteMode.IgnoreSchema);
    }
  } 
}

新しい aspx ファイルを作成し、それに ObjectDataSource と ListView を配置します。UserInfoTable クラスをベースに、ウィザードで ObjectDataSource と ListView を設定していくと、以下のようなコードになるはずです。

<asp:ObjectDataSource ID="ObjectDataSource1" 
  runat="server" 
  DeleteMethod="DeleteItem" 
  InsertMethod="InsertItem" 
  SelectMethod="GetDataSet" 
  TypeName="XmlDataSet+UserInfoTable" 
  UpdateMethod="UpdateDataSet">
  <DeleteParameters>
    <asp:Parameter Name="original_id" Type="String" />
  </DeleteParameters>
  <InsertParameters>
    <asp:Parameter Name="id" Type="String" />
    <asp:Parameter Name="name" Type="String" />
  </InsertParameters>
  <UpdateParameters>
    <asp:Parameter Name="id" Type="String" />
    <asp:Parameter Name="name" Type="String" />
    <asp:Parameter Name="original_id" Type="String" />
  </UpdateParameters>
</asp:ObjectDataSource>
<asp:ListView ID="ListView1" 
  runat="server" 
  DataSourceID="ObjectDataSource1" 
  EnableModelValidation="True">
</asp:ListView>

しかしなから、このままではうまく動きません。問題点は以下のとおりです

  • ListView に DataKeyNames="id" の定義がないので、id の値が ObjectDataSource に渡されません。
  • ObjectDataSource に OldValuesParameterFormatString="original_{0}" の設定がないので、id の新旧の区別ができません。
  • ListView の中身(Template やその中の TextBox, Button など)は自動生成されません。自力でコードを書く必要があります。
  • ListView に挿入の行を表示するため、InsertItemPosition="LastItem" を追加します。

以上の点を修正したコードは以下のとおりです。

<asp:ObjectDataSource ID="ObjectDataSource1" 
  runat="server"
  OldValuesParameterFormatString="original_{0}"
  DeleteMethod="DeleteItem" 
  InsertMethod="InsertItem" 
  SelectMethod="GetDataSet" 
  TypeName="XmlDataSet+UserInfoTable" 
  UpdateMethod="UpdateDataSet">
  <DeleteParameters>
    <asp:Parameter Name="original_id" Type="String" />
  </DeleteParameters>
  <InsertParameters>
    <asp:Parameter Name="id" Type="String" />
    <asp:Parameter Name="name" Type="String" />
  </InsertParameters>
  <UpdateParameters>
    <asp:Parameter Name="id" Type="String" />
    <asp:Parameter Name="name" Type="String" />
    <asp:Parameter Name="original_id" Type="String" />
  </UpdateParameters>
</asp:ObjectDataSource>
<asp:ListView ID="ListView1" 
  runat="server"
  DataKeyNames="id"
  DataSourceID="ObjectDataSource1"
  InsertItemPosition="LastItem"
  EnableModelValidation="True">
  <ItemTemplate>
    <tr>
      <td>
        <asp:Button ID="DeleteButton" 
          runat="server" 
          CommandName="Delete" 
          Text="削除" />
        <asp:Button ID="EditButton" 
          runat="server" 
          CommandName="Edit" 
          Text="編集" />
      </td>
      <td>
        <asp:Label ID="idLabel" 
          runat="server" 
          Text='<%# Bind("id") %>' />
      </td>
      <td>
        <asp:Label ID="nameLabel" 
          runat="server" 
          Text='<%# Bind("name") %>' />
      </td>
    </tr>
  </ItemTemplate>
  <InsertItemTemplate>
    <tr>
      <td>
        <asp:Button ID="InsertButton" 
          runat="server" 
          CommandName="Insert" 
          Text="挿入" />
        <asp:Button ID="CancelButton" 
          runat="server" 
          CommandName="Cancel" 
          Text="クリア" />
      </td>
      <td>
        <asp:TextBox ID="idTextBox" 
          runat="server" 
          Text='<%# Bind("id") %>' />
      </td>
      <td>
        <asp:TextBox ID="nameTextBox" 
          runat="server" 
          Text='<%# Bind("name") %>' />
      </td>
    </tr>
  </InsertItemTemplate>
  <LayoutTemplate>
    <table id="Table2" runat="server">
      <tr id="Tr1" runat="server">
        <td id="Td1" runat="server">
          <table ID="itemPlaceholderContainer" 
            runat="server">
            <tr id="Tr2" runat="server">
              <th id="Th1" runat="server">
              </th>
              <th id="Th2" runat="server">
                id</th>
              <th id="Th3" runat="server">
                name</th>
            </tr>
            <tr ID="itemPlaceholder" runat="server">
            </tr>
          </table>
        </td>
      </tr>
    </table>
  </LayoutTemplate>
  <EditItemTemplate>
    <tr>
      <td>
        <asp:Button ID="UpdateButton" 
          runat="server" 
          CommandName="Update" 
          Text="更新" />
        <asp:Button ID="CancelButton" 
          runat="server" 
          CommandName="Cancel" 
          Text="キャンセル" />
      </td>
      <td>
        <asp:TextBox ID="idTextBox" 
          runat="server" 
          Text='<%# Bind("id") %>' />
      </td>
      <td>
        <asp:TextBox ID="nameTextBox" 
          runat="server" 
          Text='<%# Bind("name") %>' />
      </td>
    </tr>
  </EditItemTemplate>
</asp:ListView>

Tags: , ,

ASP.NET

SqlDataSource などのパラメータ

by WebSurfer 2010年9月26日 20:16

データバウンドコントロール (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 もできるようにしたサンプルを、後日、作成して書き込んでおくことにします。

Tags: ,

ASP.NET

Web サービスで DataTable を送信

by WebSurfer 2010年9月19日 18:06

ASP.NET Web サービスで、普通の System.Data.DataTable を送ろうとすると、シリアル化できないというエラーが出て送ることができません。

Visual Studio のウィザードを使って作る「型付 DataTable」はシリアル化できるようで、それを利用すれば Web サービスから送信でき、受信側でも問題なく「型付 DataTable」を再生して利用できます。

では、DataSet はどうでしょうか?

理由は分かりませんが、自分が検証した結果によれば、少々制約がありました。忘れないように、以下に書いておきます。

(1) 制約その1

「型付 DataTable」を普通の System.Data.DataSet を入れ物に使って送信することができます。ただし受信側で、DataSet から取得した DataTable を送り側で設定した型にキャストできないという制約があります。

送信側(Web サービス)の例

[WebMethod(Description = "普通の DataSet に型付 DataTable を入れて返します")]
public DataSet CreateOrdinaryDataSet()
{
  DataSet ds = new DataSet();

  NorthwindDataSetTableAdapters.CustomersTableAdapter adapterCustomers = 
    new NorthwindDataSetTableAdapters.CustomersTableAdapter();
  ds.Tables.Add(adapterCustomers.GetDataByCountry("Germany"));

  NorthwindDataSetTableAdapters.ProductsTableAdapter adapterProducts =
    new NorthwindDataSetTableAdapters.ProductsTableAdapter();
  ds.Tables.Add(adapterProducts.GetDataByCategoryID(1));

  return ds;
}

受信側(アプリケーション)の例

MyService.Service wsMyService = new MyService.Service();
DataSet ds = wsMyService.CreateOrdinaryDataSet();

// キャストできずエラーとなる。
// MyService.NorthwindDataSet.CustomersDataTable table =
//  (MyService.NorthwindDataSet.CustomersDataTable)ds.Tables[0];

// これは OK
DataTable table = ds.Tables[0];
dataGridView1.DataSource = table;

(2) 制約その2

「型付 DataSet」を入れ物にして送る場合、「型付 DataSet」のメンバーの「型付 DataTable」に Fill するようにしないと、アプリケーション側で DataSet を再生できないという制約があります。

送信側・・・これは OK

[WebMethod (Description="型付 DataSet を返します")]
public NorthwindDataSet CreateDataSet()
{
  NorthwindDataSet ds = new NorthwindDataSet();

  NorthwindDataSetTableAdapters.CustomersTableAdapter adapterCustomers = 
    new NorthwindDataSetTableAdapters.CustomersTableAdapter();
  adapterCustomers.FillByCountry(ds.Customers, "UK");

  NorthwindDataSetTableAdapters.ProductsTableAdapter adapterProducts = 
    new NorthwindDataSetTableAdapters.ProductsTableAdapter();
  adapterProducts.FillByCategoryID(ds.Products, 1);

  return ds;
}

送信側・・・これは NG

[WebMethod(Description = "型付 DataSet を返します(その2)")]
public NorthwindDataSet CreateDataSet2()
{
  NorthwindDataSet ds = new NorthwindDataSet();

  NorthwindDataSetTableAdapters.CustomersTableAdapter adapterCustomers =
    new NorthwindDataSetTableAdapters.CustomersTableAdapter();
  ds.Tables.Add(adapterCustomers.GetDataByCountry("Germany"));

  NorthwindDataSetTableAdapters.ProductsTableAdapter adapterProducts =
    new NorthwindDataSetTableAdapters.ProductsTableAdapter();
  ds.Tables.Add(adapterProducts.GetDataByCategoryID(1));

  return ds;
}

アプリケーション側で CreateDataSet2 を呼び出すと XmlSchemaException がスローされます。エラーメッセージによると、「NorthwindDataSet.xsd:Customers の複数の定義があるため、コンテンツ モデルがあいまいになっています。」ということだそうです。

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