WebSurfer's Home

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

GridView のヘッダ、列を固定(その 2)

by WebSurfer 2013年2月4日 21:26

github というサイトの Grid というページに紹介されている JavaScript(jQuery ではない)と CSS を利用して、GridView のヘッダと列を固定する例の紹介です。

GridView に Grid.js, Grid.css を適用

先の記事「GridView のヘッダ、列を固定」で、CSS の Internet Explorer (IE) 独自実装である expression 関数を使ってテーブルのヘッダと列を固定する例を書きました。

しかしながら、expression 関数のサポートは終了していて IE でも互換モードでないと動きませんし、IE の独自実装なので、当然、Firefox など他のブラウザでは動きませんので、あまり使い道はありません。

2017/8/16 注記追加
Windows 10 IE11 では Quirks モード(IE5 相当)にしても expression 関数が働かないようです。いつそうなったのかは不明ですが、expression 関数を使ってテーブルのヘッダ・列を固定する方法は使用禁止にした方がよさそうです。

代わりに、上に紹介した Grid のページから入手できる Grid.js と Grid.css を利用して、GridView のヘッダと列を固定するサンプルを作ってみました。これなら IE7+, Firefox, Chrome, Safari, Opera コンパチなので一般的に使えると思います。

上の画像を表示したサンプルコードを以下に書いておきます。注意点は以下およびコード内のコメントに書いておきましたので読んでください。

  1. Grid.js は、table の DOM、json 文字列、xml 文字列のいずれかをソースとして、ヘッダや列を固定可能なテーブルを生成します。GridView の場合は、GridView が生成した table の DOM をソースとして使うことになります。
  2. GridView が生成した table の DOM は、Grid.js が生成した別テーブルに置き換えられます。つまり、上の画像に表示されているテーブルは GridView ではなく、Grid.js が生成した別物です。
  3. Grid.js が生成した別テーブルを操作してデータベースの更新を行うのは無理っぽいです。別テーブルは表示するだけにして、更新は別に DetailsView か FormView を表示して行うのがよさそうです。

実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。

