WebSurfer's Home

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

DataPager のバグその2

by WebSurfer 2010年10月30日 14:34

先のポスト DataPager のバグその1 に続いて、再び DataPager のバグの話です。なお、このバグも .NET 4 では修正されており、.NET 3.5 のみの問題です。

ListView の中に配置した DataPager の設定にある条件が重なると Stack Overflow を起こします。条件というのは以下の 5 つです。

  1. ListView の LayoutTemplate の中に DataPager を配置
  2. ListView に EmptyDataTemplate を配置
  3. ListView にバインドされるデータの件数が 0
  4. DataPager.QueryStringField を設定(例: QueryStringField="pageNumber")
  5. そのページを、クエリ文字列 ?pageNumber=1 として要求

残念ながら原因は分かりません。(涙)

でも、回避策は簡単で、DataPager を ListView の外に出してしまえばいいのです。件数 0 の場合は ListView の外に DataPager を配置しても、それが表示されることはないです。

または、当然ながら、DataPager.QueryStringField の設定を止めても回避できます。DataPager.QueryStringField を使う理由が、GridView.PageIndex プロパティのようにコードでページを指定したいということであれば、別の手段があります。

その「別の手段」の具体的な方法は、Resetting the Page Index in a ListView が参考になると思います。

Tags:

Paging

DataPager のバグその1

by WebSurfer 2010年10月29日 23:18

.NET 3.5 で ListView コントロールと共に導入された DataPager コントロールには、QueryStringField プロパティ周りにバグがあります。

QueryStringField プロパティというのは、クエリ文字列を利用してページの移動先を指定し、ページの移動に HTTP GET コマンド(デフォルトでは POST)を使用できるようにするためのものです。

例えば、QueryStringField="pageNumber" と設定した場合、ページャーをクリックして指定した移動先のページ番号が 5 であったとすると、URL にクエリ文字列 ?pageNumber=5 を追加して HTTP GET コマンドでそのページを要求するようになります。

MSDN ライブラリ「DataPager.QueryStringField プロパティ」によると "検索エンジンですべてのデータ ページにインデックスが付けられるようにするには、このプロパティを設定すると便利です。" とのことです。

ところが、このページを要求する際に、クエリ文字列のキーに null が含まれる場合(例えば、?pageNumber=5& とすると、& の後に null のキーがあると判断されるようです)、NullReferenceException がスローされるという問題があります。

ページ内でページャーをクリックしているだけなら、自動的に ?pageNumber=x (x は要求するページ番号)というクエリ文字列が URL に追加されて要求がかかるので間違いなく動作しますが、別のページでクエリ文字列を組み立ててそのページを要求する場合、間違って ?pageNumber=x& などとすると例外がスローされます

この問題は、2008 年 7 月に Connect に報告されています。

この報告によると、受信したクエリ文字列のキーに null が含まれていると、ライブラリ内のメソッドの以下の部分で key(String オブジェクト)が null になって NullReferenceException がスローされるということだそうです。これはバグとして次期バージョンで修正されるという話になっていました。

