Reputation: 10479
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()});
The post requests is now sending the collection data correctly. However that doesn't solve the fact that the MVC action is not receiving it but this question is big enough already.
I can't seem to get the data from collection property of my model in knockout into my MVC model when posting to an action. If I alert ko.toJSON
my identifiers()
property from below it properly shows all the data, but when I try and submit that data via a normal postback (the action just takes the EquipmentCreateModel below), it looks like this:
Identifiers is empty and when I look at the ModelState error for Identifiers, it says that it cannot convert String to Dictionary<Guid, string>
. What am I doing wrong? I thought MVC3 automatically converts JSON into objects if it can, as it did with the BuildingCode
and Room
properties?
Also why does my string data in the above picture have escaped quotes?
EDITS:
If I look at the post data, identifiers is shown as an empty array (identifiers: [{}]
). I tried jsoning identifiers in the save method like so:
self.identifiers = ko.toJSON(self.identifiers());
This causes the request data to not be empty and look like this:
identifiers:"[{\"Value\":\"sdfsd\",\"Key\":\"4554f477-5a58-4e81-a6b9-7fc24d081def\"}]"
However, the same problem occurs when I debug the action. I also tried jsoning the entire model (as outlined in knockoutjs submit with ko.utils.postJson issue):
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.
Which from looking at the request data it looks like it's being JSON-ified twice because each letter or character is it's own value in the HttpCollection and this is because .NET only allows 1000 max by default ('Operation is not valid due to the current state of the object' error during postback).
Using the $.ajax method to submit 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 due to other reasons I cannot use the $.ajax method for this, so I need it working in the normal post. Why can I toJSON
the entire viewModel in the ajax request and it works, but in the normal postback it splits 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 knock-out 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>
}
Upvotes: 3
Views: 4963
Reputation: 15984
I think I've found the issue or at least narrowed down the problem. The editable grid example uses simple js objects to represent gifts. You are using Identifier objects with sub observables. It seems that if we update the grid example to use more complex types it too breaks in the same way as your example. This is either by design or a bug.
I think the only solution is to write your own mapping function to submit the form.
Hope this helps.
Upvotes: 1