MVC Model child object null on HTTP post

10,038

Solution 1

For every property in domain model (in your case testvm) you must have an EditorFor or Input element (like TextBoxFor or so) on your view(or HiddenFor for ID or other non user ui data).It may be a pain binding nested models in MVC as the DefaultModelBinder may not be able to bind whole object.However it would be safer approach to expose only the required properties on view like

@Html.HiddenFor(model => model.studentID.ID)
@Html.HiddenFor(model => model.studentID.Name)

and later on Controller Side

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
  var originalobj=db.get //get fresh copy from data store
 originalobj.Name=testvm.Name;
//     ...other properties
//perform required operations on originalobj
}

you may use AutoMapper for this Purpose as

Mapper.CreateMap<testvm,testvm>();
originalobj=Mapper.Map<testvm,testvm>(testvm,originalobj);

you may find more information about Automapper on : https://github.com/AutoMapper/AutoMapper/wiki/Getting-started

Solution 2

Your property name is called studentId (even though standard C# property naming convention dictates that it should have been called StudentId):

public student studentID { get; set; }

But in your Bind attribute you seem to have specified some student property which doesn't really exist on your view model:

[Bind(Include = "id,date,student")]

So you probably want to get rid of this Bind attribute from your controller action:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
    ...
}

Also note that you only have a hidden field for the student id inside your form:

@Html.HiddenFor(model => model.studentID.ID)

You don't have a corresponding hidden field for the student name property, so it will never be posted back to your controller action.

Solution 3

Your attribute [Bind(Include = "id,date,student")] should include the names of the properties that you want to be set, student isn't in your model, but studentID is, they have to match.

You don't have to explicitly specify all of the field names that you want to be bound to your model, by default they will be bound anyway unless you tell the binder NOT to bind it by using [Bind(Exclude = "id,date,student")]. Therefore as it currently stands, I'd recommend removing your Include attribute to ease maintenance unless there is an important reason for using it and simply ensure that the models that you bind to only include the values you need.

Secondly, you have to make sure that any values that you are posting back from a form in your view have the same parameter names and are structured the same as the ones that you want to be bound to the request model.

This:

@Html.HiddenFor(model => model.studentID.ID)

Is not the same as:

@Html.HiddenFor(model => model.studentID)
Share:
10,038
Robert Kitching
Author by

Robert Kitching

Updated on June 10, 2022

Comments

  • Robert Kitching
    Robert Kitching almost 2 years

    Hope someone can help me. I am new to MVC, coming from a winforms/console/vb6background.

    Apologies if this has already been answered, I am stuggling to understand how I can resolve the below issue.

    I have a view model :

    public class testvm
    {
        public int id { get; set; }
        public DateTime date { get; set; }
        public student studentID { get; set; }
    
        public testvm() { }
    
        public testvm (student s)
        {
            studentID = s;
        }
    }
    

    I am pre-populating the student child object of this ViewModel before it is passed to the view.

    Student Model :

    public class student
    {
        [Key]
        public int ID { get; set; }
        public string Name { get; set; }
    }
    

    The problem I have is when the model is returned to the create HTTP post method the student child object is blank.

    The controller code :

    // GET: testvms/Create
    public ActionResult Create(int sId)
    {
        student a = db.students.Find(sId);
    
        testvm b = new testvm(a);
    
        return View(b);
    }
    
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "id,date,student")] testvm testvm)
    {
        if (ModelState.IsValid)
        {
            db.testvws.Add(testvm);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    
        return View(testvm);
    }
    

    View code:

    @model WebApplication2.Models.testvm
    
    @{
    ViewBag.Title = "Create";
    }
    
    <h2>Create</h2>
    
    
     @using (Html.BeginForm()) 
    {
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>testvm</h4>
        <hr />
    
    
        @Html.HiddenFor(model => model.studentID.ID)
    
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.date, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.date, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.date, "", new { @class = "text-danger" })
            </div>
        </div>
    
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
    }
    
    <div>
    @Html.ActionLink("Back to List", "Index")
    </div>
    
    @section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    }
    

    The model object on the view is populated with the student information. When this is passed back to Create POST controller the student child object is null!

    Can somebody please advise where I am going wrong or of the correct way to achieve this?

    My application will have many forms that will all need to be pre-populated with student information. Each student will have many forms that will need to be filled out for them.

    Many thanks in advance, Rob