WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

DropDownList への SelectList の渡し方

by WebSurfer 25. February 2019 14:37

ASP.NET MVC5 の Html ヘルパーの DropDownList および DropDownListFor に表示するデータを、アクションメソッドからビューにどのように渡すかということについて書きます。

DropDownList

上の画像は、先の記事「スキャフォールディング機能」で書いた通りにスキャフォールディング機能を使って自動生成させたコードで、その中の Edit 画面を表示したものです。

SupplierID と CategoryID が Html ヘルパーの DropDownList を使ってドロップダウン形式で表示されるようになっています。上の画像は CategoryID を展開したところで、CategoryName の一覧が表示されています。

スキャフォールディング機能で自動生成されたコードが基本になるでしょうから、それがどうなっているかを書きます。

まずアクションメソッド Edit で SelectList オブジェクトを生成し ViewBag に設定しています。以下のコードの通りです。

public ActionResult Edit(int? id)
{
  NORTHWINDEntities db = new NORTHWINDEntities();
  Products products = db.Products.Find(id);

  ViewBag.CategoryID = 
    new SelectList(db.Categories, "CategoryID", 
                   "CategoryName", products.CategoryID);

  ViewBag.SupplierID = 
    new SelectList(db.Suppliers, "SupplierID", 
                   "CompanyName", products.SupplierID);

  return View(products);
}

コンストラクタに SelectList(IEnumerable, String, String, Object) を使って、第 4 引数に selectedValue を設定しているところに注目してください。これによりビューの DropDownList が html に変換された際、select 要素内の当該 option 要素に selected 属性が付与されます。

ビューの DropDownList のコードは以下のようになります。第 1 引数がアクションメソッドで設定した ViewBag のキー名、第 2 引数が null になっているところに注目してください。

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

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

DropDownList の第 2 引数が null となっていますが、第 2 引数の設定に関わらず ViewData / ViewBag から型が IEnumerable<SelectListItem> でキー名が第 1 引数と同じものを探してきます。

(例えば、上記のアクションメソッドで ViewBag.SupplierID の設定を削除すると、ビューの DropDownList のコードで「キー 'SupplierID' を持つ ViewData 項目の型は 'System.Int32' ですが、'IEnumerable<SelectListItem>' でなければなりません」というエラーになります)

DropDownList の第 1 引数を元に ViewBag で渡されたデータ(アクションメソッドで設定された SelectList オブジェクト)を取得するので、上の画像の通りドロップダウン形式で表示できるようになります。さらに、SelectList コンストラクタの第 4 引数に設定した selectedValue によって当該 option 要素に selected 属性が設定された結果が表示されます。

なお、DropDownList の第 2 引数を (SelectList)ViewBag.Supplier としたりすると、SelectList のコンストラクタで第 4 引数に設定した selectedValue が無視されるので注意してください。理由は不明です。

ViewData / ViewBag に DropDownList の第 1 引数と同じキー名がない場合は、DropDownList の第 2 引数の設定が有効になるようです。例えば、アクションメソッドで ViewBag.SupplierID を ViewBag.Supplier に変更した場合、DropDownList("SupplierID", (SelectList)ViewBag.Supplier, ...) として selected の設定を含めて期待した結果が得られます。

ViewData / ViewBag を探す順序ですが、検証してみると、まず最初に ViewData を、それになければ ViewBag を探すという結果になりました。ViewData / ViewBag に同じキー名があると、ViewData のデータが使われます。その際、もし ViewData のデータが不正ですと(IEnumerable<SelectListItem> 型でないと)エラーになります。

以上は DropDownListFor を使っても同様です。第 1 引数は model => model.SupplierID のようになりますが、プロパティ名 SupplierID から ViewData / ViewBag を探して設定してくれます。

Tags: , ,

MVC

MVC5 のエラーメッセージ表示

by WebSurfer 15. February 2019 13:35

Visual Studio Community 2015 で作成した MVC5 アプリでは、スキャフォールディング機能を使って自動生成した View で検証エラーとなった場合のエラーメッセージは、デフォルトでは以下のように表示されます。

検証エラーメッセージ

(注:画像の「ValidationSummary に表示するために追加。」はアクションメソッドで追加した model-level エラーです。また価格のテキストボックス下の「入力形式が正しくありません。」は、デフォルトでは「The value 'zzz' is not valid for 価格.」となるのを書き換えています。そのあたりのことは別の記事に書く予定)

それを、先の記事「DataType 属性による検証」の画像(Visual Studio 2010 で作った MVC4 アプリ)のように表示するにはどうしたらよいかを書きます。

上の画像のように表示される理由は、スキャフォールディングで自動生成される View のコードで、ValidationSummary メソッドの第 1、第 2 引数と、ValidationMessageFor メソッドの第 2 引数が以下のように設定されているためです。

ValidationSummary(true, "", 
    new { @class = "text-danger" })

ValidationMessageFor(model => model.ID, "", 
    new { @class = "text-danger" })

それらを以下のように変更すれば、設定は(あくまで「設定」だけです)先の記事「DataType 属性による検証」の MVC4 アプリのものと同じになりますが、表示はそのようにはなりません。

ValidationSummary(false, "以下のエラーを修正してください。", 
    new { @class = "text-danger" })

ValidationMessageFor(model => model.ID, "*", 
    new { @class = "text-danger" })

以下の画像のように、初期画面や検証エラーのない時も ValidationSummary に「以下のエラーを修正してください。」と、各テキストボックスに「*」が表示されてしまいます。

検証エラーメッセージ

また、あるテキストボックスへのユーザー入力の検証エラーが発生した場合も当該テキストボックスの枠の色は赤に変わりません。

その理由は、Visual Studio 2010 で作った MVC4 アプリの site.css にある以下のクラス定義が、Visual Studio 2015 で作った NVC5 アプリの site.css には無いからです。

/* styles for validation helpers */
.field-validation-error {
    color: #e80c4d;
    font-weight: bold;
}

.field-validation-valid {
    display: none;
}

input.input-validation-error {
    border: 1px solid #e80c4d;
}

input[type="checkbox"].input-validation-error {
    border: 0 none;
}

.validation-summary-errors {
    color: #e80c4d;
    font-weight: bold;
    font-size: 1.1em;
}

.validation-summary-valid {
    display: none;
}

MVC4 も MVC5 も��検証結果に応じて、表示される html 要素に付与される class 属性の設定が変わります。

ValidationSummary は validation-summary-valid と validation-summary-errors、ValidationMessageFor は field-validation-valid と field-validation-error、TextBoxFor / EditorFor は valid と input-validation-error というように。

なので、MVC5 の css に上記クラス定義を追加すれば表示は MVC4 と同じになります。

Tags: ,

Validation

Linq to Entities / Objects

by WebSurfer 13. February 2019 15:17

Linq to Entities は、Linq to Objects とは違って、そのクエリ式が SQL Server などの DB で使われる SQL に変換できる必要があるという話を書きます。CodeZine の記事「LINQにも色々 ~SQLに変換されるモノと変換されないモノ」を見てください。図だけ借用して以下にも貼っておきます。

Linq to Objects / Enitities

その図を見れば一目瞭然だと思いますし、詳しいことは CodeZine の記事を読めば分かるのですが、それで終わってしまってはブログの記事としては面白くないので、どういう事例があったか(要するに失敗談)を書いておきます。(笑)

まず以下の例。これを実行すると ToList() のところで "System.NotSupportedException: LINQ to Entities does not recognize the method 'System.DateTime Parse(System.String)' method, and this method cannot be translated into a store expression." というエラーが出ます。

public class Filter
{
    public int Id { get; set; }
    public DateTime? Date { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // ADO.NET Entity Data Model
        TestDatabaseEntities ctx = new TestDatabaseEntities();
            
        var list = (from c in ctx.TestTable
                    select new Filter
                    {
                        Id = c.ID,
                        Date = DateTime.Parse(c.Date)
                    }).ToList();
    }
}

上の図の右側に「SQ に変換してデータベース上で実行」とありますが、DateTime.Parse メソッドが SQL に変換できないということでエラーになったということです。

上のコードで Filter クラスを初期化して Id, Date に代入するところからは C# に戻ってきて行うのかと期待してましたが、そうではなくて ToList の前まで全部 SQL に変換して SQL Server で実行しようとするようです。

ちなみに、Filter クラスの Date プロパティを string 型に変更して、DateTime.Parse なしで直接 c.Data を代入するようにすれば SQL に変換出来るようで、エラーなく期待した結果が得られます。

他の解決策としては、LINQ to Entities クエリで使用する SQL Server 関数を公開する SqlFunctions クラスのメソッドに該当するものがあれば、それの利用を検討するのがよさそうです。具体例は、Microsoft のドキュメント「方法: データベース関数を呼び出す」のサンプルコードを見てください。

上のコードの DateTime.Parse は該当する SqlFunctions クラスのメソッドは見つかりませんでしたが、「方法: カスタム データベース関数を呼び出す」にある方法も考えれば対応可能かもしれません。(未検証・未確認です)

なお、自分の環境 (Windows 10, VS2015, .NET 4.6.1, EF 6.2) では、上記の記事のリンク先の名前空間: System.Data.Objects.SqlClient の SqlFunctions クラスのメソッドでは NotSupportedException となり、名前空間: System.Data.Entity.SqlServer の SqlFunctions Class のメソッドを使う必要がありました。他の環境でどうかは分かりませんが注意してください。

もう一つは以下の例。これはコードのコメントにも書いてある通り delivery も test も Linq to Entities のクエリ式で、両方を合体して SQL に変換でき、foreach で DB に SQL を投げることができるので問題なく期待した結果が得られます。(Northwind の Order Details, Products テーブルから ADO.NET Entity Data Model を作成して使っています)

// これは Linq to Entities
var delivery = 
    from d in context.Order_Details
    group d by d.ProductID into g
    orderby g.Key
    select new
    {
       ItemCode = g.Key,
        Count = g.Sum(x => x.Quantity),
        SumAmount = g.Sum(x => x.UnitPrice * x.Quantity)
    };

// これも Linq to Entities
var test = from p in context.Products
           join d in delivery
           on p.ProductID equals d.ItemCode into dGroup
           from item in dGroup.DefaultIfEmpty()
           select new
           {
               ItemCode = p.ProductID,
               Name = p.ProductName,
               Count = item.Count,
               SumAmount = item.SumAmount
           };

// delivery を含めた test のコード全体を Linq to Entities と
// して SQL に変換することができ、foreach で DB に SQL を投げ
// ることができるので問題ない。
foreach (var x in test)
{
    Console.WriteLine(
      $"Name: {x.Name}, Count: {x.Count}, Sum: {x.SumAmount}");
}

上のような複雑なクエリ式が SQL に変換できるというのが驚きですが、それはちょっと置いといて、失敗事例はどういうことだったのかを書きます。

それは、DataTable から Linq to Objects のクエリ式を使って匿名型のオブジェクトのコレクションを取得し、それを Linq to Entities のクエリ式に組み合わせたことです。

具体的には、上のコードの delivery を以下のように DataTable(コードの table がそれ)から取得するようにしました。

// これは Linq to Object
var delivery = 
    from d in table.AsEnumerable()
    group d by d.Field<int>("ProductID") into g
    orderby g.Key
    select new
    {
        ItemCode = g.Key,
        Count = g.Sum(x => x.Field<Int16>("Quantity")),
        SumAmount = g.Sum(x => x.Field<decimal>("UnitPrice") * 
                               x.Field<Int16>("Quantity"))
    };

そうすると foreach のところで " System.NotSupportedException: Unable to create a constant value of type 'Anonymous type'. Only primitive types or enumeration types are supported in this context." というエラーになります。

エラーメッセージが前者の例とは違っていてため、最初、原因が分からなかったのですが、匿名型のオブジェクトのコレクションを Linq to Entities のクエリに組み込むと SQL に変換できないということが問題のようです。

なお、匿名型でなく、別にクラスとプロパティを定義し、それを初期化して各プロパティに代入するようにしても、上のエラーメッセージの 'Anonymous type' が定義したクラス名に変わるだけで同じエラーになります。

解決策は test のクエリ式も Linq to Objects にすることで、具体的には context.Products を context.Products.ToList() にすれば期待した結果が得られます。

Tags:

ADO.NET

About this blog

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

Calendar

<<  November 2019  >>
MoTuWeThFrSaSu
28293031123
45678910
11121314151617
18192021222324
2526272829301
2345678

View posts in large calendar