3

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:

state of model upon submit

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>
}
Community
  • 1
  • 1
SventoryMang
  • 10,275
  • 15
  • 70
  • 113
  • Does this work if you do not submit a form post using ko.utils and instead just serialize your model with $.ajax? What does firebug show is being passed on the request. – madcapnmckay Mar 23 '12 at 15:46
  • @madcapnmckay yes it does work, using $.ajax method, everything works as expected. When using the normal postback method, the data being passed into the request, everything looks good except identifiers is shown as `identifiers: [{}]` an empty array. I found out if I do `self.identifiers = ko.toJSON(self.identifiers());` on save, the result in the post data is : `identifiers:"[{\"Value\":\"sdfsd\",\"Key\":\"4554f477-5a58-4e81-a6b9-7fc24d081def\"}]"`, so it's actually passing data, but the result is the same. When I get into the action, identifiers is not null, but count is 0. – SventoryMang Mar 23 '12 at 16:20
  • @madcapnmckay That JSON doesn't look correct right? There shouldn't be escape quotes? When I do the `$.ajax` method the data in the request payload doesn't have escape quotes around it, and when I debug the action, the room and buildingCode don't contain escaped quote values. But when I post the normal way they do, as seen above. Do you think that's related to the problem? – SventoryMang Mar 23 '12 at 16:35
  • No it doesn't. I've never used the postJson function, i always post the json using $.ajax and stringify (Most examples I've seen do this also). If you check the KO source its actually creating a form element and adding values to it then submitting. I'd use your own ajax function, it's cleaner imo not to mix form posts with json posts. – madcapnmckay Mar 23 '12 at 17:21
  • I need to use a regular postback unfortunately, and according to this example http://knockoutjs.com/examples/gridEditor.html it uses the normal postback so it should work. – SventoryMang Mar 23 '12 at 17:37
  • Check out this example. Its the editable grid example with a postJson. The values in firebug appear to get posted correctly. http://jsfiddle.net/SZzVW/1/ – madcapnmckay Mar 23 '12 at 17:53
  • Yeah that is the same example I linked. The difference in them is that I am submitting my entire view model, which has a collection property on it, while in that example they are just submitting the collection off the view model. If you try and just and stringify the view Model, you get nothing: http://jsfiddle.net/SZzVW/5/, which is the same in my example, if I alert and stringify the model, I only get the non-collection properties, the collection isn't even listed. However, if I `toJSON` the entire view model, everything looks good, check that here: http://jsfiddle.net/SZzVW/5/. – SventoryMang Mar 23 '12 at 18:28
  • So why won't it work in postJson()? If I look at line 393 in the utils.js of knockout source code it looks like it should be unwrapping nested properties fine. BUT if I take your example and alert that: http://jsfiddle.net/SZzVW/6/ I get the expected values. However, if I try that on me by adding this line in save: `alert(ko.utils.stringifyJson(ko.utils.unwrapObservable(self.identifiers)));` it's empty, so that appears to be the big difference, but why the heck is it happening. In fact, in your example I CAN post the entire model, and it still works fine: http://jsfiddle.net/SZzVW/8/. so wth? – SventoryMang Mar 23 '12 at 18:37
  • I figured out why the collection is not posting properly, but I am not sure why. If I replace this line: self.identifiers.push(new Identifier(self.identifierToAdd(), self.selectedIdentifierTypeValue())); with this line self.identifiers.push({ Key: self.identifierToAdd(), Value: self.selectedIdentifierTypeValue()}); the identifiers data posts correctly however I still get the same problem when it reaches the action, BuildingCode and room are nested inside escaped quotes, and the identifiers collection is still count 0 =(. – SventoryMang Mar 23 '12 at 19:06
  • Its seems we both found the bug at the same time. :) – madcapnmckay Mar 23 '12 at 19:10

1 Answers1

1

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.

http://jsfiddle.net/SZzVW/9/

I think the only solution is to write your own mapping function to submit the form.

Hope this helps.

madcapnmckay
  • 15,782
  • 6
  • 61
  • 78
  • Thanks for your help man, couldn't have figured it out without you. Fixed the problem of request data not posting correctly, but the action is still not getting the collection and double quoting the simple property values, but since this question is long enough already I edited the post and will break that out into another question. – SventoryMang Mar 23 '12 at 19:53
  • Just thinking outloud here, but would it be almost easier at this point to accept a Json string in the action and parse out the values in the controller? – SventoryMang Mar 23 '12 at 19:55
  • Or if you need to move to another page like a true postback, simply post json via $.ajax and then forward them on when it succeeds. – madcapnmckay Mar 23 '12 at 20:55