WebSurfer's Home

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

ダウンロードは 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

ModalPopup を別ページから非表示

by WebSurfer 2013年1月27日 16:52

Ajax Control Toolkit の ModalPopup 内の iframe に別ページを表示し、その別ページに配置されているボタンをクリックして ModalPopup を非表示にする方法の紹介です。

ModalPopup はクライアントスクリプトで非表示にできます。それには、Modalpopup をページに配置するとダウンロードされるクライアントスクリプトに定義されている hide メソッドを使います。

従って、iframe に表示するページの適当な DOM に click イベントのリスナーを仕掛けて、それで hide メソッドを起動すれば ModalPopup を非表示にできます。

ただし、iframe に表示するページは親ページと同じドメインのものでないと DOM が取得できないので注意してください(クロスサイトスクリプト対策だそうです)。

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

親ページ

<%@ Page Language="C#" %>
<%@ Register Assembly="AjaxControlToolkit" 
    Namespace="AjaxControlToolkit" 
    TagPrefix="asp" %>

<!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></title>
    <script type="text/javascript">
    //<![CDATA[

        // iframe 内の document オブジェクトを取得。
        // contentWindow は IE 用。
        // contentDocument は Firefox 用。
        function iframeRef(frameRef) {
            return frameRef.contentWindow ?
                frameRef.contentWindow.document : 
                    frameRef.contentDocument;
        }

        function hideModalPopup() {
            var modalPopup = 
                $find('programmaticModalPopupBehavior');
            modalPopup.hide();
        }

        function showModalPopup() {
            var ifm = iframeRef($get('iframe1'));
            var btn = ifm.getElementById('hideButton');
            // hide するとリスナーも解除されてしまうので、
            // (btn.click が null になることで確認できる)
            // ModalPopup を表示するたびリスナーをアタッチ
            // する必要がある。
            if (btn.onclick == null) {
                if (window.addEventListener) {
                    btn.addEventListener('click',
                                         hideModalPopup, 
                                         false);
                } else if (window.attachEvent) {
                    btn.attachEvent('onclick', 
                                    hideModalPopup);
                }
            }

            var modalPopup = 
                $find('programmaticModalPopupBehavior');
            modalPopup.show();
        }
    //]]>
    </script>

    <style type="text/css">
        /*Modal Popup*/
        .modalBackground
        {
            background-color: Gray;
            filter: alpha(opacity=70);
            opacity: 0.7;
        }
        .popup
        {
            background-color: White;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ToolkitScriptManager 
        ID="ToolkitScriptManager1" 
        runat="server">
    </asp:ToolkitScriptManager>  

    <h1>ModalPopup Test</h1>

    <asp:Button ID="DummyButton" 
        runat="server"
        style="display: none;" />

    <input type="button" 
        value="Show ModalPopup" 
        onclick="showModalPopup();" />

    <asp:Panel 
        ID="Panel1" 
        runat="server" 
        CssClass="popup">
        <iframe 
            id="iframe1" 
            src="190-PageIniframe.aspx">
        </iframe>
    </asp:Panel>

    <asp:ModalPopupExtender 
        ID="ModalPopupExtender1" 
        runat="server"
        TargetControlID="DummyButton" 
        BehaviorID="programmaticModalPopupBehavior" 
        BackgroundCssClass="modalBackground" 
        PopupControlID="Panel1">
    </asp:ModalPopupExtender>
    </form>
</body>
</html>

iframe に表示するページ

<%@ 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></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h1>Page in iframe</h1>
        <input type="button" 
            id="hideButton" 
            value="Hide ModalPopup" />
    </div>
    </form>
</body>
</html>

Tags: ,

AJAX

About this blog

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

Calendar

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

View posts in large calendar