I have a problem ...
I have a user registration form .. in this form I have 3 fields that represent the day, month and year of birth. I like to have 3 fields distinct (3 menus). how can I create a validator that allows me to check if a valid date? (not accept dates like 30/02/2011) I could do it in JavaScript (client side), but even if I wanted to have the validator as usual in model-vew-controller?
You can bind validators to classes instead of properties.
I would do something like that:
//The Model
[DateValidator]
public class Date
{
public string Month { get; set; }
public string Day { get; set; }
public string Year { get; set; }
}
//The DataAnnotation
[AttributeUsage(AttributeTargets.Class)]
class DateValidatorAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
var date = value as Date;
Debug.Assert(date != null);
var dateString = date.Month + date.Day + date.Year;
DateTime dateTime;
var isValid = DateTime.TryParseExact(dateString, "ddMMyyyy", CultureInfo.InvariantCulture, DateTimeStyles.None,
out dateTime);
return isValid;
}
}
Edit
DateTime.TryParseExact DOES check the validity of a DateTime (i.e. it will return false if you try to parse 30/02/2011).
Create your model binder which will get those 3 fields from context and validate them altogether.
Related
I'm trying to apply a custom validation rule to my MVC model, to ensure that the value in FieldA will not be less than the value in FieldB when both fields are not null. They are nullable in my model.
Custom Validation code, "NotLessThan"
public class NotLessThan : ValidationAttribute
{
private readonly string testedValue;
public NotLessThan(string testedValue)
{
this.testedValue = testedValue;
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
var compareObject = context.ObjectType.GetProperty(this.testedValue);
var compareValue = compareObject.GetValue(context.ObjectInstance, null);
if ((decimal)value >= (decimal)compareValue)
{
return ValidationResult.Success;
}
return new ValidationResult(FormatErrorMessage(context.DisplayName));
}
}
Model Code
[NotLessThan("FieldB", ErrorMessage = "FieldA cannot be less than FieldB")]
public decimal? FieldA { get; set; }
public decimal? FieldB { get; set; }
View Code
#Html.TextBoxFor(t => t.FieldA)
#Html.ValidationMessageFor(t => t.FieldA)
I have other validation on other fields in this model, standard stuff like Required fields and so on. All that other validation triggers and returns the expected validation messages. However, even if I satisfy all my other required fields, my "NotLessThan" custom attribute does not fire, even with failing input.
What am I doing wrong?
I am using Range Validator to validate the date. My requirement is validate the input date should be between current date and current date -60 years.
This is what I tried so far,
[Range(typeof(DateTime),DateTime.UtcNow.ToString(),DateTime.UtcNow.AddYears(-60).Date.ToString())]
public DateTime? DOJ { get; set; }
But this throws error : An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type.
So I modified my code:
#region variables
public const string MaxDate = DateTime.UtcNow.ToString();
public const string MinDate = DateTime.UtcNow.AddYears(-60).Date.ToString();
#endregion
And Set property :
[Range(typeof(DateTime),maximum:MaxDate,minimum:MinDate)]
public DateTime? DOJ { get; set; }
Now the error is :The expression being assigned to 'MaxDate' must be constant.
Same for MinDate.
What's the solution?
You can't use variables in Attributes. All items in attributes must be constant. If you want to filter value based on dynamic values, then you can make new ValidationAttribute like this:
public class ValidateYearsAttribute : ValidationAttribute
{
private readonly DateTime _minValue = DateTime.UtcNow.AddYears(-60);
private readonly DateTime _maxValue = DateTime.UtcNow;
public override bool IsValid(object value)
{
DateTime val = (DateTime)value;
return val >= _minValue && val <= _maxValue;
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessage, _minValue, _maxValue);
}
}
And then you just need to place it on your property:
[ValidateYears]
public DateTime? DOJ { get; set; }
You can update FormatErrorMessage based on what you need.
How can I validate the values assigned to elements of a dropdown list? Normally I would assign ranges in the model and that field would be validated. However, if I have something like this I am not sure how to handle it.
Model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Value { get; set; }
public DateTime Given { get; set; }
public TimeSpan TimeGiven { get; set; }
public string Phase { get; set; }
public bool Active { get; set; }
public int PersonId { get; set; }
}
The name in the model is a dropdown list of different products. I am not sure how to handle the validation for the Value since the different products will have different ranges. For example, Product Named X will have a valid range of 25-30 where product Y will have a valid range of .01 - .5. The Person can have many products assigned so I have a one to many relationship set up with Person and Product.
Is there a way to validate the value based on what product they select X, Y? I will have approximately 40 different products so Ideally I could do this without having to having a separate model for each product.
You can validate using custom business rules with a ValidationAttribute
It is very straightforward you just need to do the following:
Create a class that inherits from ValidationAttribute and override the IsValid method.
Decorate your property with the attribute you just created.
For example:
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false, Inherited = false)]
public class MyBusinessRuleValidation: ValidationAttribute
{
protected override ValidationResult IsValid(object v, ValidationContext validationContext)
{
var Name = (string)v //since we decorated the property Name with this attribute;
//retrieve Value's value using validationContext
var value = (decimal) validationContext.ObjectType.GetProperty("Value").GetValue(validationContext.ObjectInstance, null);
//check whether you need to exit with error
if( name == ProductX) {
if(value > 10 && value < 25)
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
Use the validator:
public class Product
{
public int Id { get; set; }
[MyBusinessRuleValidation(ErrorMessage="Some ugly error")]
public string Name { get; set; }
....
}
I have an HttpPost controller action that takes in a simple form DTO object.
[HttpPost]
public ViewResult Index(ResultQueryForm queryForm)
{
...
}
public class ResultQueryForm
{
public DateTime? TimestampStart { get; set; }
public DateTime? TimestampEnd { get; set; }
public string Name { get; set; }
}
The DTO object has nullable datetime fields used to create a range. The reason that it is set to nullable is because the form that is bound to the model is a query form, and the user doesn't have to enter a date value in the form.
The problem I'm running into is that if the user enters an invalid date, i would like the MVC default model binding to provide an error message. This happens flawlessly if I have a controller action that takes a DateTime? type as a argument, but since I'm passing a DTO that holds a DateTime? type the model binding appears to just set the DateTime? variable to null. This causes unexpected results.
Note:
[HttpPost]
public ViewResult Index(DateTime? startDate)
{
// If the user enters an invalid date, the controller action won't even be run because the MVC model binding will fail and return an error message to the user
}
Is there anyway to tell MVC model binding to "fail" if it can't bind the DateTime? value to the form DTO object, instead of just setting it to null? Is there a better way? Passing each individual form input to the controller is infeasible, due to the large amount of properties in the form/dto object (I've excluded many of them for easy reading).
You can validate your model in the controller action.
if(!Model.IsValid)
{
return View(); // ooops didn't work
}
else
{
return RedirectToAction("Index"); //horray
}
Of course you can put whatever you want in there, and return Json object if you want to display it on your page.
Also you need to add ValidateInput(true) up the top of your action method like this: [HttpPost, ValidateInput(true)]
I think you can create a custom ValidationAttribute for this.
[DateTimeFormat(ErrorMessage = "Invalid date format.")]
public DateTime? TimestampStart { get; set; }
[DateTimeFormat(ErrorMessage = "Invalid date format.")]
public DateTime? TimestampEnd { get; set; }
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DateTimeFormatAttribute : ValidationAttribute
{
public override bool IsValid(object value) {
// allow null values
if (value == null) { return true; }
// when value is not null, try to convert to a DateTime
DateTime asDateTime;
if (DateTime.TryParse(value.ToString(), out asDateTime)) {
return true; // parsed to datetime successfully
}
return false; // value could not be parsed
}
}
I have a basic form which has a date field and I want to validate it inside the IValidatableObject. The type of the field is mapped to a DateTime property so if someone types in 26/15/2011, how do you pick that up in the Validate method? Strictly speaking its almost like validating a DateTime object with a DateTime which doesn't make sense. Any ideas on how to get around this or how to detect that its the wrong date?
Implement IValidatableObject on your method and make the validation for this field
e.g.
public class YourModel : IValidatableObject
{
public YourModel()
{
}
[Required(ErrorMessage = "date is required")]
public string Date { set; get; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
DateTime result;
bool parseDone = DateTime.TryParse(Date, out result);
if (!parseDone)
{
yield return new ValidationResult(Date + "is invalid", new[] { "Date" });
}
}
}
I suggest to use jquery validate and jqueryUI calendar for client side
Hope it helps