mvc-model date range annotation dynamic dates - c#

with reference to this thread in stackoverflow
[Range(typeof(DateTime), "1/2/2004", "3/4/2004",
ErrorMessage = "Value for {0} must be between {1} and {2}")]
public DateTime EventOccurDate{get;set;}
I tried to add some dynamic dates into my model's date range validator as:
private string currdate=DateTime.Now.ToString();
private string futuredate=DateTime.Now.AddMonths(6).ToString();
[Range(typeof(DateTime),currdate,futuredate,
ErrorMessage = "Value for {0} must be between {1} and {2}")]
public DateTime EventOccurDate{get;set;}
But Error Occurs.Is there no way to set dynamic date range validation in MVC?

You cannot use dynamic values in attributes because they are metadata that is generated at compile-time. One possibility to achieve this is to write a custom validation attribute or use Fluent Validation which allows for expressing more complex validation scenarios using a fluent expressions.
Here's an example of how such custom validation attribute might look like:
public class MyValidationAttribute: ValidationAttribute
{
public MyValidationAttribute(int monthsSpan)
{
this.MonthsSpan = monthsSpan;
}
public int MonthsSpan { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
var date = (DateTime)value;
var now = DateTime.Now;
var futureDate = now.AddMonths(this.MonthsSpan);
if (now <= date && date < futureDate)
{
return null;
}
}
return new ValidationResult(this.FormatErrorMessage(this.ErrorMessage));
}
}
and then decorate your model with it:
[MyValidation(6)]
public DateTime EventOccurDate { get; set; }

Related

How to reset the formatted ErrorMessages of the validation attributes in ASP.NET MVC?

I used a Custom validation attribute -AmountShouldBeLessOrEqualAttribute- that its validation process related to value of another property and this attribute works successfully.
But in the following scenario I have a problem with it:
Start the Application
Going to the Form page
Submit the Form (POST the form for first time)
The ModelBinding process cause that the value of ErrorMessage in the AmountShouldBeLessOrEqual attribute be formatted. For example:
In the ViewModel there is an Amount property with the above attibute
and
Its ErrorMessage: Your amount should be less than {0}
Will be convert to: Your amount should be less than 23
Note: 23 is the value of MaxAmount property in the ViewModel
Now I change the MaxAmount to 83
We go to the Form page again and submit the form
The ModelBinding process will be start the validation process of AmountShouldBeLessOrEqualAttibute. Now if I watch the value of ErrorMessage property it is not Your amount should be less than {0}, it remained as the old formatted text: Your amount should be less than 23. So it can not be formatted again to Your amount should be less than 83
My question:
How should I reset the formatted ErrorMessages to its Non-Formatted version each time to be formatted with new value?
In ViewModel:
[AmountShouldBeLessOrEqual(nameof(MaxAmount), ErrorMessage = "Your amount should be less than {0}")]
public decimal Amount { get; set; }
public decimal MaxAmount { get; set; }
AmountShouldBeLessOrEqualAttribute:
public class AmountShouldBeLessOrEqualAttribute : ValidationAttribute
{
private readonly string _comparisonProperty;
public AmountShouldBeLessOrEqualAttribute(string comparisonProperty)
{
_comparisonProperty = comparisonProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
ErrorMessage = ErrorMessageString;
var currentValue = (decimal)value;
var comparisonValue = GetComparisonValue(_comparisonProperty, validationContext);
if (ErrorMessage == null && ErrorMessageResourceName == null)
{
ErrorMessage = "Amount is large";
}
else
{
ErrorMessage = string.Format(ErrorMessage ?? "", comparisonValue);
}
return currentValue >= comparisonValue
? new ValidationResult(ErrorMessage)
: ValidationResult.Success;
}
public override string FormatErrorMessage(string name)
{
return base.FormatErrorMessage(name);
}
private decimal GetComparisonValue(string comparisonProperty, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(comparisonProperty);
if (property == null)
throw new ArgumentException("Not Found!");
var comparisonValue = (decimal)property.GetValue(validationContext.ObjectInstance);
return comparisonValue;
}
}
This is caused that you are setting value for ErrorMessage with string.Format(ErrorMessage ?? "", comparisonValue);. ErrorMessage is the value from [AmountShouldBeLessOrEqual(nameof(MaxAmount), ErrorMessage = "Your amount should be less than {0}")] which you should not change during IsValid.
Try to define a scoped variable in IsValid to store the formatted error message.
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
string error = "";
var currentValue = (decimal)value;
var comparisonValue = GetComparisonValue(_comparisonProperty, validationContext);
if (ErrorMessage == null && ErrorMessageResourceName == null)
{
ErrorMessage = "Amount is large";
}
else
{
error = string.Format(ErrorMessage ?? "", comparisonValue);
}
return currentValue >= comparisonValue
? new ValidationResult(error)
: ValidationResult.Success;
}

Validation ErrorMessage value

How to emit custom validator error messages for an entity like this?:
Receipts exeeded invoice amount of 15000
My property
[InvoiceAmountNotExeeded(ErrorMessage = "Receipts exeeded invoice amount of {0}")]
public int Amount {get; set; }
In validator:
var errorMsg = FormatErrorMessage(string.Format(validationContext.DisplayName,invoice.Amount))
Problem is I m getting: Receipts exeeded invoice amount of Amount.
Note how it is writting the property name instead of property value. Advice?
EDIT: Code added
public class InvoiceAmountNotExeededAttribute : ValidationAttribute {
public InvoiceAmountNotExeededAttribute()
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var factId = ....;
var db = new Entities();
var fact = db.Invoices.Find(factId);
var amountRecibos = ...;
var amount = Convert.ToInt32(value);
if (amountRecibos + amount > fact.Amount ){
var errorMsg = FormatErrorMessage(string.Format(validationContext.DisplayName,invoice.Amount));
return new ValidationResult(errorMsg);
}
return ValidationResult.Success;
}
}
The reason you have this behaviour is because you refer to validationContext.DisplayName which by default is set to property name ("Amount" in your case). So for you string.Format(validationContext.DisplayName,invoice.Amount) returns just "Amount". Instead of this try to apply this:
var errorMsg = FormatErrorMessage(invoice.Amount.ToString());
return new ValidationResult(errorMsg);
This way you will pass to FormatErroMessage not DisplayName for your property but Amount value instead and FormatErrorMessage will use it with pattern from ErrorMessage attribute property. So this should give you what you want.

Custom validation attribute calling another validation attribute

I want to create a custom validation attribute that calls other validation attributes.
For example I want to create an attribute called PasswordValidationAttribute. I want it to decorate the property it is defined on with RequiredAttribute, RegularExpressionAttribute and StringLengthAttribute with various parameters defined (such as the regular expression for a password and a maximum and minimum string length).
I'm struggling on where to begin, ascertain how much work is involved and determine if it is at all possible. Once this attribute is applied to a property I would like it to process the ValidationMessageFor HtmlHelper correctly and do a serverside call. I'm hoping I don't need to redefine them (otherwise it will be too much work).
For .net 4 it could look like:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class MyValidationAttribute : ValidationAttribute
{
private readonly bool isRequired;
public string Regex { get; set; }
public int StringLength { get; set; }
public MyValidationAttribute(bool isRequired)
{
this.isRequired = isRequired;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var composedAttributes = ConstructAttributes().ToArray();
if (composedAttributes.Length == 0) return ValidationResult.Success;
var errorMsgBuilder = new StringBuilder();
foreach (var attribute in composedAttributes)
{
var valRes = attribute.GetValidationResult(value, validationContext);
if (valRes != null && !string.IsNullOrWhiteSpace(valRes.ErrorMessage))
errorMsgBuilder.AppendLine(valRes.ErrorMessage);
}
var msg = errorMsgBuilder.ToString();
if (string.IsNullOrWhiteSpace(msg))
return ValidationResult.Success;
return new ValidationResult(msg);
}
private IEnumerable<ValidationAttribute> ConstructAttributes()
{
if (isRequired)
yield return new RequiredAttribute();
if (Regex != null)
yield return new RegularExpressionAttribute(Regex);
if (StringLength > 0)
yield return new StringLengthAttribute(StringLength);
}
}
Usage:
[MyValidationAttribute(true, Regex = "[a-z]*", StringLength = 3)]
public string Name { get; set; }
In .net 3.5 there is a limitation, that you cannot dynamically construct the message value from underlying attributes (at least I was not able get to through it). You can set only one message per whole attribute.
Everything changed is inside method IsValid.
public override bool IsValid(object value)
{
var composedAttributes = ConstructAttributes().ToArray();
if (composedAttributes.Length == 0) return true;
return composedAttributes.All(a => a.IsValid(value));
}
Note to ErrorMessage:
Return value of IsValid method of ValidationAttribute in .net 3.5 is not ValidationResult but bool. When I tried to set the ErrorMessage, I got the error that value can be set only once.

CompareAttribute for case insensitive comparison

I am using CompareAttribute in MVC3 and its working fine. But I want to use case insensitive classCode. Is there any way to get that working
Thanks in Advance
[CompareAttribute("ClassCode", ErrorMessageResourceName = "ClassCode_DontMatch", ErrorMessageResourceType = typeof(Resources.Class))]
public string ConfirmClassCode {get; set; }
A little late to the party, but here is an implementation I just wrote that also includes support for client-side validation using the IClientValidatable interface. You could use Darin Dimitrov's answer as a starting point as well, I just already had some of this.
Server-Side Validation:
//Create your custom validation attribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class CompareStrings : ValidationAttribute, IClientValidatable
{
private const string _defaultErrorMessage = "{0} must match {1}";
public string OtherPropertyName { get; set; }
public bool IgnoreCase { get; set; }
public CompareStrings(string otherPropertyName)
: base(_defaultErrorMessage)
{
if (String.IsNullOrWhiteSpace(otherPropertyName)) throw new ArgumentNullException("OtherPropertyName must be set.");
OtherPropertyName = otherPropertyName;
}
public override string FormatErrorMessage(string name)
{
return String.Format(ErrorMessage, name, OtherPropertyName);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
string otherPropVal = validationContext.ObjectInstance.GetType().GetProperty(OtherPropertyName).GetValue(validationContext.ObjectInstance, null) as string;
//Convert nulls to empty strings and trim spaces off the result
string valString = (value as string ?? String.Empty).Trim();
string otherPropValString = (otherPropVal ?? String.Empty).Trim();
bool isMatch = String.Compare(valString, otherPropValString, IgnoreCase) == 0;
if (isMatch)
return ValidationResult.Success;
else
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
Client-Side Validation
//...continuation of CompareStrings class
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
return new[] { new ModelClientValidationCompareStringsRule(FormatErrorMessage(metadata.GetDisplayName()), OtherPropertyName, IgnoreCase) };
}
}
Define ModelClientValidationCompareStringsRule which is used (above) to pass the attribute's properties to the client-side script.
public class ModelClientValidationCompareStringsRule : ModelClientValidationRule
{
public ModelClientValidationCompareStringsRule(string errorMessage, string otherProperty, bool ignoreCase)
{
ErrorMessage = errorMessage; //The error message to display when invalid. Note we used FormatErrorMessage above to ensure this matches the server-side result.
ValidationType = "comparestrings"; //Choose a unique name for your validator on the client side. This doesn't map to anything on the server side.
ValidationParameters.Add("otherprop", otherProperty); //Pass the name of the property to compare to
ValidationParameters.Add("ignorecase", ignoreCase.ToString().ToLower()); //And whether to ignore casing
}
}
Javascript:
(function ($) {
//Add an adapter for our validator. This maps the data from the ModelClientValidationCompareStringsRule
//we defined above, to the validation plugin. Make sure to use the same name as we chose for the ValidationType property ("comparestrings")
$.validator.unobtrusive.adapters.add("comparestrings", ["otherprop", "ignorecase"],
function (options) {
options.rules["comparestrings"] = {
otherPropName: options.params.otherprop,
ignoreCase: options.params.ignorecase == "true"
};
options.messages["comparestrings"] = options.message;
});
//Add the method, again using the "comparestrings" name, that actually performs the client-side validation to the page's validator
$.validator.addMethod("comparestrings", function (value, element, params) {
//element is the element we are validating and value is its value
//Get the MVC-generated prefix of element
//(E.G. "MyViewModel_" from id="MyViewModel_CompareEmail"
var modelPrefix = getModelIDPrefix($(element).prop("id"));
//otherPropName is just the name of the property but we need to find
//its associated element to get its value. So concatenate element's
//modelPrefix with the other property name to get the full MVC-generated ID. If your elements use your own, overridden IDs, you'd have to make some modifications to allow this code to find them (e.g. finding by the name attribute)
var $otherPropElem = $("#" + modelPrefix + params.otherPropName);
var otherPropValue = getElemValue($otherPropElem);
//Note: Logic for comparing strings needs to match what it does on the server side
//Trim values
value = $.trim(value);
otherPropValue = $.trim(otherPropValue);
//If ignoring case, lower both values
if (params.ignoreCase) {
value = value.toLowerCase();
otherPropValue = otherPropValue.toLowerCase();
}
//compare the values
var isMatch = value == otherPropValue;
return isMatch;
});
function getElemValue(element){
var value;
var $elem = $(element);
//Probably wouldn't use checkboxes or radio buttons with
//comparestrings, but this method can be used for other validators too
if($elem.is(":checkbox") || $elem.is(":radio") == "radio")
value = $elem.prop("checked") ? "true" : "false";
else
value = $elem.val();
return value;
}
//Gets the MVC-generated prefix for a field by returning the given string
//up to and including the last underscore character
function getModelIDPrefix(fieldID) {
return fieldID.substr(0, fieldID.lastIndexOf("_") + 1);
}
}(jQuery));
Usage is standard:
public string EmailAddress { get; set; }
[CompareStrings("EmailAddress", ErrorMessage = "The email addresses do not match", IgnoreCase=true)]
public string EmailAddressConfirm { get; set; }
This plugs into the Unobtrusive Validation framework, so you need to already have that installed and working. At the time of writing I am on Microsoft.jQuery.Unobtrusive.Validation v 3.0.0.
You could write a custom attribute that will perform the case insensitive comparison:
public class CaseInsensitiveCompareAttribute : System.Web.Mvc.CompareAttribute
{
public CaseInsensitiveCompareAttribute(string otherProperty)
: base(otherProperty)
{ }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(this.OtherProperty);
if (property == null)
{
return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "Unknown property {0}", this.OtherProperty));
}
var otherValue = property.GetValue(validationContext.ObjectInstance, null) as string;
if (string.Equals(value as string, otherValue, StringComparison.OrdinalIgnoreCase))
{
return null;
}
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
and then decorate your view model property with it:
[CaseInsensitiveCompare("ClassCode", ErrorMessageResourceName = "ClassCode_DontMatch", ErrorMessageResourceType = typeof(Resources.Class))]
public string ConfirmClassCode { get; set; }
For client side validation, put this code below in document ready-
jQuery.validator.addMethod("ignoredCaseEqualTo", function (value, element, param) {
return this.optional(element) || value.toLowerCase() === $(param).val().toLowerCase();
}, "__Your Validation message___");
$("#EmailAddress").rules("add", {
ignoredCaseEqualTo: "#EmailAddressConfirm"
});
this code adds new validation rule of case insensitive comparison.
Might not be an optimum way, but this will do your job for client side validation.

