0

I have a view with a form, simplified below:

@using (Html.BeginForm("New", "User", FormMethod.Post, new { id = "newUserForm" }))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary("There was a problem saving the user:", new { @class = "text-danger" })

    <div class="form-group">
        @Html.LabelFor(model => model.RoleID, new { htmlAttributes = new { @class = "form-label" } })
        <select class="form-control" name="RoleID" id="RoleID" data-val-required="The Role field is required." data-val-number="The field Role must be a number." data-val="true">
            @foreach(RoleOption role in Model.RoleOptions)
            {
                <option value="@role.ID" data-isdefault="@role.IsADefault" @(role.ID == Model.RoleID ? "selected" : "")>@role.DisplayName</option>
            }
        </select>
        @Html.ValidationMessageFor(model => model.RoleID, "", new { @class = "text-danger" })
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Teams, new { htmlAttributes = new { @class = "form-label" } })
        @Html.ListBoxFor(model => model.Teams, Model.TeamsOptions, new { @class = "form-control"})
        @Html.ValidationMessageFor(model => model.Teams, "", new { @class = "text-danger" })
    </div>

    <button type="submit" class="btn btn-primary w-100">Save</button>
}

I'm using jQuery validation and I have a configured the validator as follows:

$("#newUserForm").validate({
    invalidHandler: function (event, validator) {
        var errors = validator.numberOfInvalids();
    },
    submitHandler: function (form) {
        let selectedRole = $("[name='RoleID']").find("option:selected");
        let selectedRoleADefault = selectedRole.data("isdefault").toLowerCase() === "true";
        let selectedTeams = $("[name='Teams']").val();

        if (!selectedRoleADefault && selectedTeams.length === 0) {
            $("[data-valmsg-for='Teams']").text("You must select at least one Team");
        }
        else {
            form.submit();
        }
    }
});

The idea of the submit handler is to do some fairly complicated validation for which I, frankly, couldn't be bothered creating a custom rule, especially since it won't be needed on any other forms. Go with it. The invalid handler is just there to try and help me figure this problem out.

Finally, the relevant excerpt from my view model is:

[Display(Name = "Role")]
[Required]
public int RoleID { get; set; }

I've had to manually add the data-val attributes to the select with the name RoleID because I'm not using the usual Html.DropdownFor HTML Helper, so as to allow me to add the data-isdefault attribute to each option, but I still want that field to be validated.

With the code as it appears above, when I click the submit button on my form the breakpoints I've added in both the invalidHandler and submitHanlder functions are never hit. That's the case whether I select a value for RoleID or not. In other words, whether the form is valid or otherwise, neither callback is ever fired. However, the client-side validation is nevertheless working because when no value is selected the error message for the RoleID field is displayed. When a value is selected, whilst the callback doesn't fire the form does get posted to the server. I find that very odd.

If I remove the data-val attributes that I've added to the RoleID control and submit the form, the breakpoint in the submitHandler function does fire. To a point that makes sense - having removed the data-val attributes that field is no longer being validated and so the form is being considered valid. But that just makes it every weirder to me that neither callback is fired when those attributes are present, even though validation does take place.

After further thought, I've just remembered that in a separate Javascript file I have overridden some of the defaults for the validator:

$.validator.setDefaults({
    ignore: "hidden:not(.collapse :hidden)",
    showErrors: function() {...}
});

I'd forgotten about this, so I've just tried adding a submitHandler property to that code and, sure enough, that is fired when I submit my form with the data-val attributes.

It's fairly obvious at this point that I just don't fully understand how to work with jQuery validation (not the first time I've been made aware of this!), but nevertheless I don't understand:

  1. Why the submitHandler for my particular form fires when the data-val attributes are missing from the RoleID control but not when they are present.
  2. How to successfully specify a submitHandler function that will be called only when this one particular form is validated, whilst using the data-val attributes on my RoleID control (and still picking up the overridden values in my setDefaults call).
Philip Stratford
  • 4,513
  • 4
  • 45
  • 71
  • 1
    The point of the Unobtrusive plugin is to configure and construct the `.validate()` method for you. Since the plugin only recognizes one instance of `.validate()`, your version of the `.validate()` call is ignored. The workaround with Unobtrusive is to use the `.setDefaults()` method to alter the global settings without having to call `.validate()`. – Sparky Oct 04 '21 at 00:36
  • 1
    See: https://stackoverflow.com/a/4764443/594235 – Sparky Oct 04 '21 at 00:39
  • @Sparky Thanks for the explanation. I've been using jQuery validation and Microsoft's unobtrusive validation plugin for years but still don't fully understand how they interact. Whilst I've read the docs for jQuery validation (as old and clunky as it is), I can't find any official documentation for the unobtrusive validation plugin. Do you know if any exists? – Philip Stratford Oct 04 '21 at 09:05
  • I am not a Microsoft developer so I know very little about their Unobtrusive plugin. Doesn't MS have a repository of docs for their code? – Sparky Oct 04 '21 at 15:52

0 Answers0