I've been away from ASP.NET MVC for a while so forgotten some of the basics.
I have scoured SO for an answer, but none really seem to apply/work so this may seem like a duplicate question but it's really not, perhaps I just can't see the wood through the trees. I know I'm missing something obvious but cant remember what
I have a partial that I pass the model to that updates a property on the model (AddressDetails
& ContactDetails
).
Main page
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" ></div>
<div >
<label asp-for="Name" ></label>
<input asp-for="Name" />
<span asp-validation-for="Name" ></span>
</div
@await Html.PartialAsync("../AddressDetails/Create.cshtml", Model)
@await Html.PartialAsync("../ContactDetails/Create.cshtml", Model)
<div >
<input type="submit" value="Create" />
</div>
</form>
And partial page
@model CareHome.Models.CareHomes
<div >
<h4>AddressDetails</h4>
<hr />
</div>
<div asp-validation-summary="ModelOnly" ></div>
<div >
<label asp-for="AddressDetails.NumberStreetName" ></label>
<input asp-for="AddressDetails.NumberStreetName" />
<span asp-validation-for="AddressDetails.NumberStreetName" ></span>
</div>
<div >
<label asp-for="AddressDetails.Locality" ></label>
<input asp-for="AddressDetails.Locality" />
<span asp-validation-for="AddressDetails.Locality" ></span>
</div>
<div >
<label asp-for="AddressDetails.Town" ></label>
<input asp-for="AddressDetails.Town" />
<span asp-validation-for="AddressDetails.Town" ></span>
</div>
<div >
<label asp-for="AddressDetails.PostCode" ></label>
<input asp-for="AddressDetails.PostCode" />
<span asp-validation-for="AddressDetails.PostCode" ></span>
</div>
This is working fine when I post data back to the controller
However, I want to reuse the partial which means I want to replace
@model CareHome.Models.CareHomes
in the partial with the property class (see further below) that the model uses.
So when I change it to
main
<div >
<div >
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" ></div>
<div >
<label asp-for="Name" ></label>
<input asp-for="Name" />
<span asp-validation-for="Name" ></span>
</div
@await Html.PartialAsync("../AddressDetails/Create.cshtml", Model.AddressDetails)
@await Html.PartialAsync("../ContactDetails/Create.cshtml", Model.ContactInfo)
<div >
<input type="submit" value="Create" />
</div>
</form>
</div>
</div>
note that im passing the property through to the partial now not the model
@model CareHome.Models.AddressDetails
<div >
<h4>AddressDetails</h4>
<hr />
</div>
<div asp-validation-summary="ModelOnly" ></div>
<div >
<label asp-for="NumberStreetName" ></label>
<input asp-for="NumberStreetName" />
<span asp-validation-for="NumberStreetName" ></span>
</div>
<div >
<label asp-for="Locality" ></label>
<input asp-for="Locality" />
<span asp-validation-for="Locality" ></span>
</div>
<div >
<label asp-for="Town" ></label>
<input asp-for="Town" />
<span asp-validation-for="Town" ></span>
</div>
<div >
<label asp-for="PostCode" ></label>
<input asp-for="PostCode" />
<span asp-validation-for="PostCode" ></span>
</div>
iv now changed the partial to use
@model CareHome.Models.AddressDetails
but when I post this to the controller it comes back null
I tried a million variations on the binding
// POST: CareHomes/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
//Create([Bind("CareHomesId,Name,ContactName,ContactNumber")] CareHomes careHomes)
public async Task<IActionResult> Create([Bind( "CareHomes,AddressDetails,ContactDetails")] CareHomes careHomes)
{
if (ModelState.IsValid)
{
_context.Add(careHomes);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
ViewData["AddressDetailsId"] = new SelectList(_context.AddressDetails, "AddressDetailsId", "NumberStreetName", careHomes.AddressDetailsId);
ViewData["ContactDetailsId"] = new SelectList(_context.ContactDetails, "ContactDetailsId", "ContactName", careHomes.ContactDetailsId);
return View(careHomes);
}
but when I evaluate the ModelState I can see it's always missing. As the propertys of the model bind ok when i pass the model though why do they then not bind when i pass the property though
my classes are like so
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CareHome.Models
{
public class CareHomes
{
[Required]
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CareHomesId { get; set; }
[Required]
[Column(TypeName = "VARCHAR(256)")]
[StringLength(256, MinimumLength = 3)]
public string Name { get; set; }
public int? AddressDetailsId { get; set; }
public AddressDetails AddressDetails { get; set; }
public int? ContactDetailsId { get; set; }
public ContactDetails ContactInfo { get; set; }
public ICollection<Staff>? StaffMembers { get; set; }
}
}
and one of the properties in question
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CareHome.Models
{
public class AddressDetails
{
[Required]
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int AddressDetailsId { get; set; }
[Required]
[Column(TypeName = "VARCHAR(256)")]
[StringLength(256, MinimumLength = 3)]
[Display(Name = "House No & Street Name")]
public string NumberStreetName { get; set; }
[Column(TypeName = "VARCHAR(256)")]
[StringLength(256, MinimumLength = 3)]
public string? Locality { get; set; }
[Required]
[Column(TypeName = "VARCHAR(256)")]
[StringLength(256, MinimumLength = 3)]
public string Town { get; set; }
[Required]
[Column(TypeName = "VARCHAR(16)")]
[StringLength(16, MinimumLength = 4)]
[RegularExpression(@"^(([A-Z]{1,2}\d[A-Z\d]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?\d[A-Z]{2}|BFPO ?\d{1,4}|(KY\d|MSR|VG|AI)[ -]?\d{4}|[A-Z]{2} ?\d{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$", ErrorMessage = "Please enter a valid UK post code in upper case")]
public string PostCode { get; set; }
public CareHomes? CareHomes { get; set; }
}
}
I have tried adding bind animations like
[BindProperty]
to the property and adding hidden fields in the partual
@Html.HiddenFor(m => m.AddressDetailsId)
@Html.HiddenFor(m => m.AddressDetails)
As per some suggestions from some of the many SO searches I did, but no dice....so please...what am I missing?
I even tried @html.EditorFor but that seems to have the same problem
EDIT
Using @Jonesopolis suggestion I can see from the form being posted back when it uses the model:
?this.Request.Form.ToArray()
{System.Collections.Generic.KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues>[11]}
...
[4]: {[AddressDetails.CareHomes, {}]}
[5]: {[AddressDetailsId, {}]}
[6]: {[AddressDetails.NumberStreetName, {sad}]}
[7]: {[AddressDetails.Locality, {sad}]}
[8]: {[AddressDetails.Town, {wales}]}
[9]: {[AddressDetails.PostCode, {CF83 8RD}]}
vs when i pass the property
?this.Request.Form.ToArray()
{System.Collections.Generic.KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues>[11]}
...
[4]: {[CareHomes, {}]}
[5]: {[AddressDetailsId, {0}]}
[6]: {[NumberStreetName, {test street}]}
[7]: {[Locality, {}]}
[8]: {[Town, {wales}]}
[9]: {[PostCode, {CF83 8RD}]}
so clearly the "AddressDetails" is missing so MVC cant map the propery to the CareHomes class object on the binding because the property name is missing. So i know what the issue is not how to fix it though, How do I set the property name on the partual propertys so they map back to the parent object class. I though about a costom binder but not having much luck figuring that one out.
On a side note, intrestingly enough if in the partent model I do this :
@Html.EditorFor(m => m.AddressDetails.NumberStreetName)
then bind like so
public async Task<IActionResult> Create([Bind(include: "CareHomes,AddressDetails")] CareHomes careHomes)
I can at least get the EditorFor to pull though on the parent
CodePudding user response:
Finally worked it out, seems model binding wasn't the issue, I just had to set the id and name properties on the form controls in the partial to match that of the object on the model action, e.g. id="AddressDetails_NumberStreetName" name="AddressDetails.NumberStreetName"
so adding
<div >
<label asp-for="NumberStreetName" ></label>
<input asp-for="NumberStreetName" id="AddressDetails_NumberStreetName" name="AddressDetails.NumberStreetName" />
<span asp-validation-for="NumberStreetName" ></span>
</div>
<div >
<label asp-for="Locality" ></label>
<input asp-for="Locality" id="AddressDetails_Locality" name="AddressDetails.Locality" />
<span asp-validation-for="Locality" ></span>
</div>
<div >
<label asp-for="Town" ></label>
<input asp-for="Town" id="AddressDetails_Town" name="AddressDetails.Town" />
<span asp-validation-for="Town" ></span>
</div>
<div >
<label asp-for="Postcode" ></label>
<input asp-for="Postcode" id="AddressDetails_Postcode" name="AddressDetails.Postcode" />
<span asp-validation-for="Postcode" ></span>
</div>
allows it to properly map to the contoller model
public async Task<IActionResult> Create([Bind(include: "CareHomes, Name,AddressDetails, ContactInfo")] CareHomes careHomes)
I worked it out when I put the partial mark up in the main form and looked at the HTML markup and compared it to when it was a partial. I hope this helps someone else someday