THE CONTEXT
I have an MVC form that includes two date pickers, for which I have written a custom validation attribute to ensure that both selected dates are not in the past (>= today).
THE PROBLEM
The validation attribute works only for the first date but not for the second, because the value passed to the "value" object in the validation attribute is always reset to the value set in the form class (HomeForm.cs) constructor (HomeForm( )).
Example (VS + browser screenshot):
The picked date is the 10/08/2021 but the Object value for dateB is 16/08/2021.
issue example
THE CODE
This is my form (HomeForm.cs):
public class HomeForm
{
public HomeForm()
{
dateA = DateTime.Now.Date;
TimeSpan time = new TimeSpan(1, 0, 0, 0);
dateB = DateTime.Now.Date + time;
}
[Required(ErrorMessage = "You must select a departure date.")]
[DataType(DataType.Date)]
[DisplayName("Departure date:")]
[DateTimeRange]
public DateTime dateA { get; set; }
[Required(ErrorMessage = "You must select a return date.")]
[DataType(DataType.Date)]
[DisplayName("Return date:")]
[DateTimeRange]
public DateTime dateB { get; set; }
};
A custom Validation Attribute "DateTimeRange" is applied to dateA and dateB:
[AttributeUsage(AttributeTargets.Property)]
public class DateTimeRangeAttribute : ValidationAttribute
{
private readonly DateTime _today;
public DateTimeRangeAttribute()
{
_today = DateTime.Now.Date;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is not DateTime)
return new ValidationResult("Date format is not valid.");
if (!IsValid((DateTime)value))
return new ValidationResult("Chosen date is in the past.");
if(value.ToString().Count() != 19)
return new ValidationResult("Not a valid date. Choose a different one.");
return ValidationResult.Success;
}
private bool IsValid(DateTime value)
{
value = value.Date;
return value >= _today;
}
}
Related
In my web api project, I have model class RegisterModel, There is nullable date time, I want to validate input enter by user only when if user has enter the dob.
I'm using Json.Net Serializer
my model class
[Validator(typeof(RegisterModelValidator))]
public class RegisterModel
{
[JsonProperty("dob")]
public Nullable<DateTime> DOB { get; set; }
}
my validator
public class RegisterModelValidator : AbstractValidator<RegisterModel>
{
public RegisterModelValidator()
{
RuleFor(x => x.DOB).Must(BeAValidDate).WithMessage("Please enter valid date.");
}
private bool BeAValidDate(DateTime date)
{
if (date == default(DateTime))
return false;
return true;
}
private bool BeAValidDate(DateTime? date)
{
if (date == default(DateTime))
return false;
return true;
}
}
But when I pass value e.g: "dob":"123 APR 2015"
It ModelState.IsValid return false, But does not return validation message.
Validating DateTimes using fluent validation only does not seem to possible, as mentioned here. This answer seems to provide the most useful ways of actually validating invalid dates.
public ActionResult UpdateDateOfBirth(ModelClass model)
{
if (ModelState.IsValid)
{
// your logic
}
AddErrors(ModelState);
return View();
}
// in my case i have to be add in viewbag
private void AddErrors(ModelStateDictionary result)
{
ViewBag.ErrorMessages = result;
//foreach (KeyValuePair<string,System.Web.Mvc.ModelState> error in result)
//{
// //ModelState.AddModelError(error.Key,error.Value.Errors[0].ErrorMessage);
//}
}
// and you can add the validation message on
public class ModelClass
{
[StringLength(100, ErrorMessage = "Not a valid date of birth"]
[DataType(DataType.DateTime)]
[Display(Name = "Date of Birth")]
public string DateOfBirth{ get; set; }
}
I have a metadata class for my Customer where I validate the PurchaseDate.
The first annotation (DataType) is for formatting the date in an EditorFor, to show just the date part.
The second annotation is a custom validation to verify that the value is a DateTime, including a custom error message.
My problem is that the first annotation will cancel out the errormessage of the second annotation.
Is it possible to combine these two using only data annotations? Or do I have to format the date in the EditorFor?
[MetadataType(typeof(Customer_Metadata))]
public partial class Customer { }
public class Customer_Metadata
{
[DataType(DataType.Date)]
[MyDate(ErrorMessage = "Invalid purchase date")]
public DateTime? PurchaseDate { get; set; }
}
The same problem occurs if I try to replace the [DataType(DataType.Date)]
with
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
I won't get my custom error message.
EDIT
My main goal is to have a custom error message while also only showing the date part in the rendered input field. Is it possible with only data annotations?
Here's the MyDate attribute:
public class MyDate : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
DateTime dt;
var test = DateTime.TryParse((value ?? string.Empty).ToString(), out dt);
if (test)
{
return ValidationResult.Success;
}
else
{
return new ValidationResult(ErrorMessage);
}
}
}
Say I have 2 classes with the same set of properties:
public class MyDto
{
public int Id { get; set; }
public DateTime CreatedOn { get; set; }
}
public class MyViewModel
{
public int Id { get; set; }
public DateTime CreatedOn { get; set; }
}
I want to map with AutoMapper, adjusting the UTC date of the input class to local time of the output class, e.g., granted I am in UK where UTC offset currently is 1h:
var input = new MyDto {Id = 1, CreatedOn = DateTime.Parse("01-01-2015 14:30")};
var output = Mapper.Map<MyViewModel>(input); // output.CreatedOn = "01-01-2015 15:30"
Can I cofigure AutoMapper to this automatically for all DateTime properties?
N.B. to adjust the time I use DateTime.SpecifyKind(value, DateTimeKind.Utc)
You can create a custom type converter:
public class CustomDateTimeConverter : ITypeConverter<DateTime, DateTime> {
public DateTime Convert(ResolutionContext context) {
var inputDate = (DateTime) context.SourceValue;
var timeInUtc = DateTime.SpecifyKind(inputDate, DateTimeKind.Utc);
return TimeZoneInfo.ConvertTime(timeInUtc, TimeZoneInfo.Local);
}
}
This will make AutoMapper perform the conversion from UTC to local time for every mapping between two DateTime properties.
I have a second thing:
<td>
#Html.LabelFor(m => m.Number, titleHtmlAttrs)
</td>
<td>
<span class="element-value2">
#Html.EditorFor(m => m.Number)
#Html.ValidationTooltipFor(m => m.Number)
</span>
</td>
And this is how this field looks like in model:
[Display(Name = "Special Number")]
[StringLength(20)]
public string Number { get; set; }
Which means that if I wanted to change this field, i can have any value from empty to 20.
It's ok, but now I need an additional validation.
In model I have some fields:
public DateTime? TimeOf { get; set; }
public bool HasType { get; set; }
New validation should work ONLY if TimeOf is not null and HasType is true. New validation should prevent empty values in Number. Basically, change (from empty to 20) to (from 1 to 20).
How could I correctly accomplish this?
P.S Sorry about my bad English.
For complex validation logic, look at implementing IValidatableObject in your ViewModel and then you can place your conditional validation logic inside the Validate method. (Caveat, this is obviously server side)
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.HasType)
{
// Do other conditional validation
if (validationFails)
{
yield return new ValidationResult("descriptive error goes here");
}
}
// Other validation here.
UPDATE It seems I have misunderstood the question. As the other answer has already pointed out, you could implement the IValidatableObject for achieving this. Something like:
public class YourModelName : IValidatableObject
{
[StringLength(20)]
public string Number{ get; set; }
[Required]
public DateTime? TimeOf { get; set; }
public bool HasType { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (TimeOf != null && HasType)
Validator.TryValidateProperty(this.Number,
new ValidationContext(this, null, null) { MemberName = "Number" },
results);
if (TimeOf == null)
results.Add(new ValidationResult("Date Time must have a value"));
if (!HasType)
results.Add(new ValidationResult("Must be true"));
return results;
}
}
OLD ANSWER:
You could write your custom validator for more complex validation conditions. Something like:
public class SomeCustomValidator : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
string number = value as string;
if (value == null) throw new InvalidOperationException("Can only be used on string properties");
if (!value.IsEmpty && value.Length <= 20)
{
return ValidationResult.Success;
}
return new ValidationResult("Name must be a non-empty string smaller than 20 chars"));
}
}
And for HasType, another custom one:
public class IsTrueAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null) return false;
if (value.GetType() != typeof(bool)) throw new InvalidOperationException("can only be used on boolean properties.");
return (bool) value == true;
}
}
And on TimeOf you could use the required attribute to make sure it has a value:
[Required(ErrorMessage="Must have value")]
public DateTime? TimeOf {get;set;}
And use the custom attributes on the other two:
[SomeCustomValidator(ErrorMessage="Error msg...")]
public string Number {get;set;}
[IsTrueAttribute(ErrorMessage="Must be true")]
public bool HasType {get;set;}
I have a data validation class that checks whether the start date of a meeting is before the end date.
The model automatically passes in the date that requires validation, but i'm having a bit of difficulty passing the data that it needs to be validated against.
Here's my validation class
sealed public class StartLessThanEndAttribute : ValidationAttribute
{
public DateTime DateEnd { get; set; }
public override bool IsValid(object value)
{
DateTime end = DateEnd;
DateTime date = (DateTime)value;
return (date < end);
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
}
}
Here's the class that contains the data annotations
[StartLessThanEnd(ErrorMessage="Start Date must be before the end Date")]
public DateTime DateStart { get; set; }
And here's my controller
[HttpPost, Authorize]
public ActionResult Create(Pol_Event pol_Event)
{
ViewData["EventTypes"] = et.GetAllEventTypes().ToList();
StartLessThanEndAttribute startDateLessThanEnd = new StartLessThanEndAttribute();
startDateLessThanEnd.DateEnd = pol_Event.DateEnd;
if (TryUpdateModel(pol_Event))
{
pol_Event.Created_On = DateTime.Now;
pol_Event.Created_By = User.Identity.Name;
eventRepo.Add(pol_Event);
eventRepo.Save();
return RedirectToAction("Details", "Events", new { id = pol_Event.EventID });
}
return View(pol_Event);
}
Validation attributes that work with multiple properties should be applied to the model and not on individual properties:
[AttributeUsage(AttributeTargets.Class)]
public class StartLessThanEndAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
var model = (MyModel)value;
return model.StartDate < model.EndDate;
}
}
[StartLessThanEnd(ErrorMessage = "Start Date must be before the end Date")]
public class MyModel
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}