foreach (string key in request.QueryString.AllKeys)
{
  if (... && !key.Equals(queryStringField, 
                StringComparison.OrdinalIgnoreCase))

今日、このページをのぞいてみたら、すでに修正済みというようなことが書いてあります。ということは、少なくとも .NET 4 では修正されているはずです。

ということで、早速試してみました。結果は:

  • .NET 3.5 SP1: ダメでした。NullReferenceException がスローされます。
  • .NET 4: 例外はスローされなかったので、バグは修正されているようです。

というわけで、ASP.NET 3.5 では回避策がまだ必要なようです。上に紹介した Connect のページに回避策も提案されていますので、そちらを参照してください。

Tags:

Paging

ObjectDataSource でページング

by WebSurfer 2010年8月26日 12:00

DB のレコード一覧を表示する場合、レコード数が多い場合はページングが必要になってきます。

SqlDataSource を使って、ウィザードベースでクエリなどを設定していけば、コードを一行も書くことなく簡単にページング機能を実装できます。

しかしながら、ウィザードベースで実装した場合は、ページを切り替えるたびに全レコードを取得し、そのページの当該部分のみを表示するという動作になります。

レコード数が多い場合、ページングの都度全レコードを取得するのは負荷が大きく、できれば避けたい操作です。

ちなみに、先の記事 カスタムページャー で紹介した Repeater とカスタムページャーを使用したページングではそのあたりは考慮してあります。

今回は、先の記事と同等な内容を、ObjectDataSource と ListView を使って実現する例を書いてみます。実行結果は以下のようになります。

ListVie でのページング

ポイントは、先の記事のようなストアドプロシージャを使うのではなくて、必要なレコードのみ DB から取得するクラス/メソッドを実装して、それらを ObjectDataSource で定義されているプロパティに設定するというところです。

加えて、出来るだけ ObjectDataSource と ListView が持つページングの機能を利用し、自力でコードは書かない(上記のクラス/メソッドを除く)ということもあります。

まず、型付 DataSet + TableAdapter(xsd ファイル)を Visual Studio のウィザードを利用して作成します。

Microsoft が提供しているサンプルデータベース Northwind の Products テーブルを利用します。ウィザードに従って進めていくと xsd ファイルが作られ、これをベースに 型付 DataSet + TableAdapter のコードが自動生成されます。

xsd ファイルを開くと、以下のように表示されるはずです。

xsd ファイルの作成

自動生成されたコードは、Web アプリケーションプロジェクトの場合は xsd ファイルの直下に、Web サイトプロジェクトの場合は Temporary ASP.NET Files フォルダ内にあります。

この TableAdapter コードを partial class を使って拡張して、必要なレコードのみ DB から取得するためのクラス/メソッドを実装します。

以下のようになります。GetDataByIndex メソッドの引数名 startRowIndex, maximumRowsは、ObjectDataSource がデフォルトでその名前を参照していますので、そのまま使うのが面倒がないです(変更する場合は MaximumRowsParameterName プロパティと StartRowIndexParameterName プロパティに変更後の引数名を設定してください)。名前空間名、クラス名は、自動生成されたコードを参照してください。

using System;
using System.Data;
using System.Configuration;
using System.Data.SqlClient;
using System.ComponentModel;

namespace ProductsDataSetTableAdapters
{
  public partial class ProductsTableAdapter
  {
    [DataObjectMethod(DataObjectMethodType.Select, true)]
    public ProductsDataSet.ProductsDataTable GetDataByIndex(int startRowIndex, int maximumRows)
    {
      SqlDataAdapter adapter;
      string query = String.Format(
        "SELECT * " +
          "FROM (" + 
             "SELECT *, ROW_NUMBER() OVER (ORDER BY [ProductID] ASC) AS rownum " + 
             "FROM [Products]) AS DerivedTable " +
          "WHERE rownum BETWEEN {0} AND {1} " +
          "ORDER BY [ProductID] ASC",
           startRowIndex + 1, maximumRows + startRowIndex);
      adapter = new SqlDataAdapter(query, this.Connection);

      ProductsDataSet dataset = new ProductsDataSet();
      adapter.Fill(dataset.Products);
      return dataset.Products;
    }

    public int GetNumberOfMessages()
    {
      SqlCommand command;
      command = 
        new SqlCommand("SELECT COUNT(*) FROM [Products]", this.Connection);
      this.Connection.Open();
      int rows = (int)command.ExecuteScalar();
      this.Connection.Close();
      return rows;
    }
  }
}

最後に、ObjectDataSource と ListView を実装した 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">

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <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;
    }  
  </style>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:ObjectDataSource ID="ObjectDataSource1" 
      runat="server" 
      SelectMethod="GetDataByIndex" 
      SelectCountMethod="GetNumberOfMessages" 
      EnablePaging="True"             
      TypeName="ProductsDataSetTableAdapters.ProductsTableAdapter">
    </asp:ObjectDataSource>
    <asp:ListView ID="ListView1" 
      runat="server" 
      DataKeyNames="ProductID" 
      DataSourceID="ObjectDataSource1" 
      EnableModelValidation="True">
      <LayoutTemplate>
        <table id="Table1" runat="server" class="style1">
          <tr id="Tr1" runat="server" style="">
            <th id="Th1" rowspan="2" runat="server">ProductID</th>
            <th id="Th2" colspan="7" runat="server">ProductName</th>
            <th id="Th3" rowspan="2" runat="server">Discontinued</th>
          </tr>
          <tr>
            <th id="Th4" runat="server">SupplierID</th>
            <th id="Th5" runat="server">CategoryID</th>
            <th id="Th6" runat="server">QuantityPerUnit</th>
            <th id="Th7" runat="server">UnitPrice</th>
            <th id="Th8" runat="server">UnitsInStock</th>
            <th id="Th9" runat="server">UnitsOnOrder</th>
            <th id="Th10" runat="server">ReorderLevel</th>
          </tr>                                
          <tr ID="itemPlaceholder" runat="server">
          </tr>
        </table>
      </LayoutTemplate>
      <AlternatingItemTemplate>
        <tr class="alternate">
          <td rowspan="2">
            <asp:Label ID="ProductIDLabel" 
              runat="server" 
              Text='<%# Eval("ProductID") %>' />
          </td>
            <td colspan="7">
            <asp:Label ID="ProductNameLabel" 
              runat="server" 
              Text='<%# Eval("ProductName") %>' />
          </td>
          <td rowspan="2">
            <asp:CheckBox ID="DiscontinuedCheckBox" 
              runat="server" 
              Checked='<%# Eval("Discontinued") %>' 
              Enabled="false" />
          </td>
        </tr>
        <tr class="alternate">
          <td>
            <asp:Label ID="SupplierIDLabel" 
              runat="server" 
              Text='<%# Eval("SupplierID") %>' />
          </td>
          <td>
            <asp:Label ID="CategoryIDLabel" 
              runat="server" 
              Text='<%# Eval("CategoryID") %>' />
          </td>
          <td>
            <asp:Label ID="QuantityPerUnitLabel" 
              runat="server" 
              Text='<%# Eval("QuantityPerUnit") %>' />
          </td>
          <td>
            <asp:Label ID="UnitPriceLabel" 
              runat="server" 
              Text='<%# Eval("UnitPrice") %>' />
          </td>
          <td>
            <asp:Label ID="UnitsInStockLabel" 
              runat="server" 
              Text='<%# Eval("UnitsInStock") %>' />
          </td>
          <td>
            <asp:Label ID="UnitsOnOrderLabel" 
              runat="server" 
            Text='<%# Eval("UnitsOnOrder") %>' />
          </td>
            <td>
            <asp:Label ID="ReorderLevelLabel" 
               runat="server" 
               Text='<%# Eval("ReorderLevel") %>' />
          </td>
        </tr>
      </AlternatingItemTemplate>
      <ItemTemplate>
        <tr>
          <td rowspan="2">
            <asp:Label ID="ProductIDLabel" 
              runat="server" 
              Text='<%# Eval("ProductID") %>' />
          </td>
          <td colspan="7">
          <asp:Label ID="ProductNameLabel" 
            runat="server" 
            Text='<%# Eval("ProductName") %>' />
          </td>
            <td rowspan="2">
              <asp:CheckBox ID="DiscontinuedCheckBox" 
                runat="server" 
                Checked='<%# Eval("Discontinued") %>' 
                Enabled="false" />
            </td>
        </tr>
        <tr>
         <td>
           <asp:Label ID="Label1" 
             runat="server" 
             Text='<%# Eval("SupplierID") %>' />
         </td>
         <td>
           <asp:Label ID="Label2" 
             runat="server" 
             Text='<%# Eval("CategoryID") %>' />
         </td>
         <td>
           <asp:Label ID="Label3" 
             runat="server" 
             Text='<%# Eval("QuantityPerUnit") %>' />
         </td>
         <td>
           <asp:Label ID="Label4" 
             runat="server" 
             Text='<%# Eval("UnitPrice") %>' />
         </td>
         <td>
           <asp:Label ID="Label5" 
             runat="server" 
             Text='<%# Eval("UnitsInStock") %>' />
         </td>
         <td>
           <asp:Label ID="Label6" 
             runat="server" 
             Text='<%# Eval("UnitsOnOrder") %>' />
         </td>
         <td>
           <asp:Label ID="Label7" runat="server" 
              Text='<%# Eval("ReorderLevel") %>' />
         </td>
       <tr>
      </ItemTemplate>
    </asp:ListView>
    <asp:DataPager ID="DataPager1" 
      runat="server" 
      PageSize="7" 
      PagedControlID="ListView1">
      <Fields>
        <asp:NextPreviousPagerField 
          ButtonType="Link" 
          ShowFirstPageButton="True" 
          ShowNextPageButton="False" 
          ShowPreviousPageButton="False" 
          FirstPageText="&lt;&lt;最初" />
        <asp:NumericPagerField 
          ButtonCount="5"
          PreviousPageText="&lt;前の 5 ページ"
          NextPageText="次の 5 ページ&gt;"/>
        <asp:NextPreviousPagerField 
          ButtonType="Link" 
          ShowLastPageButton="True" 
          ShowNextPageButton="False" 
          ShowPreviousPageButton="False" 
          LastPageText="最後&gt;&gt;" />
      </Fields>
    </asp:DataPager>
  </div>
  </form>
</body>
</html>

先の記事の方法と今回の記事の方法とで、どちらが簡単かと言えば、正直言ってどっちもどっちって感じです。

Tags: , ,

Paging

About this blog

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

Calendar

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

View posts in large calendar