1. ホーム
  2. c#

[解決済み] MVC Razor ビューのネストされたforeachのモデル

2023-02-10 05:37:45

質問

よくあるシナリオを想像してください。これは私が遭遇していることの単純化したバージョンです。 私は実際に、私の上にさらにネストした 2 つのレイヤーを持っています......。

しかし、これはシナリオです。

テーマにはリストが含まれる カテゴリがリストを含む 製品に含まれるリスト

私のコントローラは、そのテーマのためのすべてのカテゴリ、このカテゴリ内の製品とそれらの順序を持つ、完全に入力されたテーマを提供します。

受注コレクションには、編集可能である必要がある数量と呼ばれるプロパティがあります(他の多くのプロパティと一緒に)。

@model ViewModels.MyViewModels.Theme

@Html.LabelFor(Model.Theme.name)
@foreach (var category in Model.Theme)
{
   @Html.LabelFor(category.name)
   @foreach(var product in theme.Products)
   {
      @Html.LabelFor(product.name)
      @foreach(var order in product.Orders)
      {
          @Html.TextBoxFor(order.Quantity)
          @Html.TextAreaFor(order.Note)
          @Html.EditorFor(order.DateRequestedDeliveryFor)
      }
   }
}

私は代わりにラムダを使用する場合、私は唯一のforeachループ内のものではないトップモデルオブジェクト、"テーマ"への参照を取得するように見える。

私がそこでやろうとしていることは可能なのでしょうか、それとも可能であることを過大評価または誤解しているのでしょうか?

上記で、TextboxFor、EditorForなどでエラーが出ました。

CS0411です。メソッドの型引数 'System.Web.Mvc.Html.InputExtensions.TextBoxFor(System.Web.Mvc.HtmlHelper.TextBoxFor(System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' の使用法から推測できません。 は、使用方法から推測することができません。型引数を明示的に指定してみてください。 を明示的に指定してみてください。

ありがとうございます。

どのように解決するのですか?

手っ取り早い答えは for() ループの代わりに foreach() ループに置き換えます。こんな感じ。

@for(var themeIndex = 0; themeIndex < Model.Theme.Count(); themeIndex++)
{
   @Html.LabelFor(model => model.Theme[themeIndex])

   @for(var productIndex=0; productIndex < Model.Theme[themeIndex].Products.Count(); productIndex++)
   {
      @Html.LabelFor(model=>model.Theme[themeIndex].Products[productIndex].name)
      @for(var orderIndex=0; orderIndex < Model.Theme[themeIndex].Products[productIndex].Orders; orderIndex++)
      {
          @Html.TextBoxFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Quantity)
          @Html.TextAreaFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Note)
          @Html.EditorFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].DateRequestedDeliveryFor)
      }
   }
}

しかし、これは なぜ を説明し、問題を解決しています。

この問題を解決する前に、最低限ざっくりと理解しておくべきことが3つあります。私は を認めなければなりません。 カーゴカルト この を長い間信じていました。そして、何が起こっているのかを本当に理解するまでに、かなり時間がかかりました。 を本当に理解するのに相当な時間がかかりました。

その3つとは

  • はどのように LabelFor といった ...For ヘルパーはMVCで機能するのでしょうか?
  • エクスプレッションツリーとは何ですか?
  • モデルバインダーはどのように動作しますか?

これら3つのコンセプトがリンクして、答えを導き出すのです。

はどのように LabelFor などの ...For ヘルパーはMVCで機能しますか?

というわけで、あなたは HtmlHelper<T> の拡張を LabelForTextBoxFor などがあり、そして を呼び出すとき、ラムダを渡すと、それが 魔法のように を生成します。 を生成します。しかし、どのように?

まず最初に注目すべきは、これらのヘルパーのシグネチャです。最も単純な TextBoxFor

public static MvcHtmlString TextBoxFor<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression
) 

まず、これは強く型付けされた拡張メソッドである HtmlHelper の、型の <TModel> . というわけで、簡単に言うと razor がこのビューをレンダリングすると、クラスが生成されます。 このクラスの内部には HtmlHelper<TModel> のインスタンスです (プロパティとして Html を使用することができる理由です。 @Html... ), ここで TModel で定義された型です。 @model ステートメントで定義された型です。つまり、あなたの場合、このビューを見ているときに TModel は常にタイプ ViewModels.MyViewModels.Theme .

さて、次の引数はちょっとやっかいです。そこで、呼び出しを見てみましょう。

@Html.TextBoxFor(model=>model.SomeProperty);

小さなラムダがあるように見えます。 この引数の型は単純に Func<TModel, TProperty> で、ここで TModel はビューモデルの型であり TProperty はプロパティの型として推論されます。

しかし、それは全く正しいことではありません。 実際の の型を見ると、その Expression<Func<TModel, TProperty>> .

ラムダを生成すると、コンパイラは他の関数と同じようにラムダをMSILにコンパイルします。 関数と同じように(デリゲート、メソッドグループ、ラムダが多かれ少なかれ同じように使えるのは、これらが単なるコード参照であるためです)。 コード参照に過ぎないからです)。

しかし、コンパイラがその型が Expression<> であることがわかると、コンパイラはラムダをすぐにMSILにコンパイルすることはせず、代わりに 表現ツリー!

とは何ですか? 表現ツリー ?

さて、式木とはいったい何なのでしょう。複雑なものではありませんが、簡単なものでもありません。というわけで。

| 式木はツリー状のデータ構造でコードを表し、各ノードは式、たとえばメソッドの呼び出しや、x < y のようなバイナリ演算です。

簡単に言うと、式木は関数をアクションのコレクションとして表現したものです。

の場合 model=>model.SomeProperty の場合、式ツリーには次のようなノードがあります: "「モデル」から「何らかのプロパティ」を取得します。

この式木は コンパイル を呼び出すことができますが、式木である限り、それは単なるノードの集まりです。

では、それは何に使えるのでしょうか?

そこで Func<> または Action<> というように、一旦手に入れたら、それはほとんど原子のようなものです。あなたが本当にできることは Invoke() であり、また、彼らに するように指示することです。

Expression<Func<>> はアクションの集合体を表し、追加や操作が可能です。 訪問 コンパイル済み と呼び出されます。

では、なぜこのような話をするのでしょうか?

ということを理解した上で Expression<> が何であるかを理解した上で、私たちは Html.TextBoxFor . テキストボックスをレンダリングする際には、以下のものが必要です。 を生成する必要があります。 について を生成する必要があります。次のようなものです。 attributes のようなものは、検証のためにプロパティの上に置かれ、具体的には この場合、それは何をすべきかを理解する必要があります。 名前 <input> タグを使用します。

これは、式ツリーを歩いて、名前を作ることによって行われます。つまり、次のような式に対して model=>model.SomeProperty のような式では、式の中を歩いて を歩き回り、要求されたプロパティを収集し、次のように構築します。 <input name='SomeProperty'> .

より複雑な例として、例えば model=>model.Foo.Bar.Baz.FooBar のような、より複雑な例では <input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

意味がわかりますか?という作業だけでなく Func<> が行う作業ではなく どのように が重要です。

(LINQ to SQLのような他のフレームワークは、式木を歩いて別の文法、この場合はSQLクエリを構築することで同様のことを行うことに注意してください)

モデルバインダーはどのように動作するのでしょうか?

では、それを理解した上で、モデルバインダーについて簡単に説明しなければなりません。フォームが投稿されるときは、単純にフラットな Dictionary<string, string> のようになり、ネストされたビューモデルが持っていたかもしれない階層的な構造は失われてしまいます。そこで モデルバインダーの仕事は、このキーと値のペアの組み合わせを受け取り、いくつかのプロパティを持つオブジェクトを再生成しようとすることです。それはどのように行うのでしょうか? を行うのでしょうか?投稿された入力の "キー" または名前を使用することによって、あなたはそれを推測しました。

というわけで、フォームの投稿が以下のような場合

Foo.Bar.Baz.FooBar = Hello

というモデルに対して投稿しています。 SomeViewModel というモデルに投稿している場合、ヘルパーが最初に行ったことの逆を行います。それは というプロパティを探します。次に、"Foo" から "Bar" というプロパティを探し、次に "Baz" を探し... といった具合になります。

最後に、値を "FooBar" の型にパースして、それを "FooBar" に割り当てようとします。

PHEW!!!!

ほら、モデルができました。モデルバインダーが構築したインスタンスは、リクエストされたActionに渡されます。


ということは、あなたのソリューションは Html.[Type]For() ヘルパーには式が必要だからです。そして、あなたは彼らに値を与えているだけなのです。それは その値のコンテキストが何なのか、そしてそれをどうすればいいのかがわからないのです。

レンダリングにパーシャルを使用することを提案した人がいます。理論的にはうまくいきますが、おそらくあなたが期待するような方法ではありません。パーシャルをレンダリングするとき、あなたは TModel のタイプを変更することになります。なぜなら、異なるビューコンテキストにいるからです。これは、より短い式でプロパティを記述できることを意味します。 これは、より短い式であなたのプロパティを記述できることを意味します。これは、ヘルパーが式の名前を生成する際に、その名前が浅くなることも意味します。これは は与えられた式に基づいてのみ生成されます (コンテキスト全体ではありません)。

例えば、(先ほどの例で) "Baz" をレンダリングするパーシャルがあったとします。そのパーシャルの内部では、次のように言うことができます。

@Html.TextBoxFor(model=>model.FooBar)

よりも

@Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar)

つまり、このような入力タグが生成されることになります。

<input name="FooBar" />

このフォームを、深くネストされた大きな ViewModel を想定しているアクションに投稿する場合、このアクションは と呼ばれる FooBar から TModel . というのは、よく言えば「ない」、悪く言えば「まったく別のもの」なのです。を受け入れる特定のアクションに投稿していた場合、そのアクションは Baz を受け入れる特定のアクションに投稿しているのであれば、これはとてもうまくいくでしょう! 実際、パーシャルはビューコンテキストを変更する良い方法です。たとえば、異なるアクションに投稿する複数のフォームを持つページがあった場合、それぞれのフォームに対してパーシャルをレンダリングするのは素晴らしいアイデアです。


さて、これらをすべて理解した上で、本当に面白いことを始めるには Expression<> で本当に面白いことができるようになります。 をプログラム的に拡張したり、他のすてきなことをすることができます。私はそのうちのどれにも触れません。しかし、願わくば、これで 舞台裏で何が起こっているのか、なぜそのように動作するのかについて、よりよく理解していただけると幸いです。