4

I am trying to post parameters to a controller action when a field changes via AJAX but finding the first parameter is always null.

The controller action I am trying to post to is:

public ActionResult Model_GroupCompanyIDChanged(ActionViewModel vm, int oldVal, int newVal)
     {
            vm.Model.Observation = "Changed from " + oldVal + " to " + newVal;

            return Refresh(vm);
     }

My post code serializes the form on the screen and posts it along with the old and new values for the field that just changed. The old and new value parameters are being set correctly in the action parameters but the vm parameter is always null in the controller action. There are no issues with the controller URL as I can debug the action and see it is called.

function ChangeRefreshScreen(ControllerURL, formActionUrl, oldVal, newVal) {
    var origModel = $("form[action='" + formActionUrl + "']").serialize();
    var data = {
        "oldVal": oldVal
        , "newVal": newVal
        , "vm": origModel

    };

    RefreshScreenPassData(ControllerURL, formActionUrl, data);
}

function RefreshScreenPassData(ControllerURL, formActionUrl, data) {
    alert(data);
    $.ajax({
        url: ControllerURL,
        type: 'POST',
        data: data,
        success: function (response) {
            RefreshScreenContent(response);
        },
        error: AjaxError
    });

}

Contrast that to the following on the same screen which IS working for handling when a certain field is clicked:

Controller Action:

public ActionResult Model_ActionDateClicked(ActionViewModel vm)
        {
            vm.Model.Observation = "Clicked";

            return Refresh(vm);
        }

JavaScript (Uses same RefreshScreenPassData function from above to do the actual POST):

function ElementClickedRefresh(ControllerURL, formActionUrl) {
    var origModel = $("form[action='" + formActionUrl + "']").serialize();
    RefreshScreenPassData(ControllerURL, formActionUrl, $("form[action='" + formActionUrl + "']").serialize());
}

In a nutshell the first change event sample first sets the parameters into an object since there is more than one parameter and only the old and new int parameters are set server side with the ViewModel vm being null. In the second clicked sample the vm is set correctly using the serialized form.

The formActionUrl parameter is the same for both and isn't the problem because I debugged the posts and the only difference I can see is for the request body in the first sample (change event) the vm parameter looks URL encoded:

oldVal=0&newVal=02&vm=__RequestVerificationToken%3DoeQx0RyQ-nOPQ1namoIeRuAWRzfVltfPx5ntsdpuaFTSdADuG_eC__Y54hlWmf8AOAigvyH8R_6qP77bJr1Mm5Yag7a7R9oSQse6e9NJMYg1%26ViewMode%3DEdit%26Model.ActionDate%3D11%252F07%252F2013%2B09%253A32%253A08%26Model.Observation%3DClicked%26Model.OriginatorUserID%3D1%26Model.ActiononUserID%3D0%26Model.ActionStatusID%3D1%26Model.ActionTypeID%3D0%26Model.ClientID%3D23%26Model.ClientContactID%3D0%26Model.PriorityID%3D0%26Model.CloseOutDate%3D%26Model.ActionNotes%3D%26Model.ProgressNotes%3D%26Model.Comments%3D%26Model.BusinessUnitID%3D0%26Model.GroupCompanyID%3D02%26Model.Confidential%3Dfalse%26Model.Id%3D1%26Model.Archived%3Dfalse%26Model.AddedUserID%3D1%26Model.AddedDateTime%3D11%252F07%252F2013%2B09%253A32%253A19%26Model.ModifiedUserID%3D1%26Model.ModifiedDateTime%3D11%252F07%252F2013%2B09%253A32%253A19

but for the second clicked event which is working it is not:

__RequestVerificationToken=oeQx0RyQ-nOPQ1namoIeRuAWRzfVltfPx5ntsdpuaFTSdADuG_eC__Y54hlWmf8AOAigvyH8R_6qP77bJr1Mm5Yag7a7R9oSQse6e9NJMYg1&ViewMode=Edit&Model.ActionDate=11%2F07%2F2013+09%3A32%3A08&Model.Observation=Changed&Model.OriginatorUserID=1&Model.ActiononUserID=0&Model.ActionStatusID=1&Model.ActionTypeID=0&Model.ClientID=23&Model.ClientContactID=0&Model.PriorityID=0&Model.CloseOutDate=&Model.ActionNotes=&Model.ProgressNotes=&Model.Comments=&Model.BusinessUnitID=0&Model.GroupCompanyID=0&Model.Confidential=false&Model.Id=1&Model.Archived=false&Model.AddedUserID=1&Model.AddedDateTime=11%2F07%2F2013+09%3A32%3A19&Model.ModifiedUserID=1&Model.ModifiedDateTime=11%2F07%2F2013+09%3A32%3A19

I tried to set my parameter string manually by changing the javascript to :

function ChangeRefreshScreen(ControllerURL, formActionUrl, oldVal, newVal) {
    var origModel = $("form[action='" + formActionUrl + "']").serialize();
    var data = "oldVal=" + oldVal + "&newVal=" + newVal + "&vm=" + origModel
    RefreshScreenPassData(ControllerURL, formActionUrl, data);
}

and the POST request body changes to

oldVal=02&newVal=023&vm=__RequestVerificationToken=xiYEcz53UNPVoGZ3RQGO_HFn54LIu0bTjQB-PB13tTEWZ7vUHMbsW25s7rI7D7lBLtACutEpynoNnk66jxijzSzFMCBO_nDoXf_FqsR9Cc81&ViewMode=Edit&Model.ActionDate=11%2F07%2F2013+09%3A32%3A08&Model.Observation=Clicked&Model.OriginatorUserID=1&Model.ActiononUserID=0&Model.ActionStatusID=1&Model.ActionTypeID=0&Model.ClientID=23&Model.ClientContactID=0&Model.PriorityID=0&Model.CloseOutDate=&Model.ActionNotes=&Model.ProgressNotes=&Model.Comments=&Model.BusinessUnitID=0&Model.GroupCompanyID=023&Model.Confidential=false&Model.Id=1&Model.Archived=false&Model.AddedUserID=1&Model.AddedDateTime=11%2F07%2F2013+09%3A32%3A19&Model.ModifiedUserID=1&Model.ModifiedDateTime=11%2F07%2F2013+09%3A32%3A19

but it still does not bind the view model parameter.

What am I doing wrong that isn't setting the parameter when it comes from a JavaScript object?

UPDATE 1

I have tried manually calling JSON stringify in an attempt to get this working but it still doesn't work:

function ChangeRefreshScreen(ControllerURL, formActionUrl, oldVal, newVal) {
    var origModel = $("form[action='" + formActionUrl + "']").serialize();
    var data = "oldVal=" + oldVal + "&newVal=" + newVal + "&vm=" + JSON.stringify(origModel)
    RefreshScreenPassData(ControllerURL, formActionUrl, data);
}

UPDATE 2

The following worked for me where it adds the old and new vals to the form but needs to remove them again otherwise the next time they end up in the form more than once which screws the old and new vals:

function ChangeRefreshScreen(ControllerURL, formActionUrl, oldVal, newVal) {
    var $myForm = $("form[action='" + formActionUrl + "']");
    $myForm.append("<input type='hidden' name='oldVal' value='" + oldVal + "' id='oldVal' />");
    $myForm.append("<input type='hidden' name='newVal' value='" + newVal + "' id='newVal' />");
    var origModel = $myForm.serialize();
    RefreshScreenPassData(ControllerURL, formActionUrl, origModel);
    $('#oldVal').remove();
    $('#newVal').remove();

}

It would be better to not add the elements to the DOM but at least it's a small piece of code and is not CPU intensive. I am still hoping to take another attempt at one of Gordatron's solutions and see if I can get it working but may not have time.

Alan Macdonald
  • 1,872
  • 20
  • 36
  • just a strange question but have you tried adding the timestamp to the ajax call ? – Pogrindis Sep 09 '13 at 14:53
  • Thanks. We tried adding cache: false in the Ajax get ooptions for JQuery and verified it adds a Timestamp to the request but it never made a difference – Alan Macdonald Sep 09 '13 at 15:16

3 Answers3

1

Method 1

It seems as if tampering with form data isn't working for you.

How about you try the following

function ChangeRefreshScreen(ControllerURL, formActionUrl, oldVal, newVal) {
    var $myForm = $("form[action='" + formActionUrl + "']");
    $myForm.append("<input type='hidden' name='oldVal' value='" + oldVal + "' />");
    $myForm.append("<input type='hidden' name='newVal' value='" + newVal + "' />");
    var origModel = $myForm.serialize();
    RefreshScreenPassData(ControllerURL, formActionUrl, origModel);
}



Update

Method 2

Although, the above solution will work in your case (because it worked in a similar case with me), there is another way of doing things here.

