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:
JSON.NET
to serialize the server-side Model
into a camel cased json.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:
add a computed value to bind the list into a json string.
vm.itemsJson = ko.computed(function() {
return ko.toJSON(vm.items);
},this);
add a hidden input to hold the json string and post with our form.
<input name="itemsjson" type="hidden" data-bind="value:itemsJson"/>
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.
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.