0

Here is the problem that I am facing and not sure how to even approach it:

I created models, controllers and views in ASP.NET MVC 4. At one point I had to create dynamic lists, so I opted out to KnockoutJS, what solves this problem extremely easy. So far so good. Then I realized that the validation I defined on my MVC models using I use Fluent Validation doesn't work anymore in the knockout view.

I searched through SO and found few viable solutions:

I tend to use the latter one, for several reasons. Mainly because it gives me opportunity not to introduce (learn, test, localize, spend time) another library.

I am quite familiar with MVC and love the way it supports localization giving full control on messages, labels etc. I also love Fluent Validation and don't want to replace it with others (more static, much harder to localize, much less flexible to my liking)

I found some examples on knockout to razor conversion, when data-bind has to become data_bind etc.

I cannot find a way to express the foreach loop with and in.

MVC view model

  public class ContactEmail
  {
    public string SelectedLabel { get; set; }
    public string Name { get; set; }
  }

 public class User
 {
   public IList<ContactEmail> Emails { get; set; }
 }


ViewBag.EmailLabels = new string[] { "label1", "label2", ... };

knockout model

  var viewModel = {
    EmailLabels: ko.observableArray(@Html.Json(ViewBag.EmailLabels as string[]) || []),
    Emails: ko.observableArray(@Html.Json(@Model.Emails) || []),
  } 

knockout view (that I wanted to transform)

    <table>
    <tbody data-bind="foreach: Emails">
      <tr>
        <td>
        @* How to make razor below work instead of knockout syntax below it? *@
        @*Html.DropDownListFor(m => ????, new { data_bind="options: $root.EmailLabels, value: SelectedLabel, optionsCaption: 'Choose...'" } )
          <select data-bind="options: $root.EmailLabels, value: SelectedLabel, optionsCaption: 'Choose...'"></select></td>
          <td>
            @* How to make razor below work as well instead of knockout syntax below ?!?!? *@
            @Html.TextBoxFor(m => ????, new { data_bind="value: Name, uniqueName: true" } )
              <input type="text" data-bind="value: Name, uniqueName: true" class="required email" />
          </td>
          <td>
              <a href="#" data-bind="click: function() { viewModel.removeEmail(this); }">Delete</a>
          </td>
      </tr>
    </tbody>
    </table>

I looked at MVC Controls toolkit what one guy mercilessly advertised will solve all my validation and localization and everything at all. I found it unusable, very proprietary and extremely hard to understand. Its like buying nuke to kill a bird.

So please those of you who had experience with marrying MVC with knockout, please step up and share your experience.

Any help will be greatly appreciated & thank you very much in advance.

Display Name
  • 4,672
  • 1
  • 33
  • 43
  • Instead of using knockout to do the foreach, use razor. You have to define an EditorTemplate for the ContactEmail class first. I'll upload an example in a few minutes. – amhed Mar 10 '13 at 13:35
  • @amhed Thanks, its a good idea, however I have to use knockout to have variable length list easy. Hope you understand what I mean. Unless you saying I can do the same functionality knockout does (variable length lists) with razor easily. Thanks for the comment – Display Name Mar 10 '13 at 13:39
  • I was thinking about rendering the controls using a foreach or an EditorFor with the Emails ViewModel and inside that defining the kcnokout bindings. – amhed Mar 10 '13 at 13:43
  • the knockout view that you see is already EditorFor based on `IList`. The main view that calls it has ko definitions. Reason is besides Emails there is other contact info as similar dynamic (variable length) lists. If you see how to implement it using multiple dynamic lists with selector for each contact type without using KO *easily*, I'd be happy to see it. KO makes it so easy, but kills the validation, hence I wanted to use @html helpers to get it back... I saw (as mentioned) few examples where people express ko in razor terms, I just fail to make it happen for `foreach` loop. – Display Name Mar 10 '13 at 13:54
  • 1
    The validation is killed because you're rendering the controls and doing the foreach on the client. You can render the controls using the HTML helpers and leave the dropdownlists empty and just bind those on the client. That way you get to render the controls with the correct data-val attributes automatically and you get to fill the controls on the client with your KO bindings – amhed Mar 10 '13 at 13:59

2 Answers2

0

Edit: Update to include definitive Knockout Bindings

index.cshtml

@model Stackoverflow5.Models.User

<form>
    <table>
        <tbody>

            @{
                var tempdropdownlist = new List<SelectListItem>();
            }
            @for (var i = 0; i < @Model.Emails.Count; i++)
            {
                <tr>
                    <td>
                        @Html.DropDownListFor(m => m.Emails[i], tempdropdownlist,
                            new { data_bind = String.Format("options: $root.EmailLabels, value: Emails()[{0}].SelectedLabel, optionsCaption: 'Choose...'", i)})
                    </td>
                    <td>
                        @Html.TextBoxFor(m => m.Emails[i].Name, 
                            new { data_bind = String.Format("value: Emails()[{0}].Name(), uniqueName: true", i) })
                    </td>
                </tr>
            }

        </tbody>
    </table>

    <button type="submit">Test</button>
</form>

** Models (with validation working) **

public class ContactEmail
    {
        public string SelectedLabel { get; set; }

        [Required]
        [StringLength(20, MinimumLength = 2)]
        public string Name { get; set; }

    }

    public class User
    {
        public User()
        {
            Emails = new List<ContactEmail>();
            EmailLabels = new List<string> {"Important", "Spam", "Family"};
        }

        public List<ContactEmail> Emails { get; set; }
        public List<string> EmailLabels { get; set; }
    }
amhed
  • 3,649
  • 2
  • 31
  • 56
  • Won't work - you passing in a list (`d.Emails`) which is `IList`, but your accept as a model in EditorTemplate a single one `ContactEmail`. If that was the case I wouldn't have a question. Am I missing something in your solution? Please let me know. Thanks. – Display Name Mar 10 '13 at 14:07
  • Since we're using EditorFor() the framework detects it's an IEnumerable and will render the EditorTemplate for each item in the list. Give it a try, just make sure it resides on ~/Views/Shared/EditorTemplates – amhed Mar 10 '13 at 14:14
  • "Validation type names in unobtrusive client validation rules must be unique. The following validation type was seen more than once: required" on line `@Html.TextBoxFor(m => m.Name, new { data_bind = "value: Name, uniqueName: true" })` No idea, I set it once in fluent validation on MVC side ... Any ideas? – Display Name Mar 10 '13 at 14:26
  • Hadn't seen that one before. Check out a comment here that talks about settings on the web.config: http://stackoverflow.com/questions/9746186/validation-type-names-in-unobtrusive-client-validation-rules-must-be-unique – amhed Mar 10 '13 at 14:28
  • Here's the project I did to test this one out: https://dl.dropbox.com/u/9764/Stackoverflow5-Project.7z – amhed Mar 10 '13 at 14:32
  • Dude, I just figured out, why your solution is not applicable. You completely don't take into account `knockout`, which is the main issue in my case. I'd say the answer you providing is not even partial, although in the right direction. I testing with KO - different behaviour as well as besides `[Requied]` no other data annotation attribute works, I tested with `[MinLength]` - it doesn't even pops up, so need to rework include KO and take it more seriously. Thanks for the attempt. – Display Name Mar 10 '13 at 15:41
  • "It doesn't even pop up" beacuse MinLength isn't a validation attribute for a string property. Correct attribute would be [StringLength]. That has nothing to do with knockout, but with razor and the jquery validation attributes it renders. I'm sorry if this hasn't worked for your particular case, but the solution for your question as described on this post is the one I posted. – amhed Mar 10 '13 at 20:06
  • I've just updated my answer to show what you were looking for – amhed Mar 10 '13 at 20:45
  • Thank you. With all due respect and appreciation of effoer, do you realize that knockout doesn't work in your scenario, which was the whole point of my exercise? I had couple buttons like in standard scenario for variable length lists - add item below the list, and delete item in every line. Non of them working if I don't use KO foreach loop, but your way. You cannot neglect the environment I am in, you answering different question than mine. I need **working KO loop with internals expressed in razor html helper terms**. Not MVC loop, which kills KO workings. – Display Name Mar 10 '13 at 20:51
  • I managed to do what I wanted: razor inside KO. And client side validation STILL DOESN'T WORK!!! http://pastebin.com/Skh3NDWj. Why would KO kill the validation? – Display Name Mar 10 '13 at 21:13
  • OK now I get it. Do you send the form back using a regular POST or do you unwrap the javascript model and send it back as a Json string? – amhed Mar 10 '13 at 21:16
  • Yeah, I definitely see the problem now, sorry I didn't completely understand at first! Gimme a moment to see if I can come up with something =D – amhed Mar 10 '13 at 21:23
0

I think this is a bit of a hack, but it works.

The controller will return a collection of emails inside of the User.Emails property with the list that need to be rendered. What the Razor View produces is HTML of a table with just one row and the validation based off the first element of the Emails IEnumerable (this must be checked for null or may cause an exception).

When ko.applyBindings() occur on the client side, then the foreach on the tbody tag will produce all rows, and since the ko ViewModel as initialized with the whole collection as a mapped JsonString, then the whole list will render. The methods removeEmail and addEmail will work as well (I just tested the removeEmail option, it works =D)

@using Newtonsoft.Json
@model Stackoverflow5.Models.User

    <table>
        @{var tempdropdownlist = new List<SelectListItem>();}

        <tbody data-bind="foreach: Emails">
            <tr>
                <td>
                    @Html.DropDownListFor(m => m.Emails.First().SelectedLabel, tempdropdownlist,
                                      new { data_bind="options: $root.EmailLabels, value: SelectedLabel, optionsCaption: 'Choose...'" })

                <td>
                    @Html.TextBoxFor(m => m.Emails.First().Name, new { data_bind="value: Name, uniqueName: true" } )
                </td>
                <td>
                    <a href="#" data-bind="click: function () { viewModel.removeEmail(this); }">Delete</a>
                </td>
            </tr>
        </tbody>
    </table>


@section scripts
{
    <script src="~/Scripts/knockout-2.2.1.js"></script>
    <script src="~/Scripts/knockout.mapping-latest.js"></script>

    <script>
        //Model definition
        var viewModel,
            ModelDefinition = function (data) {
            //Object definition
            var self = this;

            //Mapping from ajax request
            ko.mapping.fromJS(data, {}, self);

            self.removeEmail = function(row) {
                self.Emails.remove(row);
            };

            self.addEmail = function() {
                //Method for adding new rows here
            };
        };

        $(function() {
            viewModel = new ModelDefinition(@Html.Raw(JsonConvert.SerializeObject(Model)));
            ko.applyBindings(viewModel);
        });
    </script>
}
amhed
  • 3,649
  • 2
  • 31
  • 56
  • This renders well, and validation attributes are added accordingly, but the ID remains the same with all rows, and I think validation isn't working because of that. :S – amhed Mar 10 '13 at 21:43
  • Do you mind looking at my ko viewModel and say if what changes can I make to it? I just don't understand your syntax, trying to adjust to what you've built? http://pastebin.com/f7xAMjT9 Thanks! – Display Name Mar 10 '13 at 22:39
  • Your viewmodel looks fine, the only difference is that I'm rendering my whole model directly using the mapping function (you need to reference knockout.mapping.js for this). Make sure that your document.ready() function is at the end of the script after the model declaration. – amhed Mar 10 '13 at 23:23
  • I just incorporated your into my solution. What do you mean it validates? For me - I used `StringLength` to define minimum and maximum length - it posts back to server, doesn't do it on the client. Is it the same in your case? Thanks – Display Name Mar 10 '13 at 23:53
  • Oh no, I was actually trying to perform validation on the client-side using jQuery Validate. The example I provided should work fine on the server side :) – amhed Mar 11 '13 at 03:07
  • 1
    Dude, you were right when said that if I make it function in java script rather than js object, unobtrusive validation will work, It does! Thank you! – Display Name Mar 13 '13 at 12:12
  • Can you explain why are you using `@Html.DropDownListFor(m => m.Emails.First().SelectedLabel` (why Emails.First(), rather than anything else? Thank you – Display Name Mar 13 '13 at 17:10
  • Because my model has a collection of emails. I just want to get the first one in the collection to that the validation tags will be rendered, but finally the values will be overridden by the knockout foreach – amhed Mar 13 '13 at 19:50
  • Well, reason I was asking is that validation works in a very strange way ... and I cannot understand why. It renders dropdown validation message for every list rendered in the ko foreach loop based on currently selected DDL. For example if I select smth in any DDL, required message disappears for all of them, if I deselect in any DDL, required validation message rendered for all DDLs. Very Very strange ... – Display Name Mar 13 '13 at 21:07