MVC3 CompareAttribute, client-side bug

2019-01-11 14:43发布

问题:

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?

回答1:

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;
}


回答2:

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.