WebSurfer's Home

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

Validator の Display="Dynamic" 時の注意点

by WebSurfer 2014年1月16日 17:45

ASP.NET の検証コントロール(RequiredFieldValidator, RegularExpressionValidator など)を使用して、Display プロパティを Dynamic に設定したときの注意点です。

Validator の Display="Dynamic" 時の注意点

この件は stackoverflow の記事 でも報告されています。ただ、実は自分はつい最近までこの問題は知らなかったです。(汗)

例えば、以下のように RequiredFieldValidator を配置して、その直後に Button を配置したとします。

<%@ 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 Button1_Click(object sender, EventArgs e)
  {
    if (Page.IsValid)
    {
      // 何らかの処置。
    }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
    <asp:RequiredFieldValidator ID="RequiredFieldValidator1" 
      runat="server" 
      ErrorMessage="エラーメッセージ"  
      ControlToValidate="TextBox1" 
      Display="Dynamic">
    </asp:RequiredFieldValidator>
    <asp:Button ID="Button1" 
      runat="server" 
      Text="Button" 
      OnClick="Button1_Click" />                
  </div>
  </form>
</body>
</html>

上記のコードでは以下の手順で問題を再現できます。

  1. TextBox は空のまま Button をクリックする。
  2. クライアント側で検証がかかり、ErrorMessage プロパティに設定した「エラーメッセージ」が表示される。
  3. TextBox に文字を入力して Button をクリックする。 ⇒ 「エラーメッセージ」は消えるがポストバックがかからない。
  4. 再度 Button をクリックする。 ⇒ ポストバックがかかる。

期待される動きは、上記 3 で「エラーメッセージ」が消えるとともにポストバックがかかるということのはずですが、そうはなりません。

上記のコードにおける解決策は、Button の直前に改行 ( <br /> ) を入れることです。そうしないとうまく行かない理由は以下の通りです。

Validator のエラーメッセージは html コードでは span 要素となり、JavaScript による検証結果により表示/非表示を切り替えています。

Display プロパティが Dynamic に設定されている場合は、当該 span 要素の style 属性を "display:none;" または "display:inline;" に設定することにより表示/非常時を切り替えます。

検証対象の TextBox にフォーカスを当ててからフォーカスを外す(例えば、TextBox に入力してから form を submit するために Button をクリックする)と、 そのタイミングで JavaScript による検証がかかるようになっています。

検証結果によって "display:inline;" が "display:none;" に(またはその逆に)書き換えられるので、エラーメッセージの部分のページレイアウトが変わることになります。

従って、上記のコードのように、RequiredFieldValidator の直後に Button が配置されているような場合、エラーメッセージが表示/非表示になる分だけ画面上でボタンが左右に移動します。

ボタンが移動すると、<input type="submit" ... /> タイプのボタンをクリックしたにもかかわらず form が submit されません。 即ち、ポストバックされないという期待に反する動作になります。

なお Button が動くのは左右でなくても、例えばエラーメッセージを p 要素に入れると上下に移動しますが、その場合でも同じくform は submit されません。

このことは、ASP.NET の検証コントロールを使った場合に限った話ではなく、html 要素と JavaScript だけでも再現できます。 以下のサンプルコードは、html 要素と JavaScript だけでこの問題(ボタンが動くと submit されない)を再現する例です。

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

<%@ 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>WebSurfer's Page - 実験室</title>
  <script src="/scripts/jquery-1.8.3.min.js" type="text/javascript">
  </script>
  <script type="text/javascript">
  //<![CDATA[
    function toggleDisplay() {
      var validator = $('#RequiredFieldValidator1');
      if (validator.css("display") == "none") {
        validator.css("display", "inline");
      } else {
        validator.css("display", "none");
      }            
    }
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server" 
    onsubmit="javascript:return confirm('Submit しますか?');">

  <div>
    <input name="textbox1" type="text" id="1extbox1" 
      onblur="javascript: toggleDisplay();" />
    <span id="RequiredFieldValidator1" 
      style="display:none;">エラーメッセージ</span>
    <input type="submit" name="button1" value="POST" 
      id="button1" />    
  </div>
  </form>
</body>
</html>

ちなみに、Display プロパティが Static に設定されている場合は、style 属性を "visibility:hidden;" または "visibility:visible" に設定するので、 表示/非表示を切り替えてもページのレイアウトは変わりません。結果、ボタンは動かないのでこの問題は起こりません。

Tags: ,

Validation

Paging 機能付 GridView の行選択

by WebSurfer 2013年12月19日 16:59

GridView に CheckBox を配置し、ユーザーに複数行を選択してもらうというシナリオはよくあると思います。この場合、GridView にページング機能が実装されていると問題です。

Paging 機能付 GridView の行選択結果取得

例えば、Page 1 でいくつかの行を選択して CheckBox にチェックを入れてから Page 2 に移動し、再び Page 1 に戻った場合、先に Page 1 で CheckBox に入れたはずのチェックが消えてしまいます。

この問題に対処するためには、以前のチェック情報を ViewState に保持しておき、ページが変わったら ViewState からそのページのチェック情報を取得して CheckBox.Checked プロパティを true に設定してやるというような操作が必要になります。

ページャーがクリックされるとポストバックが発生しますので、そのタイミングでクライアントスクリプトによってチェック結果を取得して隠しフィールドに格納し、それをサーバーに送信して ViewState に保存するようにしてみました。

CheckBox の ID には、GridView.DataKeys プロパティから主キー値を取得し、それを設定しています。それゆえ、クライアントスクリプトでもチェックされた CheckBox の name 属性から当該レコードの主キー値が分かります。

���だし、ID から name への名付けルールが変わるとうまく行かなくなる可能性がありますので、できれば主キー値を GridView に表示して、それから取得するようにした方がいいかもしれません。

以下のソースコードは主キー値を name 属性から取得する場合の例です。上の画像がソースコードの実行結果です。

注意事項はソースコード内のコメントに書きましたので、詳しくはそれを見てください。手抜きですみません。(汗)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Collections.Generic" %>

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

<script runat="server">
  // CheckBox をクリックするたびポストバックしないで、ページン
  // グでポストバックした際に、まとめてチェック結果を取得し、
  // チェックされた行の主キー (ID) リストを更新する。

  // クライアント側で、input type="checkbox" 要素の id 属性また
  // は name 属性から、その要素のある行の主キー (ID) を取得する
  // ため、CheckBox.ID に主キー値を設定。主キー値が 17 の場合
  // id="GridView1_17_0" name="GridView1$ctl02$17" のようになる。
  // このサンプルでは、クライアントスクリプトで name 属性から
  // 主キー値を取得している。名前付けルールが変わってうまくいか
  // なくなる可能性があるので注意。
    
    
  // チェック入り行の主キー (ID) のリスト。ViewState に保持。    
  protected List<String> checkedIds;
    
  protected void Page_Load(object sender, EventArgs e)
  {
    // ViewState から ID リスト checkedIds を取得。
    if (checkedIds == null)
    {
      object obj = ViewState["CheckedIds"];
      if (obj != null)
      {
        checkedIds = (List<String>)obj;
      }
      else
      {
        // ViewState が未設定の場合は新たに初期化。
        checkedIds = new List<String>();
      }
    }

    // form を送信する際、CheckBox のチェック有無を調べて、
    // その結果をサーバーに送信するスクリプトを設定。
    String csname = "OnSubmitScript";
    Type cstype = this.GetType();
    ClientScriptManager cs = Page.ClientScript;
    if (!cs.IsOnSubmitStatementRegistered(cstype, csname))
    {
      String cstext = 
          "getCheckedIDs('" + GridView1.ClientID + "');";
      cs.RegisterOnSubmitStatement(cstype, csname, cstext);
    }
  }

  // ページャクリックの場合のイベント発生順序に注意:
  // 現ページの GridView.RowCreated ⇒ Page.Load ⇒ GridView.
  // PageIndexChanging ⇒ 次ページの GridView.RowCreated

  // ちなみに、ボタンクリックによるイベント発生順序は:
  // 現ページの GridView.RowCreated ⇒ Page.Load ⇒ 
  // Button.Click
    
  // CheckBox.ID を動的に設定するのを RowDataBound イベント
  // で行うのは NG。ボタンクリックではデータバインドが起こ
  // らないので、CheckBox.ID が設定されず(ASP.NET が勝手に
  // 独自の ID を生成する)、うまくいかない。
  protected void GridView1_RowCreated(object sender, 
        GridViewRowEventArgs e)
  {
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
      // ViewState から ID リスト checkedIds を取得。
      // ボタンクリックまたはページングによるポストバック
      // では Page.Load よりこちらのイベントが先に発生す
      // るのでここでも必要。
      if (checkedIds == null)
      {
        object obj = ViewState["CheckedIds"];
        if (obj != null)
        {
          checkedIds = (List<String>)obj;
        }
        else
        {
          // ViewState が未設定の場合は新たに初期化。
          checkedIds = new List<String>();
        }
      }
            
      // この行の id(主キー)値を取得。
      string id = ((GridView)sender).
          DataKeys[e.Row.RowIndex].Value.ToString();

      // e.Row.Cells[0] の中から CheckBox を探す。
      foreach (Control control in e.Row.Cells[0].Controls)
      {
        if (control is CheckBox)
        {
          CheckBox cb = (CheckBox)control;

          // CheckBox.ID に id を設定。クライアントスクリプト
          // で CheckBox の name 属性から id を取得できるよう
          // にするため。
          // このサンプルでは name は GridView1$ctl02$17 のよ
          // うになり、最後の 17 が id に該当。
          cb.ID = id;

          // id がリスト checkedIds にあればチェックを入れる。
          foreach (string checkedid in checkedIds)
          {
            if (checkedid == id)
            {
              cb.Checked = true;
              return;
            }
          }

          cb.Checked = false;
          break;
        }
      }
    }
  }

  // ページが変更される際にリスト checkedIds を更新する。
  protected void GridView1_PageIndexChanging(object sender, 
        GridViewPageEventArgs e)
  {
    // form の onsubmit イベントで、クライアントスクリプトが
    // CheckBox のチェック有無の情報を取得し、当該行の id を
    // 隠しフィールドに格納するので、それから id を取得する。
    char[] chars = new char[] { ',' };
    string s = Request.Form["__CHECKEDIDLIST"].Trim(chars);
    string[] check = s.Split(chars);
    s = Request.Form["__UNCHECKEDIDLIST"].Trim(chars);
    string[] uncheck = s.Split(chars);

    // 重複を避けるため、一旦、GridView 上のすべての ID を
    // リスト checkedIds から削除してから、
    foreach (string id in check)
    {
      checkedIds.Remove(id);
    }

    foreach (string id in uncheck)
    {
      checkedIds.Remove(id);
    }

    // チェック済の ID をリスト checkedIds に加える。
    foreach (string id in check)
    {
      if (!String.IsNullOrEmpty(id))
      {
        checkedIds.Add(id);
      }
    }

    // リスト checkedIds を ViewState に保持する。
    ViewState["CheckedIds"] = checkedIds;
  }
        
  // ボタンクリックでチェックされた全行の id を書き出す。
  protected void Button1_Click(object sender, EventArgs e)
  {
    // ボタンクリック直前のチェック結果をここで取得。
    char[] chars = new char[] { ',' };
    string s = Request.Form["__CHECKEDIDLIST"].Trim(chars);
    string[] check = s.Split(chars);
    s = Request.Form["__UNCHECKEDIDLIST"].Trim(chars);
    string[] uncheck = s.Split(chars);
        
    // チェック入り行の主キー (ID) リスト checkedIds を更新。
    foreach (string id in check)
    {
      checkedIds.Remove(id);
    }

    foreach (string id in uncheck)
    {
      checkedIds.Remove(id);
    }
        
    foreach (string id in check)
    {
      if (!String.IsNullOrEmpty(id))
      {
        checkedIds.Add(id);
      }
    }
        
    ViewState["CheckedIds"] = checkedIds;

    // リスト checkedIds の内容を書き出す。
    string str = "Selected Ids:";
    foreach (string id in checkedIds)
    {
      str += " " + id;
    }
    Label1.Text = str;
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
  <title></title>
  <script src="Scripts/jquery-1.8.3.js" type="text/javascript">
  </script>
  <script type="text/javascript">
  //<![CDATA[
    // CheckBox のチェック有無を調べて、その結果を隠し
    // フィールドに設定するスクリプト。
    function getCheckedIDs(gridViewClientId) {
      var checked = "";
      var unchecked = "";

      $('#' + gridViewClientId + ' input:checkbox').each(
        function () {
          // このサンプルでは name は GridView1$ctl02$17 
          // のようになり、最後の 17 が id に該当。
          // ただし MasterPage を使うなど、名前付コンテナ
          // に GridView を入れると違ってくるので注意。
          var name = $(this).attr('name');
          var arrayOfString = name.split('$');
          var id = arrayOfString[arrayOfString.length - 1];
          if ($(this).attr('checked') == "checked") {
            checked += ',' + id;
          } else {
            unchecked += ',' + id;
          }
        });

      $('#__CHECKEDIDLIST').val(checked);
      $('#__UNCHECKEDIDLIST').val(unchecked);
    }
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">

  <%--チェック結果をサーバに送信する隠しフィールド。--%>
  <input type="hidden" 
    name="__CHECKEDIDLIST" 
    id="__CHECKEDIDLIST" 
    value="" />
  <input type="hidden" 
    name="__UNCHECKEDIDLIST" 
    id="__UNCHECKEDIDLIST" 
    value="" />

  <div>        
    <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
      ConnectionString="<%$ ConnectionStrings:Northwind %>" 
      SelectCommand= "SELECT [ProductID], [ProductName] 
          FROM [Products] ORDER BY [ProductID]">
    </asp:SqlDataSource>
    <asp:GridView ID="GridView1" 
      runat="server" 
      AllowPaging="True" 
      AutoGenerateColumns="False" 
      DataKeyNames="ProductID" 
      DataSourceID="SqlDataSource1" 
      OnPageIndexChanging="GridView1_PageIndexChanging" 
      OnRowCreated="GridView1_RowCreated">
      <Columns>                
        <asp:TemplateField HeaderText="Select">                    
          <ItemTemplate>
            <asp:CheckBox runat="server" />
          </ItemTemplate>                    
        </asp:TemplateField>                
        <asp:BoundField DataField="ProductID" 
          HeaderText="ProductID" 
          InsertVisible="False" 
          ReadOnly="True" 
          SortExpression="ProductID" />
        <asp:BoundField DataField="ProductName" 
          HeaderText="ProductName" 
          SortExpression="ProductName" />
      </Columns>
    </asp:GridView>
    <asp:Button ID="Button1" 
      runat="server" 
      Text="Show Checked Ids" 
      OnClick="Button1_Click" />
    <asp:Label ID="Label1" runat="server"></asp:Label>
  </div>
  </form>
</body>
</html>

Tags: ,

ASP.NET

CheckBox 付き Calendar(その2)

by WebSurfer 2013年12月12日 21:24

先の記事 CheckBox 付き Calendar コントロール で「チェックを入れる/外すたびにいちいちポストバックしないでもすむ方法を考え付いたら、別途記事を書きます」と書きましたが、今頃になってそれを作ってみました。

CheckBox 付き Calendar

前のサンプルでは、CheckBox の onclick 属性にポストバックするスクリプトを設定して、CheckBox がクリックされるたびポストバックしてサーバ側でチェック結果を取得して ViewState に保存していました。

それをやめて、別の月に移動する時またはボタンクリックしてポストバックされる時にまとめてクライアント側でチェック結果を取得し、それをサーバーに送信して ViewState に保存するようにしました。

以下にそのコードを示します。詳しくはコメントに書きましたので、それを見てください。

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

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Collections.Generic" %>

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

<script runat="server">
  // CheckBox をクリックするたびポストバックしないで、ページン
  // グでポストバックした際に、まとめてチェック結果を取得し、
  // リスト checkedDates を更新する。

  // チェックされている日付のリスト。ViewState に保持する。
  protected List<DateTime> checkedDates;

  protected void Page_Load(object sender, EventArgs e)
  {
    // ViewState から日付リスト checkedDates を取得。
    object obj = ViewState["CheckedDates"];
    if (obj != null)
    {
      checkedDates = (List<DateTime>)obj;
    }
    else
    {
      // ViewState が未設定の場合は新たに初期化。
      checkedDates = new List<DateTime>();
    }
        
    // ポストバックの時は、隠しフィールドでポストされたデータ
    // で日付リスト checkedDates を更新する。
    if (Page.IsPostBack)
    {
      char[] chars = new char[] { ',' };
      string s = 
          Request.Form["__CHECKEDIDLIST"].Trim(chars);
      string[] check = s.Split(chars);
      s = Request.Form["__UNCHECKEDIDLIST"].Trim(chars);
      string[] uncheck = s.Split(chars);

      foreach (string id in check)
      {
        if (!String.IsNullOrEmpty(id))
        {
          checkedDates.Remove(DateTime.Parse(id));
        }
      }

      foreach (string id in uncheck)
      {
        if (!String.IsNullOrEmpty(id))
        {
          checkedDates.Remove(DateTime.Parse(id));
        }
      }

      foreach (string id in check)
      {
        if (!String.IsNullOrEmpty(id))
        {
          checkedDates.Add(DateTime.Parse(id));
        }
      }

      ViewState["CheckedDates"] = checkedDates;
    }

    // form を送信する際、CheckBox のチェック有無を調べて、
    // その結果をサーバーに送信するスクリプトを設定。
    String csname = "OnSubmitScript";
    Type cstype = this.GetType();
    ClientScriptManager cs = Page.ClientScript;
    if (!cs.IsOnSubmitStatementRegistered(cstype, csname))
    {
      String cstext =
        "getCheckedDates('" + Calendar1.ClientID + "');";
      cs.RegisterOnSubmitStatement(cstype, csname, cstext);
    }
  }    

  protected void Calendar1_DayRender(object sender, 
      DayRenderEventArgs e)
  {
    CheckBox cb = new CheckBox();

    // このセルの日付を CheckBox の ID に設定。
    cb.ID = e.Day.Date.ToShortDateString();

    // このセルの日付がリスト checkedDates にあれば CheckBox
    // にチェックを入れる。
    foreach (DateTime day in checkedDates)
    {
      if (e.Day.Date == day)
      {
        cb.Checked = true;
        e.Cell.Controls.Add(cb);
        return;
      }            
    }
        
    cb.Checked = false;
    e.Cell.Controls.Add(cb);
  }

  // ボタンクリックで最終結果を取得。
  protected void Button1_Click(object sender, EventArgs e)
  {
    string str = "Selected Dates:";
    foreach (DateTime day in checkedDates)
    {
      str += "<br />" + day.ToShortDateString();
    }
    Label1.Text = str;
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
  <title>Calendar with CheckBox</title>
  <script src="Scripts/jquery-1.6.2.min.js" type="text/javascript">
  </script>
  <script type="text/javascript">
  //<![CDATA[
    // CheckBox のチェック有無を調べて、その結果を隠し
    // フィールドに設定するスクリプト。
    function getCheckedDates(calendarClientId) {
      var checked = "";
      var unchecked = "";

      $('#' + calendarClientId + ' input:checkbox').each(
        function () {
          // MasterPage を使うなど、名前付コンテナに Calendar
          // を入れると id が違ってくるので注意。
          var id = $(this).attr('id');                    
          if ($(this).attr('checked') == "checked") {
            checked += ',' + id;
          } else {
            unchecked += ',' + id;
          }
        });

      $('#__CHECKEDIDLIST').val(checked);
      $('#__UNCHECKEDIDLIST').val(unchecked);
    }
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">

  <%--チェック結果をサーバに送信する隠しフィールド。--%>
  <input type="hidden" 
    name="__CHECKEDIDLIST" 
    id="__CHECKEDIDLIST" 
    value="" />
  <input type="hidden" 
    name="__UNCHECKEDIDLIST" 
    id="__UNCHECKEDIDLIST" 
    value="" />

  <div>
    <asp:Calendar ID="Calendar1" 
      runat="server" 
      ondayrender="Calendar1_DayRender" 
      SelectionMode="None" >
    </asp:Calendar>
    <asp:Button ID="Button1" 
      runat="server" 
      Text="Show selected dates" 
      onclick="Button1_Click" />
    <br />
    <asp:Label ID="Label1" runat="server" />        
  </div>
  </form>
</body>
</html>

Tags:

ASP.NET

About this blog

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

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar