Knockout with MVC3: Knockout model won't send collection data?
ANSWER: Replacing this line:
self.identifiers.push(new Identifier(self.identifierToAdd(), self.selectedIdentifierTypeValue()));
With this line:
self.identifiers.push({ Key: self.identifierToAdd(), Value: self.selectedIdentifierTypeValue()});
Mail requests now send collection data correctly. However, this does not address the fact that the MVC action is not getting it, but the question is already big enough.
I cannot get the data from my knockout model's collection property into my MVC model when published to an action. If I alert ko.toJSON
my identifiers()
property at the bottom it displays all the data correctly, but when I try to send that data using a normal postback (the action just takes the EquipmentCreateModel below) it looks like this:
The ids are empty and when I look at the ModelState error for ids it says it is cannot convert String to Dictionary<Guid, string>
. What am I doing wrong? I thought MVC3 would automatically convert JSON to objects if possible, how was it done with properties BuildingCode
and Room
?
Also why does my string data in the above snapshot have escaped quotes?
edits:
If I look at the post data, the IDs appear as an empty array ( identifiers: [{}]
). I tried jsoning ids in the save method like:
self.identifiers = ko.toJSON(self.identifiers());
This leads to the fact that the request data will not be empty and looks like this:
identifiers:"[{\"Value\":\"sdfsd\",\"Key\":\"4554f477-5a58-4e81-a6b9-7fc24d081def\"}]"
However, the same problem occurs when I debug the activity. I also tried to find out the whole model (as described in knockoutjs submit with the ko.utils.postJson question ):
ko.utils.postJson($("form")[0], ko.toJSON(self));
But this gives a .NET error that says Operation is not valid due to the current state of the object.
that, looking at the request data, it looks like JSON-ified twice, because each letter or character is its own value in the HttpCollection, and this is because only .NET allows 1000 max by default ( 'Operation invalid due to an error in the current state of the object during postback ).
Using the $ .ajax method to send the data, everything works fine:
$.ajax({
url: location.href,
type: "POST",
data: ko.toJSON(viewModel),
datatype: "json",
contentType: "application/json charset=utf-8",
success: function (data) { alert("success"); },
error: function (data) { alert("error"); }
});
But for other reasons, I cannot use the $ .ajax method for this, so I need it to work in regular mail. Why can I have the toJSON
whole viewModel in ajax request and it works, but in normal postback it breaks it up and when I don't, all quotes are escaped in the sent JSON.
Here is my ViewModel:
public class EquipmentCreateModel
{
//used to populate form drop downs
public ICollection<Building> Buildings { get; set; }
public ICollection<IdentifierType> IdentifierTypes { get; set; }
[Required]
[Display(Name = "Building")]
public string BuildingCode { get; set; }
[Required]
public string Room { get; set; }
[Required]
[Range(1, 100, ErrorMessage = "You must add at least one identifier.")]
public int IdentifiersCount { get; set; } //used as a hidden field to validate the list
public string IdentifierValue { get; set; } //used only for knockout viewmodel binding
public IDictionary<Guid, string> Identifiers { get; set; }
}
Then my knockout script / ViewModel:
<script type="text/javascript">
// Class to represent an identifier
function Identifier(value, identifierType) {
var self = this;
self.Value = ko.observable(value);
self.Key = ko.observable(identifierType);
}
// Overall viewmodel for this screen, along with initial state
function AutoclaveCreateViewModel() {
var self = this;
//MVC properties
self.BuildingCode = ko.observable();
self.room = ko.observable("55");
self.identifiers = ko.observableArray();
self.identiferTypes = @Html.Raw(Json.Encode(Model.IdentifierTypes));
self.identifiersCount = ko.observable();
//ko-only properties
self.selectedIdentifierTypeValue = ko.observable();
self.identifierToAdd = ko.observable("");
//functionality
self.addIdentifier = function() {
if ((self.identifierToAdd() != "") && (self.identifiers.indexOf(self.identifierToAdd()) < 0)) // Prevent blanks and duplicates
{
self.identifiers.push(new Identifier(self.identifierToAdd(), self.selectedIdentifierTypeValue()));
alert(ko.toJSON(self.identifiers()));
}
self.identifierToAdd(""); // Clear the text box
};
self.removeIdentifier = function (identifier) {
self.identifiers.remove(identifier);
alert(JSON.stringify(self.identifiers()));
};
self.save = function(form) {
self.identifiersCount = self.identifiers().length;
ko.utils.postJson($("form")[0], self);
};
}
var viewModel = new EquipmentCreateViewModel();
ko.applyBindings(viewModel);
$.validator.unobtrusive.parse("#equipmentCreation");
$("#equipmentCreation").data("validator").settings.submitHandler = viewModel.save;
View:
@using (Html.BeginForm("Create", "Equipment", FormMethod.Post, new { id="equipmentCreation"}))
{
@Html.ValidationSummary(true)
<fieldset>
<legend>Location</legend>
<div class="editor-label">
@Html.LabelFor(model => model.BuildingCode)
</div>
<div class="editor-field">
@Html.DropDownListFor(model => model.BuildingCode, new SelectList(Model.Buildings, "BuildingCode", "BuildingName", "1091"), "-- Select a Building --", new { data_bind = "value:BuildingCode"})
@Html.ValidationMessageFor(model => model.BuildingCode)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Room)
</div>
<div class="editor-field">
@Html.TextBoxFor(model => model.Room, new { @class = "inline width-7", data_bind="value:room"})
@Html.ValidationMessageFor(model => model.Room)
</div>
</fieldset>
<fieldset>
<legend>Identifiers</legend>
<p>Designate any unique properties for identifying this autoclave.</p>
<div class="editor-field">
Add Identifier
@Html.DropDownList("identifiers-drop-down", new SelectList(Model.IdentifierTypes, "Id", "Name"), new { data_bind = "value:selectedIdentifierTypeValue"})
@Html.TextBox("identifier-value", null, new { @class = "inline width-15", data_bind = "value:identifierToAdd, valueUpdate: 'afterkeydown'" })
<button type="submit" class="add-button" data-bind="enable: identifierToAdd().length > 0, click: addIdentifier">Add</button>
</div>
<div class="editor-field">
<table>
<thead>
<tr>
<th>Identifier Type</th>
<th>Value</th>
<th></th>
</tr>
</thead>
<!-- ko if: identifiers().length > 0 -->
<tbody data-bind="foreach: identifiers">
<tr>
<td>
<select data-bind="options: $root.identiferTypes,
optionsText: 'Name', optionsValue: 'Id', value: Key">
</select>
</td>
<td><input type="text" data-bind="value: Value"/></td>
<td><a href="#" class="ui-icon ui-icon-closethick" data-bind="click: $root.removeIdentifier">Remove</a></td>
</tr>
</tbody>
<!-- /ko -->
<!-- ko if: identifiers().length < 1 -->
<tbody>
<tr>
<td colspan="3"> No identifiers added.</td>
</tr>
</tbody>
<!-- /ko -->
</table>
@Html.HiddenFor(x => x.IdentifiersCount, new { data_bind = "value:identifiers().length" })<span data-bind="text:identifiers"></span>
@Html.ValidationMessageFor(x => x.IdentifiersCount)
</div>
</fieldset>
<p>
<input type="submit" value="Create" />
</p>
}
source to share
I think I found the problem, or at least narrowed down the problem. The editable grid example uses simple js objects to represent gifts. You are using identity objects with additional observables. It looks like if we update the grid example to use more complex types, it breaks down too, just like your example. This is either by design or by mistake.
I think the only solution is to write your own display function to submit the form.
Hope it helps.
source to share