justSteve
justSteve

Reputation: 5524

Model binding in MVC core from KnockoutJS

I'm following the Pluralsight 'Parent-Child with EF, MVC, Knockout' course which was written for MVC4. I'm working against MVC Core and am banging my head against what I think is a model binding problem on a POST back to the server.

My Controller:

        public JsonResult Save(SalesOrderViewModel salesOrderViewModel)
    {
        if (ModelState.IsValid)
        {
            var salesOrder = new SalesOrder();
            salesOrder.CustomerName = salesOrderViewModel.CustomerName;
            salesOrder.PONumber = salesOrderViewModel.PONumber;
            _context.SalesOrder.Add(salesOrder);
            _context.SaveChanges();

            return Json(JsonConvert.SerializeObject(salesOrderViewModel));
        }
        else
        {
            return Json(JsonConvert.SerializeObject(ModelState));
        }
    }

//ModelState is coming in 'Valid' but all values are null.

My ajax call:

 SalesOrderViewModel = function (data) {
    var self = this;
    ko.mapping.fromJS(data, {}, self);

    console.log(self);

    self.save = function () {
        console.log(self);
        console.log(ko.toJSON(self));
        debugger;
        $.ajax({
            url: "/Sales/Save/",
            type: "POST",
            data: ko.toJSON(self),
            headers: {
                "contentType": "application/json"
            },

            success: function (data) {
                if (data.salesOrderViewModel)
                    ko.mapping.fromJS(data.salesOrderViewModel, {}, self);
            },
            always: function (data) {
                console.log(data);
            }
        });
    }
};  

console.log on the ajax postback is:

postback data: {"SalesOrderId":0,"CustomerName":null,"PONumber":null}

Checking Chrome's report on FormData shows: {"SalesOrderId":0,"CustomerName":"Steve","PONumber":"PO","MessageToClient":null,"__ko_mapping__":{"ignore":[],"include":["_destroy"],"copy":[],"observe":[],"mappedProperties":{"SalesOrderId":true,"CustomerName":true,"PONumber":true,"MessageToClient":true},"copiedProperties":{}}}

console.logs on the pre-post shows some strangeness re: 'self' vs. 'ko.toJSON(self)' but i'm not familiar with KO so not sure how strange it is.

enter image description here

I've seen a couple questions related to model binding and the error message I saw in the locals window ('((Microsoft.AspNetCore.Http.Internal.DefaultHttpRequest)this.Request).Form' threw an exception of type 'System.InvalidOperationException').

Changing

contentType: "application/json"

to

    headers: {
        "ContentType": "application/json"
    },

eliminated the error in Locals but did not change the behavior. Controller's model is still showing null values.

Another answer suggested adding '[FromBody]' (also '[FromForm]) in the signature -- neither helped me out. hoping you can!

Upvotes: 1

Views: 1128

Answers (2)

piedatt80
piedatt80

Reputation: 27

I'm following your own course. And, like you, I noticed that there are some problems going to Asp.net Core.

My Controller:

public JsonResult Save([FromBody]SalesOrderViewModel salesOrderViewModel)
        {
            SalesModel salesOrder = new SalesModel();
            salesOrder.CustomerName = salesOrderViewModel.CustomerName;
            salesOrder.PONumber = salesOrderViewModel.PONumber;

            _context.SalesModels.Add(salesOrder);
            _context.SaveChanges();

            salesOrderViewModel.MessageToClient = string.Format("{0}’s sales order has been added to the database.", salesOrder.CustomerName);

            return Json(new { salesOrderViewModel });
        }

My Knockout Script:

SalesOrderViewModel = function (data) {
    var self = this;
    ko.mapping.fromJS(data, {}, self);

    self.save = function () {
        $.ajax({
            url: "/Sales/Save/",
            type: "POST",
            data: ko.toJSON(self),
            contentType: "application/json",
            success: function (data) {
                if (data.salesOrderViewModel != null) {
                    ko.mapping.fromJS(data.salesOrderViewModel, {}, self);
                }
            }
        });
    }
};

My Create Page:

@model SolutionName.ViewModels.SalesOrderViewModel
@using Newtonsoft.Json

@{
    ViewBag.Title = "Create a Sales Order";
}

@{
    string data = JsonConvert.SerializeObject(Model);
    }

@section scripts{
    <script src="~/lib/knockout/dist/knockout.js"></script>
    <script src="~/lib/knockout-mapping/build/output/knockout.mapping-latest.js"></script>
    <script src="~/lib/Scripts/SalesOrderViewModel.js"></script>
    <script type="text/javascript">
        var salesOrderViewModel = new SalesOrderViewModel(@Html.Raw(data));
        ko.applyBindings(salesOrderViewModel);
    </script>
}

<h2>@ViewBag.Title</h2>

<p data-bind="text: MessageToClient"></p>

<div>
    <div>
        <label>Customer Name:</label>
        <input data-bind="value: CustomerName" />
    </div>
    <div>
        <label>P.O. Number:</label>
        <input data-bind="value: PONumber" />
    </div>
</div>

<p><button data-bind="click: save">Save</button></p>

<p>
    @Html.ActionLink("Back to List", "Index")
</p>

I used "FromBody" in my Controller and the data saved in the database works correctly. What does not work, however, is that after pressing the button saves the view does not update correctly. I understand that with asp.net Core, this piece of code does not work properly:

ko.mapping.fromJS(data.salesOrderViewModel, {}, self);

Upvotes: 0

Chris
Chris

Reputation: 472

I think you are sending the wrong data up to the controller action when you use ko.toJSON(self). This is converting the entire ko model to Json. Instead try sending just the mapped values with ko.mapping.toJSON(self)

Try:

SalesOrderViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);

console.log(self);

self.save = function () {
    console.log(self);
    console.log(ko.toJSON(self));
    debugger;
    $.ajax({
        url: "/Sales/Save/",
        type: "POST",
        data: ko.mapping.toJSON(self),
        headers: {
            "contentType": "application/json"
        },

        success: function (data) {
            if (data.salesOrderViewModel)
                ko.mapping.fromJS(data.salesOrderViewModel, {}, self);
        },
        always: function (data) {
            console.log(data);
        }
    });
}
};  

Alternatively you could just send this data, which is passed into the function, assuming it contains only the values you wish to post. data: data

Upvotes: 1

Related Questions