How to prevent Razor from adding prefixes to inputs when using nested display templates?

5.1k Views Asked by At

When I use nested display templates and add input elements through the HTML helper the Razor engine adds a prefix to the fields names.

I do understand this is done to guarantee input name uniqueness at page level (and to rebuild the whole model on post back).

However I have many small forms which perform ad-hoc actions, and I don't need neither the name uniqueness nor the ability to rebuild the whole model.

I just need that single property value, and having Razor alter the input items names breaks the model binder when I submit one of the forms, since all the names will be different.

This example contains a simplified nested model

public class Student
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<Course> Courses { get; set; }
}

public class Course
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public List<Grade> Grades { get; set; }
}

public class Grade
{
    public Guid Id { get; set; }
    public DateTime Date { get; set; }
    public decimal Value { get; set; }
}

and it has an Index view with three nested display templates

IndexView
    StudentDisplayTemplate
        CourseDisplayTemplate
            GradeDisplayTemplate

In the grade display template I add a button to remove the grade

@model Playground.Sandbox.Models.Home.Index.Grade

<li>
    @this.Model.Date: @this.Model.Value

    @using (Html.BeginForm("Remove", "Home", FormMethod.Post))
    {
        <input name="GradeId" type="hidden" value="@this.Model.Id" />

        <input type="submit" value="Remove" />
    }
</li>

and on the other side of the request my controller action receives the grade ID

public ActionResult Remove(Guid id)
{
    // Do various things.
    return this.RedirectToAction("Index");
}

If I try to do it using the model helper

@Html.HiddenFor(x => x.Id)

I get the HTML element

<input data-val="true"
       data-val-required="The Id field is required."
       id="Courses_0__Grades_1__Id"
       name="Courses[0].Grades[1].Id"
       type="hidden"
       value="76f7e7ed-a479-42cb-add5-e58c0090770c" />

where the field name gets a prefix based on the whole parent's view model tree.

Using the "manual" helper

@Html.Hidden("GradeId", this.Model.Id)

gives the HTML element

<input id="Courses_0__Grades_0__GradeId"
       name="Courses[0].Grades[0].GradeId"
       type="hidden"
       value="bbb3c11d-d2d0-464a-b33b-ff7ac9815601" />

where the prefix is still present, albeit with my name at the end.

Adding manually the hidden input

<input name="GradeId" type="hidden" value="@this.Model.Id" />

gives the HTML element

<input name="GradeId"
       type="hidden"
       value="a1a35e81-29cd-41b5-b619-bab79b767613" />

which is what I want.

Is it possible to achieve what I want, or am I getting the display templates thing wrong?

2

There are 2 best solutions below

0
On

From your question, it's not clear if you're looking to be able to submit both the entire form, or just the individual actions. If you want to be able to do both, I think you'll have to use a JS-based solution to manually submit the subform requests.

However, if you just want to submit the subforms, piecemeal, read along..

If the syntax Courses_0__Grades_1__Id for collections is causing problems, this is relatively easy to fix, in my experience.

You can get different behavior on how the names/ids are generated in child objects in collections by using foreach instead of a traditional for.

Let me explain:


1) This will break the model binding for entire-form submission. All inputs for child items will have no context of their parent path.

@foreach(var child in Model.Children)
{
    @Html.EditorFor(x=> child)
}


2) These will respect parent context and allow model binding for entire-form submission. All inputs for child items WILL HAVE context of their parent path.

@for(var i = 0; i < Model.Children.Count(); i++)
{
    @Html.EditorFor(x=> Model.Children[i])
}
// or..
var i = 0; 
@foreach(var child in Model.Children)
{
    @Html.EditorFor(x=> Model.Children[i])
    i++;
}

However

You will still have issues with objects NOT in collections, hanging off the main model, like Model.SomeOtherType.AnotherType will have inputs in the nested EditorFor with names like SomeOtherType.Property1 and SomeOtherType.Property2

For these, you can pull the object into a temporary variable in the razor:

@var tempObj = Model.SomeOtherType;
<div class='somemarkup'>
    @Html.EditorFor(x=> tempObj);
</div>
1
On

You want to set ViewData.TemplateInfo.HtmlFieldPrefix in your Grade template:

@model Playground.Sandbox.Models.Home.Index.Grade

@{
    ViewData.TemplateInfo.HtmlFieldPrefix = "";
}

This gives the desired output of:

<input id="GradeId" name="GradeId" type="hidden" />