Antarr Byrd
Antarr Byrd

Reputation: 26169

Model Values Getting Lost During PostBack

I'm trying to pass my model back to a controller. I have included all fields of the model in the form but two parts of the model are getting lost once it makes it back to the controller, Employees and EmployeesInMultipleCompanies, which are both of type IList. I have verified that the fields are present when they are passed to the view, they just don't make it back to the controller.

.cshtml

@using (Html.BeginForm("PostEmail","Import",FormMethod.Post))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>EmailViewModel</legend>
        <p>
            <input type="email" name="EmailAddresses" value=" " required="required" />
            <span class="btn_orange"><a href="#" class="remove_field" >x</a></span>
        </p>
        <p>
            <span class="btn_orange"><a class="add_email_button" href="#">Add Another Email</a></span>
        </p>
        @Html.HiddenFor(m=> Model.Employees)
        @Html.HiddenFor(m=> Model.CompanyId)
        @Html.HiddenFor(m=> Model.CompanyName)
        @Html.HiddenFor(m=> Model.PayFrequency)
        @Html.HiddenFor(m=> Model.FirstPayPeriodBeginDate)
        @Html.HiddenFor(m=> Model.LastPayPeriodEndDate)
        @Html.HiddenFor(m=> Model.NumberPayPeriods)
        @Html.HiddenFor(m=> Model.ValidEmployees)
        @Html.HiddenFor(m=> Model.InvalidEmployees)
        @Html.HiddenFor(m=> Model.EmployeesInMultipleCompanies)
        @Html.HiddenFor(m=> Model.TotalEmployeeCount)
        <p>
            <input type="submit" value="Send Email" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Cancel", "Continue", "Import")
</div>

<script type="text/javascript">
    $(document).ready(function () {
        $('body').on('click', '.remove_field', function () {
            $(this).closest('p').remove();
        });
        $('.add_email_button').closest('p').click(function () {
            var html = '<p><input type="email" required="required" name="EmailAddresses" /><span class="btn_orange"><a href="#" class="remove_field">x</a></span></p>';
            $(html).insertBefore($(this));
        });

        $(body).on('click', 'submit', function() {
            $('email').attr('required', true);
        });
    });
</script>

controller .cs

public ActionResult SendEmail(ImportViewModel model)
        {
            var editedEmployees = model.EmployeesInMultipleCompanies;
            var importModel = TempData["ImportModel"] as ImportViewModel;

            //for each employee who is in multiple companies, set user-chosen company id and the COHD related to that company
            var importService = new ImportService();
            importService.UpdateEmployeesInMultipleCompanies(editedEmployees, importModel.Employees);
            TempData["ImportModel"] = importModel;

            return this.RazorView("SendEmail", importModel);
        }


        [HttpPost]
        public ActionResult PostEmail(ImportViewModel model)
        {
            IEmailer emailer = new Emailer();
            emailer.SendEmail(model.EmailAddresses, model.Employees);
            var employees =
                model.Employees.Where(e => !string.IsNullOrWhiteSpace(e.ValidationErrorOrException)).ToList();
            var emailViewModel = new EmailViewModel(employees);
            return this.RazorView("Continue", emailViewModel);
        }

Upvotes: 1

Views: 1620

Answers (3)

xDaevax
xDaevax

Reputation: 2022

The problem is not with the controller but rather the way the values are stored on the client. The DefaultModelBinder is very smart, but it is also sometimes not that smart. It can handle interfaces in certain situations, collections of complex types and many other things as long as the properties match up. For detailed information on the behavior, see this article: http://msdn.microsoft.com/en-us/magazine/hh781022.aspx.

If you debug your controller and inspect the values in Request.Form, you will see your data stored as a single value. If model binders could talk they would say:

The Request.Form["EmployeesInMultipleCompanies"] doesn't relate to the EmployeesInMultipleCompanies property of my model because my model meta data (ModelMetaDataProvider) tells me that I should be looking for a collection type (IList of an unknown generic type) and this is clearly a single string value so I will not bind this value.

To solve this, you have to give the DefaultModelBinder enough of a hint for it to do the work. One way of doing this is to render the hidden fields in such a way that they have an indexer associated with them. This is the clue the model binder needs to function the way you expect.

If you rendered both like so:

@for(var i = 0; i < Model.Employees.Count; i++)
{
  Html.Hiddenfor(m => m.Employees[i])
}
@for(var i = 0; i < Model.EmployeesInMultipleCompanies.Count; i++)
{
  Html.Hiddenfor(m => m.EmployeesInMultipleCompanies[i])
}

Then stepped through your debugger again, you would see this now in Request.Form:

Request.Form["EmployeesInMultipleCompanies[0]"]
Request.Form["EmployeesInMultipleCompanies[1]"]

etc...

Now the model binder can do work.

Alternatively, you could create a custom model binder that knows how to read the fields from a single input.


I have created a Gist to demonstrate the different types of possible results when doing this kind of ModelBinding with a generic IList here: https://gist.github.com/xDaevax/bd35b5d88fed03709d30

Upvotes: 1

Alex Art.
Alex Art.

Reputation: 8781

Replace it with the following:

@foreach(var i = 0; i < Model.Employees.Count; i++)
{
  Html.Hiddenfor(m => m.Employees[i])
}

The same should be done for EmployeesInMultipleCompanies

Upvotes: 2

Trucktech
Trucktech

Reputation: 358

Values that are inserted to hidden fields are serialized to a string to do so.

IList<T> is not serializable because it is an interface and and therefore cannot be stored in a hidden field.

See this question for some more detail

How about you convert your IList<T> to a regular list?

Upvotes: 1

Related Questions