WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

カスタムページャー

by WebSurfer 25. August 2010 22:42

ASP.NET 2.0 で利用できるデータソースコントロール(SqlDataSource など)とデータバインドコントロール(GridView など)を用いると、ほとんどコードを自力で書くことなくページング機能を実装できます。

ところが、GridView は意外と融通性がなく、例えば 1 レコードを複数行に表示する場合は実質的に使えません(使って使えないことはないですが、問題が多いです)。

ASP.NET 3.5 が使えなかった時代は、Repeater を使うという選択になったのですが、ページングが必要な場合は自分でコードを書いて実装する必要があります。その例を書いてみました。

実行結果は以下のようになります(縮小してあります)。

Repeater を使ったページング

ASP.NET 3.5 の ListView を使えばこのような苦労はないので、今となってはこの例が役に立つことはないかもしれませんけど。

実装しなければならないのは以下のとおりです。

  • ページャー(ページングのためのユーザーインターフェイス用カスタムコントロール)
  • ページング制御しながらデータを抽出するストアドプロシージャ
  • ユーザーによるページャーの操作に応じて、ストアドプロシージャを利用して DataTable を作成し Reapter にバインドするプログラムを含む aspx ファイル。

それぞれについてコードを記載します。説明はコードの中のコメントを参照してください。

ページャー
using System;
using System.Web.UI;
using System.Web.UI.WebControls;

// 以下のようなページャーが表示される。
//
// <<最初 <前へ 1, 2, 3, 4, 5 次へ> 最後>>
//
// クリックするとポストバックがかかり、そのクリックした
// ページが表示されるようコントロールする。

namespace CustomWebFormsControls
{
  public class PageNumberer : WebControl, IPostBackEventHandler
  {
    private int _selectedPage, _count, _displayedPages;

    // 現在選択されているページ番号
    public int SelectedPage
    {
      get
      {
        if (_selectedPage == 0)
        {
          object obj = ViewState["SelectedPage"];
          _selectedPage = (obj != null) ? (int)obj : 1;
        }
        return _selectedPage;
      }
      set
      {
        ViewState["SelectedPage"] = value;
        _selectedPage = value;
      }
    }

    // ページ総数
    public int Count
    {
      get
      {
        if (_count == 0)
        {
          object obj = ViewState["Count"];
          _count = (obj != null) ? (int)obj : 1;
        }
        return _count;
      }
      set
      {
        ViewState["Count"] = value;
        _count = value;
      }
    }

    // ページャーに表示するページの最大数
    // 例えば、DisplayedPages="7" と設定すると、Count が
    // 8 ページ以上ある場合、以下のように表示される。
    // 1, 2, 3, 4, 5, 6, 7 次へ> 最後 >>
    public int DisplayedPages
    {
      get
      {
        if (_displayedPages == 0)
        {
          object obj = ViewState["DisplayedPages"];
          _displayedPages = (obj != null) ? (int)obj : 1;
        }
        return _displayedPages;
      }
      set
      {
        ViewState["DisplayedPages"] = value;
        _displayedPages = value;
      }
    }

    // html の一番外側は div 要素。
    protected override HtmlTextWriterTag TagKey
    {
      get
      {
        return HtmlTextWriterTag.Div;
      }
    }

    protected override void RenderContents(HtmlTextWriter writer)
    {
      int startPage;  // 左端に表示するページ番号
      int endPage;    // 右端に表示するページ番号

      if (Count > DisplayedPages)
      {
        // SelectedPage の前後のページの数を求める。
        // 例えば、
        //        1, 2, 3, 4, 5, 6, 7 次へ> 最後 >>
        // で 2 が SelectedPage の場合、prevCount = 1,
        // nextCount = 5 となる。
        int prevCount = Math.Abs((DisplayedPages - 1) / 2);

        if (SelectedPage <= prevCount)
        {
          prevCount = SelectedPage - 1;
        }

        int nextCount = DisplayedPages - prevCount - 1;

        if (SelectedPage + nextCount > Count)
        {
          nextCount = Count - SelectedPage;
          prevCount = DisplayedPages - nextCount - 1;
        }

        // 上で求めた prevCount, nextCount を基に
        // startPage, endPage を計算する。
        startPage = SelectedPage - prevCount;
        endPage = SelectedPage + nextCount;
      }
      else
      {
        startPage = 1;
        endPage = Count;
      }

      if (startPage > 1)
      {
        RenderItem(writer, "&lt;&lt;最初", 1);
      }

      if (SelectedPage > 1)
      {
        RenderItem(writer, "&lt;前へ", SelectedPage - 1);
      }

      for (int i = startPage; i <= endPage; i++)
      {
        string label;
        if (i != endPage)
        {
          label = i.ToString() + ",";
        }
        else
        {
          label = i.ToString();
        }

        if (i == SelectedPage)
        {
          RenderItem(writer, label, 0);
        }
        else
        {
          RenderItem(writer, label, i);
        }
      }

      if (SelectedPage < Count)
      {
        RenderItem(writer, "次へ&gt;", SelectedPage + 1);
      }

      if (endPage < Count)
      {
        RenderItem(writer, "最後&gt;&gt;", Count);
      }
    }

    void RenderItem(HtmlTextWriter writer, string text, int pageNum)
    {
      // <span>
      writer.RenderBeginTag(HtmlTextWriterTag.Span);

      // <a href="javascript:__doPostBack('ユニークID','ページ番号')">
      if (pageNum != 0)
      {
        writer.AddAttribute(
          HtmlTextWriterAttribute.Href,
          Page.ClientScript.GetPostBackClientHyperlink(
            this, pageNum.ToString())
        );
        writer.RenderBeginTag(HtmlTextWriterTag.A);
      }

      // ページ番号
      writer.Write(text);

      // </a>
      if (pageNum != 0)
      {
        writer.RenderEndTag();
      }

      // </span>
      writer.RenderEndTag();
    }

    private static readonly object EventSelectedPageChanged = null;

    // これは += と -= の定義。
    public event EventHandler SelectedPageChanged
    {
      add
      {
        Events.AddHandler(EventSelectedPageChanged, value);
      }
      remove
      {
        Events.RemoveHandler(EventSelectedPageChanged, value);
      }
    }

    // サーバーにポストバックされたとき、ページがこの
    // メソッドを呼び出して、__doPostBack() の第 2 引
    // 数(クリックされたページ番号)を eventArgument
    // に渡す。
    void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
    {
      int newPage;
      if (int.TryParse(eventArgument, out newPage))
      {
        this.SelectedPage = newPage;
        OnSelectedPageChanged(EventArgs.Empty);
      }
    }

    protected virtual void OnSelectedPageChanged(EventArgs e)
    {
      EventHandler changehandler =
        (EventHandler)Events[EventSelectedPageChanged];
      if (changehandler != null)
      {
        changehandler(this, e);
      }
    }
  }
}
ストアドプロシージャ

Microsoft が提供しているサンプルデータベース Northwind の Products テーブルを利用しています。

ALTER PROCEDURE dbo.PagedProductList4
(
  @pageNum int = 1,
  @pageSize int = 10,
  @pageCount int OUTPUT
)
/* pageNum: 現在選択されているページ番号
   pageSize: ページ当りの行数 
   @rows: スキップする行数 */
AS
  declare @rows int
  declare @rowCount float

  SET @rows = (@pageNum - 1) * @pageSize
    
SELECT @rowCount = COUNT(*) FROM [Products]

SELECT * 
FROM (
  SELECT *, ROW_NUMBER() 
    OVER (ORDER BY [ProductID] ASC) AS rownum 
    FROM [Products]
    ) AS DerivedTable 
WHERE rownum BETWEEN (@rows + 1) AND (@rows + @pageSize) 
ORDER BY [ProductID] ASC
       
SET @pageCount = CEILING(@rowCount / @pageSize)
aspx ファイル
<%@ Page Language="C#" %>
<%@ Register TagPrefix="MyControl" Namespace="CustomWebFormsControls" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Configuration" %>

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

