Reputation: 11095
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?
Upvotes: 12
Views: 5187
Reputation: 2793
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>
Upvotes: 0
Reputation: 14630
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" />
Upvotes: 12