Knockout view model posts back to ASP.NET MVC partially - how to post back complete object?
Asked Answered
O

1

1

Having these ASP.NET MVC view models:

public class User
  {
    public string Name { get; set; }
    public LabeledEmail LabeledEmail { get; set; }
  }
public class LabeledEmail
  {
    public IList<ContactLabel> Labels;
    public IList<ContactEmail> Emails;
  }

and Knockout view model like this:

  <script type="text/javascript">

    $(function() {
      ko.applyBindings(viewModel);

      $("#profileEditorForm").validate({
    submitHandler: function(form) {
      if (viewModel.save())
        window.location.href = "/";
      return false;
    }
      });
    });

    var viewModel = {

      Name: ko.observable("@Model.Name"),

      EmailLabels: ko.observableArray(@Html.Json(Model.LabeledEmail.Labels.Select(l => l.Name)) || []),
      Emails: ko.observableArray(@Html.Json(Model.LabeledEmail.Emails) || []),

      addEmail: function() {
    viewModel.Emails.push(@Html.Json(new ContactEmail()));
      },
      removeEmail: function(eml) {
    viewModel.Emails.remove(eml);
      },

      saveFailed: ko.observable(false),

      // Returns true if successful
      save: function() {
    var saveSuccess = false;
    viewModel.saveFailed(false);

    jQuery.ajax({
      type: "POST",
      url: "@Url.Action("MyAction", "MyController")",
      data: ko.toJSON(viewModel),
      dataType: "json",
      contentType: "application/json",
      success: function(returnedData) {
        saveSuccess = returnedData.Success || false;
        viewModel.saveFailed(!saveSuccess);
      },
      async: false
    });     
    return saveSuccess;
      }
    };
</script>

What posts back properly to controller is User.Name, but User.LabeledEmail is empty. I have to flatten the model the way I do in order to be able to use lists separately elsewhere. I know for fact that viewModel.Emails is populated properly while saving, but User.LabeledEmails is somehow returns null.

It basically comes down to assigning Model.LabeledEmail.Emails the viewModel.Emails and the deal will be solved, seems, but I don't know how and cannot find any appropriate examples.

  1. What is the mistake that I make and
  2. How to do it properly?

Thank you in advance.

Overcloud answered 7/3, 2013 at 14:39 Comment(0)
S
3

Your JSON data structure should match the structure of your C# classes especially the property names. So you need send a JSON which look somewhat like this:

{
    Name: 'somename',
    LabeledEmail: {
        Labels: [ somelements ],
        Emails: [ someelements ]
    }
}

So change your data: ko.toJSON(viewModel),:

data: JSON.stringify({
   Name: viewModel.Name(),
   LabeledEmail: { 
       Labels: viewModel.EmailLabels(),
       Emails: viewModel.Emails()
   }
})

Or just use the data in the same structure on both the client and server...

As a side note: Your C# viewmodels need to have properties instead of fields in order to the MVC model binder work correctly:

public class User
{
    public string Name { get; set; }
    public LabeledEmail LabeledEmail { get; set; }
}

public class LabeledEmail
{
    public IList<ContactLabel> Labels { get; set; }
    public IList<ContactEmail> Emails { get; set; }
}
Shimberg answered 7/3, 2013 at 15:5 Comment(7)
Thanks. I tried this approach (with different KO view model). If I do it this way, I cannot render Labels and Emails in my views (answered in my other question). I am building smth similar to gmail new contact - you have [work|home|etc] label and [email|phone|address|etc] - cannot have them this way in the variable length list using knockout. I think I need somehow to "combine" what was filled out in the form back into the view model. View Model change as you suggest will kill the functionality that is built this far. Hope you understand my school of thought. ThanksOvercloud
I don't follow you. If you build up the data: as I've shown you don't need to change anything of your class structure on the server or the client side and it should work (and if you use properties on the C# side)...Shimberg
1. Changed C# to properties (great observation! thanks) 2. I should leave ko.Email and ko.EmailLabels at the root level of ko, right? 3. Just need to return the way you show rather than my toJSON(vm) thing, right? Did I follow you accurately? (because this far it even breaks the view and doesn't even render properly, even though I do send the Json you described). it all makes sense, but unfortunately doesn't work. I think I will have to take some time to figure out some details as to why ...Overcloud
The key is that you can have your C# as it fits your server side and your KO view model as they fit your UI and bindings. BUT you need to transform between the two forms when you want to send back data to the server. So your jQuery.ajax data should match the structure of the C# viewmodels...Shimberg
This worked in passing the view model back to controller (After I worked out the details). So you get your right answer v mark + "the answer is useful" upvote - And thank you very much! Q-n however - the ModelState.IsValid became false and no clue as to why. Is it because I am compiling the postback object by hand? Thanks againOvercloud
I'm glad that it finally worked out. If your ModelState if invalid you should check in the debugger what went wrong. You can print out the all tje errors with something like: var errors = ModelState.Keys .Where(k => ModelState[k].Errors.Any()) .Select(k => k + "#" + string.Join(",", ModelState[k].Errors)).ToArray();Shimberg
@nemserv Thanks a lot! Was able to diagnose the the error with much simpler code: ModelState.Select(x => x.Value.Errors).ToList(); Apparently KO cannot convert string into ContactLabel. Its another level deep (see how I construct EmailLabels) And again the same problem for me - how to deconstruct? :) (EmailLabels is IList of ContactLabel rather than string). Can you modify your code please to reflect? Because I am again not sure how to ... Thank you in advance!Overcloud

© 2022 - 2024 — McMap. All rights reserved.