In my application I have many properties like
[DisplayFormat(ApplyFormatInEditMode=false,ConvertEmptyStringToNull=false,DataFormatString="{0:0.00}")]
public decimal somedecimalvalue { get; set; }
Is there any way i can generalize this whenever a decimal property is created above format is applied to it
You can manually assign metadata for decimal properties in your models by creating custom DataAnnotationsModelMetadataProvider:
public class DecimalMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (propertyName == null)
return metadata;
if (metadata.ModelType == typeof(decimal))
{
// Given DisplayFormat Attribute:
// if ApplyFormatInEditMode = true
// metadata.EditFormatString = "{0:0.00}";
// for DataFormatString
metadata.DisplayFormatString = "{0:0.00}";
// for ConvertEmptyStringToNull
metadata.ConvertEmptyStringToNull = false;
}
return metadata;
}
}
And then register this provider in Global.asax.cs in Application_Start() method:
ModelMetadataProviders.Current = new DecimalMetadataProvider();
Then you can remove DisplayFormat attribute from decimal properties. Note that this won't affect other properties and you can safely add other data annotations on your decimal properties.
Read more about MetaData class and its properties.
Happy coding! :)
Cant understand exactly what you wanted to know. describe some more
I think there is no direct feature to do that. How about having one Interface and implement the same for all your entities?
You have to check out using Model Templates.
If you want to customize the way in which the decimal is displayed throughout the application then you have to create a partial view named decimal.cshtml (if Razor) and store it in the Views/Shared/DisplayTemplates folder.
In the decimal.cshtml you can control the way in which the model decimal is being displayed.
Like that, to customize the way in which it is displayed for editing then you have to create another view with the same name and dump it into the Views/Shared/EditorTemplates folder.
The MVC framework takes care of the remaining things.
Ex.
Views/Shared/DisplayTemplates/decimal.cshtml
#model decimal
<p>
#Model.ToString("0.00")
</p>
Related
I have many properties that require 1 or more validation attributes, like the following:
public class TestModel
{
[Some]
[StringLength(6)]
[CustomRequired] // more attributes...
public string Truck { get; set; }
}
Please note all the above annotations work.
I do not want to write that all the time because whenever Some is applied, all other attributes are also applied to the property. I want to be able to do this:
public class TestModel
{
[Some]
public string Truck { get; set; }
}
Now this is doable by inheriting; therefore, I wrote a custom DataAnnotationsModelMetadataProvider and overrode the CreateMetadata. This looks for anything that is decorated with Some and then adds more attributes to it:
public class TruckNumberMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var attributeList = attributes.ToList();
if (attributeList.OfType<SomeAttribute>().Any())
{
attributeList.Add(new StringLengthAttribute(6));
attributeList.Add(new CustomRequiredAttribute());
}
return base.CreateMetadata(attributeList, containerType, modelAccessor, modelType, propertyName);
}
}
These are the attributes in case it helps:
public class CustomRequiredAttribute : RequiredAttribute
{
public CustomRequiredAttribute()
{
this.ErrorMessage = "Required";
}
}
public class SomeAttribute : RegularExpressionAttribute
{
public SomeAttribute()
: base(#"^[1-9]\d{0,5}$")
{
}
}
Usage
#Html.TextBoxFor(x => x.Truck)
HTML Rendered:
<input name="Truck" id="Truck" type="text" value=""
data-val-required="The Truck field is required."
data-val-regex-pattern="^[1-9]\d{0,5}$"
data-val-regex="The field Truck must match the regular expression '^[1-9]\d{0,5}$'."
data-val="true">
</input>
Problems/Questions
The CustomRequired was applied. But why does it pick up the message from the base class RequiredAttribute if I am using CustomRequired. The data-val-required should just say Required.
The StringLenth of 6 chars is not applied. There is no sign of anything rendered for StringLength, why?
What your custom DataAnnotationsModelMetadataProvider is doing is creating/modifying the ModelMetada associated with your property.
If you inspect the ModelMetadata class you will note that it contains properties such as string DisplayName and string DisplayFormatString which are set based on the application of the [Display] and [DisplayFormat] attributes. It also contains a bool IsRequired attribute that determines if the value of a property is required (a bit more on that later).
It does not contain anything relating to a regular expression or the maximum length, or indeed anything related to validation (except the IsRequired property and the ModelType which is used to validate that that the value can be converted to the type).
It is the HtmlHelper methods that are responsible for generating the html that is passed to the view. To generate the data-val-* attributes, your TextBoxFor() method internally calls the GetUnobtrusiveValidationAttributes() method of the HtmlHelper class which in turn calls methods in the DataAnnotationsModelValidatorProvider class which ultimately generate a Dictionary of the data-val attribute names and values used to generate the html.
I'll leave it to you to explore the source code if you want more detail (refer links below) but to summarize, it gets a collection of all attributes applied to your Truck property that inherit from ValidationAttribute to build the dictionary. In your case the only ValidationAttribute is [Some] which derives from RegularExpressionAttribute so the data-val-regex and data-val-regex-pattern attributes are added.
But because you have added your CustomRequiredAttribute in the TruckNumberMetadataProvider, the IsRequired property of the ModelMetadata has been set to true. If you insect the GetValidators() of DataAnnotationsModelValidatorProvider you will see that a RequiredAttribute is automatically added to the collection of attributes because you have not applied one to the property. The relevant snippet of code is
if (AddImplicitRequiredAttributeForValueTypes && metadata.IsRequired && !attributes.Any(a => a is RequiredAttribute))
{
attributes = attributes.Concat(new[] { new RequiredAttribute() });
}
which results in the data-val-required attribute being added to the html (and it uses the default message because it knows nothing about your CustomRequiredAttribute)
Source code files to get you started if you want to understand the internal workings
HtmlHelper.cs - refer GetUnobtrusiveValidationAttributes() methods at line 413
ModelValidatorProviders.cs - gets the various ValidatorProviders used for validation
DataAnnotationsModelValidatorProvider.cs - the ValidatorProvider for ValidationAttributes
One possible solution if you really want to use just one single ValidationAttribute would be to have it implement IClientValidatable and in the GetClientValidationRules() method, add rules, for example
var rule = new ModelClientValidationRule
{
ValidationType = "required",
ErrorMessage = "Required"
}
which will be read by the ClientDataTypeModelValidatorProvider (and delete your TruckNumberMetadataProvider class). However that will create a maintenance nightmare so I recommend you just add the 3 validation attributes to your property
UPDATE: for the tl;dr version skip to the bottom
I have a pretty simple subclass of JsonConverter that I'm using with Web API:
public class DbGeographyJsonConverter : JsonConverter
{
public override bool CanConvert(Type type)
{
return typeof(DbGeography).IsAssignableFrom(type);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = (string)reader.Value;
if (value.StartsWith("POINT", StringComparison.OrdinalIgnoreCase))
{
return DbGeography.PointFromText(value, DbGeography.DefaultCoordinateSystemId);
}
else if (value.StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase))
{
return DbGeography.FromText(value, DbGeography.DefaultCoordinateSystemId);
}
else //We don't want to support anything else right now.
{
throw new ArgumentException();
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, ((DbGeography)value).AsText());
}
}
The problem is, after ReadJson returns the application never returns a bound object to the action method as it appears to be stuck in an infinite validation loop.
Here's the top of the call stack when I pause execution:
System.Web.Http.dll!System.Web.Http.Metadata.Providers.AssociatedMetadataProvider.GetMetadataForPropertiesImpl.AnonymousMethod__0() Line 40 C#
System.Web.Http.dll!System.Web.Http.Metadata.ModelMetadata.Model.get() Line 85 C#
System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateNodeAndChildren(System.Web.Http.Metadata.ModelMetadata metadata, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext, object container) Line 94 C#
System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateProperties(System.Web.Http.Metadata.ModelMetadata metadata, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext) Line 156 C#
System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateNodeAndChildren(System.Web.Http.Metadata.ModelMetadata metadata, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext, object container) Line 130 C#
System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateElements(System.Collections.IEnumerable model, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext) Line 176 C#
After that, the DefaultBodyModelValidator.Validation* pattern of calls repeats over and over and over again. Everytime I pause execution, it appears to be at about the same depth, so it doesn't appear to be getting recursively deeper.
If I force the JsonConverter to return null, control returns to the API controller action method, I'm assuming because there's nothing to validate.
I don't have the brain juices left to figure this one out. What am I doing wrong?
UPDATE: With brain juices somewhat replenished, I've stepped through most of the code and it appears that when validating the model the DefaultBodyModelValidator is drilling way down into the SqlTypesAssembly and getting stuck in a loop reading attributes somewhere. I don't really care to find out exactly where because I don't want the DefaultBodyModelValidator drilling into DbGeography type instances to start with.
There's no reason for model validation to drill down into the DbGeography class. I need to figure out how to get the MediaTypeFormatterCollection.IsTypeExcludedFromValidation method to return true for typeof(DbGeography), which will cause the DefaultBodyModelValidator to perform shallow validation on any DbGeography instances. So now the question at hand is- how do I exclude a type from model validation? The ShouldValidateType method of DefaultBodyModelValidator is marked virtual, but is there not a simple way to add an excluded type at startup?
Whether this issue is a bug or a limitation of Web API, I do not know, but here's my workaround:
First, we need to subclass the DefaultBodyModelValidator and override the ShouldValidateType method.
public class CustomBodyModelValidator : DefaultBodyModelValidator
{
public override bool ShouldValidateType(Type type)
{
return type!= typeof(DbGeography) && base.ShouldValidateType(type);
}
}
Now in global.asax's Application_Start method, add
GlobalConfiguration.Configuration.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator());
and that's it. Shallow validation will now be performed on the DbGeography type instances and everything binds nicely.
The answer by joelmdev lead me in the right direction, but with my WebApi configuration in MVC and WebApi 5.2.3 the new validator would not get called when placed in Global.asax.
The solution was to put it in my WebApiConfig.Register method with the other WebApi routes:
config.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator());
Just had exactly the same issue but then with a custom type. After quite a lot of reasearch it turned out to be quite logical with the knowledge of this thread. The custom class had a public readonly property which returned another instance of the same class. The validator walks down all properties of the class (even if you do not do any validation at all) and gets the value. If your class return a new instance of the same class this happens again and again and...
It looks like the StartPoint property in the Geography class has this very same problem. https://msdn.microsoft.com/en-us/library/system.data.spatial.dbgeography.startpoint(v=vs.110).aspx
If you're having this same problem from an Mvc application, you may be interested in this answer. It won't suit everyone, but as I was purely interested in latitude & longitude, my eventual solution to this problem was not to ignore DbGeography properties, but rather instruct the model binder how to validate them properly, or at least how I wanted them validated. This approach allows standard validation attribute properties to work, and allowed me to use regular Html controls for latitude and longitude. I validate them as a pair, as ultimately they are being bound to a single DbGepgraphy property on your model class, so either one on it's own is not enough to be considered valid. Also, I think this requires a reference to the Microsoft.SqlServer.Types Nuget package that corresponds to the target version of SQL Server being used, but you're probably referencing that already.
using System;
using System.Data.Entity.Spatial;
using System.Web.Mvc;
...
public class CustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var latitudePropertyValue = bindingContext.ValueProvider.GetValue(string.Concat(bindingContext.ModelName, ".", nameof(DbGeography.Latitude)));
var longitudePropertyValue = bindingContext.ValueProvider.GetValue(string.Concat(bindingContext.ModelName, ".", nameof(DbGeography.Longitude)));
if (!string.IsNullOrEmpty(latitudePropertyValue?.AttemptedValue)
&& !string.IsNullOrEmpty(longitudePropertyValue?.AttemptedValue))
{
if (decimal.TryParse(latitudePropertyValue.AttemptedValue, out decimal latitude)
&& decimal.TryParse(longitudePropertyValue.AttemptedValue, out decimal longitude))
{
// This is not a typo - longitude does come before latitude here
return DbGeography.FromText($"POINT ({longitude} {latitude})", DbGeography.DefaultCoordinateSystemId);
}
}
return null;
}
}
Then a custom provider which uses it:
using System;
using System.Data.Entity.Spatial;
using System.Web.Mvc;
...
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (modelType == typeof(DbGeography))
{
return new CustomModelBinder();
}
return null;
}
}
Then in Application_Start() in Global.asax.cs:
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
Then in my model this allows me to just use the regular RequiredAttribute:
[Display(Name = "Location")]
[Required(ErrorMessage = "You must specify a location")]
public DbGeography Location { get; set; }
And finally, to use this in a view, I created display and editor templates.
In ~/Views/Shared/DisplayTemplates/DbGeography.cshtml:
#model System.Data.Entity.Spatial.DbGeography
#Html.LabelFor(m => m.Latitude),
#Html.LabelFor(m => m.Longitude)
In ~/Views/Shared/EditorTemplates/DbGeography.cshtml:
#model System.Data.Entity.Spatial.DbGeography
#Html.TextBoxFor(m => m.Latitude),
#Html.TextBoxFor(m => m.Longitude)
And then I can use this editor template in any other regular view like so:
#Html.LabelFor(m => m.Location)
#Html.EditorFor(m => m.Location)
#Html.ValidationMessageFor(m => m.Location, "", new { #class = "error-message" })
You could also use hidden fields in your editor view and add some JavaScript to your template to build a Google map, for example, provided the map script also sets the values of the hidden fields again when a coordinate is selected.
If you're having this issue in WebAPI 5.2.3, the only method for fixing this that worked for me is using a variation of a technique I've described here: https://stackoverflow.com/a/40534310/2001934
Basically I used a custom required attribute on DbGeography properties in my models, that in MVC would work entirely like the normal Required Attribute, but in WebApi I added an additional attribute adapter which always replaces the list of client validation rules with a new, empty list, so that no validation is performed in WebApi model binding for those same properties.
Only bit missing from this answer was how to replace the existing ModelValidatorProvider in WebApi:
DataAnnotationsModelValidationFactory factory = (p, a) => new DataAnnotationsModelValidator(
new List<ModelValidatorProvider>(), new CustomRequiredAttribute()
);
DataAnnotationsModelValidatorProvider provider = new DataAnnotationsModelValidatorProvider();
provider.RegisterAdapterFactory(typeof(CustomRequiredAttribute), factory);
GlobalConfiguration.Configuration.Services.Replace(typeof(ModelValidatorProvider), provider);
When I put [Required] attribute on my ViewModel's property MVC3 automatically generate error messages like:
The Price field is required.
My site's single language is Russian, so I want to have localized error messages. I can localize field's name with [Display(Name = "blablabla")], but how can I localize the field is required part?
Update:
I know, that I can change an error message for concrete field by specifying it [Required(ErrorMessage = "blablabla")], is there a way I can change it in one place for all [Required] attributes, so I could use just [Required] without additional parameters, and it took the localized error message from some ressource/config/etc?
I've created an alternative solution where you don't have to use the attributes for the localization. I've created custom model/validation meta data providers.
All you need to do is to download my code and do the following in your global.asax:
var stringProvider = new ResourceStringProvider(Resources.LocalizedStrings.ResourceManager);
ModelMetadataProviders.Current = new LocalizedModelMetadataProvider(stringProvider);
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new LocalizedModelValidatorProvider(stringProvider));
(the ResourceStringProvider is my default implementation but it's easy to create an alternative that reads from XML files or a database)
You can read about it here:
http://blog.gauffin.org/2011/09/easy-model-and-validation-localization-in-asp-net-mvc3/
I'm going to release a nuget package as soon as I'm finished with my view localization and my alternative HTML helpers.
The Required attribute has properties that allow the message to be read from a resource string. See http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.requiredattribute.aspx for the details
Granted for this you should already have some Resource Manager which can return localized texts for string keys. My ResourceManager has static accessor for this (and registrations with Unity for DI), so there's no need to pass it.
In the global.asax:
ModelMetadataProviders.Current = new LocalizedModelMetadataProvider(); //field name localization
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new LocalizedModelValidatorProvider()); //validation message localization
And the implementation is this:
public class LocalizedModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType,
Func<object> modelAccessor, Type modelType, string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (containerType == null || propertyName == null)
return metadata;
if (metadata.DisplayName == null)
metadata.DisplayName = ResourceManager.Current.GetLocalizedAttribute(containerType, propertyName);
if (metadata.Watermark == null)
metadata.Watermark = ResourceManager.Current.GetLocalizedAttribute(containerType, propertyName, "Watermark");
if (metadata.Description == null)
metadata.Description = ResourceManager.Current.GetLocalizedAttribute(containerType, propertyName, "Description");
if (metadata.NullDisplayText == null)
metadata.NullDisplayText = ResourceManager.Current.GetLocalizedAttribute(containerType, propertyName, "NullDisplayText");
if (metadata.ShortDisplayName == null)
metadata.ShortDisplayName = ResourceManager.Current.GetLocalizedAttribute(containerType, propertyName, "ShortDisplayName");
return metadata;
}
}
public class LocalizedModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
foreach (var attribute in attributes.OfType<ValidationAttribute>())
attribute.ErrorMessage = ResourceManager.Current.GetValidationMessage(attribute);
return base.GetValidators(metadata, context, attributes);
}
}
Simply add/change the globalization tag in web.config:
<system.web>
<globalization uiCulture="your culture"/>
This is more of a theoretical question.
I'm currently examining the MVC 3 validation by using ComponentModel.DataAnnotations, and everything works automagically, especially on client side.
Somehow something checks for those attributes, and generates javascript for the validation (or html5 attributes, if using unobtrusive mode), and it works.
My question is that what generates the client side javascript and how can I access and modify it? For example I want to handle the given dataannotation attributes a little differently, or handle custom attributes (I have found that I can derive them from ValidationAttribute, but maybe for some reason I don't want).
Can someone explain it to me what really happens?
(Or links to good explanations would also be good, as I have only found tutorials for actually using dataannotations)
EDIT: Also with deriving from ValidationAttribute, the client-side validation is not working automatically. Why?
MVC3 has a new jQuery Validation mechanism that link jQuery Validation and Validation Attributes Metadata, this is the jquery.validate.unobtrusive file that takes all data- attributes and work with them, just like before when you set the
<add key="UnobtrusiveJavaScriptEnabled" value="false" />
All you need to do is come up with your own Custom Validation Attributes, for that you have 2 options:
Create a Custom Validation Attribute that inherits the ValidationAttribute interface and
override the IsValid
or
Create a Self Validate Model use the model IValidatebleObject that all you need is to return the Validate method
in MVC3 you now have a method that you can override that has a ValidationContext object, where you can simply get all references, properties and values of any other object in the form
Create your own, and that unobtrusive file will handle the mapping of what your custom validator needs and will work out together with the jQuery Validation plugin.
YOU DO NOT Change the javascript... that's sooo 90's and not MVC way!
for example if you want to validate, let's say 2 dates that the last can not be less than the first (period of time for example)
public class TimeCard
{
public DateTime StartDate { get; set; }
[GreaterThanDateAttribute("StartDate")]
public DateTime EndDate { get; set; }
}
creating a Custom Validation
public class GreaterThanDateAttribute : ValidationAttribute
{
public string GreaterThanDateAttribute(string otherPropertyName)
:base("{0} must be greater than {1}")
{
OtherPropertyName = otherPropertyName;
}
public override string FormatErrorMessage(string name)
{
return String.Format(ErrorMessageString, name, OtherPropertyName);
}
public override ValidateionResult IsValid(object value, ValidationContext validationContext)
{
var otherPropertyInfo = validationContext.ObjectTYpe.GetProperty(OtherPropertyName);
var otherDate = (DateTime)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
var thisDate = (DateTime)value;
if( thisDate <= otherDate )
{
var message = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(message);
}
return null;
}
}
if using the Self Validating model then the code would be just
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if( EndDate <= StartDate )
yield return new ValidationResult("EndDate must be grater than StartDate");
}
Keep in mind that the Custom Validation is Generic, that's why much code, and Self Validating Model only works on the model applied.
Hope it helps
added
I didn't explain the Custom Client Validation part, fell free to ask if you need examples, but basically:
It's easier in MVC3 (if of course, you understand jQuery.Validate) all you need to do is:
Implement IClientValidateble
Implement a jQuery validation method
Implement an unobtrusive adapter
To create this 3 things, let's take this GreaterThanDateAttribute into account and create the Custom Client Side Validation. For that we need to code this:
append to the GreaterThanDateAttribute
public IEnumerable<ModelCLientValidation> GetCLientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelCLientValidationRule();
rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
rule.ValidationType = "greater"; // This is what the jQuery.Validation expects
rule.ValidationParameters.Add("other", OtherPropertyName); // This is the 2nd parameter
yield return rule;
}
Then you need to write the new jQuery Validator and the metadata adapter that will link the jQuery.Validation with your code providing the correct data- attributes for that field (if of course, UnobtrusiveJavaScriptEnabled is true)
create a new js file and attach to your <head> for example as
<script src="#Url.Content("~/Scripts/customValidation.js")" type="text/javascript"></script>
and append the new validation
jQuery.validator.addMethod("greater", function(value, element, param) {
// we need to take value and compare with the value in 2nd parameter that is hold in param
return Date.parse(value) > Date.parse($(param).val());
});
and then we write the adapter
jQuery.validator.unobtrusive.adapters.add("greater", ["other"], function(options) {
// pass the 'other' property value to the jQuery Validator
options.rules["greater"] = "#" + options.param.other;
// when this rule fails, show message that comes from ErrorMessage
options.messages["greater"] = options.message;
});
I'm writing a PropertiesMustMatch validation attribute that can take a string property name as a parameter. I'd like it to find the corresponding property by name on that object and do a basic equality comparison. What's the best way to access this through reflection?
Also, I checked out the Validation application block in the Enterprise Library and decided its PropertyComparisonValidator was way too intense for what we need.
UPDATE: For further clarification (to provide some context), the goal is simply validation that enforces field matching (e.g., password verification). We'd like it to work with property-level attribute data annotations that inherit from the ValidationAttribute class, if possible.
UPDATE: In case anyone is curious, I ended up solving the actual business problem through tweaking code provided as an answer to this question
You can't, basically. The code that checks the object for the presence of the attribute must also assume responsibility for telling any code which type/object it was looking at. You can't obtain any additional metadata from within an attribute.
You cannot do that. See also this question. Try to change the logic to work with the object, checking its attributes, not vice versa. You can also provide more information about your task, not just this narrow question.
You can something like this.
//target class
public class SomeClass{
[CustomRequired(ErrorMessage = "{0} is required", ProperytName = "DisplayName")]
public string Link { get; set; }
public string DisplayName { get; set; }
}
//custom attribute
public class CustomRequiredAttribute : RequiredAttribute, IClientValidatable
{
public string ProperytName { get; set; }
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var propertyValue = "Value";
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, context.Controller.ViewData.Model.GetType());
var property = parentMetaData.FirstOrDefault(p => p.PropertyName == ProperytName);
if (property != null)
propertyValue = property.Model.ToString();
yield return new ModelClientValidationRule
{
ErrorMessage = string.Format(ErrorMessage, propertyValue),
ValidationType = "required"
};
}
}