Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to set membernames from custom validation attribute in MVC2

I have created a custom validation attribute by subclassing ValidationAttribute. The attribute is applied to my viewmodel at the class level as it needs to validate more than one property.

I am overriding

protected override ValidationResult IsValid(object value, ValidationContext validationContext)

and returning:

new ValidationResult("Always Fail", new List<string> { "DateOfBirth" }); 

in all cases where DateOfBirth is one of the properties on my view model.

When I run my application, I can see this getting hit. ModelState.IsValid is set to false correctly but when I inspect the ModelState contents, I see that the Property DateOfBirth does NOT contain any errors. Instead I have an empty string Key with a value of null and an exception containing the string I specified in my validation attribute.

This results in no error message being displayed in my UI when using ValidationMessageFor. If I use ValidationSummary, then I can see the error. This is because it is not associated with a property.

It looks as though it is ignoring the fact that I have specified the membername in the validation result.

Why is this and how do I fix it?

EXAMPLE CODE AS REQUESTED:

 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class ExampleValidationAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // note that I will be doing complex validation of multiple properties when complete so this is why it is a class level attribute
            return new ValidationResult("Always Fail", new List<string> { "DateOfBirth" });
        }
    }

    [ExampleValidation]
    public class ExampleViewModel
    {
        public string DateOfBirth { get; set; }
    }
like image 955
Paul Hiles Avatar asked Nov 24 '10 12:11

Paul Hiles


3 Answers

When returning the validation result use the two parameter constructor. Pass it an array with the context.MemberName as the only value. Hope this helps

<AttributeUsage(AttributeTargets.Property Or AttributeTargets.Field, AllowMultiple:=False)>


Public Class NonNegativeAttribute
Inherits ValidationAttribute
Public Sub New()


End Sub
Protected Overrides Function IsValid(num As Object, context As ValidationContext) As ValidationResult
    Dim t = num.GetType()
    If (t.IsValueType AndAlso Not t.IsAssignableFrom(GetType(String))) Then

        If ((num >= 0)) Then
            Return ValidationResult.Success
        End If
        Return New ValidationResult(context.MemberName & " must be a positive number",     New String() {context.MemberName})

    End If

    Throw New ValidationException(t.FullName + " is not a valid type. Must be a number")
End Function

End Class
like image 128
Brian Rizo Avatar answered Nov 20 '22 15:11

Brian Rizo


hello everybody.

Still looking for solution?

I've solved the same problem today. You have to create custom validation attribute which will validate 2 dates (example below). Then you need Adapter (validator) which will validate model with your custom attribute. And the last thing is binding adapter with attribute. Maybe some example will explain it better than me :)

Here we go:

DateCompareAttribute.cs:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class DateCompareAttribute : ValidationAttribute
{
    public enum Operations
    {
        Equals,            
        LesserThan,
        GreaterThan,
        LesserOrEquals,
        GreaterOrEquals,
        NotEquals
    };

    private string _From;
    private string _To;
    private PropertyInfo _FromPropertyInfo;
    private PropertyInfo _ToPropertyInfo;
    private Operations _Operation;

    public string MemberName
    {
        get
        {
            return _From;
        }
    }

    public DateCompareAttribute(string from, string to, Operations operation)
    {
        _From = from;
        _To = to;
        _Operation = operation;

        //gets the error message for the operation from resource file
        ErrorMessageResourceName = "DateCompare" + operation.ToString();
        ErrorMessageResourceType = typeof(ValidationStrings);
    }

    public override bool IsValid(object value)
    {
        Type type = value.GetType();

        _FromPropertyInfo = type.GetProperty(_From);
        _ToPropertyInfo = type.GetProperty(_To);

        //gets the values of 2 dates from model (using reflection)
        DateTime? from = (DateTime?)_FromPropertyInfo.GetValue(value, null);
        DateTime? to = (DateTime?)_ToPropertyInfo.GetValue(value, null);

        //compare dates
        if ((from != null) && (to != null))
        {
            int result = from.Value.CompareTo(to.Value);

            switch (_Operation)
            {
                case Operations.LesserThan:
                    return result == -1;
                case Operations.LesserOrEquals:
                    return result <= 0;
                case Operations.Equals:
                    return result == 0;
                case Operations.NotEquals:
                    return result != 0;
                case Operations.GreaterOrEquals:
                    return result >= 0;
                case Operations.GreaterThan:
                    return result == 1;
            }
        }

        return true;
    }

    public override string FormatErrorMessage(string name)
    {
        DisplayNameAttribute aFrom = (DisplayNameAttribute)_FromPropertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), true).SingleOrDefault();
        DisplayNameAttribute aTo = (DisplayNameAttribute)_ToPropertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), true).SingleOrDefault();

        return string.Format(ErrorMessageString,
            !string.IsNullOrWhiteSpace(aFrom.DisplayName) ? aFrom.DisplayName : _From,
            !string.IsNullOrWhiteSpace(aTo.DisplayName) ? aTo.DisplayName : _To);
    }
}

DateCompareAttributeAdapter.cs:

public class DateCompareAttributeAdapter : DataAnnotationsModelValidator<DateCompareAttribute> 
{
    public DateCompareAttributeAdapter(ModelMetadata metadata, ControllerContext context, DateCompareAttribute attribute)
        : base(metadata, context, attribute) {
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        if (!Attribute.IsValid(Metadata.Model))
        {
            yield return new ModelValidationResult
            {
                Message = ErrorMessage,
                MemberName = Attribute.MemberName
            };
        }
    }
}

Global.asax:

protected void Application_Start()
{
    // ...
    DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(DateCompareAttribute), typeof(DateCompareAttributeAdapter));
}

CustomViewModel.cs:

[DateCompare("StartDateTime", "EndDateTime", DateCompareAttribute.Operations.LesserOrEquals)]
public class CustomViewModel
{
    // Properties...

    public DateTime? StartDateTime
    {
        get;
        set;
    }

    public DateTime? EndDateTime
    {
        get;
        set;
    }
}
like image 45
Radek Duchoň Avatar answered Nov 20 '22 13:11

Radek Duchoň


I am not aware of an easy way fix this behavior. That's one of the reasons why I hate data annotations. Doing the same with FluentValidation would be a peace of cake:

public class ExampleViewModelValidator: AbstractValidator<ExampleViewModel>
{
    public ExampleViewModelValidator()
    {
        RuleFor(x => x.EndDate)
            .GreaterThan(x => x.StartDate)
            .WithMessage("end date must be after start date");
    }
}

FluentValidation has great support and integration with ASP.NET MVC.

like image 2
Darin Dimitrov Avatar answered Nov 20 '22 14:11

Darin Dimitrov