Data Annotation Ranges of Dates

Is it possible to use [Range] annotation for dates?
something like
[Range(typeof(DateTime), DateTime.MinValue.ToString(), DateTime.Today.ToString())]
I did this to fix your problem
public class DateAttribute : RangeAttribute
{
public DateAttribute()
: base(typeof(DateTime), DateTime.Now.AddYears(-20).ToShortDateString(), DateTime.Now.AddYears(2).ToShortDateString()) { }
}
Docs on MSDN says you can use the RangeAttribute
[Range(typeof(DateTime), "1/2/2004", "3/4/2004",
ErrorMessage = "Value for {0} must be between {1} and {2}")]
public datetime Something { get; set;}
jQuery validation does not work with [Range(typeof(DateTime),"date1","date2"] --
My MSDN doc is incorrect
Here is another solution.
[Required(ErrorMessage = "Date Of Birth is Required")]
[DataType(DataType.Date, ErrorMessage ="Invalid Date Format")]
[Remote("IsValidDateOfBirth", "Validation", HttpMethod = "POST", ErrorMessage = "Please provide a valid date of birth.")]
[Display(Name ="Date of Birth")]
public DateTime DOB{ get; set; }
The simply create a new MVC controller called ValidationController and past this code in there. The nice thing about the "Remote" approach is you can leverage this framework to handle any kind of validations based on your custom logic.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Mail;
using System.Web;
using System.Web.Mvc;
namespace YOURNAMESPACEHERE
{
public class ValidationController : Controller
{
[HttpPost]
public JsonResult IsValidDateOfBirth(string dob)
{
var min = DateTime.Now.AddYears(-21);
var max = DateTime.Now.AddYears(-110);
var msg = string.Format("Please enter a value between {0:MM/dd/yyyy} and {1:MM/dd/yyyy}", max,min );
try
{
var date = DateTime.Parse(dob);
if(date > min || date < max)
return Json(msg);
else
return Json(true);
}
catch (Exception)
{
return Json(msg);
}
}
}
}
For those rare occurrences when you are forced to write a date as a string (when using attributes), I highly recommend using the ISO-8601 notation.
That eliminates any confusion as to whether 01/02/2004 is january 2nd or february 1st.
[Range(typeof(DateTime), "2004-12-01", "2004-12-31",
ErrorMessage = "Value for {0} must be between {1} and {2}")]
public datetime Something { get; set;}
I use this approach:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
internal sealed class DateRangeAttribute : ValidationAttribute
{
public DateTime Minimum { get; }
public DateTime Maximum { get; }
public DateRangeAttribute(string minimum = null, string maximum = null, string format = null)
{
format = format ?? #"yyyy-MM-dd'T'HH:mm:ss.FFFK"; //iso8601
Minimum = minimum == null ? DateTime.MinValue : DateTime.ParseExact(minimum, new[] { format }, CultureInfo.InvariantCulture, DateTimeStyles.None); //0 invariantculture
Maximum = maximum == null ? DateTime.MaxValue : DateTime.ParseExact(maximum, new[] { format }, CultureInfo.InvariantCulture, DateTimeStyles.None); //0 invariantculture
if (Minimum > Maximum)
throw new InvalidOperationException($"Specified max-date '{maximum}' is less than the specified min-date '{minimum}'");
}
//0 the sole reason for employing this custom validator instead of the mere rangevalidator is that we wanted to apply invariantculture to the parsing instead of
// using currentculture like the range attribute does this is immensely important in order for us to be able to dodge nasty hiccups in production environments
public override bool IsValid(object value)
{
if (value == null) //0 null
return true;
var s = value as string;
if (s != null && string.IsNullOrEmpty(s)) //0 null
return true;
var min = (IComparable)Minimum;
var max = (IComparable)Maximum;
return min.CompareTo(value) <= 0 && max.CompareTo(value) >= 0;
}
//0 null values should be handled with the required attribute
public override string FormatErrorMessage(string name) => string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, Minimum, Maximum);
}
And use it like so:
[DateRange("2004-12-01", "2004-12-2", "yyyy-M-d")]
ErrorMessage = "Value for {0} must be between {1} and {2}")]
I found issues with the [Range(typeof(DateTime)] annotation and would describe it as "clunky at best" it leaves too much to chance IF it works.
Remote validation seems to be a good way of: avoiding javascript in views and maintaining server side code integrity, personally never like sending code to a client to execute if I can avoid it.
Using #StackThis answer as a base and reference to an article on remote validation in MVC3
Model
public class SomeDateModel
{
public int MinYears = 18;
public int MaxYears = 110;
[Display(Name = "Date of birth", Prompt = "e.g. 01/01/1900")]
[Remote(action: "ValidateDateBetweenYearsFromNow", controller: "Validation", areaReference: AreaReference.UseRoot, AdditionalFields = "MinYears,MaxYears", HttpMethod = "GET" ,ErrorMessage = "Subject must be over 18")]
public DateTime? DOB { get; set; }
}
Controller - Deployed at the root directory
namespace Controllers
{
public class ValidationController : Controller
{
[HttpGet]
[ActionName("ValidateDateBetweenYearsFromNow")]
public JsonResult ValidateDateBetweenYearsFromNow_Get()
{
//This method expects 3 parameters, they're anonymously declared through the Request Querystring,
//Ensure the order of params is:
//[0] DateTime
//[1] Int Minmum Years Ago e.g. for 18 years from today this would be 18
//[2] int Maximum Years Ago e.g. for 100 years from today this would be 100
var msg = string.Format("An error occured checking the Date field validity");
try
{
int MinYears = int.Parse(Request.QueryString[1]);
int MaxYears = int.Parse(Request.QueryString[2]);
//Use (0 - x) to invert the positive int to a negative.
var min = DateTime.Now.AddYears((0-MinYears));
var max = DateTime.Now.AddYears((0-MaxYears));
//reset the response error msg now all parsing and assignmenst succeeded.
msg = string.Format("Please enter a value between {0:dd/MM/yyyy} and {1:dd/MM/yyyy}", max, min);
var date = DateTime.Parse(Request.QueryString[0]);
if (date > min || date < max)
//switch the return value here from "msg" to "false" as a bool to use the MODEL error message
return Json(msg, JsonRequestBehavior.AllowGet);
else
return Json(true, JsonRequestBehavior.AllowGet);
}
catch (Exception)
{
return Json(msg, JsonRequestBehavior.AllowGet);
}
}
}
}
The msg variable is displayed as part of the Html helper ValidationSummary or the Html helper ValidationFor(x=>x.DATETIME)
View
It's important to note that the fields passed as parameter 2 and 3 must exist in the view in order for the remote validation to pass the values to the controller:
#Html.EditorFor(m => m.DOB)
#Html.HiddenFor(m => m.MinYears)
#Html.HiddenFor(m => m.MaxYears)
#Html.ValidationSummary()
The model and Html helpers will do all the jquery work for you.

Categories

Resources