WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

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

ASP.NET Core MVC と Ajax ライブラリ

by WebSurfer 6. March 2020 21:10

.NET Framework の ASP.NET MVC5 アプリケーションでは AjaxHelper(Ajax.BeginForm, Ajax.ActionLink など)と Microsoft の Ajax ライブラ�� jquery.unobtrusive-ajax.js を利用して、Ajax で部分ビューを呼び出してページ内の指定の場所に表示するなどのことが、JavaScript / jQuery のコードを書かなくても容易にできるようになっています。

Ajax で部分ビューを表示

同じことが ASP.NET Core 3.1 MVC でもできるだろうと思っていましたがダメでした。

MVC5 であれば、先の記事「MVC5 で AjaxHelper が働かない」で書きましたように NuGet から Microsoft.jQuery.Unobtrusive.Ajax をインストールすれば、AjaxHelper を使って目的が果たせます。

ところが、ASP.NET Core 3.1 MVC では、まず AjaxHelper が使えないようです。また、NuGet で Microsoft.jQuery.Unobtrusive.Ajax をインストールしても、肝心の jquery.unobtrusive-ajax.js が wwwroot に配置されません。(LibMan を使えということのようですが jquery.unobtrusive-ajax.js は登録されてないようです)

そこを無理やり(?) jquery.unobtrusive-ajax.js を使って Ajax で部分ビューを呼び出してページ内の指定の場所に表示するにはどうするかを以下に書きます。

(1) まず、jquery.unobtrusive-ajax.js を入手して以下の画像のように wwwroot 下のフォルダに配置します。

jquery.unobtrusive-ajax.js を配置

(2) 外部スクリプトファイル jQuery.js と上記 (1) の jquery.unobtrusive-ajax.js がダウンロードされるよう、View に script 要素を設定します。必ず jQuery.js が先に来るようにしてください。

_Layout.cshtml を使っていれば jQuery.js は設定されるようになっているので、View には jquery.unobtrusive-ajax.js だけ追加されるように設定すれば良いはずです。

(3) View のコードの form 要素に以下の属性を追加します。

data-ajax="true" data-ajax-mode="replace" data-ajax-update="#result"

その意味は jquery.unobtrusive-ajax.js を使って ajax で要求を出し、返ってきた応答で id が result の要素の中身を書き換えるということです。属性の説明については以下の記事が詳しいので参考にしてください。

Using jQuery Unobtrusive AJAX in ASP.NET Core Razor Pages

Controller と View のサンプルコードを以下に書いておきます。使っているのは Microsoft が提供する Northwind サンプルデータベースの Customers テーブルです。

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

Controller / Action Method

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.unobtrusive-ajax.js の助けを借りて
    // 部分ビューで詳細 Customer データを表示
    public IActionResult Index()
    {
      // 全部取得すると長すぎるので 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 (Index.cshtml)

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

<h1>Index</h1>

<form asp-action="Details" asp-controller="Ajax" 
      data-ajax="true" data-ajax-mode="replace" 
      data-ajax-update="#result" method="post">

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

    <input type="submit" value="詳細表示" />

</form>

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

@section Scripts {
  <script src="...フォルダ.../jquery.unobtrusive-ajax.js">
  </script>

部分ビュー (Details.shtml)

@model MvcCoreApp.Customers

<div>
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.CustomerId)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.CustomerId)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.CompanyName)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.CompanyName)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ContactName)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ContactName)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ContactTitle)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ContactTitle)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Address)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Address)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.City)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.City)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Region)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Region)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.PostalCode)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.PostalCode)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Country)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Country)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Phone)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Phone)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Fax)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Fax)
        </dd>
    </dl>
</div>

jquery.unobtrusive-ajax.js は ASP.NET Core MVC ではサポートされてないような気がしますが、上記のようにすれば一応使えます。

jquery.unobtrusive-ajax.js を使わなくても同等のことは jQuery ajax を使ってできます。それについては別の記事「jQuery ajax で部分ビューの呼出・表示」を見てください。

Tags: , , ,

CORE

IValidatableObject を継承したカスタムモデル

by WebSurfer 28. February 2020 15:31

IValidatableObject インターフェイスを継承したカスタムモデルを使って検証を行う方法を備忘録として書いておきます。

IValidatableObject を継承したクラスで検証

先の記事「CustomValidation 属性」で書いたモデルレベルの検証と同等な機能を実装してみます。

マイクロソフト公式解説書「プログラミング Microsoft ASP.NET MVC」によると "IValidatableObject インターフェイスを実装すると、クラスレベルで CustomValidation 属性を使用するのと機能的には同じになります"、"クラスレベルの検証を実装するにあたって、クラスレベルで IValidatableObject インターフェイスと CustomValidation 属性のどちらを利用するかは、完全にあなた次第です" とのことです。

ただし、"モデルにデータアノテーションも追加されている場合、有効な状態でないプロパティが存在すると、Validate メソッドが呼び出されなくなります。IValidatableObject インターフェイスを利用する場合は、問題が起きないよう、データアノテーションを完全に削除することをお勧めします" との注記があります。

ということは、データアノテーションによるクライアント側での検証ができなくなるということで、あまり使い道はなさそうな気がしますが。

コードは以下の通りです。.NET Framework MVC5 と ASP.NET Core 3.1 MVC で同じコードが使えます。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using System.Globalization;

namespace MvcCoreApp.Models
{
  // IValidatableObject インターフェイス利用
  public class CustomValidationModel : IValidatableObject
  {
    public int PersonId { set; get; }

    [Display(Name = "名前")]
    public string Name { set; get; }

    [Display(Name = "メールアドレス")]
    public string Mail { set; get; }

    // int? にしないと未入力では Validate メソッドが動かない。
    // int, int? どちらも "1.0" とか入力された場合は Validate
    // メソッドが動かない。モデルバインドされる際に例外がスロ
    // ーされ Validate メソッドに制御が飛ばないのだと思われる
    [Display(Name = "年齢")]
    public int? Age { set; get; }

    // 検証を行う Validate メソッド
    public IEnumerable<ValidationResult> Validate(
                                ValidationContext context)
    {
      var model = 
        context.ObjectInstance as CustomValidationModel;

      if (model == null)
      {
        throw new NullReferenceException();
      }

      if (string.IsNullOrEmpty(model.Name))
      {
        yield return new ValidationResult(
            "名前は必須", new string[] { "Name" });
      }
      else if (model.Name.Length < 2 ||
               model.Name.Length > 20)
      {
        yield return new ValidationResult(
            "名前は 2 ~ 20 文字の範囲",
            new string[] { "Name" });
      }

      if (string.IsNullOrEmpty(model.Mail))
      {
        yield return new ValidationResult(
            "メールアドレスは必須",
            new string[] { "Mail" });
      }
      else
      {
        bool isValidEmai = Regex.IsMatch(model.Mail,
            @"・・・正規表現(省略)・・・",
            RegexOptions.IgnoreCase,
            TimeSpan.FromMilliseconds(250));

        if (!isValidEmai)
        {
          yield return new ValidationResult(
              "有効な Email 形式ではありません",
              new string[] { "Mail" });
        }
      }

      if (model.Age == null)
      {
        yield return new ValidationResult(
            "年齢は必須", new string[] { "Age" });
      }
      else if (model.Age < 0 || model.Age > 200)
      {
        yield return new ValidationResult(
            "年齢は 0 ~ 200 の範囲",
            new string[] { "Age" });
      }
      else if (!string.IsNullOrEmpty(model.Name) && 
               model.Name.StartsWith("佐藤") && 
               model.Age < 20)
      {
        yield return new ValidationResult(
            "佐藤さんは二十歳以上でなければなりません",
            new string[] { "" });
      }
    }
  }
}

.NET Framework MVC5 と ASP.NET Core 3.1 MVC でコードは同じものが使えますが、動作は若干異なります。主な違いは以下の通りです。

  1. Age プロパティを int 型にすると、MVC5 の場合 input 要素に data-val="true" data-val-number="フィールド 年齢 には数字を指定してください。 " data-val-required="年齢 フィールドが必要です。" という検証属性が付与される。Core には data-val-number 属性の設定はない。
  2. Age プロパティを int? 型 (null 許容) にすると、MVC5 の場合 input 要素に検証���性 data-val="true" data-val-number="フィールド 年齢 には数字を指定してください。" が付与される。Core の場合は検証属性は何も付与されない。
  3. さらに、type 属性には "number" と設定されるが、IE, Chrome, Firefox いずれも "1.0" というような整数としては不正な文字列を入力できる。 その場合、クライアント側の検証はパスしてしまう。 POST すると、MVC5 の場合は Validate メソッドに制御が飛ぶが、Core の場合はモデルバインドしようとする際に例外がスローされるようで Validate メソッドには制御が飛ばない。

    ただし、Validate メソッドに制御が飛んでも、Validate メソッドで設定したエラーメッセージはどこかで上書きされ "値 '1.0' は 年齢 に対して無効です。" となる。理由不明。

Tags: , ,

CORE

About this blog

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

Calendar

<<  April 2020  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar