I have a small PCL attribute to display a string for enum values:
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class EnumDescriptionAttribute : Attribute
{
private readonly string description;
public EnumDescriptionAttribute(string description)
{
this.description = description;
}
public string Description { get { return description; } }
}
I have made a CustomMetadataProvider for my MVC Project like that:
public class CustomMetadatprovider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (attributes.OfType<EnumDescriptionAttribute>().Any())
{
modelMetadata.DisplayName = attributes.OfType<EnumDescriptionAttribute>().First().Description;
}
return modelMetadata;
}
}
The enum where I apply the attribute on:
public enum RequestStatus
{
[EnumDescription("Unknown value")]
Unknown = 0,
[EnumDescription("New value")]
New = 10,
[EnumDescription("In Progress value")]
InProgress = 20,
[EnumDescription("Terminated value")]
Terminated = 30,
[EnumDescription("Canceled value")]
Cancelled = 40
}
In my startup.cs:
ModelMetadataProviders.Current = new CustomMetadatprovider();
I go through the my custom metada provider but my attribute is never listed on the field.
Am I missing Something?
EDIT
I made some progress, the attribute is seen if I apply it to the ViewModel like this:
public class RequestViewModel
{
public Guid Id { get; set; }
[EnumDescription("Provider are you seeing me?")]
public RequestStatus Status { get; set; }
public string Note { get; set; }
}
The metadataprovider is not seeing the EnumDescriptionAttribute on Status value
public class RequestViewModel
{
public Guid Id { get; set; }
public RequestStatus Status { get; set; }
public string Note { get; set; }
}
Related
Why db.Countries() comes null in following scenario-
1. CityController
[Authorize]
public class CityController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext("CP");
// GET: City/Create
public ActionResult Create()
{
ViewBag.CountryId = new SelectList(db.Countries.ToList(), "CountryId", "Name");
return View();
}
ApplicationDbContext
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
internal IDbSet<Country> Countries { get; set; }
...
}
Country is defined as-
[Table("Country")]
public class Country
{
#region Fields
private ICollection<City> _cities;
#endregion
#region Scalar Properties
public Guid CountryId { get; set; }
public string Name { get; set; }
public string CountryCode { get; set; }
#endregion
#region Navigation Properties
public virtual ICollection<City> Cities
{
get { return _cities ?? (_cities = new List<City>()); }
set { _cities = value; }
}
#endregion
}
City is defined as-
[Table("City")]
public class City
{
#region Fields
private ICollection<Location> _locations;
#endregion
#region Scalar Properties
public Guid CityId { get; set; }
public Guid CountryId { get; set; }
public string Name { get; set; }
public string CityCode { get; set; }
public string ZipCode { get; set; }
public Country Country { get; set; }
#endregion
#region Navigation Properties
public virtual ICollection<Location> Locations
{
get { return _locations ?? (_locations = new List<Location>()); }
set { _locations = value; }
}
#endregion
}
What could be the reason for not populating Country table records and returning countries to null?
After sparing few hours, I just noticed the Access-modifier of Countries properties which was internal. I made it Public and magic happened! It works though I don't have any explanation on WHY part of it.
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
//internal IDbSet<Country> Countries { get; set; }
public IDbSet<Country> Countries { get; set; }
public IDbSet<City> Cities { get; set; }
Thanks everyone.
Summary:
I want a data annotation validator to reference another property in the same class (TitleAuthorAndPublishingConfiguration).
However, DB.SaveChanges() is not being called on this class directly. Rather it is being called on the parent of this class (WebsiteConfiguration).
Therefore validationContext.ObjectType is returning WebsiteConfiguration and I am unable to refer to properties of TitleAuthorAndPublishingConfiguration within the data annotation validator.
WebsiteConfiguration.cs
public class WebsiteConfiguration
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public TitleAuthorAndPublishingConfiguration TitleAuthorAndPublishing { get; set; }
public BookChaptersAndSectionsConfiguration BookChaptersAndSections { get; set; }
public SocialMediaLoginsConfiguration SocialMediaLogins { get; set; }
public TagGroupsConfiguration TagGroups { get; set; }
}
public class TitleAuthorAndPublishingConfiguration
{
public string BookTitle { get; set; }
public bool IsPublished { get; set; }
// how do I access a property of current model when calling DB.SaveChanges() on parent?
[RequiredIfOtherFieldIsEnabled("IsPublished")]
public string Publisher { get; set; }
}
// ... and other sub models...
ApplicationDbContext.cs
DbSet<WebsiteConfiguration> WebsiteConfiguration {get;set;}
Example Update Code
public void SeedWebsiteConfiguration()
{
var titleAuthorAndPublishingConfiguration = new TitleAuthorAndPublishingConfiguration()
{
// seed values
};
var bookChaptersAndSectionsConfiguration = new BookChaptersAndSectionsConfiguration()
{
// seed values
};
var socialMediaLoginConfiguration = new SocialMediaLoginsConfiguration()
{
// seed values
};
var tagGroupsConfiguration = new TagGroupsConfiguration()
{
// seed values
};
var websiteConfiguration = new WebsiteConfiguration()
{
TitleAuthorAndPublishing = titleAuthorAndPublishingConfiguration,
BookChaptersAndSections = bookChaptersAndSectionsConfiguration,
SocialMediaLogins = socialMediaLoginConfiguration,
TagGroups = tagGroupsConfiguration
};
DB.WebsiteConfiguration.Add(websiteConfiguration);
DB.SaveChanges();
}
Validator Code
public class RequiredIfOtherFieldIsEnabledAttribute : ValidationAttribute
{
private string _ifWhatIsEnabled { get; set; }
public RequiredIfOtherFieldIsEnabledAttribute(string IfWhatIsEnabled)
{
_ifWhatIsEnabled = IfWhatIsEnabled;
}
protected override ValidationResult IsValid(object currentPropertyValue, ValidationContext validationContext)
{
var isEnabledProperty = validationContext.ObjectType.GetProperty(_ifWhatIsEnabled);
if (isEnabledProperty == null)
{
return new ValidationResult(
string.Format("Unknown property: {0}", _ifWhatIsEnabled)
);
}
var isEnabledPropertyValue = (bool)isEnabledProperty.GetValue(validationContext.ObjectInstance, null);
if (isEnabledPropertyValue == true)
{
if (String.IsNullOrEmpty(currentPropertyValue.ToString()))
{
return new ValidationResult(String.Format("This field is required if {0} is enabled", isEnabledProperty));
}
}
return ValidationResult.Success;
}
}
Questions
Is there a way for me to access child model properties from validationContext?
Am I misguided in my approach? Is there a better way to store multiple models as part of a larger model in a single DB table?
I was hoping not to have multiple config tables and calls to the DB. (There are 4 child models in this example, but there may be 10+ in the next app.)
The setup above meets my needs in so many ways. But I don't want to give up the functionality of DataAnnotations on the sub models!
Bonus Question
I have come across a few posts like this one:
How can I tell the Data Annotations validator to also validate complex child properties?
But that is 4 years old, and I'm wondering if anything has changed since then.
Am I trying to do something that is basically impossible (or at least very difficult)?
Am I trying to do something that is basically impossible (or at least
very difficult)?
No, there is a very simple solution that integrates perfectly with the framework and technologies using DataAnnotations.
You can create a custom ValidationAttribute that is called by EF Validation and call Validator.TryValidateObject inside. This way, when CustomValidation.IsValid is called by EF you launch child complex object validation by hand and so on for the whole object graph. As a bonus, you can gather all errors thanks to CompositeValidationResult.
i.e.
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
public class Program
{
public static void Main() {
var person = new Person {
Address = new Address {
City = "SmallVille",
State = "TX",
Zip = new ZipCode()
},
Name = "Kent"
};
var context = new ValidationContext(person, null, null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(person, context, results, true);
PrintResults(results, 0);
Console.ReadKey();
}
private static void PrintResults(IEnumerable<ValidationResult> results, Int32 indentationLevel) {
foreach (var validationResult in results) {
Console.WriteLine(validationResult.ErrorMessage);
Console.WriteLine();
if (validationResult is CompositeValidationResult) {
PrintResults(((CompositeValidationResult)validationResult).Results, indentationLevel + 1);
}
}
}
}
public class ValidateObjectAttribute: ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var results = new List<ValidationResult>();
var context = new ValidationContext(value, null, null);
Validator.TryValidateObject(value, context, results, true);
if (results.Count != 0) {
var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
results.ForEach(compositeResults.AddResult);
return compositeResults;
}
return ValidationResult.Success;
}
}
public class CompositeValidationResult: ValidationResult {
private readonly List<ValidationResult> _results = new List<ValidationResult>();
public IEnumerable<ValidationResult> Results {
get {
return _results;
}
}
public CompositeValidationResult(string errorMessage) : base(errorMessage) {}
public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) {}
protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {}
public void AddResult(ValidationResult validationResult) {
_results.Add(validationResult);
}
}
public class Person {
[Required]
public String Name { get; set; }
[Required, ValidateObject]
public Address Address { get; set; }
}
public class Address {
[Required]
public String Street1 { get; set; }
public String Street2 { get; set; }
[Required]
public String City { get; set; }
[Required]
public String State { get; set; }
[Required, ValidateObject]
public ZipCode Zip { get; set; }
}
public class ZipCode {
[Required]
public String PrimaryCode { get; set; }
public String SubCode { get; set; }
}
Does there exist an enum validation attribute which validates a certain enum value?
Or should I do that manually:
if(viewModel.State == State.None) base.AddModelError("","wrong enum selected...");
How would you do that?
Enum:
public enum SomeEnum
{
Right = 1,
Wrong = 2,
Other = 3
}
ViewModel:
public class TestModel
{
[Range((int)(SomeEnum.Right), (int)(SomeEnum.Right), ErrorMessage = "Please select an Enum value")]
public SomeEnum Value { get; set; }
}
No other code needed. Enums are basically integers so you can use the Range attribute to validate them.
I would create my own validation attribute something like (untested):
public enum SomeEnum
{
Right,
Wrong
}
[AttributeUsage(AttributeTargets.Property)]
public sealed class EnumValueAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "Cannot be that value";
public SomeEnum EnumVal { get; private set; }
public EnumValueAttribute(SomeEnum enumVal)
: base(DefaultErrorMessage)
{
EnumVal = enumVal;
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name, EnumVal);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var enumVal = (SomeEnum) Enum.Parse(typeof (SomeEnum), value.ToString());
if (enumVal != EnumVal)
{
return new ValidationResult(DefaultErrorMessage);
}
return ValidationResult.Success;
}
}
Usage:
public class TestModel
{
[EnumValue(SomeEnum.Right)]
public SomeEnum Value { get; set; }
}
UPDATED
So this is as generic as you can reasonably take it, I've not tested this code, but it does compile. Notice that I've assigned number values to the enums.
public enum SomeEnum
{
Right = 1,
Wrong = 2,
Other = 3
}
[AttributeUsage(AttributeTargets.Property)]
public sealed class DisallowEnumValueAttribute : ValidationAttribute
{
public object DissallowedEnum { get; private set; }
public Type EnumType { get; private set; }
public DisallowEnumValueAttribute(Type enumType, object dissallowedEnum)
{
if (!enumType.IsEnum)
throw new ArgumentException("Type must be an enum", "enumType");
DissallowedEnum = dissallowedEnum;
EnumType = enumType;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var disallowed = Convert.ChangeType(DissallowedEnum, EnumType);
var enumVal = Convert.ChangeType(value, EnumType);
if (disallowed == null || enumVal == null)
throw new Exception("Something is wrong"); //or return validation result
if (enumVal == disallowed)
{
return new ValidationResult("This value is not allowed");
}
return ValidationResult.Success;
}
}
public class TestModel
{
[DisallowEnumValue(typeof(SomeEnum), SomeEnum.Wrong)]
public SomeEnum Thing { get; set; }
}
My enum validation was like this and works in dataannotation validation
public enum CreatedBySelfOrOthersEnumValues
{
Self,
Others
}
public class CampaignRegisterValidationModel
{
[Required]
public string Name { get; set; }
[Required]
public CreatedBySelfOrOthersEnumValues CreatedForSelfOrOthers { get; set; }
[Required]
public int CountryCode { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
}
Then validating it
if (ModelState.IsValid)
{
}
I want to attached custom attributes from xml configuration please help me out for this.
public partial class User
{
public Nullable<int> UserId { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public Nullable<int> salary { get; set; }
}
[MetadataType(typeof(CUserAttributes))]
public class Cuser : User
{
public Nullable<bool> IsRequire { get; set; }
}
//[Serializable]
public class CUserAttributes
{
[Required]
public Nullable<bool> IsRequire { get; set; }
[Display(Name="My UserId")]
[RequiredIf(IsRequiredPropertyName = "IsRequire", ErrorMessage = "required.")]
public Nullable<int> UserId { get; set; }
[RequiredIf(IsRequiredPropertyName = "IsRequire", ErrorMessage = "required.")]
public string UserName { get; set; }
[RequiredIf(IsRequiredPropertyName = "IsRequire", ErrorMessage = "required.")]
public string Password { get; set; }
[RequiredIf(IsRequiredPropertyName = "IsRequire", ErrorMessage = "required.")]
public Nullable<int> salary { get; set; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class RequiredIf : ValidationAttribute, IClientValidatable
{
public string IsRequiredPropertyName { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var isRequiredName = validationContext.ObjectInstance.GetType().GetProperty(this.IsRequiredPropertyName);
var isRequiredNameValue = isRequiredName.GetValue(validationContext.ObjectInstance, null);
if (isRequiredNameValue != null)
{
if (Convert.ToBoolean(isRequiredNameValue) == true)
{
if (value == null)
{
return new ValidationResult(this.ErrorMessage);
}
}
}
else if (isRequiredNameValue == null)
{
throw new Exception("RequiredIf property value is not found");
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
ModelClientValidationRule mcvr = new ModelClientValidationRule();
mcvr.ValidationType = "requiredif";
mcvr.ErrorMessage = this.ErrorMessage;
mcvr.ValidationParameters.Add("isrequiredpropertyname", this.IsRequiredPropertyName);
return new List<ModelClientValidationRule> { mcvr };
}
}
I have create Model, Attributes class then custom attribute class, but now I want add those data annotations i.e Display, RequiredIf(custom attribute) from XML configuration.
it is possible to get the configuration for validations from xml,
you can follow the below link
http://www.primaryobjects.com/CMS/Article141.aspx
I am trying to attach an attribute to the particular property in the this case the FirstName but the problem is in this code it is attaching to the birthday datetime property as well . what might be the problem with this
public class CustomMetadataValidationProvider : DataAnnotationsModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
if ( metadata.PropertyName == "FirstName")
attributes = new List<Attribute>() { new RequiredAttribute() };
return base.GetValidators(metadata, context, attributes);
}
}
public class User
{
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Birthday { get; set; }
}
protected void Application_Start()
{
//ModelValidatorProviders.Providers.Clear();
//ModelValidatorProviders.Providers.Add(new CustomMetadataValidationProvider());
ModelValidatorProviders.Providers.Add(new CustomMetadataValidationProvider());
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
can someone can explain how GetValidators is working?
Your problem has nothing to do with your GetValidators method.
Value types like (int, decimal, DateTime, etc.) are required by default. Because otherwise the model binder cannot set their values if they are not sent with the request.
So you need to change your Birtday property to nullable if you don't want to be required:
public class User
{
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime? Birthday { get; set; }
}