when passing a collection to EditorFor(), it generates invalid names for input elements
Solution 1
I would recommend you sticking to conventions, i.e. replace:
@Html.EditorFor(m => m.Authors, "AuthorSelector")
with:
@Html.EditorFor(m => m.Authors)
and then rename your ~/Views/Shared/EditorTemplates/AuthorSelector.cshtml
to ~/Views/Shared/EditorTemplates/AuthorEntryModel.cshtml
and make it strongly typed to a single AuthorEntryModel
model and get rid of the loop:
@model AuthorEntryModel
@Html.TextBoxFor(o => o.FirstName)
@Html.TextBoxFor(o => o.LastName)
ASP.NET MVC will automatically render the editor template for all elements of the collection and generate proper names.
UPDATE:
After seeing your update here's my response:
In your main view:
<div class="ptr_authors_wrapper">
@Html.EditorFor(m => m.Authors)
</div>
In your editor template:
@model AuthorEntryModel
<div class="ptr_author_line">
@Html.TextBoxFor(o => o.FirstName)
@Html.TextBoxFor(o => o.LastName)
</div>
You will notice the absence of script in the template which is perfectly normal. Scripts have nothing to do in markup. They go into separate javascript files. In this file you could use jQuery to do whatever you need to do with your markup. It gives you methods such as .index()
that allow you to get the index of the element in the matched selector so that you don't need to write any loops and pollute your markup with things like data-line-index
attributes.
Solution 2
I'm a little late to the party, but hopefully this helps someone.
Digging down to System.Web.Mvc.Html.DefaultEditorTemplates.CollectionTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper)
, the framework's default template handles this by temporarily setting the HtmlFieldPrefix
to an empty string and explicitly passing the prefix and index into a call to EditorFor()
.
<div class="ptr_authors_wrapper">
@{
var prefix = ViewData.TemplateInfo.HtmlFieldPrefix;
ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;
for (int i = 0; i < Model.Count; i++)
{
<div class="ptr_author_line" data-line-index="@i">
@* You can also use null instead of "TextBox" to let the framework resolve which editor to use. *@
@Html.EditorFor(o => o[i].FirstName, "TextBox", String.Format("{0}[{1}].FirstName", prefix, i))
@Html.EditorFor(o => o[i].LastName, "TextBox", String.Format("{0}[{1}].LastName", prefix, i))
</div>
}
ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
}
</div>
<script>
...
</script>
I found this particularly useful when the framework was writing names as [0].Children.[0].ChildProperty
due to a named template for the Children collection. In my case, the solution was to call:
@Html.EditorFor(m => m[i], null, String.Format("{0}[{1}]", prefix, i))
rather than simply calling:
@Html.EditorFor(m => m[i])
Solution 3
Don't know if it's still relevant, but this blog covers a solution to your problem: http://btburnett.com/2011/03/correcting-mvc-3-editorfor-template-field-names-when-using-collections.html
--> Credits go to itsmatt for finding it :) Jakob
Solution 4
Here's an extension method you can use which will render a partial view and use the correct HTML field prefix:
Extension Method
/// <summary>
/// Helper method that renders the specified partial view as a HTML-encoded string using the specified
/// collection as the model, with the intention that the partial view will use an editor template on the items
/// in the collection.
/// </summary>
/// <typeparam name="TModel">the model type</typeparam>
/// <typeparam name="TProperty">the property type</typeparam>
/// <param name="htmlHelper">the <see cref="HtmlHelper"/> instance</param>
/// <param name="partialViewName">the name of the partial view to render</param>
/// <param name="collectionExpression">the model collection property expression</param>
/// <returns>the HTML-encoded string</returns>
public static MvcHtmlString PartialContainingEditorForCollection<TModel, TProperty>
(this HtmlHelper<TModel> htmlHelper, string partialViewName,
Expression<Func<TModel, TProperty>> collectionExpression)
where TProperty : IEnumerable
{
var viewData = htmlHelper.ViewContext.ViewData;
var model = (TModel) viewData.Model;
var collection = collectionExpression.Compile().Invoke(model);
var htmlFieldPrefix = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(
ExpressionHelper.GetExpressionText(collectionExpression));
return htmlHelper.Partial(partialViewName, collection,
new ViewDataDictionary
{
TemplateInfo = new TemplateInfo {HtmlFieldPrefix = htmlFieldPrefix}
});
}
Sample Usage
@Html.PartialContainingEditorForCollection("_TableWithSummary", m => Model.FormModel.ItemsToOrder)
Alireza Sabouri
Updated on June 15, 2022Comments
-
Alireza Sabouri almost 2 years
I have a BookCreateModel which consists of book's plane info such as Title, PublishYear & etc plus a collection of book Authors (complex type) :
public class BookCreateModel { public string Title { get; set; } public int Year { get; set; } public IList<AuthorEntryModel> Authors { get; set; } } public class AuthorEntryModel { public string FirstName { get; set; } public string LastName { get; set; } }
in CreateBook view I have used
EditorFor
helper :@Html.EditorFor(m => m.Authors, "AuthorSelector")
Edit1:
and AuthorSelector template is as below:
<div class="ptr_authors_wrapper"> @for (int i = 0; i < Model.Count; i++) { <div class="ptr_author_line" data-line-index="@i"> @Html.TextBoxFor(o => o[i].FirstName) @Html.TextBoxFor(o => o[i].LastName) </div> } </div> <script> ... </script>
the
AuthorSelector
template contains some wrapper markups which need to be aware of each rendered item's index plus some javascript which handle the child input's interactions and need to be rendered once (inside theAuthorSelector
template), thus getting rid of the for loop/or the AuthorSelector template is not possible.now the problem is EditorFor act a little strange and generate input names like this :
<input id="Authors__0__FirstName" name="Authors.[0].FirstName" type="text" value="" /> <input id="Authors__0__LastName" name="Authors.[0].LastName" type="text" value="" />
as you can see instead of generating names like
Authors[0].FirstName
it adds an extra dot which makes the default model binder unable to parse posted data.any idea ?
Thanks !
-
Alireza Sabouri almost 12 yearsthanks, the AuthorSelector also generates some wrapper markup for each item so replacing the for loop with new syntax is not possible. I updated the question, please take a look
-
Alireza Sabouri almost 12 yearsyep, that's also an option, but I think purpose of using EditorTemplate is to encapsulates bunch of markups and codes which free the developer from repeating himself over and over. the wrapper div in this case is a simplified example; in reality it's much more longer, imagine how hard it would be to copy/paste that piece of markups over and over in different places.
-
Darin Dimitrov almost 12 years@sos00, yes, this is exactly the purpose of an editor temlpate => encapsulate a reusable markup. I say markup, not scripts. Scripts go into separate (and if you will reusable) javascript files. You could write plugins and so on to make your scripts reusable.
-
Alireza Sabouri almost 12 yearsThank you :), I meant markups too, not scripts. your solution requires the developer to copy markups everywhere he wants to get a collection of Author items. anyway I was hopping to see if this is a known bug and someone knows any workaround for it, so I voted your answer up but will wait a little more to see if anyone has any more info on this bug(?).
-
Dave Watts almost 12 yearsUsing the editor templates allows you to provide a template for each item in the collection. As far as I can see you can't define a template for the collection as a whole (let's say you want a header and footer) from which you can then call the editor template for the items without having the problem of the additional dot in the input prefix. @sos00's solution of adding a wrapper class works, but it does feel like a work-around.
-
jlp over 10 yearsCould you please put parent view and child view ?
-
Erik Dietrich over 8 yearsThanks much for this. Your response just made my morning.