MVC3 CompareAttribute, client-side bug
Asked Answered
B

2

8

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?

Began answered 4/12, 2011 at 15:31 Comment(0)
D
15

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;
}
Drusi answered 4/12, 2011 at 16:20 Comment(1)
Even this fix has a bug of its own. Javascript ".replace" only replaces the first instance of "." and ignores the rest. In complex models, any property that would have more than one period would suffer the same fate as the original code. To fix it, you should use: value = value.split('.').join('\\.');Washing
C
0

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.

Commorant answered 1/2, 2014 at 23:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.