WebSurfer's Home

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

UpdatePanel と ListView

by WebSurfer 2016年11月20日 17:24

先に今回の事例から学んだ教訓を書いておきます。ちょっと今さらながらの感がありますが。

ASP.NET 4 以降では、UpdatePanel を使って非同期ポストバックを行う場合、ポストバックのトリガとするコントロールの ClientIDMode プロパティはデフォルトではなく AutoID にしておくこと。

UpdatePanel に LinkButton を含む ListView を配置した場合、LinkButton をクリックしても非同期ポストバックとならず、同期ポストバックになることからこの問題に気がつきました。(注: GridView の場合は非同期ポストバックがかかります・・・理由後述)

UpdatePanel と ListView

これは LinkButton に限らず、ASP.NET が自動生成するスクリプトの __doPostBack メソッドを利用してポストバックをかけるコントロールすべてに同様です。例えば TextBox の AutoPostBack プロパティを true にしたような場合も非同期ポストバックはかかりません。

そもそもの原因は ASP.NET サイトの記事 ASP.NET 4 Breaking Changes / ClientIDMode Changes にあるように、ASP.NET 4 からは ClientID の命名がデフォルトで Predictable となったからです。(正確に言うと、コントロールの ClientIDMode はデフォルトで Inherit だが、ページの ClientIDMode はデフォルトで Predictable なので、それを継承して結果的に Predictable になるということです)

ScriptManager が生成するスクリプトは前の方法、すなわち AutoID モードでの ClientID を期待しているので、デフォルトの Predictable では非同期ポストバックのトリガとなるコントロールとしては認識されません。

具体的にどういう話かというと、以下の通りです。

上の画像のように ListView 内に LinkButton を配置すると、ASP.NET がそれを html ソースにレンダリングする時、ClientIDMode が Predictable の場合と AutoID の場合とで id は以下のように異なります。上が Predictable、下が AutoID の場合です。

<a id="ListView1_LinkButton1_0" 
 href="javascript:__doPostBack('ListView1$ctrl0$LinkButton1','')">
 LinkButton
</a>

<a id="ListView1_ctrl0_LinkButton1"
 href="javascript:__doPostBack('ListView1$ctrl0$LinkButton1','')">
 LinkButton
</a>

__doPostBack メソッドの第一引数にはコントロールの UniqueID が設定されます。UniqueID の命名規則は、親名前付けコンテナーの ID 値とコントロールの ID 値を '$' で連結するもので、上の例では 'ListView1$ctrl0$LinkButton1' となります。

AutoID の場合、UniqueID の '$' を '_' に置き換えた文字列が id の文字列と同じになるとことに注目してください。Predictable ではそうはなりません。

ScriptManager がページに配置されると、それが生成する JavaScript のコードでポストバックを制御するようになりますが、その時 UniqueID を使ってトリガとなった html 要素を探し、同期 or 非同期どちらのトリガになるかを判定します。

具体的には、ScriptManager が生成する _doPostBack という名前のメソッドの中で、ポストバックのトリガとなったコントロールの UniqueID の '$' を '_' に変換した id で getElementById メソッドを使って要素を探しに行きます。

なので、ClientIDMode がデフォルトでは見つからない、AutoID の場合は見つかるということになります。

上のステップで html 要素が見つかった場合は非同期ポストバックのトリガと判定され、直ちに非同期ポストバックがかかるようになっています。

上のステップで直接当該要素を見つけられなかった場合は、_findNearestElement というメソッドを使って上位の要素を探しに行きます。上の例の html ソースの UniqueID の場合は最終的に ListView1 という名前の id の要素を探しに行きます。

ところが ID="ListView1" という ListView コントロールから ASP.NET が生成する html 要素(table)の id は "ListView1_itemPlaceholderContainer" になり、ListView1 という名前の id の要素は見つからないという結果になります。

直接見つからず、その親も見つからなかった場合は、当該要素は非同期ポストバックのトリガとは見なされず、同期ポストパックがかかるという結果になります。

ただし、ListView に代えて GirdView を使った場合は、その中に配置した LinkButton クリックでも非同期ポストパックがかかります。その理由は、例えばサーバー側での ID を "GridView1" とした場合、html 要素(table)の id は同じく "GridView1" になるので、_findNearestElement メソッドで親が見つかるからです。