There are two formats in which to POST data to the server. One is form encoding, that you are achieving using $("form[action='" + formActionUrl + "']").serialize() and the other is using JSON.

In your question above, you are using a mixture of both

var data = {
    "oldVal": oldVal,
    "newVal": newVal,
    "vm": origModel
};

i.e. In a JSON object, you are adding a serialized form as vm. This is the reason why you are getting a null in vm in your action while oldVal and newVal are properly populated.

I'd suggest that if you do not wish to use the solution provided above in my answer, you should go with JSON only approach. i.e. create a JSON object with your form values and post that using

$.ajax({
    type: 'POST',
    url: ControllerURL,
    contentType: "application/json",
    data: JSON.stringify(data),   
    success: function (response) {
         RefreshScreenContent(response);
    },
    error: AjaxError
});

Notice: contentType: "application/json"

I know that getting all the values of form using id/names and creating a JSON object will be a little exhausting but fortunately for you I wrote a small toJSON plugin some time back that does that for you.

All you need to do is following

var form = $("form[action='" + formActionUrl + "']").toJSON();

var data = {
    "oldVal": oldVal,
    "newVal": newVal,
    "vm": form 
};

Hope this helps

U.P
  • 7,357
  • 7
  • 39
  • 61
  • Thanks. I can give that a try but given we will be using this technique on many screens I would have to either declare the old and new values on each ViewModel object or add them as type Object to my base ViewModel class (or use generics) so that the values can be bound to ViewModel properties wouldn't I? That doesn't necessarily mean I won't do it, it's just getting slightly less clean and I want to be sure there isn't a better solution. – Alan Macdonald Jul 23 '13 at 09:45
  • What I missed with this answer was 2 things. 1. Adding values to the form will bind with parameters on an Action without requiring adding new properties to the ViewModel. 2. I didn't think the way the elements were added to the form was right and it wasn't, because it keeps adding items to the form so the second time a user changes. I'm not quite sure how this happens because the form html is replaced with the view html from the action which does not have . For that reason they need to be removed. I will edit the answer to reflect this. – Alan Macdonald Sep 10 '13 at 09:45
1

I did a little test to see if i could re-create your issue. I managed to and I managed to get it to work but I dont know if its the best way to do it or what you want.

To recreate it I created a test app and added the model:

public class Item {
    public string id {get;set;}
    public string name { get; set; }
}

the view:

<h2>Test</h2>
@using(Html.BeginForm()){

@Html.EditorFor(Model => Model.id)

@Html.EditorFor(Model => Model.name)

}

with the js:

$(function () {

    var origModel = $("form").serialize();
    var data = {
        "oldVal": 'old'
        , "newVal": 'new'
        , "vm": origModel
    };

    RefreshScreenPassData("/Home/Test", data);
});

function RefreshScreenPassData(ControllerURL, data) {
    alert(data);
    $.ajax({
        url: ControllerURL,
        type: 'POST',
        data: data,
        dataType:'json',
        success: function (response) {
            RefreshScreenContent(response);
        },
        error: AjaxError
    });

}

function AjaxError(e) {
    alert(e);
}

Controller:

    [HttpGet]
    public ActionResult Test() {
        return View(new Item() { id = "1", name = "name" });
    }

    [HttpPost]
    public ActionResult Test(Item vm, string oldVal,string  newval) {
        return View(new Item() { id = vm.id+"oo", name = vm.name+"000" });
    }

What I found out:

Manual submission of this does not work

$.ajax({
        url: ControllerURL,
        type: 'POST',
        data: {
                "oldVal": 'old'
            , "newVal": 'new'
            , "vm": {"id":"1","name":"name"}
            },
        success: function (response) {
            RefreshScreenContent(response);
        },
        error: AjaxError
    });

your first parameter on the action is not null but its properties are null (i.e. vm is not null but vm.id and vm.name are)

this DOES WORK:

 $.ajax({
        url: ControllerURL,
        type: 'POST',
        data: {
                "oldVal": 'old'
            , "newVal": 'new'
            , "id": "1"
            , "name": "name"
            },
        success: function (response) {
            RefreshScreenContent(response);
        },
        error: AjaxError
    });

NEXT PROBLEM:

that the .serialise method on a form is returning a url parameter type string:

id=1&name=name

not json. so i quickly search for a way to get json form a form and found this (Convert form data to JavaScript object with jQuery).. so I then fudged this together:

jQuery(function () {

    var data = {
        "oldVal": 'old'
        , "newVal": 'new'

    };

    var o = {};
    var a = $("form").serializeArray();

    $.each(a, function () {
        if (o[this.name] !== undefined) {
            if (!o[this.name].push) {
                o[this.name] = [o[this.name]];
            }
            o[this.name].push(this.value || '');
        } else {
            o[this.name] = this.value || '';
        }
    });

    jQuery.extend(data, o);

    RefreshScreenPassData("/Home/Test", data);
});

function RefreshScreenPassData(ControllerURL, data) {
    alert(JSON.stringify(data));
    $.ajax({
        url: ControllerURL,
        type: 'POST',
        data: data,
        success: function (response) {
            alert(response);
        },
        error: AjaxError
    });

}

It just converts the form to an object and then uses extend to "merge" it with the other properties you are wanting to pass back.

This is not really a solution to your problems but i hope it gives enough to move you on.

Community
  • 1
  • 1
  • This gives me an Ajax error with Internal Server Error as well! – Alan Macdonald Sep 09 '13 at 16:36
  • I think that is because you need ot add a Prefix parameter to the model binder.. see other answer –  Sep 10 '13 at 08:07
  • ;) not sure what doesn't work for you using the example structure in my other answer the view model with model property is working.. it generates this json that the server interprets perfectly:{"oldVal":"old","newVal":"new","model.id":"1","model.name":"name","OtherField":"bubba"} –  Sep 11 '13 at 15:53
  • Note there are properties on the ViewModel directly that need to come through not just the inner Model property e.g. ViewModel.Mode AND ViewModel.Model.SomeProperty and is a framework component so cannot assume the structure is a ViewModel with a Model property. – Alan Macdonald Sep 13 '13 at 15:51
0

other option don't send json.. just serialise all the way.

model:

public class Item {
    public string id {get;set;}
    public string name { get; set; }
}

viewModel:

public class ViewModel {
    public Item model { get; set; }
    public String CapsName { get { return this.model.name.ToUpper(); } }
    public String OtherField { get; set; }
}

Controller:

    [HttpGet]
    public ActionResult Test2() {
        return View("ViewModelTest",new ViewModel() { 
                                  model = new Item() {
                                           id = "1"
                                           , name = "name" 
                                       }
                                  , OtherField="bubba"});
    }

    [HttpPost]
    public ActionResult Test2(ViewModel vm, string oldVal, string newval) {
        return View(new Item() { 
                      id = vm.model.id + "oo"
                       , name = vm.model.name + "000" 
                      });
        //pointless code to allow me to put in a break point
    }

View:

    @using(Html.BeginForm()){

        @Model.CapsName<br />

        @Html.EditorFor(m => m.model.id)

        @Html.EditorFor(m => m.model.name)

        @Html.EditorFor(m=>m.OtherField)
        }

JS:

        jQuery(function () {

            var data = {
                "oldVal": 'old'
                , "newVal": 'new'
            };
            RefreshScreenPassData("/Home/Test2", 
                        $.param(data) + "&" 
                        + $("form").serialize());
        });


        function RefreshScreenPassData(ControllerURL, data) {
            alert(data);
            $.ajax({
                url: ControllerURL,
                type: 'POST',
                data: data,
                success: function (response) {
                    alert(response);
                },
                error: AjaxError
            });

        }

the data that this posts is:

oldVal=old&newVal=new&model.id=1&model.name=name&OtherField=bubba 

and then the breakpoint is hit i have a full viewmodel, model and old and new values as expected.

  • though the net effect is very similar to what comet is proposing –  Sep 09 '13 at 15:23
  • This gives me an Ajax error with Internal Server Error. function ChangeRefreshScreen(ControllerURL, formActionUrl, oldVal, newVal) { var origModel = $("form[action='" + formActionUrl + "']").serialize(); var data = { "oldVal": oldVal , "newVal": newVal }; RefreshScreenPassData(ControllerURL, formActionUrl, $.param(data) + "&" + origModel); } – Alan Macdonald Sep 09 '13 at 16:21
  • WHich is a shame because this is a far preferrable solution if I can get it working – Alan Macdonald Sep 09 '13 at 16:21
  • Is that not a .net error? - any more info available? is it to do with model binding? maybe add the prefix? ;) –  Sep 10 '13 at 07:30
  • i updated this answer to be my best understanding of your current situation... without anything else –  Sep 10 '13 at 12:47