WebSurfer's Home

Filter by APML

jQuery ajax で部分ビューの呼出・表示 (CORE)

by WebSurfer 7. March 2020 12:25

jQuery ajax を使って、部分ビューを応答として返すアクションメソッドにフォームデータを POST 送信し、返ってきた部分ビューの html をページ内の指定の場所にレンダリングする方法を書きます。

jQuery ajax で部分ビューの呼出・表示

先の記事「ASP.NET Core MVC と Ajax ライブラリ」では、Microsoft の Ajax ライブラリ jquery.unobtrusive-ajax.js を利用して、Ajax で部分ビューを呼び出してページ内の指定の場所に表示する方法を書きました。

それと機能的に同じことを、Microsoft の Ajax ライブラリの助けを借りないで、jQuery ajax を利用して実装する方法です。

CSRF 用の隠しフィールドのデータを含めてフォームデータを全て jQuery ajax を使って POST 送信し、部分ビュー用のアクションメソッドに付与した [ValidateAntiForgeryToken] 属性での検証ができるようにしていることに注目してください。

問題はフォームデータをクライアント側でどのように取得するかですが、jQuery の .serialize() メソッドを使うと form データを application/x-www-form-urlencoded 形式の文字列として取得できます。それを jQuery ajax の data パラメータに設定して POST 送信してやればアクションメソッドで受けることができます。

Controller と View のサンプルコードを以下に書いておきます。コードは ASP.NET Core 3.1 MVC のものです。.NET Framework MVC5 の場合はライブラリ jquery.unobtrusive-ajax.js と AjaxHelper を使う方が簡単でよさそうです。

使っているのは Microsoft が提供する Northwind サンプルデータベースの Customers テーブルです。その CompanyName をドロップダウンリストに表示し、ユーザーが選択してボタンをクリックすると部分ビューを呼び出して、選択した Customers のレコードの詳細を指定した場所(下の View のコードで言うと <div id="result"></div> の中)に書き出すものです。

Controller / Action Method

先の記事「ASP.NET Core MVC と Ajax ライブラリ」のものと同じです。

using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MvcCoreApp.Controllers
{
  public class AjaxController : Controller
  {
    private readonly NorthwindContext _context;

    public AjaxController(NorthwindContext context)
    {
      _context = context;
    }

    // jQuery ajax で下のアクションメソッド Details
    // を呼んで詳細 Customer データの部分ビューを表示。
    // CSRF 用の隠しフィールドのデータも送信して検証で
    // きるようにする。
    public IActionResult Index2()
    {
      // 全部取得すると長すぎるので Take(10) した
      var list = _context.Customers.Take(10);
      ViewData["customers"] = 
        new SelectList(list, "CustomerId", "CompanyName");
      return View();
    }

    // 部分ビュー用のアクションメソッド
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Details(string customerid)
    {
      if (string.IsNullOrEmpty(customerid))
      {
        return NotFound();
      }

      var customer = _context.Customers.Find(customerid);

      if (customer == null)
      {
        return NotFound();
      }

      return PartialView(customer);
    }
  }
}

View (Index2.cshtml)

ボタンには input type="submit" タイプ(クリックすると form を submit する)と input type="button" タイプ(クリックしても何も起こらない)の 2 つを配置して、両方期待通り動くことを確認してみました。

@{
    ViewData["Title"] = "Index2";
}

<h1>Index2</h1>

<form id="form1" asp-action="Details" 
      asp-controller="Ajax" method="post">

    <select id="customerid" name="customerid" 
            asp-items="@ViewBag.customers">
    </select>

    <input type="submit" value='詳細表示 (type="submit")' />
    <input id="button1" type="button"
                         value='詳細表示 (type="button")' />

</form>

<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        // [詳細表示 (type="submit")] ボタンをクリック
        // したときの処理

        // ボタンクリックで form が submit されるという
        // 動きになるので、form 要素の submit イベントに
        // リスナをアタッチして jQuert ajax で処理する
        $("#form1").on("submit", function (event) {
            // submit されては困るのでキャンセル
            event.preventDefault();

            // jQuery ajax を使って、部分ビューを応答と
            // して返すアクションメソッド Ajax/Deteils
            // にフォームデータを POST 送信する
            $.ajax({
                type: "POST",
                url: "/Ajax/Details",

                // フォームデータを取得
                data: $(this).serialize(),

                // コールバック function の引数 data に
                // 部分ビューの html ソースが渡される 
                success: function (data) {                    
                    $("#result").empty();
                    $("#result").append(data);
                },

                error: function (jqXHR, status, error) {
                    $('#result').text('Status: ' + status +
                        ', Error: ' + error);
                }
            });
        })

        //]]>
    </script>

    <script type="text/javascript">
        //<![CDATA[

        // [詳細表示 (type="button")] ボタンをクリック
        // したときの処理

        // ボタン要素の click イベントにリスナをアタッチ
        // して jQuert ajax で処理する
        $("#button1").on("click", function () {

            // input type="button" タイプのボタンはクリ
            // ックしても from が submit されることは
            // ないので event.preventDefault(); は不要

            // jQuery ajax を使って、部分ビューを応答と
            // して返すアクションメソッド Ajax/Deteils
            // にフォームデータを POST 送信する
            $.ajax({
                type: "POST",
                url: "/Ajax/Details",

                // フォームデータを取得
                data: $("#form1").serialize(),

                // コールバック function の引数 data に
                // 部分ビューの html ソースが渡される 
                success: function (data) {
                    $("#result").empty();
                    $("#result").append(data);
                },

                error: function (jqXHR, status, error) {
                    $('#result').text('Status: ' + status +
                        ', Error: ' + error);
                }
            });
        })

        //]]>
    </script>
}

