0

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" })
    ....
}
Tieson T.
  • 20,774
  • 6
  • 77
  • 92
Keith Harris
  • 1,118
  • 3
  • 13
  • 25
  • It's `$("#form").serialize()` (not `.serializeArray()`) and remove `contentType: 'application/json; charset=utf-8',` and do not stringify (just use `data: $("#form").serialize(),`) –  Sep 17 '16 at 10:35
  • This doesn't work (I get error "Not found") because it creates a query string of key and values like this: AccessVM.Id=1&AccessVM.AccessMode=1&AccessVM.LoginFailLimit=1&AccessVM.PwdWCap=false&AccessVM.PwdWNum=false&AccessVM.PwdWSC=false&AccessVM.PwdMinLen=3... The Web API endpoint expects an object of type "policy" to be sent to it (which means a JSON object). But if I could somehow take THIS string and turn it into a JSON object that would be great! – Keith Harris Sep 18 '16 at 05:57
  • What is you `Policy` model. If it contains a complex property named `AccessVM` which contains properties `Id`, `AccessMode` etc, it will bind fine. And show part of the view and the controller (based on what you saying works correctly, then the model in the POST method is not the same as the model in the view (which it should be) –  Sep 18 '16 at 06:08
  • Technically, there is no "post" method but for our purposes you can think of it that way. The view is strongly typed (based on the same model the API is to receive). The problem I'm having is getting the object in the proper format. See my edited question for details... – Keith Harris Sep 18 '16 at 06:43
  • The model in your view is `PolicyViewModel` so the parameter in the method you posting to needs to be `PolicyViewModel` (not `AccessItemViewModel`) –  Sep 18 '16 at 06:50
  • No, because PolicyViewModel is a compounded View Model (a View Model of View Models). PolicyViewModel CONTAINS AccessItemViewModel in the form of "AccessVM". If what you say is true then my original code would not work at all! – Keith Harris Sep 18 '16 at 06:59
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/123625/discussion-between-stephen-muecke-and-keith-harris). –  Sep 18 '16 at 07:01

1 Answers1

1

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