MVC-binder fails to bind complex-viewmodel-propert

2019-09-11 04:09发布

问题:

I'm using Umbraco MVC to build a form four our webpage. I have a ViewModel which is actually a property to another ViewModel, and is populated using a partial-view in the razor-form below. For whatever reason, the sub-viewmodel (RequiredHealthInputData, se below) is always null after binding! The binder will successfully bind all the form values to my main viewmodel but my sub-viewmodel will remain null!

(I have research many similar questions regarding binding of complex types but I believe but none of the solutions I've found so far (prefixing, renaming my properties to support routing etc) works.

I have a PtjInsurancePickerViewModel.cs (the main viewmodel) which looks as follows:

public class PtjInsurancePickerViewModel : BaseViewModel, IValidatableObject
{
    [Required]
    public string InsuranceNameStr { get; set; } = "-1";

    public HealthDeclarationModel RequiredHealthInputData { get; set; }
}

HealthDeclarationModel is a ViewModel as well and has the following values:

public class HealthDeclarationModel : BaseViewModel, IValidatableObject
{
    [Display(Name = "Extra comment")]
    public string ExtraComments { get; set; }

    [Display(Name = "EnsurancePTName")]
    public string EnsurancePTName { get; set; }
}

And I have the a view InsurancePicker.cshtml with the following form:

@using (Html.BeginUmbracoForm<InsurancePickerSurfaceController>("ProcessSelections"))
{
    <h3>Select insurance</h3>
    @Html.Partial("../Partials/InsurancePicker/_vsFamilyInsuranceProtection", Model)

    <h3>Select extra options</h3>
    @Html.Partial("../Partials/_Healthdeclaration", Model.RequiredHealthInputData)

    <h3>Send!</h3>
    <input type="submit" class="btn btn-success" value="Send!" title="Confirm choices"/>
}

Note that the form consists of two partials, one which gets the PtjInsurancePickerViewModel and one which gets the HealthDeclarationModel which is a property of PtjInsurancePickerViewModel

Here is the partial for my HealthDeclarationModel:

        <div class="HealthDeclarationModel-Panel">

            <strong>2. Är du fullt arbetsför?</strong>
            @Html.DropDownListFor(x => x.EnsurancePTName, Model.YesNoLista, new { @class = "form-control", @id = "EnsurancePTNameSelect" })
            @Html.ValidationMessageFor(x => x.EnsurancePTName)


            @Html.TextAreaFor(x => x.ExtraComments, new { @class = "form-control", style = "max-width:100%; min-width: 100%;" })
            @Html.ValidationMessageFor(x => x.ExtraComments)

        </div>

(Note. I give the input fields in my partial special IDs (used by jquery for statemanagement). Perhaps this could be part of the issue? I've tried submitting with the Ids prefixed with the model name "HealthDeclarationModel .EnsurancePTNameSelect" instead of just "EnsurancePTNameSelect" for example, but to no avail)

The submit function leads to the follow method:

[Authorize(Roles = "Forsakrad")]
public class InsurancePickerSurfaceController : BaseSurfaceController
{

    [HttpPost]
    public ActionResult ProcessSelections(PtjInsurancePickerViewModel filledModel)
    {
        //Checks if the very important RequiredHealthInputData submodel is null

        if (filledModel.RequiredHealthInputData == null)
        {
            throw new ApplicationException("Critical data missing!!");
        }
        else
        {
            DoImportantStuffWithRequiredHealthInputData(filledModel.RequiredHealthInputData)
        }

        return CurrentUmbracoPage();
    }
}

Now here's my problem. RequiredHealthInputData will always be null as the MVC binder for can't bind HealthDeclarationModel to PtjInsurancePickerViewModel. The result of the default binder will look something like this:

    {IndividWebb.Models.PtjInsurancePickerViewModel}
       InsuranceNameStr: "0"
       HalsodeklarationDataModel: null

However, the form collection will look something like this:

    controllerContext.HttpContext.Request.Form.AllKeys
    {string[17]}
       [2]: "InsuranceNameStr"
       [9]: "EnsurancePTName"
       [12]: "ExtraComments"
        14]: "HalsodeklarationDataModel"

    (Some results have been excluded)

Also note that EnsurancePTName which is an input that belongs to HalsodeklarationDataModel has the id EnsurancePTName and not HalsodeklarationDataModel.EnsurancePTName.

I have really no idea how to proceed with this or even how to debug it. I've just a customed helper to verify that the binding does indeed exclude all values from HalsodeklarationDataModel

回答1:

The reason that is does not bind is because your use of @Html.Partial("../Partials/_Healthdeclaration", Model.RequiredHealthInputData) is generating form controls with name attributes that do not relate to PtjInsurancePickerViewModel, for example, it generates

<textarea name="ExtraComments" ....>

where as it needs to be

<textarea name="RequiredHealthInputData.ExtraComments" ....>

One option is to pass the HtmlFieldPrefix to the partial as AdditionalViewData

@Html.Partial("../Partials/_Healthdeclaration", Model.RequiredHealthInputData, 
new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "RequiredHealthInputData" }})

Refer also getting the values from a nested complex object that is passed to a partial view that include the code for a HtmlHelper extension method to make this simpler.

However the preferred approach is to use an EditorTemplate. Rename your partial to HealthDeclarationModel.cshtml (i.e. to match the name of the class) and move it to the /Views/Shared/EditorTemplates folder (or /Views/yourControllerName/EditorTemplates folder), and then in the view use

@Html.EditorFor(m => m.RequiredHealthInputData)

which will generate the correct name attributes.



回答2:

Change your _Healthdeclaration view so that it uses the PtjInsurancePickerViewModel as a view model. In short pass the same model to all partials to ensure correct input names.