<%@ 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">
  // データソース用の DataTable を作成
  protected DataTable CreateDataTable()
  {
    DataTable dt = new DataTable();
    DataRow dr;

    dt.Columns.Add(new DataColumn("ID", typeof(Int32)));
    dt.Columns.Add(new DataColumn("Name", typeof(string)));
    dt.Columns.Add(new DataColumn("Type", typeof(string)));
    dt.Columns.Add(new DataColumn("Price", typeof(Int32)));
    dt.Columns.Add(new DataColumn("Qty", typeof(Int32)));
    dt.Columns.Add(new DataColumn("Amount", typeof(Int32)));
    dt.Columns.Add(new DataColumn("CategoryID", typeof(Int32)));
    dt.Columns.Add(new DataColumn("Note", typeof(string)));
    dt.Columns.Add(new DataColumn("Discontinued", typeof(bool)));
    dt.Columns.Add(new DataColumn("DateTime", typeof(DateTime)));

    for (int i = 0; i < 50; i++)
    {
      dr = dt.NewRow();
      dr["ID"] = i;
      dr["Name"] = "Product Name_" + i.ToString();
      dr["Type"] = "Product Type " + (100 - i).ToString();
      dr["Price"] = 123000 * (i + 1);
      dr["Qty"] = (i + 1) * 20;
      dr["Amount"] = 123000 * (i + 1) * (i + 1);
      dr["CategoryID"] = 100 - i;
      dr["Note"] = "Note_" + i.ToString();
      dr["Discontinued"] = (i%2 == 0)? true : false;
      dr["DateTime"] = DateTime.Now.AddDays(i);
      dt.Rows.Add(dr);
    }
    return dt;
  }

  // GridView に上記メソッドで作った DataTable をバインド
  protected void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack)
    {
      GridView1.DataSource = CreateDataTable();
      GridView1.DataBind();
    }
  }

  // ソースが DOM の場合、thead, tbody が必要。
  // GridView はデフォルトでは thead, tbody は生成
  // されないので、以下のコードを使って追加する。
  protected void GridView1_RowCreated(
    object sender, GridViewRowEventArgs e)
  {
    if (e.Row.RowType == DataControlRowType.Header)
    {
      e.Row.TableSection =
        System.Web.UI.WebControls.TableRowSection.TableHeader;
    }
    else if (e.Row.RowType == DataControlRowType.DataRow)
    {
      e.Row.TableSection =
        System.Web.UI.WebControls.TableRowSection.TableBody;
    }
    // フッターがある場合(GridView.ShowFooter が true の場合)は
    // 以下のコードのコメントアウトを解除。
    //else if (e.Row.RowType == DataControlRowType.Footer)
    //{
    //  e.Row.TableSection =
    //    System.Web.UI.WebControls.TableRowSection.TableFooter;
    //}
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
  <script src="/Scripts/Grid.js" type="text/javascript"></script>
  <link href="/css/Grid.css" rel="stylesheet" type="text/css" />
  <script type="text/javascript">
  //<![CDATA[
    window.onload = function () {
      // GridView は div 要素で囲った table を生成するの
      // で、その div 要素を利用する。div 要素に id を付
      // 与して初期サイズを指定する CSS を設定する。
      // div 要素の直下に table がないと、Grid.js による
      // 置き換えがうまくいかない(GridView の table が
      // 残ってしまう)ので注意。
      var tableGridView = 
        document.getElementById("<%=GridView1.ClientID%>");
      var parentElement = tableGridView.parentNode;
      parentElement.setAttribute("id", "myGrid");
      parentElement.setAttribute("class", "style1");

      // ソートするために各列のデータ型を指定。デフォルト
      // は string なので、数字などの列がある場合は指定し
      // ないとソート結果が期待通りにならない。
      var gridColSortTypes =
        ["number", "string", "string", "number", "number", 
        "number", "number", "string", "none", "date"];

      // GridView (table) の場合、srcType は "dom" に設定。
      // 下のコードの myGrid は table を囲う div 要素の id。
      // SrcData には table の id(GridView の ClientID)を
      // 設定。その他のパラメータ設定は Grid のページ参照。
      // Grid.js は、SrcData に指定された table の DOM を
      // ソースに使って別テーブルを生成し、元の table と置
      // き換える。
      new Grid("myGrid", {
        srcType: "dom",
        srcData: "<%=GridView1.ClientID%>",
        allowGridResize: true,
        allowColumnResize: true,
        allowClientSideSorting: true,
        allowSelections: true,
        allowMultipleSelections: true,
        showSelectionColumn: false,
        colSortTypes : gridColSortTypes,
        fixedCols: 1
      });
    };
  //]]>
  </script>

  <style type="text/css">
    /* テーブルの初期サイズの指定 */
    .style1
    {
      width: 400px;
      height: 360px;
    }    
  </style>
</head>
<body>
  <form id="form1" runat="server">
  <asp:GridView ID="GridView1" 
    runat="server"
    OnRowCreated="GridView1_RowCreated" 
    EnableViewState="False">
  </asp:GridView>
  </form>
</body>
</html>

Tags: ,

JavaScript

defer 属性つき script 定義と IE の問題

by WebSurfer 2012年12月3日 21:37

外部スクリプトファイルを定義する script 要素 に defer="defer" 属性を追加すると、あるケースで、internet explorer (IE) がそのスクリプトファイルを解析できなくなるという問題の紹介です。

defer 属性つき script 定義と IE の問題

「あるケース」というのは、div 要素などの innerHTML を書き換えることです。自分でそのようなコートを書かなくても、例えば、SWFObject を使って Flash を埋め込む場合に innerHTML の書き換えが行われます。

ただし、html コードを書く順番が問題で、defer 属性を追加した script タグが出現した後、innerHTML を書き換える場合に限ります。順番が反対の場合は問題は起こりません。

確証がないのではっきりしたことは言えませんが、自分が試した限りでは、スクリプトの取得に時間がかかる(サーバーの応答が遅い)と問題が発生する確率が高いようです。ブラウザの解析の速度も関係があるようで、IE6 であればほぼ 100% 問題が発生するのに対し、IE8 は微妙なタイミングで問題が発生したりしなかったりします。

検証のため、スクリプトをダウンロードする HTTP ハンドラを作って、Thread.Sleep メソッドを使って応答に時間がかかるようにしてみました。

<%@ WebHandler Language="C#" Class="JavaScriptHandler" %>

using System;
using System.Web;
using System.Text;
using System.Threading;
using System.Diagnostics;

public class JavaScriptHandler : IHttpHandler
{
  public void ProcessRequest (HttpContext context)
  {
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    StringBuilder sb = new StringBuilder();
    sb.Append("DateTime accessed: " 
      + DateTime.Now.ToString("d MMM yyyy HH:mm:ss zzz",
        System.Globalization.DateTimeFormatInfo.InvariantInfo)
      + ", ");
       
    string delay = context.Request.QueryString["delay"];
    int time;
    bool result = Int32.TryParse(delay, out time);
    if (result)
    {
      Thread.Sleep(time);
      sb.Append(String.Format(
        "delay time set: {0} ms", time) + ", ");
    }
    else
    {
      sb.Append("delay time set: none, ");
    }
       
    context.Response.ContentType = "text/javascript";
    context.Response.Cache.VaryByHeaders["Accept-Encoding"] = 
      true;
    context.Response.Cache.SetCacheability(
      HttpCacheability.NoCache);
    context.Response.Cache.SetExpires(
      DateTime.Now.ToUniversalTime());
    context.Response.Cache.SetMaxAge(
      new TimeSpan(0, 0, 0, 0));
    context.Response.AppendHeader("Pragma", "no-cache");

    stopWatch.Stop();
    TimeSpan ts = stopWatch.Elapsed;
    sb.Append(String.Format(
      "TimeSpan measured: {0:000} ms", ts.Milliseconds));
    string script = sb.ToString();
    script = "var msg = '" + script + "'";       
    context.Response.Write(script);
  }
 
  public bool IsReusable
  {
    get
    {
      return false;
    }
  }
}

上記の HTTP ハンドラを呼び出す際、例えば、クエリ文字列を delay=200 とすると、リクエストを受けてから約 200ms 後にアクセスした時間、クエリ文字列の設定、実際に計った時間をスクリプトとして返します。

以下のような簡単な HTML コードで試すことができます。たぶん delay はもっと少なくても問題が再現すると思います。実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript" 
        src="JavaScriptHandler.ashx?delay=200" defer="defer">
    </script>
    <script type="text/javascript">
    //<![CDATA[
        function write(id){
            document.getElementById(id).innerHTML = 
                "<h1>innerHTML changed!<\/h1>";
        }
        
        function ScriptTest() {
            var x = msg;
            alert(x);
        }
    //]]>
    </script>
</head>
<body>
    <div id="myContent">
        <h1>This will be replaced by write method</h1>
    </div>
    <script type="text/javascript">
    //<![CDATA[
        write("myContent");
    //]]>
    </script>
    <br />
    <input type="button" value="Script Test" 
        id="button1" onclick="javascript:ScriptTest();" />   
</body>
</html>

自分が検証した限りでは、IE6-9 で同じ問題が出ました(IE10 は未検証)。対応策は、(1) defer="defer" 属性を使用しない、または、(2) innnerHTML を書き換えた後で defer="defer" 属性付の script タグを読むよう順序を変更する、のいずれかしかなさそうです。

Tags: , ,

JavaScript

キャプチャリングとバブリング

by WebSurfer 2012年12月1日 00:14

JavaScript や jQuery を使ったプログラミングで、DOM イベントのバブリングという言葉を耳にします。本などを読んでもピンと来なかったので、理解するためにサンプルを作って動かしてみました。あまり面白くないかもしれませんが、せっかく作ったので書いておきます。

イベントのキャプチャリングとバブリング

上の画像のサンプルは、実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。ソースコードはこの記事の下の方に記載しています。

DOM イベントは、イベントの原因となったオブジェクトで発生するだけでなく、キャプチャリングとバブリングというイベントの伝播があります。ここが .NET Framework のイベントとは異なっていて、自分が理解に苦しんだところです。

どこかの DOM オブジェクトでイベントが発生すると、window オブジェクト(ブラウザによっては document オブジェクト)とイベントが起きたオブジェクトの間を、DOM ツリーの親子関係を順にたぐって、イベントが伝播していきます。

伝播は 3 つのフェーズに分かれており、Capturing Phase(捕捉フェーズ)⇒ Target Phase(対象フェーズ)⇒ Bubbling Phase(浮上フェーズ)という順番になります。それぞれのフェーズの説明は以下の通りです。

  1. Capturing Phase では、window ⇒ document ⇒ その中の親 ⇒ 子 ⇒ 孫 ⇒ ひ孫 ・・・といった具合に、親子関係を親側から順にたぐって、イベントが起きたオブジェクトの親まで(「親まで」という点に注意)の各オブジェクトにイベントが送信されていきます。
  2. Target Phase では、イベントの原因となったオブジェクトにイベントが送信されます。
  3. Bubbling Phase では、イベントを発生させたオブジェクトの親から(「親から」という点に注意)順に浮上していき、window に達するまで各オブジェクトに順にイベントが送信されていきます。

window からイベントを発生させたオブジェクトの間の、親子関係にある任意のオブジェクトにリスナーを登録しておけば、そのオブジェクトにイベントが送信されたタイミングで必要な処置ができます。

オブジェクトにリスナーを登録する方法は、(1) 当該オブジェクトの addEventListener メソッド を呼び出す、(2) HTML 要素の属性を利用する、(3) 当該オブジェクトのプロパティを利用する、の 3 つがあります。

注意しなければならないのは、上記の (1) 以外の方法では、Capturing Phase でイベントを捕捉できないことです。

さらに注意しなければならないのは、IE8 以前では addEventListener メソッドはサポートされていないことです。代わりに attachEvent メソッドが使えますが、これは Capturing Phase でイベントを捕捉できません。(attachEvent メソッドの詳細は後述します)

IE9 は DOM Level 3 Events をサポートしているそうなので(詳しくは MSDN の IEBlog DOM Level 3 Events support in IE9 を参照)、addEventListener メソッド を使用可能です。

addEventListener メソッドを利用してリスナーを登録し、Capturing Phase、Target Phase、Bubbling Phase でイベントを補足するサンプルを書いてみました。そのソースコードをアップしておきます。

なお、Target Phase では、addEventListener メソッドで第 3 引数 useCapture を false に指定して登録したリスナーのみが呼び出されることになっているそうですが、自分が試した限りでは、useCapture を true に指定して登録したリスナーまでもが呼び出されてしまいました。

検証に使ったブラウザは、IE9、Firefox 17.0, Chrome 23.0.1271.95 m, Opera 12.02, Safari 5.1.7 で、すべて同じ結果になりました。

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Event Capturing and Bubbling</title>
    <style type="text/css">
        #div1 { border: 1px solid black; width: 200px; 
            height: 100px; }
        #table1 { border: 1px solid red; width: 150px; }
        td { border: 1px solid green; }
        #span1 { background-color: yellow; }
    </style>
    <script type="text/javascript">
    //<![CDATA[

        // IE9, Mozzilla 用リスナー。
        function listener1(event) {
            // イベントが発生すると event オブジェクトが生成
            // され、その参照が第一引数に渡される。それから
            // target プロパティでイベントを発生させたオブジ
            // ェクトを、eventPhase プロパティでフェーズ情
            // 報を取得できる。this はアタッチしたオブジェク
            // トへの参照となる。
            var e = document.getElementById("result");
            var phase = "";
            if (event.eventPhase === 1) {
                phase = "capturing phase";
            } else if (event.eventPhase === 2) {
                phase = "target phase";
            } else if (event.eventPhase === 3) {
                phase = "bubbling phase";
            }
            var str = "fired by: " + event.target.id + 
                      ", listened at: " + this.id + 
                      ", during " + phase + "<br />";
            e.innerHTML += str;
        }

        // IE6-8 用リスナー。
        function listener3(element) {
            // attachEvent を使うと、this が参照するオブジェ
            // クトは window になってしまい、アタッチしたオ
            // ブジェクトへの参照は取得できない。止むを得な
            // いので、オブジェクトへの参照は引数として渡す。
            // Mozilla 系ではリスナーの第一引数には event オ
            // ブジェクトへの参照が渡されるので注意。
            var e = document.getElementById("result");
            var str = 
                "fired by: " + window.event.srcElement.id +
                ", listened at: " + element.id + "<br />";
            e.innerHTML += str;
        }

        // Bubbling Phase のリスナーをアタッチ。
        function attachForBubbling(element) {
            var e = document.getElementById("result");
            if (element.addEventListener) {
                // Bubbling Phase のリスナーをアタッチするに
                // は第三引数を false に設定する。
                element.addEventListener('click', listener1, 
                    false);
                e.innerHTML += "listener1 attached to " +
                    element.id +
                    " by addEventListener" + 
                    " w/ useCapture=false<br />";
            } else if (element.attachEvent) {
                // アタッチするオブジェクトへの参照をリスナ
                // ーの引数に渡すため、以下のように匿名関数
                // を使う。ただし、匿名関数を使うとデタッチ
                // できなくなることに注意。
                element.attachEvent('onclick', 
                    function () { listener3(element) });
                e.innerHTML += "listener3 attached to " + 
                    element.id + " by attachEvent<br />";
            }
        }

        // Capturing Phase のリスナーをアタッチ。
        // (IE9, Mozilla のみ) 
        function attachForCapturing(element) {
            var e = document.getElementById("result");
            if (element.addEventListener) {
                // Capturing Phase 用リスナーをアタッチするに
                // は第三引数を true に設定する。
                element.addEventListener('click', listener1, 
                    true);
                e.innerHTML += "listener1 attached to " +
                    element.id +
                    " by addEventListener" + 
                    " w/ useCapture=true<br />";
            }
        }

        // リスナーを各オブジェクトにアタッチ。
        window.onload = function () {
            var element = document.getElementById("div1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("table1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("td1");
            attachForBubbling(element);
            attachForCapturing(element);

            element = document.getElementById("span1");
            attachForBubbling(element);
            attachForCapturing(element);
        }
    //]]>
    </script>    
</head>
<body>
    <div id="div1">
        <table id="table1">
            <tr>
                <td id="td1">
                    <span id="span1">span1</span>
                </td>
            </tr>
            <tr>
                <td id="td2">
                    <span id="span2">span2</span>
                </td>
            </tr>
        </table>
    </div>
    <input type="button" value="Clear Results" 
        onclick="javascript:result.innerHTML = '';"/>
    <br />
    <div id="result"></div>
</body>
</html>

上でも述べましたように、IE6-8 では、addEventListener メソッドはサポートされていませんが、代わりに attachEvent メソッド がリスナーを登録するのに使えます。

IE6-8 をサポートするためには以下のようにします。上のサンプルコードでもこのようにして IE6-8 でリスナーを登録しています。

if (element.addEventListener) {
    element.addEventListener('click', listener, false);
} else if (element.attachEvent){
    element.attachEvent('onclick', listener);
}

attachEvent メソッドには以下のデメリットがあるので注意してください。

  1. リスナー内の this で取得できるのが、window オブジェクトへの参照になる。(addEventListener メソッドの場合はリスナーをアタッチしたオブジェクトへの参照になる)
  2. Capturing Phase ではイベントを補足できない。(Bubbling Phase と Target Phase では補足可能)

上記 1 の問題に対応するため、サンプルコードでは、リスナーの引数にアタッチするオブジェクトへの参照を渡しています。さらに、attachEvent メソッドの引数に匿名関数を使って、リスナーを登録しています。

Tags: , , ,

JavaScript

About this blog

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

Calendar

<<  2024年5月  >>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar