MVC Razor displays a nested foreach model

Imagine a general scenario, this is a simpler version of what I see. I actually have several layers of further nesting on my ...

But this is the scenario

The topic contains a list. The category contains a list. The product contains a list.

My Controller provides a fully-filled topic with all categories for this topic, Products within these categories and their orders.

A collection of orders has the “Quantity” property (among many others), which must be editable.

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

If I use lambda instead, then it seems to me that I am getting a reference to the top Model object, "Theme", and not the ones inside the foreach loop.

Am I trying to do what I am trying to do, even if I overestimate or misunderstand what is possible?

With the above, I get an error in TextboxFor, EditorFor, etc.

CS0411: type arguments for the method "System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)" cannot be taken out of use. Try specifying type arguments explicitly.

Thank.

+85
c # view asp.net-mvc-3 razor
Jan 17 '12 at 12:16
source share
5 answers

The quick answer is to use a for() loop instead of your foreach() loops. Something like:

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

But this is silent about why this fixes the problem.

There are three things that you have at least a quick understanding before you can solve this problem. I have to admit that I am a truck for a long time, when I started working with the frame. And it took me quite a while to really get what was happening.

These are three things:

  • How do LabelFor and others ...For helpers work in MVC?
  • What is an expression tree?
  • How does the Model Linker work?

All three concepts combine to produce an answer.

How LabelFor and others ...For helpers work in MVC?

So, you used the HtmlHelper<T> LabelFor for LabelFor and TextBoxFor and others, and you probably noticed that when you call them, you pass them a lambda, and it magically generates some html. But how?

So, the first thing to notice is the signature for these assistants. Let's look at the simplest overload for TextBoxFor

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

Firstly, it is an extension method for a strongly typed HtmlHelper , such as <TModel> . So, just what happens behind the scenes, when the razor displays this view, it generates a class. Inside this class there is an instance of HtmlHelper<TModel> (as an Html property, so you can use @Html... ), where TModel is the type defined in your @model . So, in your case, when you look at this view, TModel will always be of type ViewModels.MyViewModels.Theme .

Now the next argument is a bit more complicated. So let's look at the challenge

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

Looks like we have a little lambda. And if you guess the signature, you might think that the type for this argument would be just Func<TModel, TProperty> , where TModel is the type of the view model and TProperty is called as the type of the property.

But this is not entirely correct if you look at the actual type of the argument Expression<Func<TModel, TProperty>> .

So, when you usually generate lambda, the compiler takes the lambda and compiles it to MSIL, like any other (so you can use delegates, method groups and lambdas more or less interchangeably because they are just code references.)

However, when the compiler sees that the type is Expression<> , it does not immediately compile the lambda to MSIL, instead it generates an Expression Tree!

What is an expression tree ?

So what is this expression tree. Well, it’s not difficult, but not a walk in the park. To specify ms:

| Expression trees represent code in a tree-like data structure, where each node is an expression, such as a method call or a binary operation such as x <y.

Simply put, an expression tree is a representation of a function as a collection of "actions".

In the case of model=>model.SomeProperty there will be a node in the expression tree that says: "Get" Some property "from" model "

This expression tree can be compiled into a function that can be called, but as long as it is an expression tree, it is just a collection of nodes.

So what's good for?

So Func<> or Action<> , once you get them, they are pretty much atomic. All you really can do is Invoke() them, and also tell them to do the work they have to do.

Expression<Func<>> , is a set of actions that can be added, processed, visited or compiled and called.

So why are you telling me all this?

So, with an understanding of what Expression<> , we can return to Html.TextBoxFor . When he creates a text box, he needs to generate a few things about the property that you give him. Things like attributes for the property to check, and in particular in this case, he needs to figure out what the <input> tag is called.

He does this by “walking” the expression tree and creating a name. Thus, for an expression of type model=>model.SomeProperty it executes the expression by collecting the properties that you request and builds <input name='SomeProperty'> .

For a more complex example, for example model=>model.Foo.Bar.Baz.FooBar , it can generate <input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

Make sense? This is not just the job Func<> does, but how she does her job here.

(Note that other frameworks, such as LINQ to SQL, perform similar actions by moving the expression tree and creating another grammar, in this case an SQL query)

How does the binder model work?

So, once you understand this, we need to briefly talk about model binding. When the form is submitted, it simply resembles the apartment Dictionary<string, string> , we lost the hierarchical structure that our nested representation model could have. This is a model to take this key-value pair combination and try to translate an object with some properties. How to do it? You guessed it using the "key" or the name of the message you entered.

So, if the form message looks like

 Foo.Bar.Baz.FooBar = Hello 

And you submit to a model called SomeViewModel , then it does the opposite of what the helper did first. He is looking for the property "Foo". Then he searches for the property "Bar" off "Foo", then he searches for the "Baz" ... and so on ...

Finally, he tries to parse the value as "FooBar" and assign it to "FooBar".

Phew !!!

And voila, you have your model. The instance that the Binder model has just built is passed to the requested action.




So your solution does not work, because Html.[Type]For() helpers need an expression. And you just give them value. It has no idea what context is for this value, and he does not know what to do with it.

Now some people have suggested using partials for rendering. Now this will theoretically work, but probably not the way you expect. When you do partial, you change the type of TModel because you are in a different view context. This means that you can describe your property with a shorter expression. It also means that the helper generates a name for your expression, it will be small. This will only be generated based on the expression it expresses (and not the entire context).

So, let's say that you had a partial that “Baz” had just appeared (from our example earlier). Inside this part you could just say:

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

Instead

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

This means that it will generate an input tag as follows:

 <input name="FooBar" /> 

What if you submit this form to an action that expects a large, deeply nested ViewModel, then it will try to FooBar property called FooBar off TModel . Which at best does not exist, and at worst is something completely different. If you send to a specific action that takes a Baz , not a root model, then that would be great! In fact, partial files are a good way to change the context of the presentation, for example, if you have a page with several forms that all submit to different actions, then providing partial for each of them would be a great idea.




Now that you have all this, you can start doing really interesting things with Expression<> , expanding them programmatically and doing other neat things with them. I will not delve into this. But hopefully this will give you a better idea of ​​what is going on behind the scenes and why everything works the way they are.

+291
Jan 17 '12 at 15:00
source share

You can simply use EditorTemplates for this, you need to create a directory called "EditorTemplates" in the controller’s viewing folder and place a separate view for each of your nested objects (called the entity class name)

The main view:

 @model ViewModels.MyViewModels.Theme @Html.LabelFor(Model.Theme.name) @Html.EditorFor(Model.Theme.Categories) 

Category view (/MyController/EditorTemplates/Category.cshtml):

 @model ViewModels.MyViewModels.Category @Html.LabelFor(Model.Name) @Html.EditorFor(Model.Products) 

Product Type (/MyController/EditorTemplates/Product.cshtml):

 @model ViewModels.MyViewModels.Product @Html.LabelFor(Model.Name) @Html.EditorFor(Model.Orders) 

etc.

in this way the Html.EditorFor helper will generate the element names in an orderly manner, and therefore you will not have any additional problem to get the sent theme object in general

+18
Jan 17 2018-12-17T00:
source share

You can add a partial element and a partial product, each of which will occupy a smaller part of the main model, since it is its own model, i.e. the category model type can be IEnumerable, you must go to Model.Theme to it. Partially, Product may be IEnumerable that you pass Model.Products to (from the Part part).

I’m not sure that this will be the right way forward, but it will be interesting to know.

EDIT

After posting this answer, I used EditorTemplates and found this the easiest way to handle duplicate input groups or items. It handles all your problems with a verification message and automatically associates problems with the submit / model.

+4
Jan 17 '12 at 12:37
source share

When you use the foreach loop in a view for a bound model ... It is assumed that your model is in the specified format.

i.e

 @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
Feb 09 '15 at 5:26
source share

This is clear from the error.

HtmlHelpers added to "For" expects a lambda expression as a parameter.

If you pass the value directly, it is better to use Normal.

eg.

Instead of TextboxFor (....) use Textbox ()

Syntax

for TextboxFor will be similar to Html.TextBoxFor (m => m.Property)

In your scenario, you can use basic for loop as it will give you an index to use.

 @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
Jan 17 2018-12-17T00:
source share



All Articles