WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

MVC の DropDownList での NULL の処置

by WebSurfer 17. October 2021 13:31

ASP.NET MVC アプリでデータベースの CRUD 操作を行う際ドロップダウンリストを利用してユーザー入力に便宜を図ることがあると思います。その際、NULL 可になっている列にドロップダウンリストから NULL を入力できるようにするにはどうするかという話を書きます。

ドロップダウンリスト

この記事は ASP.NET MVC5 および ASP.NET Core MVC の場合です。ASP.NET Web Forms アプリの場合は別の記事「DropDownList での NULL の処置」に書きましたのでそちらを見てください。

先の記事「スキャフォールディング機能」および「スキャフォールディング機能 (CORE)」で作ったアプリのドロップダウンリストから NULL を入力できるようにしてみます。

そのアプリは Northwind サンプルデータベースの Products, Suppliers, Categories テーブルから作ったコンテキストクラス、エンティティクラスをベースに、Visual Studio のスキャフォールディング機能を使って CRUD 操作を行うための Controller / View のコードを一式自動生成したものです。

自動生成されたコードの Create, Edit 画面では CatrgoryID, SupplierID 列にドロップダウンリストを使用して CategoryName, CompanyName (名前) が表示され、ID (数字) ではなく名前を見て選択できるようになっています。上の画像を見てください。

Products テーブルの SupplierID, CategoryID 列は NULL 可なのですが、自動生成されたコードでは NULL を選択することはできません。それを、Create, Edit 操作の際ドロップダウンリストから NULL を選択して、その結果を Products テーブルに反映できるようにします。

必要なコードの追加・変更はわずかで、自動生成されている View のコードの一部に以下の修正を行うだけです。

ASP.NET MVC5

View に自動生成されている DropDownList メソッドの第 3 引数として "NULL" という文字列を追加。

@Html.DropDownList("SupplierID", null, "NULL", 
    htmlAttributes: new { @class = "form-control" })

ASP.NET Core MVC

View に自動生成されている select タグヘルパーに以下のように option 要素を追加。

<select asp-for="SupplierID" class="form-control" 
    asp-items="ViewBag.SupplierID">
    <option value="">NULL</option>
</select>

上記の設定の結果、ブラウザに送信される html ソースは以下のようになります。value="" で NULL という項目が追加されているところに注目してください。

<select class="form-control" id="SupplierId" name="SupplierId">
  <option value="">NULL</option>
  <option value="1">Exotic Liquids</option>
  <option value="2">New Orleans Cajun Delights</option>
  <option value="3">Grandma Kelly's Homestead</option>
  ・・・中略・・・
</select>

これによりブラウザに表示されたドロップダウンリストを開くと NULL という選択肢が含まれるようになります。

NULL の選択

そして NULL を選択して POST すれば、データは SupplierId= という形 (name=value の value が空) でフォームに含まれて送信されます。

送信結果

モデルバインディングに使う Product クラスの定義は以下のようになっています。SupplierId, CategoryId プロパティの型が int? であるところに注目してください。

public partial class Product
{
    [Key]
    [Column("ProductID")]
    public int ProductId { get; set; }
    [Required]
    [StringLength(40)]
    public string ProductName { get; set; }
    [Column("SupplierID")]
    public int? SupplierId { get; set; }
    [Column("CategoryID")]
    public int? CategoryId { get; set; }

    // ・・・中略・・・
}

それをアクションメソッドの引数に使っていますので、POST されてきたデータは以下のようにモデルバインディングされます。SupplierId が null になっているところに注目してください。

モデルバインディング

これにより、自動生成されたアクションメソッドのコードで Products テーブルの当該レコードの SupplierID 列は NULL になります。

Tags: , ,

MVC

DropDownList への SelectList の渡し方 (CORE)

by WebSurfer 22. January 2021 18:10

.NET Framework 版 MVC の話は先の記事「DropDownList への SelectList の渡し方」に書きましたが、その Core 版の話を聞きます。

DropDownList で複数項目を選択

