What is the best way to prohibit integer value for Enum action's parameter - asp.net-mvc

I use some Enum as parameter in some action. For example we have the following code
public enum SomeEnum { SomeVal1 = 1, SomeVal2 = 2 }
[HttpGet]
public void SomeAction(SomeEnum someParameter) { }
By default asp.net engine allows to use both string and integer values that's why we are able to call it like this 'http://host/SomeController/SomeAction/SomeVal1' or this 'http://host/SomeController/SomeAction/1' or even this 'http://host/SomeController/SomeAction/54'! I would like to stay with the first sample using string value. For this I've implemented the following model binder:
public class RequireStringsAttribute : ModelBinderAttribute
{
public RequireStringsAttribute() : base(typeof(ModelBinder))
{
}
private class ModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue;
var isValid = Enum.GetNames(bindingContext.ModelType).Any(name =>
name.Equals(value, StringComparison.OrdinalIgnoreCase));
if (isValid)
{
bindingContext.Result = ModelBindingResult.Success(Enum.Parse(bindingContext.ModelType, value, ignoreCase: true));
}
else
{
bindingContext.ActionContext.ModelState.AddModelError(bindingContext.FieldName, $"The value '{value}' is not valid.");
}
return Task.CompletedTask;
}
}
}
And I've applied it:
[HttpGet]
public void SomeAction([RequireStrings]SomeEnum someParameter) { }
It works fine but I just want to know is there a better way to do it?

Related

How to modify binded value in MVVM Cross

I have a model from my json in a Xamarin MVVM app(ios). I want to add the "%" after the value? in the list "coinmarketcaplist" contains the value 24h_change, this is the value I want to add a % to, it's a string. I know that I should use a getter for it, but I don't know how since I'm fairly new to this. below is my ViewModel code:
public class CMCTableViewModel : MvxViewModel
{
protected readonly ICoinMarketCapService _coinMarketCapService;
public CMCTableViewModel(ICoinMarketCapService coinMarketCapService)
{
_coinMarketCapService = coinMarketCapService;
LoadData();
}
private List<CoinMarketCapModel> _coinMarketCapModelList;
public List<CoinMarketCapModel> CoinMarketCapModelList
{
get
{
return _coinMarketCapModelList;
}
set
{
_coinMarketCapModelList = value;
RaisePropertyChanged(() => CoinMarketCapModelList);
}
}
public async void LoadData()
{
CoinMarketCapModelList = await _coinMarketCapService.GetCoins();
}
}
TableCell:
internal static readonly NSString Identifier = new NSString("CMCTableCell");
public override void LayoutSubviews()
{
base.LayoutSubviews();
MvxFluentBindingDescriptionSet<CMCTableCell, CoinMarketCapModel> set = new MvxFluentBindingDescriptionSet<CMCTableCell, CoinMarketCapModel>(this);
set.Bind(lblName).To(res => res.Name);
set.Bind(lblPrice).To(res => res.percent_change_24h);
set.Bind(imgCoin)
.For(img => img.Image)
.To(res => res.image)
.WithConversion<StringToImageConverter>();
set.Apply();
}
}
edit: added cellview
Use a converter in your binding:
1) Define converter:
public class StringFormatValueConverter : MvxValueConverter
{
public override object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
return null;
if (parameter == null)
return value;
return string.Format(parameter.ToString(), value);
}
}
2) Use it in your binding:
set.Bind(lblPrice).To(res => res.percent_change_24h).WithConversion<StringFormatValueConverter>("{0} %");
You can use this converter when you want to modify the input string by adding something around it, for example unit or currency

Receive an array of json objects and validate in the controller