<script runat="server">
  // ついでに、SqlDataSorce, ObjectDataSource は使わな
  // いことで考えました。
  DataTable CreateDataSource(int pageNum, int pageSize)
  {
    string connString = 
      ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
    using (SqlConnection sqlConn = new SqlConnection(connString))
    {
      SqlCommand sqlCom = 
        new SqlCommand("dbo.PagedProductList4", sqlConn);
      sqlCom.CommandType = CommandType.StoredProcedure;
      sqlCom.Parameters.AddWithValue("@pageNum", pageNum);
      sqlCom.Parameters.AddWithValue("@pageSize", pageSize);
      SqlParameter param = 
        new SqlParameter("@pageCount", SqlDbType.Int);
      param.Direction = ParameterDirection.Output;
      sqlCom.Parameters.Add(param);

      SqlDataAdapter adapter = new SqlDataAdapter();
      adapter.SelectCommand = sqlCom;

      DataSet ds = new DataSet();
      adapter.Fill(ds);

      pn1.Count = (int)param.Value;
      pn2.Count = (int)param.Value;

      return ds.Tables[0];
    }
  }
    
  // ページ当りの行数
  protected int pageSize = 7;
    
  void Page_Load(Object sender, EventArgs e)
  {
    if (!IsPostBack)
    {
      Repeater1.DataSource = 
        CreateDataSource(pn1.SelectedPage, pageSize);
      Repeater1.DataBind();
    }
  }

  protected void pn1_SelectedPageChanged(object sender, EventArgs e)
  {
    Repeater1.DataSource = 
      CreateDataSource(pn1.SelectedPage, pageSize);
    Repeater1.DataBind();
    pn2.SelectedPage = pn1.SelectedPage;
  }

  protected void pn2_SelectedPageChanged(object sender, EventArgs e)
  {
    Repeater1.DataSource = 
      CreateDataSource(pn2.SelectedPage, pageSize);
    Repeater1.DataBind();
    pn1.SelectedPage = pn2.SelectedPage;
  }
</script>

<html>
<head>
<title>無題のページ</title>
  <style type="text/css">
    table.style1
    {
      border-style: solid;
      border-width: 2px;
      border-color: Black;
      text-align: center;
      border-collapse: collapse;
    }
       
    table.style1 th
    {
      border-style: solid;
      border-width: 2px 1px 2px 1px;
      border-color: Black;
      background-color: #6699FF;
      color: #FFFFFF;
    }
        
    table.style1 td
    {
      border-style: solid;
      border-width: 1px;
      border-color: Black;        
    }
        
    .alternate
    {
      background-color: #CCFFFF;
    }
        
    .PageNumbers
    {
      display:inline;
    }

    .PageNumbers span
    {
      padding-left: 10px;
    }        
  </style>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <MyControl:PageNumberer ID="pn1" 
      runat="server" 
      DisplayedPages="5" 
      onselectedpagechanged="pn1_SelectedPageChanged" 
      CssClass="PageNumbers"/>
    <asp:Repeater ID="Repeater1" runat="server">
      <HeaderTemplate>
        <table class="style1">
          <tr>
            <th rowspan="2">ProductID</th>
            <th colspan="7">ProductName</td>
            <th rowspan="2">Discontinued</th>
          </tr>
          <tr>
            <th>SupplierID</th>
            <th>CategoryID</th>
            <th>QuantityPerUnit</th>
            <th>UnitPrice</th>
            <th>UnitsInStock</th>
            <th>UnitsOnOrder</th>
            <th>ReorderLevel</th>
          </tr>
      </HeaderTemplate>

      <ItemTemplate>
          <tr>
            <td rowspan="2"><%# Eval("ProductID")%></td>
            <td colspan="7"><%# Eval("ProductName")%></td>
            <td rowspan="2">
              <asp:CheckBox ID="CheckBox1" 
                runat="server" 
                Enabled="False" 
                Checked='<%# Eval("Discontinued") %>' />
            </td>
          </tr>
          <tr>
            <td><%# Eval("SupplierID")%></td>
            <td><%# Eval("CategoryID")%></td>
            <td><%# Eval("QuantityPerUnit")%></td>
            <td><%# Eval("UnitPrice")%></td>
            <td><%# Eval("UnitsInStock")%></td>
            <td><%# Eval("UnitsOnOrder")%></td>
            <td><%# Eval("ReorderLevel")%></td>
          </tr>
      </ItemTemplate>
            
      <AlternatingItemTemplate>
          <tr class="alternate">
            <td rowspan="2"><%# Eval("ProductID")%></td>
            <td colspan="7"><%# Eval("ProductName")%></td>
            <td rowspan="2">
              <asp:CheckBox ID="CheckBox1" 
                runat="server" 
                Enabled="False" 
                Checked='<%# Eval("Discontinued") %>' />
            </td>
          </tr>
          <tr class="alternate">
            <td><%# Eval("SupplierID")%></td>
            <td><%# Eval("CategoryID")%></td>
            <td><%# Eval("QuantityPerUnit")%></td>
            <td><%# Eval("UnitPrice")%></td>
            <td><%# Eval("UnitsInStock")%></td>
            <td><%# Eval("UnitsOnOrder")%></td>
            <td><%# Eval("ReorderLevel")%></td>
          </tr>
      </AlternatingItemTemplate>
      <FooterTemplate>
        </table>
      </FooterTemplate>
    </asp:Repeater>
    <MyControl:PageNumberer ID="pn2" 
      runat="server" 
      DisplayedPages="5" 
      onselectedpagechanged="pn2_SelectedPageChanged" 
      CssClass="PageNumbers"/>
  </div>
  </form>
