WebSurfer's Home

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

EditorFor と DisplayFor の違い

by WebSurfer 2013年3月27日 21:38

Html Helper の EditorFor と DisplayFor が、POST 要求に対する応答で、異なった値を表示することがあります。その理由と解決策を備忘録として書いておきます。

EditFor と DisplayFor の違い

上の画像を表示した Model, View, Controller のコードは以下の通りです(説明に関係ない部分は省略してあります)。

Model

public class FileModel
{
  public string FileName { get; set; }
}

View

@model MvcApplication1.Models.FileModel

@using (Html.BeginForm())
{
  @Html.EditorFor(m => m.FileName)
  <input type="submit" value="POST" />
  @Html.DisplayFor(m => m.FileName)
}

Controller

public class FileController : Controller
{
  [HttpPost]
  public ActionResult Index(FileModel m)
  {
    m.FileName += ".ext";
    return View(m);
  }
}

ユーザーがテキストボックス(EditorFor 相当)にファイル名を入力し、form を submit(POST 要求)すると、Controller で model の FileName プロパティに拡張子 ".ext" を追加します。

この場合、応答画面では、DisplayFor には拡張子 ".ext" が追加されて表示されるものの、EditorFor には ".ext" は追加されません。 上の画像の例を見てください。

つまり、EditorFor には POST された値がそのまま使われています。

何故そうなるかと言うと、例えば、EditorFor の入力にユーザーが間違った値を入力した場合、サーバーに POST された時に検証 NG とし、エラーメッセージ(例:入力が間違っています)を表示するとともに、EditorFor にはユーザー入力をそのまま表示したいという理由だそうです。

具体的には、POST された値は ModelState ディクショナリ(model ではない)に格納されていて、Html Helper はまず ModelState ディクショナリを調べて、そこに値があればそれを表示するようになっています。

詳しくは以下のページを見てください。

ASP.NET MVC’s Html Helpers Render the Wrong Value!

value が POST されるか否かが問題になります。例えば、EditorFor に限らず、HiddenFor もその value は POST されるので、結果は EditorFor と同じになります。

一方、DisplayFor には、POST される value などというものはないので、model の値が使われます。

EditorFor にも DisplayFor と同様に model の値(拡張子 .ext が追加されたもの)を表示するには、ModelState ディクショナリを Clear することです。

そうすれば model から値を取ってくるので、期待した結果になります。具体的には以下のコードを追加します。

if (ModelState.IsValid)
{
  ModelState.Clear();
}

ただしこのようにして、検証結果 OK で POST 要求への応答をそのまま返すのは、二重 POST の問題が起こりうるので、好ましくないようです。

二重 POST 問題

MVC に限らず、Web アプリケーション開発の基本として、Post/Redirect/Get (PRG) パターンを使う・・・即ち、POST 要求への応答をそのまま返すのは検証結果 NG の場合のみとし、検証結果 OK の場合は、例え同じページを表示するにしても、リダイレクトしてブラウザに GET 要求させるのがよいそうです。(詳しくはリンク先を見てください)

Post/Redirect/Get による問題解決

Post/Redirect/Get パターンを使うように、上記のコードを書き直すと、以下のようになると思います。

public class FileController : Controller
{
  [HttpGet]
  public ActionResult Index()
  {
    FileModel model;

    object obj = TempData["ValidationResult"];
    if (obj is FileModel)
    {
      // 検証 OK ⇒ 再確認
      model = (FileModel)obj;
    }
    else
    {
      // 初期画面
      model = new FileModel();
    }
    return View(model);
  }

  [HttpPost]
  public ActionResult Index(FileModel m)
  {
    if (ModelState.IsValid)
    {
      // 検証 OK ⇒ 再確認
      m.FileName =
        ModelState["FileName"].Value.AttemptedValue
        + ".ext";
      TempData["ValidationResult"] = m;
      return RedirectToAction("Index");
    }
    else
    {
      // 検証失敗
      return View(m);
    }
  }
}

Tags:

MVC

ダウンロードは HTTP ハンドラで

by WebSurfer 2013年2月16日 16:50

画像、データ、ファイルなど、Page でないコンテンツをダウンロードするには、.aspx ページを利用するより、HTTP ハンドラ(.ashx)を利用したほうが良さそうだという話です。

.aspx ページを利用してファイルをダウンロードするサンプルは、MSDN ライブラリなどでもよく目にしますが、いろいろ問題がありそうです。

MSDN ライブラリの HttpResponse クラス にある画像をダウンロードするサンプルコードを例に取って問題点を説明します。

このコードでは、まず bmp.Save(Response.OutputStream, ImageFormat.Jpeg) メソッドによって画像データが jpeg 形式で出力ストリームに保存され、その後 Response.Flush メソッドによって保存された画像データが chunked コーディングされてクライアントに送信されます。

そのあと直ちに、データの終了を示す 0 が送信されればいいのですが、サーバーは <!DOCTYPE ... で始まる html コードも生成し、それもクライアントに送信してから終了します。

従って、jpeg 形式のデータとしては余計な html コードが含まれてしまいます。IE9 で試した限りでは画像は表示されましたが、すべてのブラウザで問題ないかは保障の限りではありません。

以下のような解決策を考えましたが、.aspx ページを利用する限り決定打はなさそうです。

  1. Response.Flush の後、Response.Close する ⇒ 時々そのようなサンプルを目にしますが、これは解決策というよりは改悪です。Response.Close は "クライアントへのソケット接続を閉じます" ということなので、 chunked コーディングでデータの終了を示す 0 が送信されないまま終了してしまいます。IE9 でしか試してませんが、画像は表示されません。
  2. Response.Flush に替えて Response.End を使う ⇒ 応答ヘッダに Content-Length が指定されて(chunked コーディングされず)、データはまとめて送信されます。送信されるデータの内容も OK です。ただし、最近知ったのですが、.NET 4 以降の MSDN ライブラリの説明で、End メソッドの使用は非推奨になっています。理由は、End メソッドでスローされる ThreadAbortException がパフォーマンスに悪影響を及ぼすからだそうです。代わりに HttpApplication.CompleteRequest メソッドを呼び出せとのことです。
  3. Response.Flush に替えて HttpApplication.CompleteRequest を呼び出す ⇒ やはり <!DOCTYPE ... で始まる html コードも送信されてしまいます。Response.Flush と異なる点は、chunked コーディングされず、Content-Length が指定されてデータが(html コードも含めて)まとめて送信されることです。
  4. html コードを全部削除し、Flush メソッドも End メソッドも使わない ⇒ これは見かけよさそうです。余計な html コードは��っついてきません。
  5. Response.Flush の後 Response.SuppressContent を true に設定する(2016/6/28 追記) ⇒ そうすると画像のみが送信され、<!DOCTYPE html... 以下は送信されません。ただし、Response.Flush の前で true に設定するとコンテンツは一切送信されない(ヘッダと chunked の終わりを示す 0 のみ送信される)ので注意してください。

.aspx をページを使う場合は上記 4 または 5 の方法がよさそうですが、普通と違うことをして思わぬところで副作用が出る可能性が否定しきれません。.aspx をページを使って余計な心配をするより、代わりに http ハンドラ(.ashx)を使った方が良さそうです。http ハンドラなら、Response.End メソッドで強制する等の処置は必要なく、HTTP パイプラインの最後まで普通に実行させれば済みます。

http ハンドラを使ったサンプルを書いておきます。コメントに注意事項を書いたので参考にしてください。

(2014/3/2 追記:IE の場合は UrlEncode を使ってファイル名をエンコードしていますが、半角空白は "+" に変換されるので、ブラウザ側ではそのまま "+" になってしまいます。それが気に入らない場合は、ダウンロードファイル名の文字化け に紹介したサンプルコードを参考に対処してください)

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

using System;
using System.Web;

public class Handler : IHttpHandler 
{
    public void ProcessRequest (HttpContext context) 
    {
        HttpResponse response = context.Response;
        HttpRequest request = context.Request;
        
        string fileName = "日本語.txt";        
        
        // IE の場合、日本語ファイル名の文字化け対策が必要。
        // Firefox, Chrome, Safari, Opera の場合は不用。

        // 2014/3/3 修正
        // IE11 では Browser プロパティは "Mozilla" (.NET 2.0)
        // または "InternetExplorer" (.NET 4) になる。IE の場合
        // User Agent には必ず "Trident" と言う文字列が入ってい
        // るらしいので、そちらで判定した方がよさそう。
        if (request.Browser.Browser.ToUpper().IndexOf("IE") >= 0
            || request.UserAgent.Contains("Trident"))
        {
            fileName = context.Server.UrlEncode(fileName);
        }
        
        // キャッシュを許可するか否か、許可する場合は有効期限を
        // 指定しておくべき。
        // 以下のコードはキャッシュを許可しない場合の例。応答ヘ
        // ッダーは次のようになる。
        //    Cache-Control: no-cache
        //    Pragma: no-cache
        //    Expires: -1
        response.Cache.SetCacheability(HttpCacheability.NoCache);
        response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
        response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

        // ブラウザによってファイルの種類を判断する方法が異なる。
        // IE は、Content-Disposition: ヘッダが存在する場合は、
        // filename パラメータで設定されたファイル名の拡張子を
        // 優先的に使う。Content-Type: ヘッダの指定は無視される。
        // 逆に、Opera など、Content-Type: ヘッダの指定を優先的
        // に使うものもある。なので、Content-Disposition: ヘッダ
        // と Content-Type: ヘッダの両方を正しく指定しておいた方
        // がよさそう。        
        response.AppendHeader("Content-Disposition",
            "attachment;filename=" + fileName);
        response.ContentType = "text/plain";

        // 文字列 "Hello World" を応答 HTTP ストリームに書込み。
        // ファイルの場合は TransmitFile メソッドを使うのがお勧
        // め(WriteFile メソッドは大きなファイルは扱えないので)
        response.Write("Hello World");
        
        // Flush, End, Close メソッド等は使用しないこと。
    }
 
    public bool IsReusable 
    {
        get 
        {
            return false;
        }
    }

}

呼び出し方は、a 要素の href 属性に HTTP ハンドラの URL を設定するのがよさそうです。

Tags: ,

Upload Download

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

About this blog

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

Calendar

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

View posts in large calendar