しかし、やはり正解は、ListView、GridView に関係なく、UpdatePanel を使って非同期ポストバックを行う場合トリガとするコントロールの ClientIDMode プロパティは AutoID にしておくことだと思います。

Tags: , ,

AJAX

SQL Server 接続プロトコル順序

by WebSurfer 2016年11月16日 23:43

SQL Server の接続プロトコルとして (1) 共有メモリ、(2) TCP/IP、(3) 名前付きパイプが有効になっている場合、どの順序で接続をトライするかという話を書きます。

SQL Server の Protocol Order

元は MSDN Forum の「アプリケーションからSQLServerへの接続プロトコル(名前付きパイプ)について」という表題のスレッドでの話です。

MSDN Forum で回答を書くためググって調べて見つけた記事 SQL Server clients may change protocols when the client computers try to connect to an instance of SQL Server に書いてあったことですが、レジストリの ProtocolOrder で指定されている順番で接続をトライするとのことです。

確かに自分の PC(SQL Server 2005 Developer Edition と SQL Server 2008 Express がインストールされています)のレジストリを見ると、上の画像のような設定がありました。

レジストリの設定は変えてないので、デフォルトで sm tcp np の順、即ち、(1) 共有メモリ ⇒ (2) TCP/IP ⇒ (3) 名前付きパイプの順に接続をトライしていくようです。(実際にその順番で接続をトライしているかは確認していませんが)