</body>
</html>

Tags: ,

Paging

IE8 と traget frame

by WebSurfer 24. August 2010 22:42

IE7 では問題なかった異なるウィンドウの target frame への表示が、IE8 ではうまくいきません。<=(やり方の問題でした。下の方に書いた追記参照)

親ウィンドウの iframe に子ウィンドウの更新結果を表示

確証はありませんが、IE8 では異なるウィンドウはプロセスが異なることが原因のように思われます。

GridView と iframe をもつ親ページ (Parent.aspx)、DetailsView を持つ子ページ (Child.aspx) を例にとって、シナリオを説明します。

  1. 親ウィンドウを開き Parent.aspx を要求。
  2. Parent.aspx は、DB からデータを取得し、GirdView にレコード一覧を表示。
  3. GridView 上でレコードを選択すると、親ウィンドウは開いたまま、子ウィンドウを開いて Child.aspx を要求。
  4. Child.aspx は、選択されたレコードの詳細を DetailsView に Edit モードで表示。
  5. 子ウィンドウでレコードを編集し、ボタンクリックで Child.aspx にポストバックして DB を更新。
  6. Child.aspx は、ReadOnly モードの DetailsView の形で、更新後のレコードを応答。
  7. Child.aspx の応答を、開いたままの親ウィンドウの Parent.aspx の iframe に表示。
  8. 子ウィンドウを閉じる。

問題は、上記 7 の、親ウィンドウを開いたままにしておいて、その iframe に子ウィンドウで行った更新結果を表示するところです。

これは、Parent.aspx の iframe の name を、Child.aspx の form 要素の target に設定するということで可能なはずです。

しかしながら、IE8 では新たに別ウィンドウが開いてそれに Child.aspx が表示され、target としたはずの iframe には何も表示されません。

以前 IE7 で試した時はうまくいきました(今は環境がないので試せませんが、間違いないです)。Firefox 3.6.8, Opera 10.61(この時点での最新版)は、以前と同様、期待通りに動きます。

どうも IE8 では、親ウィンドウと子ウィンドウのプロセスが違うということが問題のようです。つまり、親ウィンドウの iframe の name を、子ウィンドウのプロセスが認識できず、そのような名前の iframe はないと判断されるので、新たに別ウィンドウを開いて Child.aspx を表示するのではないかと思います。

ただ、ググって見ても上記のような話は見当たらないので、ひょっとして何か思い違いをしている可能性はゼロではなさそうですけど。(汗)