Core 版 MVC アプリでは従前から提供されている Html ヘルパーの DropDownList, DropDownListFor メソッドに加えて、タグヘルパーというものが使えますのでそれを使った場合の例も書いておきます。

タグヘルパーに関する詳細は以下の Microsoft のドキュメントが参考になると思いますので、そちらを見てください(手抜きでスミマセン)。

SelectList のコンストラクタの第 4 引数に設定した selectedValue の通りに html にレンダリングされた時の option 要素に selected 属性を設定するにはどうするかという点がポイントでしたが、Html ヘルパーの DropDownList, DropDownListFor メソッドについてはそのあたりは .NET Framework 版と同様でした。

例えば ViewBag.CategoryID で SelectList を DropDownList メソッドに渡す場合は、第 1 引数は "CategoryID" という文字列、第 2 引数は null にします。詳しい説明は先の記事「DropDownList への SelectList の渡し方」を見てください。

select タグヘルパーを使う場合ですが、SelectList を ViewBag (または ViewData) 経由で渡す場合でかつ Model を使用しない場合は asp-for は使えませんので、select タグヘルパーに id, name 属性を設定して asp-items 属性に ViewBag (または ViewData) を渡します。

さらに、上に紹介した記事「ASP.NET Core のフォームのタグ ヘルパー」の「選択タグ ヘルパー」のセクションに Model で渡すサンプルコードが書いてあって、そのようにしても option 要素に selected 属性の設定が可能です。

その記事によると "We don't recommend using ViewBag or ViewData with the Select Tag Helper. A view model is more robust at providing MVC metadata and generally less problematic." とのことで、可能ならば SelectList を渡すには Model を使った方が良さそうです。(Model を使えないケースも多々ありそうですが)

以下に、(1) 1 項目だけ選択できるドロップダウンリスト、(2) 複数項目が選択可能なドロップダウンリスト、(3) 初期表示の際複数項目を指定して選択状態にするドロップダウンリストのサンプルコードを書いておきます。

(1) 1 項目だけ選択可

先の記事「DropDownList への SelectList の渡し方」と同じ条件です。

Model

select タグヘルパーに Model 経由で SelectList と初期状態の選択項目を渡すために使います。上に紹介した 3 番目の記事によると "asp-for 属性に指定されているプロパティが IEnumerable の場合、選択タグ ヘルパーは multiple = "multiple" 属性を自動的に生成します" とのことですので注意してください。

public class CategoryViewModel
{
    public int Category { get; set; }

    public SelectList Categories { get; set; }
}

Controller / Action Method

public async Task<IActionResult> Edit()
{
    int selected = 3;
    var categoryList = await _context.Categories.ToListAsync();

    ViewBag.CategoryId = new SelectList(categoryList, 
                                        "CategoryId", 
                                        "CategoryName", 
                                        selected);

    var model = new CategoryViewModel
    {
        Category = selected,
        Categories = new SelectList(categoryList,
                                    "CategoryId",
                                    "CategoryName")
    };

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int categoryid, 
                          int categoryid2, 
                          int category)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Index");
    }

    return View();
}

View

@model MvcCore5App.Controllers.CategoryViewModel

@{
    ViewData["Title"] = "Edit";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Edit</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div class="form-group">
                <label class="control-label" for="CategoryId">
                    @@Html.DropDownList 利用
                </label>
                @Html.DropDownList("CategoryId", null,
                    htmlAttributes: new { @class = "form-control" })
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + ViewBag 利用
                </label>
                <select id="categoryid2" name="categoryid2"
                        asp-items="@ViewBag.CategoryId"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + Model 利用
                </label>
                <select asp-for="Category"
                        asp-items="Model.Categories"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create"
                       class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

(2) 複数項目の選択可

Model

上記 (1) の Model と同じ。

Controller / Action Method

[HttpPost] 側のアクションメソッドの引数が int[] 型になっている点に注目してください。int[] 型にすることにより、ユーザーが複数項目を選択した結果を受け取れます。

public async Task<IActionResult> Edit2()
{
    int selected = 3;
    var categoryList = await _context.Categories.ToListAsync();

    ViewBag.CategoryId = new SelectList(categoryList,
                                        "CategoryId",
                                        "CategoryName",
                                        selected);

    var model = new CategoryViewModel
    {
        Category = selected,
        Categories = new SelectList(categoryList,
                                    "CategoryId",
                                    "CategoryName")
    };

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit2(int[] categoryid, 
                           int[] categoryid2, 
                           int[] category)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Index");
    }

    return View();
}

View

ユーザーが複数項目を選択できるよう multiple = "multiple" を付与しているところに注目してください。上にも書きましたが、asp-for 属性に指定されるプロパティが IEnumerable の場合、選択タグ ヘルパーは multiple = "multiple" 属性を自動的に生成しますとのことです。

@model MvcCore5App.Controllers.CategoryViewModel

@{
    ViewData["Title"] = "Edit2";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Edit2</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit2">
            <div class="form-group">
                <label class="control-label" for="CategoryId">
                    @@Html.DropDownList 利用
                </label>
                @Html.DropDownList("CategoryId", null,
                    htmlAttributes: new
                    {
                        @class = "form-control",
                        multiple = "multiple"
                    })
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + ViewBag 利用
                </label>
                <select id="categoryid2" name="categoryid2"
                        asp-items="@ViewBag.CategoryId"
                        multiple="multiple"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + Model 利用
                </label>
                <select asp-for="Category"
                        asp-items="Model.Categories"
                        multiple="multiple"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create"
                       class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

(3) 初期表示の際複数項目を選択状態にする

上の画像を表示したのがこれです。

Model

SelectList に代えて MultiSelectList を使っているところに注目してください。

public class MultiCategoryViewModel
{
    public int[] Category { get; set; }

    public MultiSelectList Categories { get; set; }
}

Controller / Action Method

初期表示の時点で複数項目が選択されるようにするため、int selected を int[] selected に代えて、それを MultiSelectList の第 4 引数に渡している点に注目してください。View に渡す Model も CategoryViewModel から MultiCategoryViewModel に変えています。

public async Task<IActionResult> Edit3()
{
    int[] selected = new int[] { 1, 3, 5 };
    var categoryList = await _context.Categories.ToListAsync();

    ViewBag.CategoryId = new MultiSelectList(categoryList,
                                             "CategoryId",
                                             "CategoryName",
                                             selected);

    var model = new MultiCategoryViewModel
    {
        Category = selected,
        Categories = new MultiSelectList(categoryList,
                                         "CategoryId",
                                         "CategoryName")
    };

    return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit3(int[] categoryid, 
                           int[] categoryid2, 
                           int[] category)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Index");
    }

    return View();
}

View

Model を CategoryViewModel から MultiCategoryViewModel に変えたこと、下のタグヘルパー <select> + Model 利用のコードには multiple = "multiple" を設定してないこと以外は上記 (2) の View と同じです。上の Model のコードの通り Category プロパティを int[] 型にしてそれを asp-for 属性に指定していますので、html には multiple = "multiple" 属性が自動的に生成されます。

@model MvcCore5App.Controllers.MultiCategoryViewModel

@{
    ViewData["Title"] = "Edit3";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Edit3</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit3">
            <div class="form-group">
                <label class="control-label" for="CategoryId">
                    @@Html.DropDownList 利用
                </label>
                @Html.DropDownList("CategoryId", null,
                    htmlAttributes: new
                    {
                        @class = "form-control",
                        multiple = "multiple"
                    })
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + ViewBag 利用
                </label>
                <select id="categoryid2" name="categoryid2"
                        asp-items="@ViewBag.CategoryId"
                        multiple="multiple"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <label class="control-label" for="CategoryId2">
                    タグヘルパー &lt;select&gt; + Model 利用
                </label>
                <select asp-for="Category"
                        asp-items="Model.Categories"
                        class="form-control">
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create"
                       class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

Tags: , , ,

CORE

連動ドロップダウンリスト (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

About this blog

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

Calendar

<<  October 2021  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar