Search code examples
asp.net-mvcrazorknockout.jsknockout-mapping-plugin

Knockout Nested List Post Null


I'm trying integrate ko on one of my razor views. I've researched quite a bit yesterday but haven't been able to find a similar solution for my problem.

I have a record model:

public class Record
{
    public Int Request Id {get;set;}
    public string RecordName {get;set;}
    public Person Person {get;set;}
    public IList<Person> People { get; set; }        
}

And a Person Model:

public class Person
{        
    public int Id { get; set; }    
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public IList<Alias> Aliases { get; set; }
}

My View:

@model Record
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@using (Html.BeginForm("PostRequest", "Request", FormMethod.Post))
{
@Html.LabelFor(m => m.Person.FirstName)
@Html.TextBoxFor(m => m.Person.FirstName)

<h5>People</h5>
<table>
    <tbody data-bind="foreach: People">
        <tr>
            <td>@Html.LabelFor(m => m.Person.FirstName)</td>
            <td><input type="text" data-bind="value: FirstName, attr: {name:     'People[' + $index() + '].FirstName'}" /></td>
            <td>@Html.LabelFor(m => m.Person.LastName)</td>
            <td><input type="text" data-bind="value: LastName, attr: {name: 'People[' + $index() + '].LastName'}" /></td>
        </tr>

        <tr>                
            <td><button id="removePerson" data-bind="click: $root.removePerson">Remove Person</button></td>
        </tr>
        <tr>
            <td>
                <button id="addAlias" data-bind="click: addAlias">Add Alias</button>
            </td>
        </tr>
        <!-- ko foreach: Aliases -->
        <tr>
            <td>@Html.LabelFor(m => m.Alias.FirstNameAlias)</td>
            <td><input type="text" data-bind="value: FirstNameAlias, attr: {name: 'Aliases[' + $index() + '].FirstNameAlias'}" /></td>
            <td>@Html.LabelFor(m => m.Alias.LastNameAlias)</td>
            <td><input type="text" data-bind="value: LastNameAlias, attr: {name: 'Aliases[' + $index() + '].LastNameAlias'}" /></td>
            <td><button id="removeAlias" data-bind="click: $root.removeAlias">Remove Alias</button></td>
        </tr>
        <!-- /ko -->
    </tbody>
</table>

<button id="addPerson" data-bind="click: addPerson">Add Person</button>
<button>Submit</button>
<input id="clickMe" type="button" value="clickme" onclick="submit();" />

}

And Script:

@section scripts
{

<script type="text/javascript">
    $(function () {

        var personItem = function () {
            var self = this;

            self.LastName = ko.observable();
            self.FirstName = ko.observable();
            self.Aliases = ko.observableArray();

        };

        var model = ko.mapping.fromJS(@Html.Raw(Model.ToJson()));

        alert('The length of the array is ' + model.People().length);
        alert('The first element is ' + model.People()[0].Aliases().length);
        alert(ko.toJSON(model));
        model.addPerson = function () {
            model.People.push(new personItem());
        };

        model.removePerson = function (person) {
            model.People.remove(person);
        };

        ko.applyBindings(model);
    })

    function submit() {
        $.ajax({
            url: '/Request/PostRequest',
            type: 'POST',
            contentType: 'application/json; charset=utf-8',
            data: ko.toJSON(model),

            success: function (status) {
                alert(status);

            }
        });
    };
</script>
}

The Controller action that GETs this view pre initializes some data for the record model, so the binding appears to be working properly, however when I post data, the data for Alias List comes back Null.

Controller:

    public ActionResult CreateRequest(Record viewModel)
    {
        var list = new Record
        {

            People = new List<Person> {

                    new Person {FirstName = "My First Person", Aliases = new List<Alias> { new Alias{FirstNameAlias = "firstnamealias",LastNameAlias = "lastnamealias1"}},},
                    new Person {FirstName = "My Second Person", Aliases = new List<Alias> { new Alias{FirstNameAlias = "firstnamealias2", LastNameAlias ="lastnamealias2"}},}

                },
            Person = new Person()
            {
                FirstName = "me"
            }

        };


        return View(list);
    }
    [HttpPost]
    public JsonResult PostRequest(Record viewModel)
    {


        return Json(String.Format("'Success':'false','Error':'"));
    }

I feel that I am missing something with ko.mapping. But so close, as the data populates from the GET controller action, but fails to POST.

SOLUTION

I updated my KO script, and the way I was rendering the ko data in the view. This has solved my problem.

Updated Script

<script>


var initialData = @Html.Raw(Serialize(Model.People));

