简体   繁体   中英

How to data bind to a list of objects with knockout?

Here is my view model.

Server side:

public class ShoppingListModel
{
    public string Name { get; set; }
    public List<ItemModel> Items{ get; set; }

    public ShoppingListModel()
    {
        Items=new List<ItemModel>();
    }
}

On the client side, I use knockout.mapping .

ShoppingListModel = function(data) {
    var vm = ko.mapping.fromJSON(data);
    return vm;
};

To bind the server-side model to the client-side model:

@{
    var jsonSerializerSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
    var data = new JavaScriptSerializer().Serialize(JsonConvert.SerializeObject(Model, jsonSerializerSettings));
}

@section scripts {
<script src="~/App/ShoppingListModel.js"></script>

    <script>    
        var vm = ShoppingListModel(@Html.Raw(data));
        ko.applyBindings(vm);
    </script>
}

The code above:

  1. use JSON.NET to serialize the server-side Model into a camel cased json.
  2. build the client-side view model.
  3. ko.applyBinding()

Now I want to take advantage of the two-way binding.

First I tested on the Name :

@Html.HiddenFor(model => model.Name, new { @data_bind="value:name"})
<input type="text" data-bind="value:name"/>

It went well, I was able to edit the Name value on a text input and persist the value into the hidden input . The updated value could reach the POST action when the form is submitted.

Now the question: how to implement the binding on the list?

My test is to remove one item from the 'Items' list:

@Html.HiddenFor(model => model.Items, new { data_bind = "value: items" })

<tbody data-bind="foreach:items">
    <tr>
        <td>

            <span data-bind="text:name"></span>
        </td>
        <td><span data-bind="text:count"></span></td>
        <td>
            <button class="btn btn-xs" data-bind="click:$parent.remove">
                <i class="fa fa-trash"></i>
            </button>
        </td>
    </tr>
</tbody>

A console.log() tells me that the client-side model has been updated, but this time, the binding on the HiddenFor has never worked! When the form is submitted, Items is always null.

I guess it is reasonable because in Html:

<input type="hidden" value="xxx" />

we are expecting the value of an input to be a simple value.

I was thinking about loop through the list and data-bind from there. But it is also difficult. The knockout foreach is on tbody tag while a C# foreach is placed around tr (inside tbody ).

Then what is the correct way to bind the list?


Here is my solution based on Fabio's suggestion:

  1. In the client-side ko model,

add a computed value to bind the list into a json string.

vm.itemsJson = ko.computed(function() {
    return ko.toJSON(vm.items);
},this);
  1. In the html view,

add a hidden input to hold the json string and post with our form.

<input name="itemsjson" type="hidden" data-bind="value:itemsJson"/>
  1. In the server side view model,

besides

public List<ItemModel> Items{ get; set; }

Add another string property to hold the posted json.

public string ItemsJson { get; set; }

At this point, we are able to see the ItemsJson value successfully sent to the controller action.

  1. Parse the json string to the List on the server side.

Since it is typed model, we are to use JSON.Net to deserialize.

var items=JArray.Parse(model.ItemsJson);

model.Items = items.
    Select(i => new ItemModel {Name = (string) i["name"], Count = (int) i["count"]})
    .ToList();
return View(model);

Be sure to use JArray.Parse() for a List instead of JObject.Parse() .

It works.

Let's see if there is any better way than manually parsing json string. Otherwise, I would mark Fabio's answer as our solution after this weekend.

You could use computed observables to bind the hidden field, like this:

function YourViewModel() {
     var self = this;
     self.items = ko.observableArray(); //fill it with your stuff;
     self.valueForHiddenField = ko.computed(function() {
         return ko.toJSON(self.items);
     }, this); //use this observable as value of your hidden field
}

For more information http://knockoutjs.com/documentation/json-data.html

EDIT 1

You don't need to convert the json inside your controller. Instead sending a json to your server, send the collection using list of hidden fields. Like this:

<form>
   <!-- ko foreach: items -->
       <input type="hidden" data-bind="value: property1, attr: { name: 'Items[' + $index() + '].Property1' }">
        <input type="hidden" data-bind="value: property2, attr: { name: 'Items[' + $index() + '].Property2' }">
   <!-- /ko -->

</form>

Then you can send the post, and don't need to worry about the collection, it will refresh automatically when you changed the items observableArray.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM