Remote ViewModel validation of nested objects not working
Asked Answered
H

3

11

I have a class user which looks like this:

public class User
{
    public int UserId { get; set; }

    [Required(ErrorMessage = "A username is required.")]
    [StringLength(20, ErrorMessage = "Your username must be 4-20 characters.", MinimumLength = 4)]
    [RegularExpression("^[a-zA-Z0-9]*$", ErrorMessage = "Your username can only consist of letters and numbers.")]
    [Remote("UsernameExists", "RemoteValidation", ErrorMessage = "Username is already taken")]
    public string Username { get; set; }

    [Required(ErrorMessage = "A password is required.")]
    [MinLength(4, ErrorMessage = "Your password must have at least 4 letters.")]
    public string Password { get; set; }

    [Required(ErrorMessage = "An email address is required.")]
    public string Email { get; set; }
}

For the Register functionality I have created a ViewModel that holds a User object and a string for the password confirmation:

public class RegistrationViewModel
{
    public User User { get; set; }

    [DisplayName("Password confirmation")]
    [Required, Compare("User.Password", ErrorMessage = "The password do not match")]
    public string PasswordConfirmation { get; set; }
}

The first problem I run into is that I can't seem to get the validation for Compare("User.Password") to work as it does not seem to find the property on the user. Is there any way to validate the PasswordConfirmation property against the User.Password property?

The second problem is the Remote validation of the Username field. I followed David Hayden's tutorial at http://davidhayden.com/blog/dave/archive/2011/01/04/ASPNETMVC3RemoteValidationTutorial.aspx but the parameter username in the UsernameExists method is always null. Am I missing something here?

Edit:

I'm sorry but I was actually not clear enough on the error I receive for the password comparison. It works fine when filling in the fields, if the passwords do not match I will receive an error. However, when submitting the form I get the following error in the validation summary: Could not find a property named UserToRegister.Password.

Edit 2:

I have figured out part of the problem thanks to Joe's post. The remote validator posts back URL/?UserToRegister.Username=temp which obviously does not match the username parameter of my controller action. In order to map my action parameter to UserToRegister.Username the following is required:

public ActionResult UsernameExists([Bind(Prefix = "UserToRegister.Username")]string username)

This now correctly passes the parameter to the method. However I still get the error when using the Compare attribute on the password field.

Thanks.

Herschelherself answered 16/2, 2011 at 23:51 Comment(5)
I updated my answer after looking at the request that you provided. You can make your action take a User rather than a string to work around specifying a prefix.Blasphemous
For the compare attribute you need to compare to another property on the model, so if you leave your user in the model you would need a property that returns the password of the user, but can you take a look at the model in the ASP.NET MVC default templates? These do not contain the user themselves but relevant fields from the user such as password, name, confirm password etc. Then the compare attribute can be used to compare these fields.Blasphemous
Thanks Joe, I was thinking of using only the fields on the model that I require such as password, username and email. But then I thought it would double up the validation attributes and I have two places to maintain them if something changes. Is there a clean way for doing this (without inheritance because then again my viewmodel will contain fields I don't require).Herschelherself
You could either always take a user and then whitelist/blacklist only the fields you are interested in for that particular action or you could create different models such as a RegisterModel and a LogOnModel that contained only the properties of the user that you are interested in, this is what the default project templates do.Blasphemous
Telling users that a username is taken is an obvious security breach. Especially via a special 'service' that checks if a username is being used.Cosmic
S
5

The issue with the validation of the PasswordConfigurmation property against the User.Password property is caused by a bug in in the 'jquery.validate.unobtrusive.js' file.

Originally, the jquery 'equalTo' function is:

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

You just need to modify this line:

element = $(options.form).find(":input[name=" + fullOtherName + "]")[0];

to:

element = $(options.form).find(":input[name='" + fullOtherName + "']")[0];

Note the addition on the single quotes around the 'fullOtherName' selector. Once you've made this change, the client side validation works as expected.

Shornick answered 1/3, 2012 at 20:33 Comment(4)
I had the exact same problem as the asker and this fixed it. I had to do the replacement in both the .js and the min.js.Floyd
BTW - I filed a bug in github. github.com/jzaefferer/jquery-validation/issues/327Floyd
I've told the ASP.NET MVC team about this and will find the author.Monitor
Just to follow this up, I've just come across this page and I can see that in the latest version, added via NuGet in VS2010, this appears to be fixed.Nemathelminth
B
1

For the remote validation part nothing jumps out at me. It might be helpful to open up firebug and see what the request that is being fired looks like. You should see something roughly like this if everything is properly wired up...

http://localhost:14547/[Controller]/[ActionName]?[ParamName]=[paramValue]

From the request you provided, you can either use a prefix like you ended up doing or you can make your action take a user named UserToRegister and then within the action access the UserName property. This is the recommended way of dealing with remote validation of objects and might be a little easier to think about than using a Bind attribute.

For the compare validation, it appears that on the client side validation is succeeding but on the server side validation fails because the validation context does not contain a property named User.Password, only a property named User.

Blasphemous answered 17/2, 2011 at 1:21 Comment(1)
The request fired looks like this: Request URL:http://localhost:58777/RemoteValidation/UsernameExists?UserToRegister.Username=temp it seems it appends the property getters as the name of the parameter. Does this mean my name parameter name on the method should be UserToRegister.Username? This of course won't work...Herschelherself
B
0

Inheritance seems like a standard way of adding functionality in this case. How about having your RegistrationViewModel derive from the UserViewModel:

public class RegistrationViewModel : UserViewModel
{
    [DisplayName("Password confirmation")]
    [Required]
    [Compare("Password", ErrorMessage = "The password do not match")]
    public string PasswordConfirmation { get; set; }
}

and:

public ActionResult UsernameExists(string Username)
{
   ...
}
Babbette answered 17/2, 2011 at 7:11 Comment(2)
Thanks, I will give this a try. But surely there must be a way of having ViewModels consist of other objects without inheritance. In this simple case it might work but what If I had an Address object that gets populated using the same form? I could only inherit from one and not User and Address at the same time.Herschelherself
@b3n, normally if you had Address you wouldn't need to do a [Compare] on an Address property with some property that's on the main view model. So try to design properly your view models.Babbette

© 2022 - 2024 — McMap. All rights reserved.