jqueryasp.net-mvcasp.net-mvc-5serializearraywebapi2

Serialize form data for Web Api put request MVC 5


I have a strongly-typed Razor View that shows a record for a class. This view displays a form that the user can use to update the record (a dropdown, a few checkboxes, and a few textboxes). When the user modifies a field by changing the value of a control, an ajax call is immediately sent to the Web API to update that field. The following Ajax call works fine:

$(document).ready(function () {
    $(this).on("change", function (e) {
        var editRecordURL = $("#editRecordURL").val();
        var key = $("#AccessVM_Id").val();
        var mode = $("#AccessVM_AccessMode").val();
        var failLim = $("#AccessVM_LoginFailLimit").val();
        var cap = $("#AccessVM_PwdWCap").is(':checked');
        ...
        var rec = {
            Id: key,
            AccessMode: mode,
            LoginFailLimit: failLim,
            PwdWCap: cap,
            ....
        };

        $.ajax({
            type: "PUT",
            url: editRecordURL,
            data: JSON.stringify(rec),
            contentType: 'application/json; charset=utf-8',
            success: function (msg) {
                bootbox.alert("Success: Record updated successfully!");
            },
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                bootbox.alert("Error: " + errorThrown);
            }
        });
    });
});

However, I would like to make this function more generic so it can be reused for other forms (especially those with more data to update). Ideally the rec object would be populated like this:

var rec = $("#form").serializeArray();

Unfortunately this does not work because after applying JSON.stringify() on rec it does not return the expected JSON string. Instead it sends a series of name/value pairs like this:

[{"name":"__RequestVerificationToken","value":"qVedWHJ6HIrqtLJpTxp4m5D2ehZ_AdjCOvQtz4Jyzlg0cdocsWqcTCiE2jzIEB7UsJPwuSZeZF7y1GsluHNrNCDV1wrHjU1UJO5vMMGTLB41"},{"name":"AccessVM.Id","value":"1"},{"name":"AccessVM.AccessMode","value":"1"},{"name":"AccessVM.LoginFailLimit","value":"10"},{"name":"AccessVM.PwdWCap","value":"true"},{"name":"AccessVM.PwdWCap","value":"false"},{"name":"AccessVM.PwdWNum","value":"true"},{"name":"AccessVM.PwdWNum","value":"false"},{"name":"AccessVM.PwdWSC","value":"true"},{"name":"AccessVM.PwdWSC","value":"false"},{"name":"AccessVM.PwdMinLen","value":"6"},{"name":"AccessVM.PwdMaxLen","value":"25"},{"name":"AccessVM.PwdChange","value":"90"},{"name":"AccessVM.PwdPrevUsedLimit","value":"10"}]

So how do I fix this so the string looks like:

[{"AccessVM.Id": 1, "AccessVM.AccessMode": 1, "AccessVM.LoginFailLimit": 10, "AccessVM.PwdWCap": true, "AccessVM.PwdWNum": true, "AccessVM.PwdWSC": false, "AccessVM.PwdMinLen": 6, "AccessVM.PwdMaxLen": 25, "AccessVM.PwdChange": 90, "AccessVM.PwdPrevUsedLimit": 10}]

Web API Controller:

public class PoliciesController : ApiController
{
    ....
    // PUT: api/policies/1
    [ResponseType(typeof(void))]
    public IHttpActionResult PutPolicy(int id, AccessItemViewModel polDto)
    {
        ....
    }
    ....
}

View Models

public class PolicyViewModel
{
    public AccessItemViewModel AccessVM { get; set; }
    ....
}

public class AccessItemViewModel
{
    public int Id { get; set; }
    public int AccessMode { get; set; }  
    public int LoginFailLimit { get; set; } 
    public bool PwdWCap { get; set; }   
    ....
}

Razor View:

@model App.Web.ViewModels.PolicyViewModel
....
@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary("", new { @class = "text-danger" })
    <input type="hidden" id="editRecordURL" value="@Model.ExtraVM.EditRecordUrl" />
    @Html.LabelFor(m => m.AccessVM.AccessMode, new { @class = "col-xs-5" })
    @Html.HiddenFor(m => m.AccessVM.Id)
    @Html.DropDownListFor(m => m.AccessVM.AccessMode, new SelectList(Model.AccessModes, "Mode", "Description"), null, new { @class = "input-sm col-xs-7" })
    @Html.LabelFor(m => m.AccessVM.LoginFailLimit, new { @class = "col-xs-9" })
    @Html.DropDownListFor(m => m.AccessVM.LoginFailLimit, new SelectList(Model.LoginLimits, "LoginFailLimit", "Value"), null, new { @class = "input-sm col-xs-3" })
    @Html.LabelFor(m => m.AccessVM.PwdWCap, new { @class = "col-xs-11" })
    @Html.CheckBoxFor(m => m.AccessVM.PwdWCap, new { @class = "input-xs col-xs-1" })
    ....
}

Solution

  • You would need to use .serialize() (not .serializeArray() which wont work correctly with your bool properties and the DefaultModelBinder) and remove the contentType option so it uses the default application/x-www-form-urlencoded; charset=UTF-8 and do not stringify the data. Yoru script should be

    $.ajax({
        type: "PUT",
        url: editRecordURL,
        data: $('form').serialize(),
        success: function (msg) {
    

    However the model in your view is PolicyViewModel and your generating form controls based on that model, so the .serialize() function will serialize name/values pairs for PolicyViewModel. This means the model in your PutPolicy method must match. The method needs to be

    public IHttpActionResult PutPolicy(int id, PolicyViewModel model) // not AccessItemViewModel
    

    If the method parameter cannot be changed, then your view needs to be based on AccessItemViewModel or you need to use a partial view for the AccessVM property so that you generate `name attributes without the "AccessVM" prefix. For example

    _AccessVM.cshtml

    @model AccessItemViewModel
    @Html.HiddenFor(m => m.Id)
    ....
    

    and in the main view, use @Html.Action() or @Html.Partial() to generate its html