MSDN Forum の質問者さんのケースでは「時々名前付きパイプで接続しにいき失敗している」ということでしたが、時々何らかの問題で TCP/IP の接続に失敗した後、最後に名前付きパイプで接続に行って失敗し、「SQL Server への接続を確立しているときに・・・ (provider: 名前付きパイプ プロバイダ・・・」というエラーメッセージになったものと思われます。

Tags:

SQL Server

CustomValidator で jQuery.ajax 利用

by WebSurfer 2016年10月20日 23:18

ASP.NET Web Forms アプリで、ユーザー入力の検証に CustomValidator と jQuery.ajax を用い、クライアント側での検証を行うというコードを書いたときに、無知な自分がハマったことを忘れないように備忘録として書いておきます。

CustomValidator

シナリオは、ユーザー入力が有効かどうかサーバー側でなければ確認できない場合、クライアント側から jQuery.ajax を使ってサーバーに問合せて検証し、検証結果が NG であればエラーメッセージを表示してユーザーに再入力を促すというものです。

CustomValidator は検証用のコードを自力で書かなければなりませんが、その分自由度が高く、クライアント側での検証用スクリプトを自分で書いて、jQuery.ajax を使ってサーバー側に問い合わせ、その結果で検証するようにできます。

(ちなみに、RequiredFieldValidator とか RegularExpressionValidator などは、クライアント側での検証用スクリプトとサーバー側での検証用コードの両方とも ASP.NET が自動的に生成してくれます)

また、ASP.NET Web Forms 用に多々用意されている検証コントロールの一つなので、他の検証コントロールと合わせて、全体的に整合が取れたユーザー入力の検証を行うことができます。

というわけで、今回のシナリオには CustomValidator を使うのがよさそうなので、CustomValidator と jQuery.ajax を組み合わせて使ったサンプルコードを書いてみました。下にアップしたのがそれです。

Microsoft が提供するサンプルデータベース Northwind の Customers テーブルを利用し、ユーザーが TextBox に入力した文字列が Customers テーブルの CustomerID フィールドに存在しない場合は検証 NG としています。他に、未入力および入力形式(アルファベット 5 文字)の検証もしています。

で、自分がハマったことですが、以下の 1 と 2 です。3 以降は自分的に備忘録として残しておいた方がよさそうだと思って書きました。下のサンプルコードを参照しながら読んでください。

  1. jQuery.ajax の設定 の async オプションを false に設定する。
    デフォルトは true で「非同期」になります。非同期では、検証用スクリプトの ClientValidate 関数は、$.ajax({ ... }); が実行されると、success, fail オプションに設定されたコールバックが実行されないまま即完了してしまいます。結果、ClientValidate 関数では args.IsValid には何も設定されません(初期値の true のままになる)。
    なので async:false に設定する必要があります。API Documentation の説明によると "Note that synchronous requests may temporarily lock the browser, disabling any actions while the request is active." とのことなので、あまりお勧めはできないようですが、他に手段が見つからないのでやむを得ません。
    なお、async:false とした場合は、jqXHR.done() は使わないで、success, error, complete にコールバックを書けと言うことですのでそうしてあります。
  2. jQuery.ajax によるサーバーへの要求が 2 回出てしまうケースがある。
    TextBox にアルファベット 5 文字を入力してから(正規表現による検証はパスさせてから)Button をクリックすると 2 回要求が出てしまいます(1 回無駄)。理由は、Button クリックで TextBox からフォーカスが外れて検証がかかり、Button クリックでもう一度検証かかかるからです。
    もともとそのようなケースでは検証が 2 回かかってしまうのが仕様(?)のようですが、今回のようにサーバーのデータベースへの問合せが行われており、それが 1 回余分になるのは何とかした方がよさそうな気がします。回避方法は調査中です。(見つからないかも)
  3. ユーザーが未入力の場合の検証は RequiredFieldValidator を利用しています。
    先の記事「CustomValidator と RequiredFieldValidator」で書きましたが、CustomValidator で未入力の検証も可能です。しかし、検証コントロールのエラーメッセージは固定的なので、CustomValidator 一つで済ませる場合、エラーメッセージは "未入力もしくはデータベースに存在しません" というようにせざるを得ません。
    それより、RequiredFieldValidator と CustomValidator を併用して、未入力の時は "未入力です" というエラーメッセージを、入力はあるが無効な場合は "データベースに存在しません" とした方がユーザーフレンドリーだと思います。
  4. 上で「検証コントロールのエラーメッセージは固定的」と書きましたが、サーバー側での検証メソッドでは検証内容に応じてエラーメッセージを書き換えることができます。
    しかしながら、先の記事「FileUpload と CustomValidator」でもいろいろ考えましたが、クライアント側でのエラーメッセージを書き換える方法が分かりません。それも RequiredFieldValidator と CustomValidator を併用した理由です。
  5. RegularExpressionValidator と併用すると表示が 2 重になる。
    最初、RequiredFieldValidator で未入力の検証、RegularExpressionValidator でアルファベット 5 文字であることの検証、CustomValidator でデータベースにデータがあるかの検証を行うつもりでした。
    しかしユーザー入力があって RequiredFieldValidator での検証をパスした場合、RegularExpressionValidator と CustomValidator の両方の検証がかかり、両方のエラーメッセージ(例えば、「アルファベット 5 文字としてください」と「データベースに存在しません」)が出てしまいます。
    それが、以下のサンプルコードで、CustomValidator でアルファベット 5 文字とデータベースの有無の両方の検証をすることにした理由です。
  6. 上の画像のエラーメッセージ "CustomValidator" は、CustomeValidator をドラッグ&ドロップしたときのデフォルトです。
    ユーザー入力 abcde はデータベースに存在しないので、クライアント側での検証結果が NG となり、ErrorMessage プロパティに設定されたエラーメッセージ "CustomValidator" が赤文字で表示されたところです。
    もちろんこれは初期設定で変更できます。サーバー側ではエラーメッセージを動的に書き換えることもできます。下のコードで、クライアント側での検証が働かないようにして(ClientValidate 関数の中のコードを全てコメントアウトするなどして)サーバー側だけで検証をかけると、ServerValidate メソッドで書き換えられたエラーメッセージが表示されます。
    ただし、上にも書きましたが、クライアント側でのエラーメッセージは固定的になります。
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Text.RegularExpressions " %>
<%@ Import Namespace="System.Web.Configuration" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Web.Services" %>

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

<script runat="server">

  // jQuery.ajax で POST された文字列を受け、DB に存在するか
  // をチェックするためのページ内の静的メソッド。
  // public static にして WebMethodAttribute 属性を付与
  [WebMethod]
  public static string ValidateId(string id)
  {
    // Northwind サンプルデータベース Customers テーブルの
    // CustomerID フィールドにユーザーが入力した文字列と同じ
    // データが存在するかを調べ、存在すればその CustomerID
    // データを、無ければ null を返す        
    string query = "SELECT CustomerID FROM Customers " + 
                   "WHERE CustomerID = @CustomerID";
    string connString = WebConfigurationManager.
        ConnectionStrings["NORTHWINDConnectionString"].
        ConnectionString;

    using (SqlConnection conn = new SqlConnection(connString))
    {
      using (SqlCommand cmd = new SqlCommand(query, conn))
      {
        cmd.Parameters.Add("@CustomerID", SqlDbType.NChar);
        cmd.Parameters["@CustomerID"].Value = id;
        conn.Open();
        return (string)cmd.ExecuteScalar();
      }
    }
  }
    
  // CustomValidator のサーバー側での検証用メソッド
  protected void ServerValidate(object source, 
      ServerValidateEventArgs args)
  {
    // CustomerID はアルファベット 5 文字なので、まず
    // 正規表現でアルファベット 5 文字にマッチするかを
    // チェック
    string pattern = "^[a-zA-Z]{5}$";

    if (Regex.IsMatch(args.Value, pattern))
    {
      // 上のヘルパメソッドを流用。DB にデータが
      // 存在しない場合 null が id に代入される
      string id = ValidateId(args.Value);

      if (id != null)
      {
        args.IsValid = true;
      }
      else
      {
        // サーバー側でならエラーメッセージを書き換
        // えることが可能
        ((CustomValidator)source).ErrorMessage = 
                    "データベースに存在しません";
        args.IsValid = false;
      }
    }
    else
    {
      ((CustomValidator)source).ErrorMessage = 
              "アルファベット 5 文字としてください";
      args.IsValid = false;
    }
  }   

  protected void Button1_Click(object sender, EventArgs e)
  {
    // IsValid で検証結果を調べて処置を行うのが原則
    if (Page.IsValid)
    {
      Label1.Text = "OK";
    }
    else
    {
      Label1.Text = "NG";
    }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
  <script src="/Scripts/jquery-1.11.1.js" type="text/javascript">
  </script>
  <script type="text/javascript">
  //<![CDATA[
    // async: false を設定しないとコールバックは非同期で実行
    // され args.IsValid が設定できないので注意
    // TextBox に入力してから Button をクリックすると 2 回要
    // 求が出でしまう。理由は、Button クリックする際 TextBox
    // からフォーカスが外れて検証がかかり、Button クリックで
    // もう一度検証かかかるから(デフォルトでそういう仕様)
    function ClientValidate(sender, args) {
      // 正規表現パターン(アルファベット 5 文字)            
      if (args.Value.match(/^[a-zA-Z]{5}$/)) {
        $.ajax({
          type: "POST",
          async: false,
          url: "0180-CustomValidatorAjax.aspx/ValidateId",
          data: '{"id":"' + args.Value + '"}',
          contentType: "application/json; charset=utf-8",

          success: function (data) {
            // .NET 3.5 で追加された d パラメータの処置
            if (data.hasOwnProperty('d')) {
              data = data.d;
            }
            if (data) {
              args.IsValid = true;
            } else {
              args.IsValid = false;
            }
          },

          error: function (jqXHR, textStatus, errorThrown) {
            args.IsValid = false;
          }
        });
        } else {
          args.IsValid = false;
        }
      }
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
    
  <asp:TextBox ID="TextBox1" runat="server">
  </asp:TextBox>
    
  <asp:RequiredFieldValidator 
    ID="RequiredFieldValidator1" 
    runat="server" 
    ErrorMessage="必須入力です" 
    Display="Dynamic"
    ControlToValidate="TextBox1" 
    ForeColor="Red">
  </asp:RequiredFieldValidator>

  <asp:CustomValidator 
    ID="CustomValidator1" 
    runat="server" 
    ErrorMessage="CustomValidator" 
    ControlToValidate="TextBox1" 
    Display="Dynamic"
    ForeColor="Red" 
    OnServerValidate="ServerValidate" 
    ClientValidationFunction="ClientValidate">
  </asp:CustomValidator>

  <br />
  <asp:Button ID="Button1" runat="server" 
    Text="送信" OnClick="Button1_Click" />
  <br />
  <asp:Label ID="Label1" runat="server"></asp:Label>
  </form>
</body>
</html>

Tags: ,

Validation

About this blog

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

Calendar

<<  2017年7月  >>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar