Home > Software engineering >  ASP.NET MVC partial not binding when a passing property though
ASP.NET MVC partial not binding when a passing property though

Time:08-02

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

Working

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

Not working

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

  • Related