部分ビューのコードは、先の記事「ASP.NET Core MVC と Ajax ライブラリ」のものとほとんど同じなので省略します。

Microsoft の Ajax ライブラリ jquery.unobtrusive-ajax.js は ASP.NET Core MVC ではサポートされてないのかもしれません。なので、上記のように jQuery ajax を使うのが正解のような気がします。

Tags: , , ,

CORE

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

XMLHttpRequest, jQuery, fetch でファイルダウンロード

by WebSurfer 5. February 2019 18:38

XMLHttpRequest, jQuery, fetch を使ってファイルをダウンロードするにはどうすれば良いかということを調べましたので備忘録として書いておきます。

Ajax でファイルダウンロード

例えば、先の記事「MVC でファイルのダウンロード」で書いたようなアクションメソッドがあるとして、XMLHttpRequest, jQuery, fetch などを使ってその URL に非同期要求をかけたとします。

そうすると、同期要求した場合と全く同様に、応答ヘッダには Content-Type と Content-Disposition が適切に設定され、コンテンツ(ファイルのバイナリデータ)も正しく返ってきます。

しかしながら、上の画像のような通知バーは表示されませんし、ファイルは PC のディスクには保存されません。

では、ファイルを PC のディスクに保存するにはどうすればいいかと言うと、応答コンテンツを Blob(生データ)として取得し、それを保存するためのスクリプトを書くことになります。

jQuery ajax を使う場合、バージョン 3.0 以降であれば Blob を取得できますが、バージョン 2.x 以前では Blob を取得できないので、ネイティブの XMLHttpRequest を使うことになるそうです (fetch API が使えればそちらを使う方がお勧め)。

詳しくは stackoverflow の記事 Using jQuery's ajax method to retrieve images as a blob にある回答を読んでください。

取得した Blob をファイルとして PC に保存するには、IE, 旧 Edge の場合は msSaveBlob method を使います。

Chrome, Chromium ベースの新 Edge, Firefox, Opera の場合は URL.createObjectURL を使って Blob の URL を取得し、それを html の a 要素の href 属性に設定し、download 属性にファイル名を設定してスクリプトでクリックするようにします。

具体的には、ネイティブの XMLHttpRequest を使った例ですが、以下のコードの通りです。(下の方に jQuery Ajax と fetch API を利用したサンプルコードを追記しました)

function download() {
  var url = "/Home/FileDownload";
  var filename = "testfile";

  var xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.responseType = 'blob';
  xhr.onreadystatechange = function (e) {
    if (this.readyState == 4 && this.status == 200) {
      var blob = this.response;

      //IE, 旧 Edge とその他で処理の切り分け
      if (window.navigator.msSaveBlob) {
        window.navigator.msSaveBlob(blob, filename + ".pdf");
      } else {
        var a = document.createElement("a");
        // IE11, 旧 Edge  は URL API をサポートしてない
        var url = window.URL;
        a.href = url.createObjectURL(new Blob([blob],
                                     { type: blob.type }));
        document.body.appendChild(a);
        a.style = "display: none";
        a.download = filename + ".pdf";
        a.click();
      }
    }
  };
  xhr.send();
}

2, 3 注意点を書いておきます。

IE11 は window.URL をサポートしてないのでファイルとして保存するには msSaveBlob メソッドを使う以外に手はなさそうです。

msSaveBlob の第 2 引数を設定しないと、ファイル名は IE の場合 1A31A31A-1F57-4D8D-8C70-150839D02536.pdf のように、Edge の場合は (1) となってしまいます。

MDN のドキュメントには、Content-Disposition ヘッダーで download 属性の指定と異なるファイル名が与えられた場合は、この属性より HTTP ヘッダーが優先するということが書いてありますが、実際試すと download 属性の指定通りとなります。"" を設定したりすると aae9adeb-1005-407f-a3a6-046fa79c4351.pdf のようになります。


【追記 2023/12/6】

jQuery Ajax を利用

上に紹介した stackoverflow の記事によると jQuery バージョン 3.0 以降であれば Blob を取得できるそうで、サンプルコードも載っていましたので試してみました。以下のコードでダウンロードできます。

const jqueryAjax = () => {
    const url = "/Download/VirtualFileResult";
    const filename = "testjquery";

    $.ajax({
        url: url,
        cache: false,
        xhrFields: { responseType: "blob" }
    }).done(blob => {
        const a = document.createElement("a");
        const url = window.URL;
        a.href = url.createObjectURL(new Blob([blob],
            { type: blob.type }));
        document.body.appendChild(a);
        a.style = "display: none";
        a.download = filename + ".pdf";
        a.click();
    }).fail((jqXHR, textStatus, errorThrown) => {
        // エラー処理(省略)
    });
};

fetch API を利用

fetch 関数の戻り値の Response オブジェクトの blob メソッドで blob を取得できるそうなので試してみました。以下のコードがそれです。async / await と組み合わせることにより、上の 2 つの例より、可読性がかなり向上すると思います。

const fetchApi = async () => {
    const url = "/Download/VirtualFileResult";
    const filename = "testfetch";

    const response = await fetch(url);
    if (response.ok) {
        const blob = await response.blob();
        const a = document.createElement("a");
        const url = window.URL;
        a.href = url.createObjectURL(new Blob([blob],
            { type: blob.type }));
        document.body.appendChild(a);
        a.style = "display: none";
        a.download = filename + ".pdf";
        a.click();
    } else {
        // エラー処理(省略)
    }
};

(メモ: サンプルは Visual Studio 2022 > AspNet7 > McvNet7App > DownloadController)

Tags: , , , ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  December 2025  >>
MoTuWeThFrSaSu
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar