Imagine a common scenario, this is a simpler version of what I'm coming across. I actually have a couple of layers of further nesting on mine....
But this is the scenario
Theme contains List Category contains List Product contains List
My Controller provides a fully populated Theme, with all the Categories for that theme, the Products within this categories and the their orders.
The orders collection has a property called Quantity (amongst many others) that needs to 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 then I only seem to get a reference to the top Model object, "Theme" not those within the foreach loop.
Is what I'm trying to do there even possible or have I overestimated or misunderstood what is possible?
With the above I get an error on the TextboxFor, EditorFor, etc
CS0411: The type arguments for method 'System.Web.Mvc.Html.InputExtensions.TextBoxFor(System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
Thanks.
You could add a Category partial and a Product partial, each would take a smaller part of the main model as it's own model, i.e. Category's model type might be an IEnumerable, you would pass in Model.Theme to it. The Product's partial might be an IEnumerable that you pass Model.Products into (from within the Category partial).
I'm not sure if that would be the right way forward, but would be interested in knowing.
EDIT
Since posting this answer, I've used EditorTemplates and find this the easiest way to handle repeating input groups or items. It handles all your validation message problems and form submission/model binding woes automatically.
You can simply use EditorTemplates to do that, you need to create a directory named "EditorTemplates" in your controller's view folder and place a seperate view for each of your nested entities (named as entity class name)
Main view :
Category view (/MyController/EditorTemplates/Category.cshtml) :
Product view (/MyController/EditorTemplates/Product.cshtml) :
and so on
this way Html.EditorFor helper will generate element's names in an ordered manner and therefore you won't have any further problem for retrieving the posted Theme entity as a whole
When you are using foreach loop within view for binded model ... Your model is supposed to be in listed format.
i.e
It is clear from the error.
The HtmlHelpers appended with "For" expects lambda expression as a parameter.
If you are passing the value directly, better use Normal one.
e.g.
Instead of TextboxFor(....) use Textbox()
syntax for TextboxFor will be like Html.TextBoxFor(m=>m.Property)
In your scenario you can use basic for loop, as it will give you index to use.
The quick answer is to use a
for()
loop in place of yourforeach()
loops. Something like:But this glosses over why this fixes the problem.
There are three things that you have at least a cursory understanding before you can resolve this issue. I have to admit that I cargo-culted this for a long time when I started working with the framework. And it took me quite a while to really get what was going on.
Those three things are:
LabelFor
and other...For
helpers work in MVC?All three of these concepts link together to get an answer.
How do the
LabelFor
and other...For
helpers work in MVC?So, you've used the
HtmlHelper<T>
extensions forLabelFor
andTextBoxFor
and others, and you probably noticed that when you invoke 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 helpers. Lets look at the simplest overload for
TextBoxFor
First, this is an extension method for a strongly typed
HtmlHelper
, of type<TModel>
. So, to simply state what happens behind the scenes, when razor renders this view it generates a class. Inside of this class is an instance ofHtmlHelper<TModel>
(as the propertyHtml
, which is why you can use@Html...
), whereTModel
is the type defined in your@model
statement. So in your case, when you are looking at this viewTModel
will always be of the typeViewModels.MyViewModels.Theme
.Now, the next argument is a bit tricky. So lets look at an invocation
It looks like we have a little lambda, And if one were to guess the signature, one might think that the type for this argument would simply be a
Func<TModel, TProperty>
, whereTModel
is the type of the view model andTProperty
is inferred as the type of the property.But thats not quite right, if you look at the actual type of the argument its
Expression<Func<TModel, TProperty>>
.So when you normally generate a lambda, the compiler takes the lambda and compiles it down into MSIL, just like any other function (which is why 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 an
Expression<>
, it doesn't immediately compile the lambda down to MSIL, instead it generates an Expression Tree!What is an Expression Tree?
So, what the heck is an expression tree. Well, it's not complicated but its not a walk in the park either. To quote ms:
| Expression trees represent code in a tree-like data structure, where each node is an expression, for example, 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
, the expression tree would have a node in it that says: "Get 'Some Property' from a 'model'"This expression tree can be compiled into a function that can be invoked, but as long as it's an expression tree, it's just a collection of nodes.
So what is that good for?
So
Func<>
orAction<>
, once you have them, they are pretty much atomic. All you can really do isInvoke()
them, aka tell them to do the work they are supposed to do.Expression<Func<>>
on the other hand, represents a collection of actions, which can be appended, manipulated, visited, or compiled and invoked.So why are you telling me all this?
So with that understanding of what an
Expression<>
is, we can go back toHtml.TextBoxFor
. When it renders a textbox, it needs to generate a few things about the property that you are giving it. Things likeattributes
on the property for validation, and specifically in this case it needs to figure out what to name the<input>
tag.It does this by "walking" the expression tree and building a name. So for an expression like
model=>model.SomeProperty
, it walks the expression gathering the properties that you are asking for and builds<input name='SomeProperty'>
.For a more complicated example, like
model=>model.Foo.Bar.Baz.FooBar
, it might generate<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />
Make sense? It is not just the work that the
Func<>
does, but how it does its work is important here.(Note other frameworks like LINQ to SQL do similar things by walking an expression tree and building a different grammar, that this case a SQL query)
How does the Model Binder work?
So once you get that, we have to briefly talk about the model binder. When the form gets posted, it's simply like a flat
Dictionary<string, string>
, we have lost the hierarchical structure our nested view model may have had. It's the model binder's job to take this key-value pair combo and attempt to rehydrate an object with some properties. How does it do this? You guessed it, by using the "key" or name of the input that got posted.So if the form post looks like
And you are posting to a model called
SomeViewModel
, then it does the reverse of what the helper did in the first place. It looks for a property called "Foo". Then it looks for a property called "Bar" off of "Foo", then it looks for "Baz"... and so on...Finally it tries to parse the value into the type of "FooBar" and assign it to "FooBar".
PHEW!!!
And voila, you have your model. The instance the Model Binder just constructed gets handed into requested Action.
So your solution doesn't work because the
Html.[Type]For()
helpers need an expression. And you are just giving them a value. It has no idea what the context is for that value, and it doesn't know what to do with it.Now some people suggested using partials to render. Now this in theory will work, but probably not the way that you expect. When you render a partial, you are changing 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 when the helper generates the name for your expression, it will be shallow. It will only generate based on the expression it's given (not the entire context).So lets say you had a partial that just rendered "Baz" (from our example before). Inside that partial you could just say:
Rather than
That means that it will generate an input tag like this:
Which, if you are posting this form to an action that is expecting a large deeply nested ViewModel, then it will try to hydrate a property called
FooBar
off ofTModel
. Which at best isn't there, and at worst is something else entirely. If you were posting to a specific action that was accepting aBaz
, rather than the root model, then this would work great! In fact, partials are a good way to change your view context, for example if you had a page with multiple forms that all post to different actions, then rendering a partial for each one would be a great idea.Now once you get all of this, you can start to do really interesting things with
Expression<>
, by programatically extending them and doing other neat things with them. I won't get into any of that. But, hopefully, this will give you a better understanding of what is going on behind the scenes and why things are acting the way that they are.