WebSurfer's Home

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

連動ドロップダウンリスト (ASP.NET Core MVC)

by WebSurfer 2024年2月8日 15:12

ASP.NET Core MVC での連動ドロップダウンリストの実装方法を備忘録として書いておきます。先の記事「連動ドロップダウンリスト (MVC5 版)」をターゲットフレームワーク .NET 8.0 の ASP.NET Core MVC ベースで書き直したものです。

ASP.NET Core MVC の連動 DropDownList

上の画像がそれで、先の記事と同様に、Microsoft の SQL Server サンプルデータベース Northwind の Categories テーブルと Products テーブルを使って、1 つめのドロップダウンリストで分類 (Categories) を選択すると、2 つめのドロップダウンリストにはその分類に属する製品 (Products) が絞り込まれて表示されるようにしています。

Categories テーブルと Products テーブルは以下の通りとなっています。(EDM のダイアグラムを表示したものです)

Categories  テーブルと Products テーブル

分類ドロップダウンリストの選択に応じて jQuery ajax を利用してアクションメソッドに要求を出し、選択された分類に該当するProducts テーブルの ProductID と ProductName を JSON 形式の応答として取得し、製品ドロップダウンリストの内容を書き換えるようにしています。

アプリの作り方の概要を以下に書きます。

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

Visual Studio 2022 のテンプレートの中から「ASP.NET Core Web アプリ (Model-View-Controller)」を選び ASP.NET Core MVC アプリのプロジェクトを作成します。

テンプレート

この記事の例ではターゲットフレームワークを .NET 8.0 にしています。その他のバージョンではこの記事の内容と多少違いがあるかもしれません。

(2) コンテキストクラスとエンティティクラスの作成

Entity Framework を利用するので、リバースエンジニアリングという手法を使って、SQL Server データベース Northwind からコンテキストクラスとエンティティクラスを生成します。

作成方法は Microsoft のドキュメント「スキャフォールディング (リバース エンジニアリング)」を見てください。各パラメータについては、こちらの記事 Scaffold-DbContext がまとまっていて分かりやすいと思います。

参考に、EF Core パッケージマネージャーコンソール (PMC) ツールを使った場合の Scaffold-DbContext コマンドの例を下に載せておきます。

Scaffold-DbContext -Connection "Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=Northwind;Integrated Security=True" -Provider Microsoft.EntityFrameworkCore.SqlServer -ContextDir Data -OutputDir Models -Tables Products, Categories, Suppliers, Customers, Orders, Employees, Shippers, "Order Details" -DataAnnotations

(3) コンテキストクラスを DI コンテナに追加

上のステップ (2) で作成したコンテキストクラス NorthwindContext を DI コンテナに追加します。Program.cs に以下のコードを追加してください。"NorthwindConnection" は接続文字列名で任意です。

builder.Services.AddDbContext<NorthwindContext>(options =>
    options.UseSqlServer(builder.Configuration
           .GetConnectionString("NorthwindConnection")));

appsettings.json に接続文字列を追加します。名前は上のコードで設定したもの(この例では "NorthwindConnection")とし、接続文字列本体はリバースエンジニアリングの Scaffold-DbContext コマンドで使ったものと同じにします。ただし、文字列中の \ は \\ にエスケープする必要があるので注意してください。

(4) PruductsController と View の作成

Visual Studio のスキャフォールディング機能を使って、Products テーブルの CRUD (Create / Read / Upadate / Delete) を行う Controller と View 一式を生成します。

アプリを実行してみて CRUD 機能が期待通り動くことを確認します。

(5) アクションメソッドの追加

この記事の一番上の画像の連動ドロップダウンリスト用のアクションメソッドを作成し、上のステップ (4) で作った PruductsController に追加します。

まずベースとなるビューモデルを作成します。コードは以下の通りです。クラス名は任意ですがここでは Sales としています。

using System.ComponentModel.DataAnnotations;

namespace MvcNet8App.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; }
    }
}

連動ドロップダウンリスト用のアクションメソッド CascadingDropDown と、分類ドロップダウンリストの選択に応じて Products テーブルから選択された分類に該当する ProductID と ProductName を取得して JSON 形式で返すアクションメソッド GetProducts を作成し、PruductsController に追加します。

コードは以下の通りです。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcNet8App.Data;
using MvcNet8App.Models;

namespace MvcNet8App.Controllers
{
    public class ProductsController : Controller
    {
        private readonly NorthwindContext _context;

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

        // 中略(スキャフォールディングで自動生成されたコード)

        public IActionResult CascadingDropDown()
        {
            ViewData["CategoryId"] = 
                new SelectList(_context.Categories, "CategoryId", "CategoryName");
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult CascadingDropDown(
            [Bind("ProductId,CategoryId,Comment")] Sales sales)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");
            }

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

        // 製品ドロップダウンに表示する ProductID と ProductName を
        // JSON 形式で取得するアクションメソッド。引数 id が分類ドロ
        // ップダウンで選択された CategoryID
        public async Task<IActionResult> GetProducts(int id)
        {
            var products = _context.Products
                           .Where(p => p.CategoryId == id)
                           .Select(p => new
                           {
                               ProductId = p.ProductId,
                               ProductName = p.ProductName
                           });

            return Json(await products.ToListAsync());
        }
    }
}

上のアクションメソッド GetProducts が返す JSON 文字列は、デフォルトでは Camel Casing される ( {"name":"value"} の "name" の先頭の文字が小文字になる) ので注意してください。(詳しくは先の記事「JsonSerializer の Camel Casing (CORE)」を見てください)

Camel Casing されないようにするには、Program.cs のコードの中に AddControllersWithViews() メソッドが含まれているはずなので、それに以下のように .AddJsonOptions(opttions => ... 以下のコードを追加します。

builder.Services.AddControllersWithViews().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

これをしないで、クライア���トスクリプトで対応しても OK です。いずれにしても、対応しないと製品ドロップダウンリストの内容が undefined となってしまいますので注意してください。

(6) View の追加

上のステップ (5) で作成したアクションメソッド CascadingDropDown に対応する View をスキャフォールディング機能を利用して作成します。レイアウトページ _Layout.cshtml を利用しています。コードは以下の通りです。説明はコメントに書きましたので、それを見てください

@model MvcNet8App.Models.Sales

@{
    var data = ViewBag.CategoryID;
    
    ViewData["Title"] = "CascadingDropDown";    
}

<h1>CascadingDropDown</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="CascadingDropDown">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="CategoryId" class="control-label"></label>

                @* 
                スキャフォールディングで生成される input を select に書き換え
                *@
                <select asp-for="CategoryId" class="form-control" 
                    asp-items="ViewBag.CategoryId">
                    <option value="">▼分類を選択してください▼</option>
                </select>

                <span asp-validation-for="CategoryId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ProductId" class="control-label"></label>

                @*
                スキャフォールディングで生成される input を select に書き換え
                asp-for="ProductId" が無いと検証が動かないので注意 
                *@
                <select asp-for="ProductId" class="form-control"></select>

                <span asp-validation-for="ProductId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Comment" class="control-label"></label>
                <input asp-for="Comment" class="form-control" />
                <span asp-validation-for="Comment" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}

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

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

            // 以下は初期画面および検証結果 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');
                }
            });
        });

        //]]>
    </script>
}

Tags:

CORE

JavaScript の console.log()

by WebSurfer 2024年1月31日 20:15

JavaScript の console.log() で開発者ツールの Console に JavaScript オブジェクトや html 要素 (DOM) を出力すると、Console を開いた時点での内容 (下の (1) の時点での内容ではなくて) が表示されるという話を書きます。

開発者ツールの Console 出力

Qiita の質問「javascriptとブラウザのコンソールについて」で調べたことですが、忘れそうなので自分のブログに備忘録として残しておこうと思った次第です。

上の画像の JavaScript のコードの (1) を見てください。obj = { prop: 123 } という JavaScript オブジェクトを作成してから即 console.log(obj) とし、その後で prop の値を 456 に書き換えています。しかしながら、コンソールを開いて見ると prop が書き換えられた後の 456 という結果が表示されています。

これは、MDN のドキュメント console: log() static method の「Logging objects」のセクションに書いてあるように、"Information about an object is lazily retrieved. This means that the log message shows the content of an object at the time when it's first viewed, not when it was logged." ということによります。

コード上で console.log() とした時点での JavaScript オブジェクトのログを取りたい場合は、MDN のドキュメントに書いてあるように "A common way is to JSON.stringify() and then JSON.parse() it" とするのが良さそうです。それが上の画像の JavaScript のコード (2) です。

Console を開いた時点での内容が表示されるというのは、JavaScript オブジェクトだけでなく、 html 要素 (DOM) を JavaScript で書き換えても同じことが起こります。

上の画像の (4) を見てください。JavaScript で html の p 要素を生成して id と textContent を設定してから即 console.log(p) し、その後で id と textContent を書き換えています。しかし、Console には書き換えた後の結果が表示されています。

コード上で console.log() とした時点での p 要素のログを取りたい場合は、p.outerHTML を出力するのが良さそうです。それが上の画像の (5) です。

JavaScript はブラウザ依存ですが、Windows 10 PC の Edge 121.0.2277.83, Chrome 121.0.6167.140, Firefox 122.0, Opera 106.0.4998.66 で試して同じ結果となることは確認しました。

Tags: , ,

JavaScript

interface メンバーの「既定の実装」

by WebSurfer 2024年1月17日 14:54

C# 8.0 以降で、interface のメンバーに「既定の実装 (default implementation)」を設定できるようになり、それに関連してアクセス修飾子に private, protected, internal などを設定することが可能になりました。

ちなみに、C# 8.0 より前 (.NET Core 3 より前、.NET Framework はすべて) では interface のメンバーのアクセス修飾子は public しか許されておらず、継承する class 側でメンバーを実装する際にもアクセス修飾子に public と明示的に指定する必要がありました。

public 以外が許されなかった理由は、自分が調べたことの要約ですが、以下のようなことと理解しています。 (理由が明確に書いてある Microsoft のドキュメントは見つかりませんでした)

  • class が interface を継承すると、その class が必ず interface に定義されているメンバーを実装して公開するという外部との契約となる。(Interface の仕様に "An interface defines a contract. A class or struct that implements an interface shall adhere to its contract." と書いてあります)
  • 別の言い方をすると、そもそも interface に指定されるメンバーを実装して class を利用する外部に公開するのが目的なのに、private とか protected で隠ぺいするのは理にかなってない。

上記にもかかわらず、C# 8.0 以降で interface のメンバーに private や protected などを設定できるようになったのは何故か、その理由に興味があったので調べてみました。以下に調べたことを備忘録として書いておきます。

調べたことを簡単に書くと、C# 8.0 で interface に「既定の実装」という機能を追加する際に、ついでに public 以外のあらゆるアクセス修飾子を設定できるようにし、「既定の実装」に対するアクセスコントロールを可能にするというのが目的らしいです。

そのあたりのことが書いてあったドキュメントと、関係する部分の抜粋を以下に書いておきます。

  1. アクセス修飾子 (C# プログラミング ガイド) の「その他の型」
    "インターフェイス メンバー宣言には、あらゆるアクセス修飾子を含めることができます。 そのことは、クラスを実装するあらゆるもので必要になる共通実装を静的メソッドから与えるときに最も役に立ちます。Interface member declarations may include any access modifier. This is most useful for static methods to provide common implementations needed by all implementors of a class."
  2. interface (C# リファレンス)
    "インターフェイスによってメンバーの既定の実装を定義できます。 共通の機能を 1 回で実装する目的で static メンバーも定義できます。 An interface may define a default implementation for members. It may also define static members in order to provide a single implementation for common functionality."
  3. default interface methods
    "Add support for virtual extension methods - methods in interfaces with concrete implementations. A class or struct that implements such an interface is required to have a single most specific implementation for the interface method, either implemented by the class or struct, or inherited from its base classes or interfaces."
  4. インターフェイスのデフォルト実装
    "メソッド、プロパティ、インデクサー、イベントのアクセサーの実装を持てるようになった。アクセシビリティを明示的に指定できるようになった。静的メンバーを持てるようになった・・・中略・・・狭義にはこの1番目の機能こそが「デフォルト実装」です。 ただ、これのついでに実装されたものなので2番目、3番目には具体的な名前がついていません"

以下に、interface のメンバーを「既定の実装」とし、アクセス修飾子に public, private, protected, internal を使ったサンプルを載せておきます。説明はコード中にコメントとして書きましたのでそちらを見てください。

namespace ConsoleAppInterface
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Sample sample = new();
            ((ISampleDerived)sample).PublicMethod();
            sample.PublicMethod2();
            sample.Protected3();
            //((ISampleDerived)sample).Protected2();  // アクセス不可
        }
    }

    // C# 8 以降で、interface のメンバーに「既定の実装 (default
    // implementation)」を設定できるようになり、関連してアクセス修飾子に
    // private, protected, internal などを設定することが可能になった
    interface ISample
    {
        // デフォルトで public なのは以前と同じ。なので、アクセス修飾子を付
        // けない場合は public になる
        void Public()
        {
            Console.WriteLine("ISample.Public");
            Private();    // private メンバーにアクセス
        }

        internal void Internal()
        {
            Console.WriteLine("ISample.Internal");
        }

        protected void Protected()
        {
            Console.WriteLine("ISample.Protected");
        }

        // private の場合「既定の実装」は必須。無いと以下のエラー:
        // エラー CS0501 'ISample.Private()' は abstract、extern、または
        // partial に指定されていないため、本体を宣言する必要があります
        private void Private()
        {
            Console.WriteLine("ISample.Private");
        }
    }

    interface ISampleDerived : ISample
    {
        void PublicMethod()
        {
            // interface で interface を継承する場合、継承元の public, 
            // internal, protected メソッドを呼べる。「既定の実装」の有無
            // も関係なく呼べる
            Public();
            Internal();
            Protected();
            // Private();  private はもちろんダメ
        }

        void Default()
        {
            Console.WriteLine("ISampleDerived.Defualt");
        }

        // 派生先から protected メンバーにアクセスできるのは interface
        // だけ。class から呼ぶことはできない。呼ぶとエラーになる。下の
        // Sample の実装の PublicMethod2 メソッド内の説明を参照
        protected void Protected2()
        {
            Console.WriteLine("ISampleDerived.Protected2");
        }

        // 以下のような「既定の実装」がない場合、継承する class 側で実装が
        // 必要。ただし、継承する class 側では public にしないとエラー
        protected void Protected3();
    }

    public class Sample : ISampleDerived
    {
        // 継承元の ISampleDerived 内で「既定の実装」がされているメソッド
        // (この例では PublicMethod, Default, Protected2)は継承するクラ
        // スでの実装が無くてもエラーにならない

        public void PublicMethod2()
        {
            // Default を呼ぶには 1 段キャストが必要。単に Default(); とし
            // たのではエラー
            ((ISampleDerived)this).Default();

            // interface と違って class では protected なものは呼べない。

            //((ISampleDerived)this).Protected2();

            // ・・・とすると以下のエラーとなる:
            // エラー CS1540 'ISampleDerived' 型の修飾子をとおしてプロ
            // テクト メンバー 'ISampleDerived.Protected2()' にアクセスす
            // ることはできません。修飾子は 'Sample' 型、またはそれから派
            // 生したものでなければなりません
        }

        // ISampleDerived に「既定の実装」がない Protected3() があるので
        // class側で実装が必要。

        public void Protected3()
        {
            Console.WriteLine("Sample.Protected3");
        }

        // ただし、アクセス修飾子を public にしないと以下のエラー:
        // エラー CS0737 'Sample' は、インターフェイス メンバー
        // 'ISampleDerived.Protected3()' を実装していません。
        // 'Sample.Protected3()' は public ではないため、インターフェイス
        // メンバーを実装できません。

        // と言って class 側で Protected3 を実装しないと以下のエラー:
        // エラー CS0535 'Sample' はインターフェイス メンバー
        // 'ISampleDerived.Protected3()' を実装しません
    }
}

// 結果は:
// ISample.Public
// ISample.Private
// ISample.Internal
// ISample.Protected
// ISampleDerived.Defualt
// Sample.Protected3

Tags: , ,

.NET Framework

About this blog

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

Calendar

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

View posts in large calendar