16

so according to Gu IValidatableObject.Validate() should get called when a controller validates it's model (i.e. before ModelState.IsValid) however simply making the model implement IValidatableObject doesn't seem to work, because Validate(..) doesn't get called.

Anyone know if there is something else I have to wire up to get this to work?

EDIT:

Here is the code as requested.

public class LoginModel : IValidatableObject
{
    [Required]
    [Description("Email Address")]
    public string Email { get; set; }

    [Required]
    [Description("Password")]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [DisplayName("Remember Me")]
    public bool RememberMe { get; set; }

    public int UserPk { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var result = DataContext.Fetch( db => {

            var user = db.Users.FirstOrDefault(u => u.Email == Email);

            if (user == null) return new ValidationResult("That email address doesn't exist."); 
            if (user.Password != User.CreateHash(Password, user.Salt)) return new ValidationResult("The password supplied is incorrect.");

            UserPk = user.UserPk;
            return null;
        });

        return new List<ValidationResult>(){ result };
    }
}

The action. ( I don't do anything special in the Controller...)

[HttpPost]
public ActionResult Login(LoginModel model)
{
    if (ModelState.IsValid)
    {
        FormsAuthentication.SetAuthCookie(model.Email, model.RememberMe);
        return Redirect(Request.UrlReferrer.AbsolutePath);
    }

    if (ControllerContext.IsChildAction || Request.IsAjaxRequest())
        return View("LoginForm", model);

    return View(model);
}

I set a break point on the first line of LoginModel.Validate() and it doesn't seem to get hit.

Master Morality
  • 5,837
  • 6
  • 31
  • 43
  • You're code looks just fine. Exactly like it should. Just a point of interest but do you have a duplicate model? I know I have a view model and a db model for each object. Could your controller be referencing the wrong model? – Buildstarted Sep 19 '10 at 16:19
  • 2
    Also, as a side note: you should definitely return only one error if the username or password is invalid and not distinct errors. This is simply for security as I can test each field individually to find a username and then work on the password for that user. It's not required but it's a good idea :) – Buildstarted Sep 19 '10 at 16:20
  • You could use `yield return DataContext...` rather than returning a new List. It would be prettier and faster. – pipedreambomb Jan 23 '17 at 14:16

2 Answers2

18

There isn't anything more than that you just have to add it to the model you're validating. Here's an example of validation

public class User : IValidatableObject {
    public Int32 UserID { get; set; }
    public string Name { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
        //do your validation

        return new List<ValidationResult>();
    }
}

And your controller would use this model

public ActionResult Edit(User user) {
    if (ModelState.IsValid) {
    }
}

Hope this helps. Other requirements are .net 4 and data annotations - which you obviously need jsut for ivalidatableobject. Post any issues and we'll see if we can't resolve them - like post your model and your controller...you might be missing something.

Buildstarted
  • 26,529
  • 10
  • 84
  • 95
  • 24
    You are correct, the one caveat is that if you have any validation attributes that result in the model being invalid, Validate never gets called. That was my issue. – Master Morality Sep 19 '10 at 16:13
  • Ah, how interesting. Thanks for that. – Buildstarted Sep 19 '10 at 18:37
  • 2
    That caveat strikes me as a very handy thing to know :) +1 to both – Darko May 11 '11 at 04:58
  • According to ScottGu, MVC 3 uses both DataAnnotations and IValidatableObject (see http://weblogs.asp.net/scottgu/archive/2010/12/10/class-level-model-validation-with-ef-code-first-and-asp-net-mvc-3.aspx): "ASP.NET MVC 3 also now supports both DataAnnotations and IValidatableObject as well". From The Flower Guy's post below (thank you!), obviously data annotations take precedence. – Suncat2000 Apr 05 '12 at 13:30
  • If you want to use the ModelState fully, you should provide the name(s) of the invalid field(s) as the second parameter of the ValidationResult constructor. They will be used by MVC to generate the key(s) of the ModelState dictionary that the error(s) is/are added against. In this example, it would be `new ValidationResult(errorMessage, new []{nameof(Email)})` – pipedreambomb Jan 23 '17 at 14:06
6

Validation using the DefaultModelBinder is a two stage process. First, Data Annotations are validated. Then (and only if the data annotations validation resulted in zero errors), IValidatableObject.Validate() is called. This all takes place automatically when your post action has a viewmodel parameter. ModelState.IsValid doesn't do anything as such. Rather it just reports whether any item in the ModelState collection has non-empty ModelErrorCollection.

Sergio
  • 6,900
  • 5
  • 31
  • 55
Paul Hiles
  • 9,558
  • 7
  • 51
  • 76
  • I always wondered about that. Whether validation was called automagically in the DefaultModelBinder, or if ModelState.IsValid checked some hidden `_hadBeenValidated` property and called validate if `_hasBeenValidated == false`. – Master Morality Jan 29 '11 at 15:49
  • 2
    The described behavior isn't exactly the real one: please see http://stackoverflow.com/questions/8153602/ivalidatableobject-validate-combined-with-dataannotations – Diego Nov 16 '11 at 14:52