WebSurfer's Home

Filter by APML

連動ドロップダウンリスト (MVC5 版)

by WebSurfer 22. August 2020 14:53

ASP.NET MVC での連動ドロップダウンリストの実装方法を備忘録として書いておきます。

ASP.NET MVC の連動 DropDownList

ASP.NET Web Forms アプリの場合は先の記事「DetailsView 中の連動 DropDownList」に書きました。それと同様な連動機能を MVC アプリで実装してみます。

記事「DetailsView 中の連動 DropDownList」と同様に、Northwind サンプルデータベースの Categories テーブルと Products テーブルを使って例を書きます。

上の画像に示すように 2 つのドロップダウンリストを配置し、1 つめのドロップダウンリストで分類を選択すると、2 つめのドロップダウンリストにはその分類に属する製品が絞り込まれて表示されるようにします。

データベースへのアクセスやデータの取得は先の記事「スキャフォールディング機能」のステップ 1 ~ 10 で書きました Entity Data Model (EDM) を利用します。Categories テーブルと Products テーブルの EDM ダイアグラムは以下の通りです。

Categories  テーブルと Products テーブル

記事「DetailsView 中の連動 DropDownList」の場合は、分類ドロップダウンリストの選択が変更されるとポストバックがかかり、サーバー側で変更に応じて製品ドロップダウンリストの内容を書き換えてページ全体を再描画するというものでした。いちいちポストバックして再描画というのがちょっとやりすぎの感があります。

Ajax Control Toolkit の中に CascadingDropDown というものがありますが、こちらは Ajax を利用して、上位ドロップダウンリストの変更があると、サーバー側の Web サービスに下位ドロップダウンリストに表示するデータを要求し、Web サービスから戻ってきた JSON 形式のデータで下位ドロップダウンリストの内容を書き換えるというものです。

この記事の MVC 版連動ドロップダウンリストでも、Ajax Control Toolkit の CascadingDropDown のやり方にならって、jQuery ajax を利用して、分類ドロップダウンリストの選択に応じて製品ドロップダウンリストに表示する ProductID と ProductName を JSON 形式で取得し、製品ドロップダウンリストの内容を書き換えるようにしました。

コードは以下の通りです。説明はコード内にコメントとして書きましたのでそちらを見てください。手抜きでスミマセン。

Model

using System.ComponentModel.DataAnnotations;

namespace Mvc5App.Models
{
    public class Sales
    {
        public int Id { get; set; }

        [Display(Name = "分類")]
        [Required(ErrorMessage = "{0} の選択は必須")]
        public int CategoryID { get; set; }

        [Display(Name = "製品")]
        [Required(ErrorMessage = "{0} の選択は必須")]
        public int ProductID { get; set; }

        [Display(Name = "コメント")]
        [Required(ErrorMessage = "{0} は必須")]
        public string Comment { get; set; }
    }
}

Controller / Action Method

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web;
using System.Web.Mvc;
using Mvc5App;

namespace Mvc5App.Controllers
{
    public class ProductsController : Controller
    {
        private NORTHWINDEntities db = new NORTHWINDEntities();

        public ActionResult CascadingDropDown()
        {
            ViewBag.CategoryID = 
                new SelectList(db.Categories, "CategoryID", "CategoryName");
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult CascadingDropDown(
            [Bind(Include = "ProductID,CategoryID,Comment")] 
            Mvc5App.Models.Sales sales)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");
            }

            // 検証結果 NG で再描画する際は以下のように再度 ViewBag
            // に SelectList を設定しないと分類ドロップダウンの中身
            // が表示されないので注意
            ViewBag.CategoryID = 
                new SelectList(db.Categories, "CategoryID", "CategoryName");
            return View(sales);
        }

        // 製品ドロップダウンに表示する ProductID と ProductName を
        // JSON 形式で取得するアクションメソッド。引数 id が分類ドロ
        // ップダウンで選択された CategoryID
        public async Task<ActionResult> GetProducts(int id)
        {
            var products = from p in db.Products
                           where p.CategoryID == id
                           select new
                           {
                               ProductID = p.ProductID,
                               ProductName = p.ProductName
                           };
            return Json(await products.ToListAsync(), 
                        JsonRequestBehavior.AllowGet);
        }
    }
}

View

@model Mvc5App.Models.Sales

@{
    ViewBag.Title = "CascadingDropDown";
}

<h2>CascadingDropDown</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Sales</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.CategoryID, 
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.DropDownList("CategoryID", null, 
                    "▼分類を選択してください▼", 
                    htmlAttributes: new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.CategoryID, 
                    "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ProductID, 
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <select class="form-control" id="ProductID" name="ProductID"></select>
                @Html.ValidationMessageFor(model => model.ProductID, 
                    "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Comment, 
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Comment, 
                    new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Comment, 
                    "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")

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

        // 長くなるので JavaScript / jQuery のコードは下に別掲

        //]]>
    </script>
}

JavaScript / jQuery

実際は以下のコードは View の @section Scripts { ... } ブロックの中にインラインで書いています。View のコードが長くなって見難くなるので別掲にしました。

$(function () {
    // 分類ドロップダウンの jQuery オブジェクトを取得。
    // html コードの id は html ヘルパの Id メソッドで取得
    var categoryDDL = $('#@Html.Id("CategoryID")');

    // 製品ドロップダウンの jQuery オブジェクトを取得。
    // こちらは <select id="ProductID" とハードコーディン
    // グされているので以下で OK (Id メソッドは不要)
    var productDDL = $("#ProductID");

    // DropDownList の第 3 引数 "▼分類を選択してください▼"
    // は以下の option 要素になる (必ず value="" になると書
    // いた Microsoft の文書が見つからないのが不安要素):
    // <option value="">▼分類を選択してください▼</option>

    // 以下は初期画面および検証結果 NG での再描画の際必要。
    // 初期画面では categoryDDL.val() は上の option 要素の
    // value="" を取得するので if 文の条件は false となる。
    // 検証結果 NG で再描画される際は分類ドロップダウンの
    // 選択に応じて製品ドロップダウンの内容を書き換える
    if (categoryDDL.val() != "") {
        productDDL.children().remove();
        productDDL.append(
            '<option value="">▼製品を選択してください▼</option>');
        productDDL.removeAttr('disabled');

        // 分類ドロップダウンの選択結果 (CategoryID) を
        // jQuery ajax でサーバー側の GetProducts アクション
        // メソッドに送信。アクションメソッドは Products テ
        // ーブルに SELECT クエリを発行し、CategoryID に属す
        // 製品の ProductID, ProductName を取得して JSON 文
        // 字列として返す。その JSON 文字列から option 要素
        // の文字列を組み立てて製品ドロップダウンに追加する
        $.ajax({
            url: '/Products/GetProducts/' + categoryDDL.val(),
            method: 'get',
        }).done(function (data) {
            $.each(data, function (key, val) {
                productDDL.append('<option value=' +
                    val.ProductID + '>' + val.ProductName +
                    '</option>');
            });
        }).fail(function (jqXHR, textStatus, errorThrown) {
            alert('Error getting products!');
        });
    } else {
        productDDL.children().remove();
        productDDL.attr('disabled', 'disabled');
    }

    // 分類ドロップダウンの選択が変更されると change イベント
    // が発生 するのでそのリスナで製品ドロップダウンの内容を
    // 分類に応じて書き換え。リスナの中身は上と同じコード
    categoryDDL.on("change", function () {
        if (categoryDDL.val() != "") {
            productDDL.children().remove();
            productDDL.append(
                '<option value="">▼製品を選択してください▼</option>');
            productDDL.removeAttr('disabled');

            $.ajax({
                url: '/Products/GetProducts/' + categoryDDL.val(),
                method: 'get',
            }).done(function (data) {
                $.each(data, function (key, val) {
                    productDDL.append('<option value=' +
                        val.ProductID + '>' + val.ProductName +
                        '</option>');
                });
            }).fail(function (jqXHR, textStatus, errorThrown) {
                alert('Error getting products!');
            });
        } else {
            productDDL.children().remove();
            productDDL.attr('disabled', 'disabled');
        }
    });
});

このコードで、この記事の上の画像のとおり表示されます。