I'm sending a JSON like this:
[
{col1: 'value', col2: 'value'},
{col1: 'value2', col2: 'value2'},
...
]
The action in my controller has a List parameter that is requiring a custom model binder, like this:
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
String bodyText;
Stream stream = null;
try
{
stream = controllerContext.HttpContext.Request.InputStream;
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream))
{
stream = null;
bodyText = reader.ReadToEnd();
}
}
finally
{
if (stream != null)
stream.Dispose();
}
if (string.IsNullOrEmpty(bodyText))
{
return null;
}
var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
return model;
// return base.BindModel(controllerContext, bindingContext);
}
It's working, except that it is not considering the data annotations for validations (required, range, etc).
How can I get this working with validation?
UPDATE
Controller Action
[HttpPost]
public ActionResult ActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<List<EntityName>>))]List<EntityName> viewModel)
Entity
public class EntityName
{
[Display(Name = "Data Entrada")]
[DataType(DataType.Date)]
[Required]
public DateTime? DataEntrada { get; set; }
// ....
}
I have revised my answer. There were a few problems that I ran into when trying to get this working. Detailed below is the problem and the solution I used.
The json: The json you provided did not match the Model you provided. So I assumed the json string should have included something like this:
`DataEntrada: "1/1/2014"`
The model: Your model describes only EntityName. The deserialized json is a list. These are two different things. So I modified the json to be an object that defines EntityNames (list of EntityName), like this:
`data = { EntityNames: [{ DataEntrada: "1/1/2014" }] };`
and then I implemented this class..this will be the result of deserialization:
public class EntityInfo
{
public EntityName[] EntityNames { get; set; }
}
and finally, modified the ActionMethod like so:
public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)
Validation: Validating EntityNames was not as easy to implement as I thought it would be. I could not get the validation attribute for EntityName to fire during model binding (being a member of a list). So, I implemented a custom validator derived from 'ValidationAttribute' like this:
public class EntityNamesValidation : ValidationAttribute
{
public override bool IsValid(object value)
{
EntityName[] list = (EntityName[])value;
foreach (EntityName e in list)
{
if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
return false;
// more checks performed here
}
return true;
}
}
and then I applied EntityNamesValidation attribute to EntityNames and EntityInfo, like so:
[EntityNamesValidation]
public EntityName[] EntityNames { get; set; }
Incorrect model during bind: The JsonArrayValidationModelBinder was using a bindingContext that did not have an instance of anything. If you debug BindModel before base.BindModel you will see that bindingContext.Model is null. So what I did was set bindingContext.ModelMetadata.Model = model after deserialization and before the call to base.BindModel. I also moved base.BindModel in the code to fire just before model is returned...see below
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
[...]
bindingContext.ModelMetadata.Model = model;
base.BindModel(controllerContext, bindingContext);
return model;
}
Verification: I did not unit test this, but I did place a breakpoint in the ActionMethod. I then used the following json:
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };
when the code reached the breakpoint, ModelState.IsValid is false. I then changed json to this:
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: "2/19/2014" }] };
when the code reached the breakpoint, ModelState.IsValid is true.
This approach works, but is not ideal. I think you want validation to occur without creating custom code and use MVC to handle this.
I hope this gets you a step further.
ALL THE CODE
javascript
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };
var jsonOfLog = JSON.stringify(data);
$.ajax({
type: 'POST',
dataType: 'text',
url: "/EntityData/SaveActionName",
data: jsonOfLog,
success: function (data) {
alert(data);
},
error: function (result) {
alert(result);
}
,
async: false
});
models
public class EntityInfo
{
[EntityNamesValidation]
public EntityName[] EntityNames { get; set; }
}
public class EntityName
{
[Display(Name = "Data Entrada")]
[DataType(DataType.Date)]
[Required]
public DateTime? DataEntrada { get; set; }
}
custom validator
public class EntityNamesValidation : ValidationAttribute
{
public override bool IsValid(object value)
{
EntityName[] list = (EntityName[])value;
foreach (EntityName e in list)
{
if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
return false;
// more checks performed here
}
return true;
}
}
BindModel
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
String bodyText;
Stream stream = null;
try
{
stream = controllerContext.HttpContext.Request.InputStream;
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream))
{
stream = null;
bodyText = reader.ReadToEnd();
}
}
finally
{
if (stream != null)
stream.Dispose();
}
if (string.IsNullOrEmpty(bodyText))
{
return null;
}
var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
bindingContext.ModelMetadata.Model = model;
base.BindModel(controllerContext, bindingContext);
return model;
}
ActionMethod
[HttpPost]
public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)
Deriving from DefaultModelBinder will give you what you are looking for. In your override, call base method, like so
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// base BindModel should validate your model
base.BindModel(controllerContext, bindingContext);
// (optional) Capture validation result
bool ModelIsValid = bindingContext.ModelState.IsValid;
var contentType = controllerContext.HttpContext.Request.ContentType;
[...]
}

Get custom attribute for parameter when model binding

I've seen a lot of similar posts on this, but haven't found the answer specific to controller parameters.
I've written a custom attribute called AliasAttribute that allows me to define aliases for parameters during model binding. So for example if I have: public JsonResult EmailCheck(string email) on the server and I want the email parameter to be bound to fields named PrimaryEmail or SomeCrazyEmail I can "map" this using the aliasattribute like this: public JsonResult EmailCheck([Alias(Suffix = "Email")]string email).
The problem: In my custom model binder I can't get a hold of the AliasAttribute class applied to the email parameter. It always returns null.
I've seen what the DefaultModelBinder class is doing to get the BindAttribute in reflector and its the same but doesn't work for me.
Question: How do I get this attribute during binding?
AliasModelBinder:
public class AliasModelBinder : DefaultModelBinder
{
public static ICustomTypeDescriptor GetTypeDescriptor(Type type)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var descriptor = GetTypeDescriptor(bindingContext.ModelType);
/*************************/
// this next statement returns null!
/*************************/
AliasAttribute attr = (AliasAttribute)descriptor.GetAttributes()[typeof(AliasAttribute)];
if (attr == null)
return null;
HttpRequestBase request = controllerContext.HttpContext.Request;
foreach (var key in request.Form.AllKeys)
{
if (string.IsNullOrEmpty(attr.Prefix) == false)
{
if (key.StartsWith(attr.Prefix, StringComparison.InvariantCultureIgnoreCase))
{
if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
return request.Form.Get(key);
}
}
else if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
if (attr.HasIncludes)
{
foreach (var include in attr.InlcludeSplit)
{
if (key.Equals(include, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(include);
}
}
}
}
return null;
}
}
AliasAttribute:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class AliasAttribute : Attribute
{
private string _include;
private string[] _inlcludeSplit = new string[0];
public string Prefix { get; set; }
public string Suffix { get; set; }
public string Include
{
get
{
return _include;
}
set
{
_include = value;
_inlcludeSplit = SplitString(_include);
}
}
public string[] InlcludeSplit
{
get
{
return _inlcludeSplit;
}
}
public bool HasIncludes { get { return InlcludeSplit.Length > 0; } }
internal static string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(new char[] { ',' })
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray<string>();
}
}
Usage:
public JsonResult EmailCheck([ModelBinder(typeof(AliasModelBinder)), Alias(Suffix = "Email")]string email)
{
// email will be assigned to any field suffixed with "Email". e.g. PrimaryEmail, SecondaryEmail and so on
}
Gave up on this and then stumbled across the Action Parameter Alias code base that will probably allow me to do this. It's not as flexible as what I started out to write but probably can be modified to allow wild cards.
what I did was make my attribute subclass System.Web.Mvc.CustomModelBinderAttribute which then allows you to return a version of your custom model binder modified with the aliases.
example:
public class AliasAttribute : System.Web.Mvc.CustomModelBinderAttribute
{
public AliasAttribute()
{
}
public AliasAttribute( string alias )
{
Alias = alias;
}
public string Alias { get; set; }
public override IModelBinder GetBinder()
{
var binder = new AliasModelBinder();
if ( !string.IsNullOrEmpty( Alias ) )
binder.Alias = Alias;
return binder;
}
}
which then allows this usage:
public ActionResult Edit( [Alias( "somethingElse" )] string email )
{
// ...
}

Serialize IList property on model when passed into Html.ActionLink

I'm trying to generate an Html.ActionLink with the following viewmodel:
public class SearchModel
{
public string KeyWords {get;set;}
public IList<string> Categories {get;set;}
}
To generate my link I use the following call:
#Html.ActionLink("Index", "Search", Model)
Where Model is an instance of the SearchModel
The link generated is something like this:
http://www.test.com/search/index?keywords=bla&categories=System.Collections.Generic.List
Because it obviously is only calling the ToString method on every property.
What I would like to see generate is this:
http://www.test.com/search/index?keywords=bla&categories=Cat1&categories=Cat2
Is there any way I can achieve this by using Html.ActionLink
In MVC 3 you're just out of luck because the route values are stored in a RouteValueDictionary that as the name implies uses a Dictionary internally which makes it not possible to have multiple values associated to a single key. The route values should probably be stored in a NameValueCollection to support the same behavior as the query string.
However, if you can impose some constraints on the categories names and you're able to support a query string in the format:
http://www.test.com/search/index?keywords=bla&categories=Cat1|Cat2
then you could theoretically plug it into Html.ActionLink since MVC uses TypeDescriptor which in turn is extensible at runtime. The following code is presented to demonstrate it's possible, but I would not recommend it to be used, at least without further refactoring.
Having said that, you would need to start by associating a custom type description provider:
[TypeDescriptionProvider(typeof(SearchModelTypeDescriptionProvider))]
public class SearchModel
{
public string KeyWords { get; set; }
public IList<string> Categories { get; set; }
}
The implementation for the provider and the custom descriptor that overrides the property descriptor for the Categories property:
class SearchModelTypeDescriptionProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(
Type objectType, object instance)
{
var searchModel = instance as SearchModel;
if (searchModel != null)
{
var properties = new List<PropertyDescriptor>();
properties.Add(TypeDescriptor.CreateProperty(
objectType, "KeyWords", typeof(string)));
properties.Add(new ListPropertyDescriptor("Categories"));
return new SearchModelTypeDescriptor(properties.ToArray());
}
return base.GetTypeDescriptor(objectType, instance);
}
}
class SearchModelTypeDescriptor : CustomTypeDescriptor
{
public SearchModelTypeDescriptor(PropertyDescriptor[] properties)
{
this.Properties = properties;
}
public PropertyDescriptor[] Properties { get; set; }
public override PropertyDescriptorCollection GetProperties()
{
return new PropertyDescriptorCollection(this.Properties);
}
}
Then we would need the custom property descriptor to be able to return a custom value in GetValue which is called internally by MVC:
class ListPropertyDescriptor : PropertyDescriptor
{
public ListPropertyDescriptor(string name)
: base(name, new Attribute[] { }) { }
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { throw new NotImplementedException(); }
}
public override object GetValue(object component)
{
var property = component.GetType().GetProperty(this.Name);
var list = (IList<string>)property.GetValue(component, null);
return string.Join("|", list);
}
public override bool IsReadOnly { get { return false; } }
public override Type PropertyType
{
get { throw new NotImplementedException(); }
}
public override void ResetValue(object component) { }
public override void SetValue(object component, object value) { }
public override bool ShouldSerializeValue(object component)
{
throw new NotImplementedException();
}
}
And finally to prove that it works a sample application that mimics the MVC route values creation:
static void Main(string[] args)
{
var model = new SearchModel { KeyWords = "overengineering" };
model.Categories = new List<string> { "1", "2", "3" };
var properties = TypeDescriptor.GetProperties(model);
var dictionary = new Dictionary<string, object>();
foreach (PropertyDescriptor p in properties)
{
dictionary.Add(p.Name, p.GetValue(model));
}
// Prints: KeyWords, Categories
Console.WriteLine(string.Join(", ", dictionary.Keys));
// Prints: overengineering, 1|2|3
Console.WriteLine(string.Join(", ", dictionary.Values));
}
Damn, this is probably the longest answer I ever give here at SO.
with linq of course...
string.Join("", Model.Categories.Select(c=>"&categories="+c))

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; }
}
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;
}
}
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.
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
You need to set the ErrorMessage property, so for example:
public class DOBValidAttribute : ValidationAttribute
{
private static string _errorMessage = "Date of birth is a required field.";
public DOBValidAttribute() : base(_errorMessage)
{
}
//etc......overriding IsValid....

Resources