MVC3 CompareAttribute, client-side bug

I am using MVC3 and I want to have LogIn form and Register form on the same page. To achieve that I built LogInRegisterViewModel as following:

public class LogInRegisterViewModel
{
    public LogInViewModel LogIn { get; set; }
    public RegisterViewModel Register { get; set; }
}

It gives me what I want (two forms on the same screen) and posts the data to correct controllers and returns and displays errors for forms (if any). The only problem I have is with CompareAttribute that I have above ConfirmPassword property in my RegisterViewModel:

public class RegisterViewModel
{
    [Required]
    [Display(Name = "Friendly user name")]
    public string UserName { get; set; }

    [Required]
    [Display(Name = "E-mail address")]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    [StringLength(16, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "Passwords do not match.")]
    public string ConfirmPassword { get; set; }
}

Client-side the passwords are never equal (~ I always get a validation error from Compare with a message telling me that they are not equal), even if they are (I am sure about that). HTML in the browser is:

    <div class="editor-label">
        <label for="Register_Password">Password</label>
    </div>
    <div class="editor-field">
        <input class="valid" data-val="true" data-val-length="The Password must be at least 6 characters long." data-val-length-max="16" data-val-length-min="6" data-val-required="The Password field is required." id="Register_Password" name="Register.Password" type="password">
        <span class="field-validation-valid" data-valmsg-for="Register.Password" data-valmsg-replace="true"></span>
    </div>

    <div class="editor-label">
        <label for="Register_ConfirmPassword">Confirm password</label>
    </div>
    <div class="editor-field">
        <input class="input-validation-error" data-val="true" data-val-equalto="Passwords do not match." data-val-equalto-other="*.Password" id="Register_ConfirmPassword" name="Register.ConfirmPassword" type="password">
        <span class="field-validation-error" data-valmsg-for="Register.ConfirmPassword" data-valmsg-replace="true"><span class="" generated="true" for="Register_ConfirmPassword">Passwords do not match.</span></span>
    </div>

I have a feeling that it's all about this attribute: data-val-equalto-other="*.Password"

CompareAttribute works fine, when I use RegisterViewModel directly. Anyone has come into this before? Is it a bug or am I doing something wrong? How to make Compare to work in my case?

Answers


It should work with the [Compare("Password", ErrorMessage = "Passwords do not match.")] attribute but it seems this is really a bug in the jquery.validate.unobtrusive.js file. The problem is in this code:

adapters.add("equalto", ["other"], function (options) {
    var prefix = getModelPrefix(options.element.name),
        other = options.params.other,
        fullOtherName = appendModelPrefix(other, prefix),
        element = $(options.form).find(":input[name=" + fullOtherName + "]")[0];

    setValidationValues(options, "equalTo", element);
});

So it tries to find the other control with the JQuery find method. But the "." dot character is not escaped in the fullOtherName variable (in your case it will contain: "Register.Password") as described in this SO question. That is the reason why it works when you only use RegisterViewModel directly because then there is no dots in the names.

To fix it you need to add one line to the appendModelPrefix function:

//original
function appendModelPrefix(value, prefix) {
    if (value.indexOf("*.") === 0) {
        value = value.replace("*.", prefix);
    }
    return value;
}

//fixed
function appendModelPrefix(value, prefix) {
    if (value.indexOf("*.") === 0) {
        value = value.replace("*.", prefix);
    }
    value = value.split('.').join('\\.');
    return value;
}

Very good answer, nemesv.

Only one thing to add for rookies:

function g(a,b){if(a.indexOf("*.")===0)a=a.replace("*.",b);return a}

becomes

function g(a,b){if(a.indexOf("*.")===0)a=a.replace("*.",b);a=a.split('.').join('\\.');return a}

in .min.js

Otherwise when you publish, the error comes back.


Need Your Help

Replay on BG Music

java android

I made a service that play background music, but when the music finished I want to replay it again. Which method can I use in my service?

Transfer attributes from one directive to another

angularjs kendo-ui angular-directive kendo-dropdown

I have directive foo that I want to use on another directive dropdown. The problem is that the dropdown directive is using yet another directive called kendo-drop-down-list in the template.