分類ドロップダウンリストの選択変更に連動して製品ドロップダウンリストの内容が変わります。送信ボタンクリックの際のドロップダウンリストの検証は、クライアント側ではかかりませんが、サーバー側では検証されてエラーメッセージは期待通り表示されます。

サーバー側での検証結果が NG の場合、同じ画面が再描画されユーザーに選択の修正を促すようにしていますが、その際のドロップダウンリストの連動も OK なことを確認しています。

Tags: , ,

MVC

ASP.NET MVC の Model

by WebSurfer 29. May 2020 13:46

ASP.NET MVC で言う Model がどういうものかについて書いておきます。ネットの記事などで目にする一般的な MVC の Model の説明とは少々違っていて理解し難いかもしれませんので (実は自分がそうでした)。

ASP.NET MVC の Model

上の画像はマイクロソフト公式解説書「プログラミング Microsoft ASP.NET MVC」に記載されている図を借用したものです。ASP.NET MVC には「入力モデル」「ビューモデル」「ドメインモデル」という 3 種類の Model があることを示しています。それら各モデルが重なっている点にも注意してください。

重なっているのは、Model 自体は一つのクラスとして定義されており同じものですが、使われ方によって「入力モデル」「ビューモデル」「ドメインモデル」というように役割が変わるということと理解すれば良いと思います。

一方、ネットなどでよく目にする一般的な MVC の Model の説明は、例えば Wikipedia の記事 Model View Controller にあるように「アプリケーションデータ、ビジネスルール、ロジック、関数」という役割を持ち、クライアント側から見て View と Controller の背後にあるという構成で説明されていることが多いと思います。

ASP.NET MVC の 3 種類の Model のうち、「ドメインモデル」が上で言う「アプリケーションデータ、ビジネスルール、ロジック、関数」に近いものです。

「ビューモデル」は Controller から View にデータを渡すために使われます。「入力モデル」はクライアントから送信されてくるデータを Controller のアクションメソッドに渡す時に使われます。

以下に説明のための具体例を書きます。

まず、プロジェクトの Models フォルダに、以下の Student クラスの定義(即ち、Model の定義)を含むクラスファイルがあるとします。

using System;
using System.Collections.Generic;

namespace Mvc5App.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

さらに、データーベースに Student テーブルがあって、それを Entity Framework を利用して編集・更新を行うための Controller と View を作るとします。

編集用の画面を表示するための Controller のアクションメソッドは以下のようになります。

public async Task<ActionResult> Edit(int? id)
{
    Student student = await db.Students.FindAsync(id);

    return View(student);
}

db.Students.FindAsync(id) の db は DbContext クラスを継承するコンテキストクラスで、Entity Framework を利用してデータベースへのクエリの実行などを行うものです。このコードでは、指定された id のレコードをデータベースから取得して Student オブジェクトを作っています。その部分は上の図で「ドメインモデル」に該当します。

return View(student) で View に Student オブジェクトを渡していますが、それが「ビューモデル」に該当します。

アクションメソッドに対応する View (Edit.cshtml) のコードの一行目に @model Mvc5App.Models.Student と書けば、View の中では Model プロパティで Student オブジェクトを取得できます。また、EditorFor などの Html ヘルパーでは、例えばその引数を model => model.LastName とすれば model に Student オブジェクトが渡されます。

View によって html ソースが生成されクライアント (ブラウザ) に送信されます。ユーザーがブラウザに表示されたテキストボックスの内容を編集後、ボタンクリック操作などでデータを送信するとサーバー側ではモデルバインディングという操作が行われます。

ブラウザから送信されてきたデータを受け取ってデーターベースを更新する Controller のアクションメソッドは以下のようになっています。モデルバインディングというのは、送信されてきたデータで Student オブジェクトを作成し、それをアクションメソッドの引数 student に渡す操作ですが、その時使うのが上の図の「入力モデル」になります。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(Student student)
{
    if (ModelState.IsValid)
    {
        db.Entry(student).State = EntityState.Modified;
        await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    return View(student);
}

ModelState.IsValid が true の場合(送信データの検証結果が OK の場合)、if 文の中の最初の 2 行で既存のレコードが更新されます。その部分も上の図で「ドメインモデル」に該当します。

ASP,NET MVC アプリを作っているときに目立つのが「ビューモデル」と「入力モデル」と思いますが、それらは上に紹介した Wikipedia の記事にあるような一般的な MVC の Model の説明の機能「アプリケーションデータ、ビジネスルール、ロジック、関数」は持たないので、説明と違うということが混乱を招くところと思います。

Tags: , ,

MVC

ASP.NET Identity で MySQL 利用 (.NET 版)

by WebSurfer 6. May 2020 21:43

.NET Framework 版(Core 版ではありません)の ASP.NET Identity でユーザー情報のストアに MySQL を利用するにはどうするかということを書きます。(Core 3.1 版は別の記事「ASP.NET Identity で MySQL 利用 (CORE 版)」を見てください。Visual Studio 2022 + .NET 6.0 の場合は「.NET 6.0 ASP.NET Identity に MySQL 使用 (CORE)」を見てください)

参考にさせていただいたのは「ASP.NET MVC + MySQL で開発環境構築」という記事です(以下、参考記事と書きます)

(1) MVC プロジェクトの作成

Visual Studio 2019 のテンプレートを利用して .NET Framework 版の ASP.NET MVC5 アプリのプロジェクトを自動生成します。

新しい ASP.NET Web アプルケーションを作成する

認証に「個別のユーザーアカウント」を選ぶと ASP.NET Identity を使ったクッキーベースの認証システムが実装され、Entity Framework Code First の機能を使って LocalDB (SQL Server) にユーザー情報を保持するためのデーターベースを作るコードが生成されます。

それを LocalDB (SQL Server) ではなくて MySQL にユーザー情報を保持するためのデーターベースを作り、それを利用するように変更します。

(2) 参照設定

ソリューションエクスプローラーで参照を右クリックして MySql.Data と MySql.Data.EntityFramework を参照に追加します。下の画像を見てください。

参照設定

先の記事「MySQL をインストールしました(その 3)」で書きましたように MySQL 8.0.19 をインストールしており、その時同時にインストールした Connector/NET 8.0.19 に含まれる MySql.Data, MySql.Data.EntityFramework を使うことにしました。

参考記事では NuGet で MySql.Data.Entity をインストールしたと書いてありましたが、NuGet にあるのは今でもバージョンが古いようです。

(3) 接続文字列の変更

テンプレートで自動生成された web.config の接続文字列は LocalDB を利用するように設定されていますので、それを MySQL に接続するように変更します。

以下のような感じです。例えば、database=identity というようにデータベース名を指定すると、Entity Framework Code First の機能を使って identity という名前のデータベースを新たに生成し、そこに必要なテーブルを生成してくれます。(データベース名は任意で、この記事で identity としたのは単なる例です)

<add name="DefaultConnection"
    connectionString="server=localhost;user id=root;password=*****;
    database=identity"
    providerName="MySql.Data.MySqlClient" />

(4) Enable-Migrations

上記 (3) の設定の後、Enable-Migrations を実行すると以下のエラーになると思います��

"ADO.NET プロバイダーに、不変名が 'MySql.Data.MySqlClient' の Entity Framework プロバイダーがありません。アプリケーションの構成ファイルの "entityFramework" セクションにプロバイダーが登録されていることを確認してください"

それを解決するためには、エラーメッセージに応じて、自動生成された web.config の entityFramework/providers セクションに以下のコードを追加します。

<provider 
  invariantName="MySql.Data.MySqlClient" 
  type="MySql.Data.MySqlClient.MySqlProviderServices, 
        MySql.Data.EntityFramework,Version=8.0.19.0, Culture=neutral, 
        PublicKeyToken=c5687fc88969c44d" />

NuGet で MySql.Data.Entity をインストールした場合は web.config への上記の provider の追加は NuGet が面倒を見てくれるようです。

上のコードを web.config に追加した後 Enable-Migrations を実行すると Migrations フォルダに Configuration.cs ファイルが生成されているはずです。

(5) Add-Migration

Add-Migration Initial を実行します(Initial という名前は任意です)。そうすると、参考記事と同様に以下のエラーになると思います。

"プロバイダー 'MySql.Data.MySqlClient' で MigrationSqlGenerator が見つかりませんでした。対象の移行構成クラスで SetSqlGenerator メソッドを使用して、追加の SQL ジェネレーターを登録してください"

エラーメッセージに従って Configuration.cs に手を加えます。以下のコードのコメントで「追加」としている部分がそれです。

namespace Mvc5AppIdentityMySql.Migrations
{
  using System;
  using System.Data.Entity;
  using System.Data.Entity.Migrations;
  using System.Linq;

  // 追加
  using MySql.Data.EntityFramework;

  internal sealed class Configuration : 
    DbMigrationsConfiguration<Mvc5AppIdentityMySql.Models.ApplicationDbContext>
  {
    public Configuration()
    {
      AutomaticMigrationsEnabled = false;

      // 追加
      SetSqlGenerator("MySql.Data.MySqlClient", 
                      new MySqlMigrationSqlGenerator());
    }

    protected override void Seed(
      Mvc5AppIdentityMySql.Models.ApplicationDbContext context)
    {
    }
  }
}

エラーメッセージの SetSqlGenerator メソッドというのは DbMigrationsConfiguration クラスの SetSqlGenerator(String, MigrationSqlGenerator) メソッド です。これをコンストラクタに追加します。

SetSqlGenerator メソッドの第 1 引数には MySQL のプロバイダ名 "MySql.Data.MySqlClient" を与え、第 2 引数には MySqlMigrationSqlGenerator クラスを初期化して与えます。

MySqlMigrationSqlGenerator クラスが属する名前空間は Connector/NET 8.0 になって変わったようで MySql.Data.EntityFramework となっていますので注意してください。(v6.x の時代は MySql.Data.Entity だったはずです)

参考記事に書いてあったコードは上記と違いますが、参考記事の通りとしても問題なく Add-Migration Initial, Update-Database できることは確認できました。しかし、参考記事のコードがエラーメッセージの「追加の SQL ジェネレーターを登録」をどう解決しているのか分からなかったので、エラーメッセージ通りにコーディングしました。

(6) Update-Database

Update-Database で MySQL に必要なデータベースとテーブルを生成します。成功すると、上のステップ「(5) Add-Migration」で生成された Configuration.cs ファイルの内容に従って ASP.NET Identity 用のテーブルが生成されます。下の画像を見てください。

生成されたデータベース

データーベース / テーブルが生成できましたので、Visual Studio から MVC アプリを動かしてユーザー登録できます。登録したユーザーはデーターベースに反映されています。もちろん登録した ID とパスワードでアプリにログインできるようになります。

参考記事では Update-Database で "Specified key was too long; max key length is 767 bytes" というエラーとなったそうです。なぜ参考記事でその問題が起こったのか、この記事では問題なかったかは以下のようなことだと思います。

テーブル AspNetUserRoles は UserId と RoleId がそれぞれ nvarchar(128) で連結主キーの設定になるのですが、charset が utf8 で一文字 3 バイトですと 128 x 2 x 3 = 768 となって制限の 767 バイトを超えます。それが参考記事の問題のようです。

一方、この記事では MySQL 8.x を使っているので上限が 3,072 バイトになっています。(MySQL 5.7 以降では _large_prefix が ON となっており、767 バイトの制限が 3,072 バイトに拡張されているとのこと)。文字コードは utf8mb4 なので一文字 4 バイトになり、128 x 2 x 4 = 1024 < 3072 なので問題ないということのようです。


旧来のフォーム認証の場合、ASP.NET の認証・承認システムとデータベースの間にプロバイダを配置して、プロバイダを入れ替えることによりいろいろなデータベースに対応していました。例えば SQL Server の場合は専用の SqlMembershipProvider が提供されています。

異なるデーターベースを使う場合はプロバイダを入れ替えて対応します。例えば MySQL の場合は Oracle が提供している MySQLMembershipProvider を利用できます。ただし、プロバイダが提供されていなければ自力でコードを書いてプロバイダを作成しなければなりません。

ASP.NET Identity では上に書いたように参照設定の追加、web.config の設定、追加の SQL ジェネレーター登録(これはすべてのケースで必要かは分かりませんが)で可能なようです。個人的には以前より分かりにくくなった感じがしますが・・・

Tags: ,

MVC

About this blog

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

Calendar

<<  January 2026  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar