2

I have a custom model that holds some DateTime values, and a custom DataAnnotation that's built to compare these values.

Here's the properties with their annotations:

[Required]
[DataType(System.ComponentModel.DataAnnotations.DataType.Date)]
[Display(Name = "Start Date")]
public DateTime StartTime { get; set; }

[DataType(System.ComponentModel.DataAnnotations.DataType.Date)]
[Display(Name = "End Date")]
[CompareTo(this.StartTime, CompareToAttribute.CompareOperator.GreaterThanEqual)]
public DateTime? EndTime { get; set; }

The CompareTo attribute is the one in question. I get an error:

Keyword 'this' is not available in the current context

I've tried placing only StartTime in the annotation with no luck. How can I pass in a property value from the same model class?

keeehlan
  • 7,874
  • 16
  • 56
  • 104

4 Answers4

5

If anyone is still wondering how to compare two dates and use that in a validation DataAnnotation, you can simply add an extension method that will compare the start date and the end date like the following.

Assuming that this is your class:

using System;
using System.ComponentModel.DataAnnotations;

namespace Entities.Models
{
    public class Periode
    {
        [Key]
        public int PeriodeID { get; set; }

        public string Name { get; set; }

        [DataType(DataType.Date)]
        [Display(Name ="Start Date")]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime StartDate { get; set; }

        [DataType(DataType.Date)]
        [Display(Name = "End Date")]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EndDate { get; set; }
    }
}

You simply add the following class as a validator:

namespace Entities.Models
{

    public class StartEndDateValidator : ValidationAttribute
    {
        protected override ValidationResult
                IsValid(object value, ValidationContext validationContext)
        {
            var model = (Models.Periode)validationContext.ObjectInstance;
            DateTime EndDate = Convert.ToDateTime(model.EndDate);
            DateTime StartDate = Convert.ToDateTime(value);

            if (StartDate > EndDate)
            {
                return new ValidationResult
                    ("The start date must be anterior to the end date");
            }
            else
            {
                return ValidationResult.Success;
            }
        }
    }
}

And then you need to add that DataAnnotation on the StartDate as following

namespace Entities.Models
{
    public class Periode
    {
        [Key]
        public int PeriodeID { get; set; }

        public string Name { get; set; }

        [DataType(DataType.Date)]
        [Display(Name ="Start Date")]
        // You need to add the following line
        [StartEndDateValidator]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime StartDate { get; set; }

        [DataType(DataType.Date)]
        [Display(Name = "End Date")]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EndDate { get; set; }
    }
}
Hassen Ch.
  • 1,693
  • 18
  • 31
3

I've tried placing only StartTime in the annotation with no luck. How can I pass in a property value from the same model class?

That's impossible because attributes are metadata that is baked into the assembly at compile-time. This means that you can pass only CONSTANT parameters to an attribute. Yeah, that's a hell of a limitation because in order to perform such an obvious validation thing as comparing 2 values in your model you will have to write gazzilion of plumbing code such as what I have illustrated here for example: https://stackoverflow.com/a/16100455/29407 I mean, you will have to use reflection! Come on Microsoft! Are you serious?

Or just cut the crap of data annotations and start doing validation the right way: using FluentValidation.NET. It allows you to express your validation rules in a very elegant way, it greatly integrates with ASP.NET MVC and allows you to unit test your validation logic in isolation. It also doesn't rely on reflection so it is super fast. I have benchmarked it and using it in very heavy traffic production applications.

Data annotations just don't cut the mustard compared to imperative validation rules when you start writing applications that are a little more complicated than a Hello World and which require a little more complex validation logic than you would have in a Hello World application.

Community
  • 1
  • 1
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 2
    Why would you be offended? No offense, I guess the blog posts around the internet are just missing important aspects of ASP.NET MVC. It's not your mistake that you read somewhere that validation in ASP.NET MVC is done with Data Annotations and attempted to implement it in your application until you hit its limitations. Don't worry I have gone exactly the same way as you and did the same mistakes. That's why I answered with an alternative approach hoping that people won't repeat those mistakes in the future. – Darin Dimitrov Jul 18 '13 at 21:05
  • 1
    I don't think that comparing 2 properties in a validator is a dumb issue. It's so common. Come on Microsoft, what were you thinking when you took the design decision of building validation logic with data annotations? – Darin Dimitrov Jul 18 '13 at 21:07
  • Sure, you also have the choice of writing the plumbing code that I have linked to in my answer if you absolutely need to use Data Annotations. But think of what your validation logic will resemble if you need a little more complex rules. Think of the poor Souls that will have to maintain such code. – Darin Dimitrov Jul 18 '13 at 21:11
  • Oh no, this crap is here to stay :-) – Darin Dimitrov Jul 18 '13 at 21:12
1

I like Hassen's answer.

Same as Hassen's example, but suggest: 1) aborting if no end date if end date is optional. 2) putting a validator on end date in case user only changes end date

Data Annotation:

[Required]
[Display(Name = "Start Effective Date", Description = "Start Date")]
[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
[DataType(DataType.Date)]
[StartDateValidator]
public DateTime StartEffectiveDate { get; set; }

[Display(Name = "End Effective Date", Description = "End Date")]
[DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
[DataType(DataType.Date)]
[EndDateValidator]
public DateTime? EndEffectiveDate { get; set; }

Code:

public class StartDateValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;
        if (model.EndEffectiveDate == null)  // Abort if no End Date
            return ValidationResult.Success;

        DateTime EndDate = model.EndEffectiveDate.GetValueOrDefault();
        DateTime StartDate = Convert.ToDateTime(value);  // value = StartDate

        if (StartDate > EndDate)
            return new ValidationResult("The start date must be before the end date");
        else
            return ValidationResult.Success;
    }
}

public class EndDateValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;
        if (model.EndEffectiveDate == null)  // Abort if no End Date
            return ValidationResult.Success;

        DateTime EndDate = Convert.ToDateTime(value); // value = EndDate
        DateTime StartDate = model.StartEffectiveDate; 

        if (StartDate > EndDate)
            return new ValidationResult("The start date must be before the end date");
        else
            return ValidationResult.Success;
    }
}

Would have commented on Hassen's answer, but don't have enough reputation.

Eric Wood
  • 331
  • 1
  • 3
  • 14
0
public class StartDateValidator : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;
        if (model.EndEffectiveDate == null)  // Abort if no End Date
            return ValidationResult.Success;

        DateTime EndDate = model.EndEffectiveDate.GetValueOrDefault();
        DateTime StartDate = Convert.ToDateTime(value);  // value = StartDate

        if (StartDate > EndDate)
            return new ValidationResult("The start date must be before the end date");
        else
            return ValidationResult.Success;
    }
}

In the above example there is one issue , This solution can't be used as a common solution for validating the dates . Because the type casting of below line is not generic

var model = (CostCenterAllocationHeader)validationContext.ObjectInstance;

It means that the validation can only be applied to the specific model "costCenterAllocationHeader" . What needs to be done by passing the member name to the constructor of the validator and get the value from the ValidationContext using reflection. By this method we can use this attribute as a generic solution and can be applied in any ViewModels.