0

With the following ASP.NET models

public class User
  {
    public string Name { get; set; }
    public LEmail LEmail { get; set; }
  }
public class LEmail
  {
    public IList<CLabel> Labels;
    public IList<CEmail> Emails;
  }
public class CLabels
  {
    public IList<CLabel> Labels { get; set; }
  }
public class CLabel
  {
    public string Name { get; set; }
  }
public abstract class CEmail
  {
    public string SelectedLabel { get; set; }
    public string Name { get; set; }
  }

Filling it out with dummy data and sending to appropriate view as User object, I have the following knockout definitions in the view:

@using (Html.BeginForm("MyAction", "MyController", FormMethod.Post, new { id = "MyEditor" }))
{
  @Html.EditorFor(m => @Model.LEmail)
 <p>
    <input type="submit" value="Save" data-bind="enable: Emails().length > 0" />
    <a href="/">Cancel</a>
  </p>

  <p data-bind="visible: saveFailed" class="error">A problem occurred saving the data.</p>

  <div id="debug" style="clear: both">
    <hr />
    <h2>Debug:</h2>
    <div data-bind="text: ko.toJSON(viewModel)"></div>
  </div>
}

<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"),

    Labels: ko.observableArray(@Html.Json(Model.LEmail.Labels) || []),
    Emails: ko.observableArray(@Html.Json(Model.LEmail.Emails) || []),
    addEmail: function() {
      viewModel.Emails.push(@Html.Json(new CEmail()));
    },
    removeEmail: function(eml) {
      viewModel.Emails.remove(eml);
    },

    saveFailed: ko.observable(false),

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

      // Executed synchronously for simplicity
      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>

And finally the editor template that is actually supposed to take care of variable length list that look like this:

@model MyDomain.ViewModels.LEmail

<table>
    <tbody data-bind="template: { name: 'EmailsTemplate', foreach: Emails }" />
</table>

<button data-bind="click: addEmail">Add Email</button>

<script id="EmailsTemplate" type="text/html">
      <tr>
        <td>
      @* PROBLEM IS HERE!! Labels won't show (they will it this code taken out of template) *@
           <select data-bind="options: Labels"></select></td>
        <td>
          <input type="text" data-bind="value: Name, uniqueName: true" class="required" /></td>
        <td>
          <a href="#" data-bind="click: function() { viewModel.removeEmail(this); }">Delete</a></td>
      </tr>
</script>

Essentially I

  1. cannot make it work in the EditorTemplate for combobox (or dropdownlist). It won't attach to Labels no matter what I do. If I take it outside the template somewhere else - it works as expected.
  2. Also, based on selection to fill out the "SelectedValue" inside the Email - how to do that.

  3. After everything is selected, (this must be simple) how to post it all back without losing values on the way (its a super nested model as you see).

Thank you very much in advance!

Display Name
  • 4,672
  • 1
  • 33
  • 43
  • What do you mean, "Cannot make it work" - are you getting an error? Are there no results? – Judah Gabriel Himango Mar 06 '13 at 22:58
  • I mean it doesn't show the expected values in the dropdown - its practically empty. If I take this piece of code outside the loop (template) - it works as expected. Seems I cannot access the .Labels attribute, but I MUST do it, in order to label the email (whether its work/home/custom). Thanks – Display Name Mar 06 '13 at 23:00

1 Answers1

1

Labels is on your view model, not each email. Since the template is rendered within the context of a Knockout foreach binding, the binding context has changed to an email.

Here's how I'd write your view:

@model FiveW.ViewModels.LabeledEmail

<table>
    <tbody data-bind="foreach: Emails">
        <tr>
           <td>
              <select data-bind="options: $root.Labels, value: SelectedLabel"></select>
           </td>
           <td>
              <input type="text" data-bind="value: Name, uniqueName: true" class="required" />
           </td>
           <td>
              <a href="#" data-bind="click: function() { viewModel.removeEmail(this); }">Delete</a>
           </td>
      </tr>
    </tbody>
</table>

<button data-bind="click: addEmail">Add Labeled Email</button>

The fix is in $root.Labels: we need to tell Knockout to use $root (your view model), since Labels is actually on your view model, and not on an individual email.

Also notice I didn't use an named template. This is preferable. Unless you are using the template in more than one place in your view, you should use anonymous, inline templates like I did above.

Judah Gabriel Himango
  • 58,906
  • 38
  • 158
  • 212
  • It worked, but no values passed into `Email.SelectedLabel` - its practically GUI with no use. Also on postback, the User.LabeledEmail is null - how to post it all back please? Thank you! – Display Name Mar 07 '13 at 01:07
  • You need the value binding on your – Judah Gabriel Himango Mar 07 '13 at 01:10
  • Documentation for the – Judah Gabriel Himango Mar 07 '13 at 01:11
  • Judah, thank you very much, it helped! This is why I marked as the answer. Can you please help some more? Essentially - how to post it back as User, as now it comes with name and LabeledEmail as null, which is not what I'm after :) Thanks in advance! – Display Name Mar 07 '13 at 14:12
  • I'm confused. Are you posted your view model as a user, and on the server, user.LabeledEmail is null? – Judah Gabriel Himango Mar 07 '13 at 18:05
  • Already solved http://stackoverflow.com/questions/15273994/knockout-view-model-posts-back-to-asp-net-mvc-partially-how-to-post-back-compl/15274554#comment21551069_15274554. I was flattening the view model (MVC to KO) and was failing to put it back together on the way back (KO to MVC). Now its working, need to work on KO validation though, that doesn't work by default, guess, as soon as you step off the standard path, SUV is required :) Thanks – Display Name Mar 07 '13 at 18:25
  • Glad you got it working. Validation can be done with Knockout as well: https://github.com/ericmbarnard/Knockout-Validation – Judah Gabriel Himango Mar 07 '13 at 19:28
  • Thanks for the link. Isn't it a duplication (you define your fluent validation (or data annotation) in MVC and then you redefine the same story in knockout)? (Trying to understand, since I am completely new to KO, first baby steps). Thanks. – Display Name Mar 07 '13 at 19:45