検証に使ったサンプルも書いておきます。DB は Microsoft が提供しているサンプル、Northwind の Employee テーブルを使用しています。

Parent.aspx
<%@ 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">
  protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
  {
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
      Button btn = (Button)e.Row.FindControl("Button1");
      int id = (int)((DataRowView)e.Row.DataItem)["EmployeeID"];
      btn.OnClientClick = 
        "javascript:window.open('042_Child.aspx?employeeid=" + 
        id.ToString() + "&mode=edit', null, " + 
        "'menubar=no, scrollbars=yes, status=no, width=500, height=600'); " + 
        "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:SqlDataSource ID="SqlDataSource1" runat="server" 
        ConnectionString="<%$ ConnectionStrings:Northwind %>" 
        SelectCommand=
          "SELECT EmployeeID, LastName + ' ' + FirstName AS Name 
           FROM Employees">
      </asp:SqlDataSource>
      <asp:GridView ID="GridView1" 
        runat="server" 
        AutoGenerateColumns="False" 
        DataKeyNames="EmployeeID" 
        DataSourceID="SqlDataSource1" 
        EnableModelValidation="True" 
        OnRowDataBound="GridView1_RowDataBound">
        <Columns>
          <asp:BoundField DataField="EmployeeID" 
            HeaderText="ID" 
            InsertVisible="False" 
            ReadOnly="True" 
            SortExpression="EmployeeID" />
          <asp:BoundField DataField="Name" 
            HeaderText="名前" 
            ReadOnly="True" 
            SortExpression="Name" />
          <asp:TemplateField HeaderText="選択">
            <ItemTemplate>
              <asp:Button ID="Button1" 
                runat="server" 
                CommandName="Select" 
                Text="Select" />
            </ItemTemplate>
          </asp:TemplateField>
        </Columns>
      </asp:GridView>
    </div>
    <hr />
    <iframe name="DetailedList" height="450px" width="350px">
    </iframe>
  </form>
</body>
</html>
Child.aspx
<%@ 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">
  protected void Page_Load(object sender, EventArgs e)
  {
    string employeeid = Request.QueryString["employeeid"];
    string mode = Request.QueryString["mode"];
        
    if (Page.IsPostBack)
    {
      mode = "readonly";
    }

    if (!String.IsNullOrEmpty(employeeid) && 
      !String.IsNullOrEmpty(mode))
    {
      Panel1.Visible = true;
      Panel2.Visible = false;
      Label1.Text = employeeid;
            
      if (mode == "edit")
      {                
        DetailsView1.DefaultMode = DetailsViewMode.Edit;
        ((Button)DetailsView1.FindControl("Button1")).OnClientClick =
          "javascript:setTimeout('self.close()', 100);";
        ((Button)DetailsView1.FindControl("Button2")).OnClientClick =
          "self.close(); return false;";
      }
      else
      {
        DetailsView1.DefaultMode = DetailsViewMode.ReadOnly;
      }
    }
    else
    {
      Panel1.Visible = false;
      Panel2.Visible = true;
    }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" target="DetailedList" runat="server">
    <asp:Panel ID="Panel1" runat="server">
      <h1>詳細表示</h1>
      <span>
        選択された Employee ID: <asp:Label ID="Label1" runat="server" />
      </span>
      <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
        ConnectionString="<%$ ConnectionStrings:Northwind %>" 
        SelectCommand=
         "SELECT [EmployeeID], [LastName], [FirstName], [Title], 
            [TitleOfCourtesy], [Address], [City], [Region], 
            [PostalCode], [Country], [HomePhone] 
          FROM [Employees] 
          WHERE ([EmployeeID] = @EmployeeID)" 
        UpdateCommand=
         "UPDATE [Employees] 
          SET [LastName] = @LastName, [FirstName] = @FirstName, 
            [Title] = @Title, [TitleOfCourtesy] = @TitleOfCourtesy, 
            [Address] = @Address, [City] = @City, [Region] = @Region, 
            [PostalCode] = @PostalCode, [Country] = @Country, 
            [HomePhone] = @HomePhone 
          WHERE [EmployeeID] = @EmployeeID" >
        <SelectParameters>
          <asp:ControlParameter ControlID="Label1" 
            Name="EmployeeID" 
            PropertyName="Text" 
            Type="Int32" />
        </SelectParameters>
        <UpdateParameters>
          <asp:Parameter Name="LastName" Type="String" />
          <asp:Parameter Name="FirstName" Type="String" />
          <asp:Parameter Name="Title" Type="String" />
          <asp:Parameter Name="TitleOfCourtesy" Type="String" />
          <asp:Parameter Name="Address" Type="String" />
          <asp:Parameter Name="City" Type="String" />
          <asp:Parameter Name="Region" Type="String" />
          <asp:Parameter Name="PostalCode" Type="String" />
          <asp:Parameter Name="Country" Type="String" />
          <asp:Parameter Name="HomePhone" Type="String" />
          <asp:Parameter Name="EmployeeID" Type="Int32" />
        </UpdateParameters>
      </asp:SqlDataSource>
      <asp:DetailsView ID="DetailsView1" 
        runat="server" 
        AutoGenerateRows="False" 
        DataKeyNames="EmployeeID" 
        DataSourceID="SqlDataSource1" 
        EnableModelValidation="True">
        <Fields>
          <asp:BoundField DataField="EmployeeID" 
            HeaderText="EmployeeID" 
            InsertVisible="False" 
            ReadOnly="True" 
            SortExpression="EmployeeID" />
          <asp:BoundField DataField="LastName" 
            HeaderText="LastName" 
            SortExpression="LastName" />
          <asp:BoundField DataField="FirstName" 
            HeaderText="FirstName" 
            SortExpression="FirstName" />
          <asp:BoundField DataField="Title" 
            HeaderText="Title" 
            SortExpression="Title" />
          <asp:BoundField DataField="TitleOfCourtesy" 
            HeaderText="TitleOfCourtesy" 
            SortExpression="TitleOfCourtesy" />
          <asp:BoundField DataField="Address" 
            HeaderText="Address" 
            SortExpression="Address" />
          <asp:BoundField DataField="City" 
            HeaderText="City" 
            SortExpression="City" />
          <asp:BoundField DataField="Region" 
            HeaderText="Region" 
            SortExpression="Region" />
          <asp:BoundField DataField="PostalCode" 
            HeaderText="PostalCode" 
            SortExpression="PostalCode" />
          <asp:BoundField DataField="Country" 
            HeaderText="Country" 
            SortExpression="Country" />
          <asp:BoundField DataField="HomePhone" 
            HeaderText="HomePhone" 
            SortExpression="HomePhone" />          
          <asp:TemplateField ShowHeader="False">
            <EditItemTemplate>
              <asp:Button ID="Button1" 
                runat="server" 
                CausesValidation="True" 
                CommandName="Update" 
                Text="更新">
              </asp:Button>
               
              <asp:Button ID="Button2" 
                runat="server" 
                CausesValidation="False" 
                CommandName="Cancel" 
                Text="キャンセル">
              </asp:Button>
            </EditItemTemplate>
          </asp:TemplateField>          
        </Fields>
      </asp:DetailsView>
    </asp:Panel>
    <asp:Panel ID="Panel2" runat="server">
      <p>クエリ文字列 employeeid または mode が指定されていません。</p>
    </asp:Panel>
  </form>
</body>
</html>

---------- 2010/8/29 追記 ----------

思い違いというか、やり方の問題でした。

Visual Studio で、[デバッグ] → [デバッグ開始] または [デバッグなしで開始] で IE8 を起動して試していたのでダメでした。

普通に IE8 を起動して、アドレスバーに url を直打ちして試したら、期待通り子ウィンドウのポストバック結果が親ウィンドウの iframe に表示されました。

お騒がせしましたが、違うことが認識できて、一つ利口(?)になったということで・・・(苦笑)

でも、何で違うんでしょう???

Tags: , ,

ASP.NET

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

About this blog

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

Calendar

<<  July 2020  >>
MoTuWeThFrSaSu
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar