when passing a collection to EditorFor(), it gener

2019-03-15 15:58发布

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 the AuthorSelector 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 !

6条回答
男人必须洒脱
2楼-- · 2019-03-15 16:09

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])
查看更多
你好瞎i
3楼-- · 2019-03-15 16:13

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.

查看更多
Emotional °昔
4楼-- · 2019-03-15 16:23

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)
查看更多
叛逆
5楼-- · 2019-03-15 16:24

I didn't find any solution to this bug yet, but as a workaround I changed my UpdateModel by using a custom wrapper class instead of using a collection directly :

public class BookCreateModel
{
    public string Title { get; set; }
    public int Year { get; set; }
    public BookAuthorsList Authors { get; set; }
}

public class BookAuthorsList
{
    public IList<AuthorEntryModel> AuthorsList { get; set; }
}

public class AuthorEntryModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

and thus generated inputs wouldn't make naming issues anymore :)

<input  id="Authors_AuthorsList_0__FirstName" name="Authors.AuthorsList[0].FirstName" type="text"/>
<input  id="Authors_AuthorsList_0__LastName" name="Authors.AuthorsList[0].LastName" type="text"/>
查看更多
萌系小妹纸
6楼-- · 2019-03-15 16:26

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

查看更多
家丑人穷心不美
7楼-- · 2019-03-15 16:26

I stumbled upon this when I was trying to find the solution for pretty much the same issue. My workaround was to kick the can down the road, ie. make wrapper model for the collection and use that in the editor template. In provided case, it would be:

public class BookCreateModel
{
    public string Title { get; set; }
    public int Year { get; set; }
    public BookAuthorsModel Authors { get; set; }
}

public class BookAuthorsModel
{
    IList<AuthorEntryModel> Items { get; set; }
}

Then rename your editor template to "BookAuthorsModel.cshtml" and make it like this:

@model BookAuthorsModel
<div class="ptr_authors_wrapper">
    @for (int i = 0; i < Model.Items.Count; i++)
    {
       <div class="ptr_author_line" data-line-index="@i">
        @Html.TextBoxFor(o => Items.o[i].FirstName)
        @Html.TextBoxFor(o => Items.o[i].LastName)
       </div>
    }
</div>
<script>
     ...
</script>

And when you want to use it, simply call:

@Html.EditorFor(m => m.Authors)

It should then generate input fields like so:

<input id="Authors_Items_0__FirstName" name="Authors.Items[0].FirstName" type="text" value="" />
<input id="Authors_Items_0__LastName" name="Authors.Items[0].LastName" type="text" value="" />

In my case I also changed Automapper mapping settings appropriately Controller code. This is not usable for some more complex scenarios however and is thus probably just a workaround.

查看更多
登录 后发表回答