web-dev-qa-db-ja.com

MVC Razorビューのネストされたforeachのモデル

一般的なシナリオを想像してみてください。これは、私が遭遇しているもののより単純なバージョンです。私は実際に私にさらにネストのいくつかの層を持っています....

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

テーマがリストを含むカテゴリがリストを含む製品がリストを含む

私のコントローラーは、そのテーマのすべてのカテゴリー、このカテゴリー内の製品、およびその注文を含む、完全に入力されたテーマを提供します。

注文コレクションには、編集可能にする必要のあるQuantityというプロパティがあります。

@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)
      }
   }
}

代わりにlambdaを使用すると、foreachループ内の「テーマ」ではなく、最上位のModelオブジェクトへの参照のみを取得するように見えます。

私がそこでやろうとしていることは可能ですか、可能性を過大評価または誤解していますか?

上記により、TextboxFor、EditorForなどでエラーが発生します

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

ありがとう。

94
David C

簡単な答えは、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)
      }
   }
}

しかし、これで問題が解決しますwhy

この問題を解決する前に、少なくとも大まかに理解していることが3つあります。私は cargo-culted これを長い間フレームワークで作業を始めたときに認めなければなりません。そして、何が起こっているのかを本当に理解するのにかなり時間がかかりました。

これらの3つのことは次のとおりです。

  • LabelForおよびその他の...ForヘルパーはMVCでどのように機能しますか?
  • 式ツリーとは何ですか?
  • モデルバインダーはどのように機能しますか?

これら3つの概念はすべてリンクして答えを導きます。

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

したがって、LabelForおよびTextBoxForなどにHtmlHelper<T>拡張機能を使用しました。これらを呼び出すと、ラムダとそのmagicallyは、いくつかのhtmlを生成します。しかし、どのように?

したがって、最初に気付くのは、これらのヘルパーの署名です。 TextBoxForの最も単純なオーバーロードを見てみましょう

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

第一に、これはタイプ<TModel>の強く型付けされたHtmlHelperの拡張メソッドです。そのため、舞台裏で何が起こるかを簡単に述べると、カミソリがこのビューをレンダリングすると、クラスが生成されます。このクラスの内部にはHtmlHelper<TModel>のインスタンスがあります(プロパティHtmlとして、@Html...を使用できる理由です)。ここで、TModel@modelステートメント。したがって、あなたの場合、このビューを見ているとき、TModelは常にViewModels.MyViewModels.Themeタイプになります。

さて、次の議論は少しトリッキーです。それでは、呼び出しを見てみましょう

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

少しのラムダがあるように見えます。署名を推測する場合、この引数の型は単にFunc<TModel, TProperty>であると考えるかもしれません。ここで、TModelはビューモデルの型です。 TPropertyはプロパティのタイプとして推測されます。

しかし、actual型の引数のExpression<Func<TModel, TProperty>>を見ると、それはまったく正しくありません。

したがって、通常ラムダを生成すると、コンパイラはラムダを取得し、他の関数と同様にMSILにコンパイルします(これは、コード参照であるため、デリゲート、メソッドグループ、ラムダを多かれ少なかれ交換可能に使用できる理由です) )

ただし、コンパイラは、型がExpression<>であることを認識すると、ラムダをすぐにMSILにコンパイルせず、代わりに式ツリーを生成します!

式ツリー とは何ですか?

それで、一体何が式ツリーです。まあ、それは複雑ではありませんが、公園を散歩するわけでもありません。 msを引用するには:

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

簡単に言えば、式ツリーは「アクション」のコレクションとしての関数の表現です。

model=>model.SomePropertyの場合、式ツリーには「「モデル」から「いくつかのプロパティ」を取得する」というノードがあります。

この式ツリーは、呼び出すことができる関数にコンパイルできますが、式ツリーである限り、単なるノードのコレクションです。

それで何が良いのでしょうか?

したがって、Func<>またはAction<>を取得すると、それらはほとんどアトミックです。本当にできるのは、Invoke()それら、別名彼らがすることになっている仕事をするように彼らに言うことです。

一方、Expression<Func<>>は、追加、操作、 visited 、または compiled で呼び出し可能なアクションのコレクションを表します。

それで、なぜあなたは私にすべてこれを言っているのですか?

Expression<>とは何かを理解したら、Html.TextBoxForに戻ることができます。テキストボックスをレンダリングするとき、それが与えているプロパティについていくつかのものaboutを生成する必要があります。検証用のプロパティのattributesなど。特にこの場合は、<input>nameを把握する必要があります。鬼ごっこ。

これは、式ツリーを「歩いて」名前を作成することで行います。したがって、model=>model.SomePropertyのような式の場合、要求しているプロパティを収集する式を調べて、<input name='SomeProperty'>を構築します。

model=>model.Foo.Bar.Baz.FooBarのようなより複雑な例では、<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />を生成します

理にかなっていますか? Func<>が行うのは単なる作業ではありませんが、ここではhow作業が重要です。

(LINQ to SQLのような他のフレームワークは、式ツリーをたどって異なる文法を構築することで同様のことを行います。この場合はSQLクエリです)

モデルバインダーはどのように機能しますか?

そのため、それを取得したら、モデルバインダーについて簡単に説明する必要があります。フォームが投稿されると、それは単にフラットDictionary<string, string>のようになり、ネストされたビューモデルが持っていた階層構造を失いました。このキーと値のペアのコンボを取得し、いくつかのプロパティを持つオブジェクトを復元するのは、モデルバインダーの仕事です。これはどのように行われますか? 「キー」または投稿された入力の名前を使用して、推測しました。

フォーム投稿が次のように見える場合

Foo.Bar.Baz.FooBar = Hello

そして、あなたはSomeViewModelと呼ばれるモデルに投稿し、それはヘルパーが最初にしたことの逆を行います。 「Foo」というプロパティを探します。次に、「Foo」から「Bar」というプロパティを探し、次に「Baz」を探します...など...

最後に、値を「FooBar」のタイプに解析して「FooBar」に割り当てようとします。

PHEW !!!

そして出来上がり、あなたはあなたのモデルを持っています。モデルバインダーが作成したばかりのインスタンスは、要求されたアクションに渡されます。


したがって、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<>で本当に面白いことを始めることができます。私はそのいずれにも入りません。しかし、うまくいけば、これにより、舞台裏で何が起こっているのか、そして物事がそのように振る舞う理由をよりよく理解できるようになります。

303
J. Holmes

EditorTemplatesを使用してそれを行うことができます。コントローラーのビューフォルダーに「EditorTemplates」という名前のディレクトリを作成し、ネストされたエンティティ(エンティティクラス名)ごとに個別のビューを配置する必要があります。

メインビュー:

@model ViewModels.MyViewModels.Theme

@Html.LabelFor(Model.Theme.name)
@Html.EditorFor(Model.Theme.Categories)

カテゴリビュー(/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Products)

製品ビュー(/MyController/EditorTemplates/Product.cshtml):

@model ViewModels.MyViewModels.Product

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Orders)

等々

このようにして、Html.EditorForヘルパーは要素の名前を順序付けられた方法で生成します。したがって、投稿されたテーマエンティティを全体として取得するために、それ以上の問題は発生しません。

18
Alireza Sabouri

CategoryパーシャルとProductパーシャルを追加することができます。それぞれがメインモデルの小さな部分を占めます。つまり、CategoryのモデルタイプはIEnumerableで、Model.Themeを渡します。製品のパーシャルは、Model.Productsを(カテゴリパーシャル内から)渡すIEnumerableである可能性があります。

それが正しい方法かどうかはわかりませんが、知りたいと思っています。

編集

この回答を投稿して以来、私はEditorTemplatesを使用し、繰り返し入力グループまたはアイテムを処理する最も簡単な方法を見つけました。すべての検証メッセージの問題とフォーム送信/モデルバインディングの問題を自動的に処理します。

バインドされたモデルのビュー内でforeachループを使用している場合...モデルはリスト形式であることが想定されています。

すなわち

@model IEnumerable<ViewModels.MyViewModels>


        @{
            if (Model.Count() > 0)
            {            

                @Html.DisplayFor(modelItem => Model.Theme.FirstOrDefault().name)
                @foreach (var theme in Model.Theme)
                {
                   @Html.DisplayFor(modelItem => theme.name)
                   @foreach(var product in theme.Products)
                   {
                      @Html.DisplayFor(modelItem => product.name)
                      @foreach(var order in product.Orders)
                      {
                          @Html.TextBoxFor(modelItem => order.Quantity)
                         @Html.TextAreaFor(modelItem => order.Note)
                          @Html.EditorFor(modelItem => order.DateRequestedDeliveryFor)
                      }
                  }
                }
            }else{
                   <span>No Theam avaiable</span>
            }
        }
2
Pranav Labhe

エラーから明らかです。

「For」が付加されたHtmlHelpersは、パラメーターとしてラムダ式を想定しています。

値を直接渡す場合は、通常の値を使用することをお勧めします。

例えば.

TextboxFor(....)の代わりにTextbox()を使用します

textboxForの構文はHtml.TextBoxFor(m => m.Property)のようになります

シナリオでは、使用するインデックスを提供するため、基本的なforループを使用できます。

@for(int i=0;i<Model.Theme.Count;i++)
 {
   @Html.LabelFor(m=>m.Theme[i].name)
   @for(int j=0;j<Model.Theme[i].Products.Count;j++) )
     {
      @Html.LabelFor(m=>m.Theme[i].Products[j].name)
      @for(int k=0;k<Model.Theme[i].Products[j].Orders.Count;k++)
          {
           @Html.TextBoxFor(m=>Model.Theme[i].Products[j].Orders[k].Quantity)
           @Html.TextAreaFor(m=>Model.Theme[i].Products[j].Orders[k].Note)
           @Html.EditorFor(m=>Model.Theme[i].Products[j].Orders[k].DateRequestedDeliveryFor)
      }
   }
}
0
Manas