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


94

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

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

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

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

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

@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を使用すると、最上位のModelオブジェクトへの参照のみを取得しているように見えます。「テーマ」は、foreachループ内のものではありません。

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

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

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

ありがとう。


1
あなたは@すべてforeachの前に持ってはいけませんか?ラムダHtml.EditorForHtml.EditorFor(m => m.Note)たとえば)と残りのメソッドも含めるべきではありませんか?誤解しているかもしれませんが、実際のコードを貼り付けてもらえますか?私はMVCにかなり慣れていませんが、部分的なビューやエディター(それが名前なら?)を使えば、かなり簡単に解決できます。
Kobi

category.name私はそれがaでstringあり...For、最初のパラメータとして文字列をサポートしていないと確信しています
balexandre

はい、@を逃しました。追加されました。ありがとう。。私は(M => M @ Html.TextBoxForを入力し始める場合は、ラムダのためとして、私は唯一のトップモデルオブジェクトではなく、foreachループ内のものへの参照を取得するように見える。
デビッド・C

@DavidC-答えるのに十分なMVC 3をまだ知りません-しかし、あなたの問題だと思います:)
Kobi 2012年

2
私は電車に乗っていますが、仕事に出るまでに答えられない場合は、回答を投稿します。簡単な答えは、for()ではなく通常のものを使用することforeachです。理由を説明しますが、それは長い間、私の地獄を混乱させていました。
J.ホームズ

回答:


304

簡単な答えは、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でどのように機能しますか?
  • 式ツリーとは何ですか?
  • Model Binderはどのように機能しますか?

これらの3つの概念はすべて、答えを得るために互いにリンクしています。

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

だから、あなたが使ってきたHtmlHelper<T>ための拡張機能LabelForTextBoxFor、他の、そしてあなたはおそらく、あなたがそれらを呼び出すとき、あなたは彼らにラムダを渡すことに気づき、それは魔法の一部のHTMLを生成します。しかし、どうやって?

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

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

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

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

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

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

しかし、引数itの実際のタイプを見ると、それは正しくありませんExpression<Func<TModel, TProperty>>

したがって、通常はラムダを生成すると、コンパイラはラムダを取得し、それを他の関数と同様にMSILにコンパイルします(デリゲート、メソッドグループ、およびラムダは、単にコード参照であるため、ほぼ同じ意味で使用できます) 。)

ただし、コンパイラが型がであるExpression<>ことを検出すると、ラムダをすぐにMSILにコンパイルするのではなく、式ツリーを生成します。

式ツリーとは何ですか?

だから、一体何が表現ツリーなのか。さて、それは複雑ではありませんが、公園の散歩でもありません。msを引用するには:

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

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

の場合model=>model.SomeProperty、式ツリーにはノードがあり、「「モデル」から「一部のプロパティを取得」」と表示されます。

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

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

そうFunc<>Action<>、あなたがそれらを持っていたら、彼らはかなりアトミックです。あなたが本当にできるのはInvoke()彼らだけです、別名彼らに彼らがすることになっている仕事をするように言います。

Expression<Func<>>一方、はアクションのコレクションを表し、追加、操作、訪問、またはコンパイルして呼び出すことができます。

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

つまり、an 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クエリです)

Model Binderはどのように機能しますか?

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

フォームの投稿が次のようになっている場合

Foo.Bar.Baz.FooBar = Hello

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

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

ふ!!!!

そして出来上がり、あなたのモデルがあります。作成したModel Binderのインスタンスは、要求されたアクションに渡されます。


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

今、一部の人々は、パーシャルを使用してレンダリングすることを提案しました。これで理論的にはこれでうまくいきますが、おそらくあなたが期待する方法ではありません。パーシャルをレンダリングするTModelと、別のビューコンテキストにいるため、のタイプが変更されます。これは、より短い式でプロパティを説明できることを意味します。また、ヘルパーが式の名前を生成するときに、浅くなります。コンテキスト全体ではなく、指定された式に基づいてのみ生成されます。

それで、(前の例から)「Baz」をレンダリングしたパーシャルがあるとしましょう。その部分の中では、あなたはただ言うことができます:

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

のではなく

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

つまり、次のような入力タグが生成されます。

<input name="FooBar" />

これは、深くネストされた大きなViewModelを必要とするアクションにこのフォームを投稿する場合、FooBaroff と呼ばれるプロパティをハイドレートしようとしTModelます。せいぜいそこにはなく、最悪の場合、まったく別のものです。Bazルートモデルではなくを受け入れる特定のアクションに投稿した場合、これはうまく機能します!実際、パーシャルはビューコンテキストを変更するための良い方法です。たとえば、複数のフォームを持つページがあり、すべてが異なるアクションに投稿する場合、それぞれにパーシャルをレンダリングすることは素晴らしいアイデアです。


これをすべて取得したらExpression<>、プログラムを使用して拡張し、他のきちんとしたことを行うことで、で本当に面白いことを始めることができます。私はそのいずれにも入りません。しかし、うまくいけば、これにより、舞台裏で何が起こっているのか、そしてなぜ物事が彼らのように行動しているのかについての理解が深まります。


4
素晴らしい返信。私は現在それを消化しようとしています。:)カーゴカルティングにも有罪です!その説明のように。
デビッドC

4
この詳細な回答をありがとう!
コビ

14
これには複数の賛成票が必要です。+3(説明ごとに1つ)、貨物カルト信者には+1。絶対に素晴らしい答え!
Kyeotic

3
これが私がSOを愛する理由です:短い回答+詳細な説明+素晴らしいリンク(貨物カルト)。ものの内部の仕組みについての知識が非常に重要であると思わない人には、カーゴカルトについての投稿を見せたいと思います!
user1068352

18

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ヘルパーは順序付けられた方法で要素の名前を生成するため、投稿されたThemeエンティティ全体を取得するためにこれ以上の問題は発生しません。


1
受け入れられた回答は非常に良いものですが(私もそれを支持しました)、この回答の方が保守しやすいオプションです。
アーロン

4

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

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

編集する

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


それは私に起こりました、私がそれを読んで更新するときにそれがどのようにそれを処理するのか分かりませんでした。
David C

1
近いですが、これはユニットとして投稿されるフォームであるため、正しく機能しません。パーシャルの内側に入ると、ビューコンテキストが変更され、深くネストされた式がなくなります。Themeモデルにポストバックすると、適切に水和されません。
J.ホームズ

それも私の心配事です。私は通常、製品を表示するための読み取り専用のアプローチとして上記を行い、各製品に/ Product / Edit / 123アクションメソッドへのリンクを提供して、独自のフォームで各製品を編集します。私は、MVCの1つのページで多くのことを実行しようとすると、取り消される可能性があると思います。
Adrian Thompson Phillips

@AdrianThompsonPhillipsはい、私が持っている可能性は非常に高いです。私はフォームの背景から来たので、編集を行うためにページを離れる必要があるという考えに常に慣れることができません。:(
デビッドC

2

バインドされたモデルのビュー内で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>
            }
        }

上記のコードがコンパイルされても驚いています。@ Html.LabelForはパラメータとしてFUNC操作を必要としますが、それは必要ありません
Jenna Leaf

上記のコードがコンパイルされるかどうかはわかりませんが、ネストされた@foreachが機能します。MVC5。
アントニオ2016年

0

エラーから明らかです。

「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

別のはるかに簡単な可能性は、プロパティ名の1つが間違っていることです(おそらく、クラスで変更したばかりの可能性があります)。これは、RazorPages .NET Core 3で私が使用していたものです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.