var BackgroundModel = function(people) {
    var self = this;
    self.people = ko.mapping.fromJS(people);


    self.addPerson = function() {
        self.people.push({
            FirstName: "",
            LastName: "",
            Aliases: ko.observableArray()
        });
    };

    self.removePerson = function(person) {
        self.people.remove(person);
    };

    self.addAlias = function(person) {
        person.Aliases.push({
            //todo
            FirstNameAlias: "",
            LastNameAlias: ""
        });
    };

    self.removeAlias = function(Alias) {
        $.each(self.people(), function() { this.Aliases.remove(Alias) })
    };

    self.save = function() {
        self.lastSavedJson(JSON.stringify(ko.toJS(self.people), null, 2));
        var subModel = JSON.stringify(ko.toJS(self));
        $.ajax({
            url: '/Request/PostRequest',
            type: 'POST',
            contentType: 'application/json; charset=utf-8',
            data: subModel,

            success: function (status) {
                alert(status);

            }
        });
    };

    self.lastSavedJson = ko.observable("")
};

ko.applyBindings(new BackgroundModel(initialData));

</script>

Updated View

            <div data-bind="foreach: people">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h4 class="panel-title"> <a data-bind="attr:{href:'#collapseOne'+$index()}, text:FirstName() +' '+ LastName()" @*href="#collapse_One"*@ class="accordion-toggle" data-toggle="collapse" data-parent="#accordion"></a> </h4>
                    </div>
                    <div data-bind="attr:{id:'collapseOne'+$index()}" class="panel-collapse collapse in" @*data-bind="attr:{id:_collapseId, class:_collapsedIn}"*@>
                        <div class="panel-body">
                            <table>
                                <tbody>
                                    <tr>
                                        <td>@Html.DisplayNameFor(x => x.Person.FirstName)</td>
                                        <td>@Html.DisplayNameFor(x => x.Person.LastName)</td>                                          
                                    </tr>
                                    <tr>
                                        <td><input data-bind="value: FirstName, name: FirstName" /></td>
                                        <td><input data-bind="value: LastName, name: LastName" /></td>
                                    </tr>

                                    <tr>                                        
                                        <td><a href='#' data-bind='click: $root.removePerson'>Remove Person</a></td>
                                    </tr>
                                </tbody>
                            </table>
                            <!-- ko foreach: Aliases -->


                            <table>
                                <tbody>
                                    <tr>
                                        <td>@Html.DisplayNameFor(x => x.Alias.FirstNameAlias)</td>
                                        <td>@Html.DisplayNameFor(x => x.Alias.LastNameAlias)</td>
                                    </tr>
                                    <tr>
                                        <td><input data-bind="value: FirstNameAlias, name: FirstNameAlias" /></td>
                                        <td><input data-bind="value: LastNameAlias, name: LastNameAlias" /></td>
                                    </tr>
                                    <tr>
                                        <td><a href='#' data-bind='click: $root.removeAlias'>Remove Alias</a></td>
                                    </tr>
                                </tbody>
                            </table>

                            <!-- /ko -->

                            <a href='#' data-bind='click: $root.addAlias'>Add Alias</a>
                        </div>
                    </div>
                </div>
            </div>

Solution

  • The variable model in your submit funtion is going to be undefined it's not the model you're expecting it to be.

    Move the submit method to in to your model...

    $(function () {
    
        var personItem = function () {
            var self = this;
    
            self.LastName = ko.observable();
            self.FirstName = ko.observable();
            self.Aliases = ko.observableArray();
    
        };
    
        var model = ko.mapping.fromJS(@Html.Raw(Model.ToJson()));
    
        alert('The length of the array is ' + model.People().length);
        alert('The first element is ' + model.People()[0].Aliases().length);
        alert(ko.toJSON(model));
        model.addPerson = function () {
            model.People().push(new personItem());
        };
    
        model.removePerson = function (person) {
            model.People().remove(person);
        };
    
        model.submit = function() {
            $.ajax({
                url: '/Request/PostRequest',
                type: 'POST',
                contentType: 'application/json; charset=utf-8',
                data: ko.toJSON(model),
    
                success: function (status) {
                    alert(status);
                }
            });
        };
    
        ko.applyBindings(model);
    });
    

    and update the binding on your button to

    <input id="clickMe" type="button" value="clickme" data-bind="click: submit" />
    

    Also

    Controller.Json expects an object to serialize rather than a string

    [HttpPost]
    public JsonResult PostRequest(Record viewModel)
    {
        return Json(String.Format("'Success':'false','Error':'"));
    }
    

    Should be:

    [HttpPost]
    public JsonResult PostRequest(Record viewModel)
    {
        return Json(new { success = false, error: String.Empty });
    }
    

    Update

    Also noticed another issue with updating the model.People array

    model.addPerson = function () {
        model.People.push(new personItem());
    };
    
    model.removePerson = function (person) {
        model.People.remove(person);
    };
    

    In these functions model.People should be model.People(), i.e.

    model.addPerson = function () {
        model.People().push(new personItem());
    };
    
    model.removePerson = function (person) {
        model.People().remove(person);
    };
    

    Notice the () after model.People - included in the code block above.