WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

コンマ区切りスクリプトと検証の整合

by WebSurfer 28. November 2019 15:41

数字を 3 桁でコンマ区切りする JavaScript と ASP.NET MVC5 のクライアント側での検証の話です。

コンマ区切りスクリプトと検証の整合

元の話は Teratail のスレッド「3桁コンマ区切り数字をコンマ無しでFrom送信したい」です。

コンマ区切り用 JavaScript のコードの動作は、初期画面では数字を 3 桁でコンマ区切りし、ユーザーが編集するときはコンマを除去し、編集完了後は再び 3 桁でコンマ区切りするというものです。コードは Teratail のスレッドの私 SurferOnWww の回答にありますので見てください。

そのコンマ区切り用 JavaScript の動作と、以下のようにモデルにアノテーション属性を付与するとデフォルトで有効になる控えめな JavaScript による検証が、かなり微妙ながら基本的なところでは整合を取って動きましたので、備忘録として書いておくことにしました。(実は、バッティングして動かないと思い込んでました(汗))

public class PurchaseRecord
{
  // ・・・中略・・・

  [Display(Name = "価格")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,6}$", 
    ErrorMessage = "数字 1 ~ 6 文字")]
  [Range(100, 10000, 
    ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
  [DisplayFormat(DataFormatString = "{0:N0}", 
      ApplyFormatInEditMode = true)]
  public decimal Price { get; set; }
}

どのような動きになるかと言うと以下の通りです:

  1. テキストボックスの初期表示は 1,234
  2. ユーザーが編集動作に入る時 focus イベントが発生しスクリプトで 1234 に書き換わる
  3. ユーザーが例えば 3210 というように編集
  4. 次の作業に移るためフォーカスを外す
  5. change イベントが発生し 3210 に対し検証がかかる
  6. blur イベントが発生しスクリプトで 3,210 に書き換える
  7. ユーザーが送信ボタンをクリック
  8. submit イベントが発生しスクリプトで 3,210 を 3210 に書き換える
  9. サーバーで 3210 を受信、サーバー側での検証は OK となる。

・・・という順序になってうまくいきます。

以上、基本的な動きは OK ではあるものの、かなり微妙なところで動いていますので、実際に運用に使う場合は十分な検証が必要だと思います。思いつくのは:

(1) change ⇒ blur の順序でイベントが発生しなければならないが、全てのブラウザでそうかは不明。(メジャーなブラウザは大丈夫のようですが、昔の Forefox は反対だったという話があります)

(2) 上のステップ 3 でユーザーが数字だけ入力してくれると期待するのは無理がある。(上のサンプルでは RegularExpression 属性を追加してチェックするようにしてますが、それで十分か?)

(3) ユーザーがブラウザの JavaScript を無効にした場合はサーバー側だけで検証することになる。

・・・などです。

特に、不特定多数のユーザーが不特定多種のブラウザでアクセスしてくるインターネットに公開するような場合は別の方法(サーバー側だけで検証するとか、String 型にするとか)を考えた方が良いかもしれません。

今のところ気が付いた問題点は以下の通りです:

問題 1: ステップ 3 でユーザーが 3,210 とカンマを入れて入力すると、ステップ 5 の時点ではカンマ入りなので正規表現での検証で引っかかるという問題があります。

問題 2: コンマ区切り用 JavaScript のコードには全角 ⇒ 半角変換の機能が実装されていますが、タイミングの問題で検証に引っかかってしまいます。どういうことかと言うと、全角数字を入力するとステップ 6 の時点で半角に変換しますが、検証がかかるステップ 5 の時点ではまだ全角なので正規表現による検証で NG となります。

その後、ステップ 6 の時点で半角に変換されるので、見かけは正しく半角なのにエラーが出て混乱を招くと思います。なので、全角 ⇒ 半角変換のコードは削除した方がよさそうです。

最後にオマケを二つ書いておきます。

その 1: 上のステップ 2 で編集操作に入った時、キャレットが末尾にあるのが自然と思いますが、そうしたい場合は以下のように 2 行追加してください。

elm.addEventListener('focus',
  function () {
    this.value = delFigure(this.value);

    // キャレットを文字列の末尾に持ってくる
    // ため以下の 2 行を追加
    var len = this.value.length;
    this.setSelectionRange(len, len);
  }, false);

その 2: クライアント側での検証を無効にすると「価格」として有効でない文字列、例えば 123x とかでもサーバーに送信されてしまいます。その場合、モデルバインディングできないのでアノテーション属性に設定した検証がかかる以前にエラーとなります。

そのエラーメッセージが気に入らないので自分で設定したいという場合は Controller にコードを追加して書き換えることができます。詳しくは別の記事「int 型プロパティの検証、エラーメッセージ」を見てください。

Tags: , , ,

MVC

ASP.NET / IIS の要求サイズの制約

by WebSurfer 20. November 2019 13:55

ASP.NET と IIS には要求のサイズを制限する組み込みのセキュリティ機能があります。httpRuntime 要素の maxRequestLength 属性と requestLimits 要素の maxAllowedContentLength 属性が良く知られています。

それ以外に serverRuntime 要素の uploadReadAheadSize 属性というのもあって、それに気が付かないでハマるということがありましたので、備忘録としてまとめて書いておきます。元の話は Teratail のスレッド「asp.net ajax upload でエラーになる」です。

(1) httpRuntime 要素の maxRequestLength 属性

ASP.NET の制限でデフォルトで 4MB になっています。詳しくは Microsoft のドキュメント httpRuntime 要素 (ASP.NET 設定スキーマ) を見てください。肝心な部分のみ以下に抜粋しておきます。

"入力ストリームのバッファリングしきい値の限界値を KB 単位で指定します。 この限界値は、たとえば大きいファイルをサーバーにポストするユーザーなどにより引き起こされる、サービス拒否攻撃を防止するために使用できます。既定値は 4096 です。 しきい値を超えると、ConfigurationErrorsException 例外がスローされます。"

(2) requestLimits 要素の maxAllowedContentLength 属性

IIS7 から導入された要求のフィルタリング <requestFiltering>の機能の中の requestLimits 要素の maxAllowedContentLength 属性がそれで、デフォルト値が 30,000,000 バイトになっています。下の IIS Manager での設定画面を見てください。

maxAllowedContentLength 属性

設定変更は上の画像のように IIS Manager で行うのがお勧めです。applicationHost.config ファイルをメモ帳などで開いて編集することでも可能ですが。

(3) serverRuntime 要素の uploadReadAheadSize 属性

以下の IIS Manager の画像の通りデフォルトで 49,152 バイトになっており、この制約にかかると HTTP 413 Request Entity Too Large というエラーになります。上に紹介した Teratail のスレッドの話がこの問題です。

uploadReadAheadSize 属性

Microsoft のドキュメント Solution for “Request Entity Too Large” errorLarge file upload failure for Web application calling WCF service – 413 Request entity too large によると、SSL 通信を利用しているとき、または WCF に要求を出す場合は uploadReadAheadSize 属性の制約の影響を受けるとのことです。

ただ、SSL だけでそういう問題が出るとすると FAQ レベルの話&周知の事例なのに、ググって調べた限りそうでもなさそうなのが不思議でした。

さらに調べてみると、IIS randomly returns 413 Request Entity Too Large when uploading large files and using TLS という記事が見つかり、それによると client certificate も絡んだ問題と書いてあります。

上の記事の回答で黄色のバックグラウンドとなっている部分は IIS Express を使った時のエラーメッセージのようです。その中に:

Most likely causes: The Web server cannot service the request because it is trying to negotiate a client certificate but the request entity is too large.

If using client certificates, try: Increasing system.webServer/serverRuntime@uploadReadAheadSize

・・・とあります。

実際、上に紹介した Teratail の記事の問題のサイトでは client certificate を使っているようです。

ただし、自分の環境の Visual Studio Community 2015 で IIS Express で SSL 通信を利用する設定にし、sslFlags を SslNegotiateCert に設定して試してみましたが、uploadReadAheadSize はデフォルトの 49,152 バイトのままでも問題を再現できませんでしたが・・・

(4) JSON 文字列を送信する場合の制約

JSON 文字列のデシリアライズに使用されている(と思われる)JavaScriptSerializer クラスの MaxJsonLength プロパティによって、Web サービスの場合はデフォルトで 102,400 文字に、ASP.NET MVC のアクションメソッドの場合は 2,097,152 文字に制限されています。

前者(Web サービス)の方は jsonSerialization 要素の maxJsonLength 属性の設定によって変更可能です。後者(ASP.NET MVC のアクションメソッド)の場合は 2,097,152 文字から変更できません。

2013 年時点の情報なので今は変更されているかもしれませんが、詳しくは別の記事「MVC は maxJsonLength を無視」を見てください。

Tags: , ,

ASP.NET

Canvas に複数の画像を順番通り描画

by WebSurfer 24. October 2019 12:30

Canvas に複数の画像を指定した順序で描画し、その後 Canvas に描画された画像を Data Url 形式で取得する方法を書きます。元の話は Teratail のスレッド「CanvasにdrawImageで描写した画像をtoDataURLで取得したい」です。

Canvas と img タグの画像

上の画像がその結果を Chrome で表示したもので、左が Canvas の画像、右が canvas.toDataURL メソッドを使って Data Url 形式で取得した画像データを img 要素の src 属性に設定して表示した結果です。

Canvas に表示する複数の画像の数だけ new Image() で img 要素を生成し、その src 属性に画像の url を設定すると img 要素が画像に読み込みを完了した時点で load イベントが発生するので、それにリスナをアタッチして Canvas への書き込みと Data Url 取得の処理を行うのが基本です。

ただし、問題は、Canvas に描く画像が複数ある場合、img 要素による画像の読み込みにかかる時間が画像のサイズなどによって異なるので、複数ある img 要素の load イベント発生の順番は不定となることです。

もう一つ、canvas.toDataURL メソッドを使って Canvas から Data Url 形式のデータを取得するタイミングも問題です。全ての画像の Canvas への描画が終わるのを待たなければなりません。

上の問題に対応するには、最後の load イベントの発生を待って、そのイベントリスナで複数ある全ての img 要素の画像 を希望する順番に context.drawImage メソッドで Canvas に書き込むのがよさそうです。

context.drawImage は同期メソッドなので、同じリスナ内で、全ての画像の context.drawImage が終わった後に canvas.toDataURL メソッドを記述すれば Data Url 形式のデータを取得できます。(リスナの外に canvas.toDataURL メソッドを置くのはダメです。img 要素の load イベントが発生する前に canvas.toDataURL に制御が飛ぶので)

検証に使ったコード、即ち上の画像を表示したコードを以下に書いておきます。(ASP.NET のページを利用していますが、そこは本題とは関係ないので気にしないでください) 元の画像は 125px x 75px の色が異なる .png 画像 5 枚です。それらを 20px づつ右下にずらしながら描画しています。

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="0076Canvas2.aspx.cs" Inherits="_0076Canvas2" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; 
  charset=utf-8"/>
  <title>0076Canvas2.aspx</title>
  <script src="/Scripts/jquery.js"></script>
  <script type="text/javascript">
  //<![CDATA[
    window.onload = function () {
      // それぞれ 125px x 75px の単色 png 画像
      // 色は名前の通り赤, 青, 緑, 黄, 橙
      var srcs = [
          "/Images/red.png",
          "/Images/blue.png",
          "/Images/green.png",
          "/Images/yellow.png",
          "/Images/orange.png"
      ];

      var canvas = document.getElementById('mycanvas');
      var context = canvas.getContext('2d');

      // canvas に描かれる順番を保証するには以下のようにする。
      // そうすればリスナの中に canvas.toDataURL("image/png");
      // を記述でき Data Url が取得できる
      var images = [];
      var loadCount = 0;
      for (let i = 0; i < srcs.length; i++)
      {
        images[i] = new Image();                
        images[i].onload = function () {
          loadCount++;
          if (loadCount == images.length)
          {
            for (let j = 0; j < images.length; j++)
            {
              context.drawImage(images[j], j * 20, j * 20);
            }

            // image.onload のリスナの中でないと Data Url
            // は取得できないので注意
            var result = canvas.toDataURL("image/png");
            document.getElementById("image1").src = result;
          }
        };
        images[i].src = srcs[i];
      }
    }
    //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <div style="float: right;">
    <canvas id="mycanvas" width="205" height="155">
    </canvas>
  </div>
  <div>
    <img id="image1" alt="" />
  </div>
  </form>
</body>
</html>

他に jQuery.Deferred() を使う方法もあります。そのコードを以下に書いておきます。

// jQuery.Deferred を使用
var images = [];
var dfds = [];
for (let i = 0; i < srcs.length; i++)
{
    images[i] = new Image();
    let dfd = new $.Deferred();
    dfds[i] = dfd;
    images[i].onload = function () { dfd.resolve(); };
    images[i].src = srcs[i];
}

$.when.apply($, dfds).done(function () {
    for (let i = 0; i < images.length; i++)
    {
        context.drawImage(images[i], i * 20, i * 20);
    }
    var result = canvas.toDataURL("image/png");
    document.getElementById("image1").src = result;
});

全ての img 要素の load イベントが発生し終わるのを待って Canvas への描画と Data Url 形式のデータを取得するというところは同じですが、見かけは何となくカッコいいかも。(笑)

Tags: , , , ,

JavaScript

About this blog

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

Calendar

<<  December 2019  >>
MoTuWeThFrSaSu
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar