We have an application with three layers: UI, Business, and Data. The data layer houses Entity Framework v4 and auto-generates our entity objects. I have created a buddy class for the entity VendorInfo:
namespace Company.DataAccess
{
[MetadataType(typeof(VendorInfoMetadata))]
public partial class VendorInfo
{
}
public class VendorInfoMetadata
{
[Required]
public string Title;
[Required]
public string Link;
[Required]
public string LinkText;
[Required]
public string Description;
}
}
I want this validation to bubble up to the UI, including custom validation messages assigned to them. In MVC this is a piece of cake but in web forms I have no clue where to begin. What is the best way to utilize model validation in asp.net web forms?
I did find an article that explains how to build a server control for it, but I can't seem to get it working. It compiles and even recognizes the control but I can never get it to fire.
Any ideas?
Thanks everyone.
I solved it. It would appear that the server control I found was not designed to read fields in a buddy class via the MetadataType attribute. I modified the code to look for its validation attributes in the buddy class rather than the entity class itself.
Here is the modified version of the linked server control:
[DefaultProperty("Text")]
[ToolboxData("<{0}:DataAnnotationValidator runat=server></{0}:DataAnnotationValidator>")]
public class DataAnnotationValidator : BaseValidator
{
#region Properties
/// <summary>
/// The type of the source to check
/// </summary>
public string SourceTypeName { get; set; }
/// <summary>
/// The property that is annotated
/// </summary>
public string PropertyName { get; set; }
#endregion
#region Methods
protected override bool EvaluateIsValid()
{
// get the type that we are going to validate
Type source = GetValidatedType();
// get the property to validate
FieldInfo property = GetValidatedProperty(source);
// get the control validation value
string value = GetControlValidationValue(ControlToValidate);
foreach (var attribute in property.GetCustomAttributes(
typeof(ValidationAttribute), true)
.OfType<ValidationAttribute>())
{
if (!attribute.IsValid(value))
{
ErrorMessage = attribute.ErrorMessage;
return false;
}
}
return true;
}
private Type GetValidatedType()
{
if (string.IsNullOrEmpty(SourceTypeName))
{
throw new InvalidOperationException(
"Null SourceTypeName can't be validated");
}
Type validatedType = Type.GetType(SourceTypeName);
if (validatedType == null)
{
throw new InvalidOperationException(
string.Format("{0}:{1}",
"Invalid SourceTypeName", SourceTypeName));
}
IEnumerable<MetadataTypeAttribute> mt = validatedType.GetCustomAttributes(typeof(MetadataTypeAttribute), false).OfType<MetadataTypeAttribute>();
if (mt.Count() > 0)
{
validatedType = mt.First().MetadataClassType;
}
return validatedType;
}
private FieldInfo GetValidatedProperty(Type source)
{
FieldInfo field = source.GetField(PropertyName);
if (field == null)
{
throw new InvalidOperationException(
string.Format("{0}:{1}",
"Validated Property Does Not Exists", PropertyName));
}
return field;
}
#endregion
}
This code only looks in the buddy class. If you want it to check an actual class and then its buddy class, you'll have to modify it accordingly. I did not bother doing that because usually if you are using a buddy class for validation attributes it's because you are not able to use the attributes in the main entity class (e.g. Entity Framework).
For model validation in web forms I'm using DAValidation library. It supports validation on client side (including unobtrusive validation), extensibility based on same principles as in MVC. It is MS-PL licensed and available via Nuget.
And here is bit out of date article describing with what thoughts control was build.
Related
We used DB-first approach to generate models in a .NET core application. DataAnnotations were put in a "buddy" metadata class so as to avoid writing in an autogenerated file. When controller calls TryValidateModel, all works well, Name property is required.
public partial class User
{
public string Name { get; set; }
}
[ModelMetadataType(typeof(UserMetaData))]
public partial class User : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { }
}
public class UserMetaData
{
[Required]
public string Name { get; set; }
}
On a service layer of the app, we want to implement additional validation, that also checks if objects are valid in regards to data annotations. This is done via
Validator.TryValidateObject()
which successfully calls Validate method, but disregards data annotations - user is valid, even with an empty name.
TL;DR:
MVC (web project) knows how to consider data annotations put in a "buddy" class via ModelMetadataType attribute, service layer project does not.
I thought i have found the answer here, but it seems that
TypeDescriptor.AddProviderTransparent
does not work for .net core apps.
Any ideas would be greatly appreciated.
I really hoped for a one line solution :)
I abused ashrafs answer to his own question like so:
var metadataAttr = typeof(T).GetCustomAttributes(typeof(ModelMetadataTypeAttribute), true).OfType<ModelMetadataTypeAttribute>().FirstOrDefault();
if (metadataAttr != null)
{
var metadataClassProperties = TypeDescriptor.GetProperties(metadataAttr.MetadataType).Cast<PropertyDescriptor>();
var modelClassProperties = TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>();
var errs =
from metaProp in metadataClassProperties
join modelProp in modelClassProperties
on metaProp.Name equals modelProp.Name
from attribute in metaProp.Attributes.OfType<ValidationAttribute>()
where !attribute.IsValid(modelProp.GetValue(model))
select new ValidationResult(attribute.FormatErrorMessage(Reflector.GetPropertyDisplayName<T>(metaProp.Name)), new[] { metaProp.Name });
validationResults.AddRange(errs);
}
I have the following method where I read from a key-value XML file. I pass in a key and am returned a value where I used to display on my view.
public static class TextManager
{
public static string GetValue(string key)
{
string returnVal = null;
XmlSerializer serializer = new XmlSerializer(typeof(Entries));
string path = HttpContext.Current.Server.MapPath("/App_Data/text-key-value.xml");
if (File.Exists(path))
{
Entries entries = (Entries)serializer.Deserialize(File.OpenRead(path));
var entry = entries.Where(u => u.Key == key).FirstOrDefault();
if (entry != null)
{
returnVal = entry.Value;
}
}
return returnVal;
}
}
Basically I want to be able to use this method in my model class as a data-annotation that will pull directly from my site text file and set to the display name property.
For instance I want to replace
[Display(Name = "Reference Code")]
public string ReferenceCode { get; set; }
With this
[DisplaySiteText("ReferenceCodeKey")]
public string ReferenceCode { get; set; }
DisplaySiteText would pass the string reference "ReferenceCodeKey" to the GetValue method, file the reference in the file and then set the standard Display name attribute to whatever was in the file.
How do I create my own custom model annotation to do this, I've written custom validation annotations in the past by creating a class that inherits from ValidationAttribute, but I don't think that will work in this case.
You can inherit DisplayNameAttribute for this purpose
public class DisplaySiteTextAttribute : DisplayNameAttribute
{
private string _key;
public DisplaySiteTextAttribute(string key)
{
_key = key;
}
public override string DisplayName
{
get
{
return TextManager.GetValue(_key);
}
}
}
There are several options to customize model metadata:
Customize the way that framework provides metadata. (Create ModelMedatadaProvider)
Create new Metadata attributes. (Implement IMetadataAware)
Modify existing attributes. (Derive existing attributes.)
The 3rd option has been discussed in the other answer. Here in this post, I'll share first and second options.
Option 1 - Customize the way that framework provides metadata
You can change the logic of getting display text without changing the attribute.
In fact it's responsibility of ModelMetaDataProvider to get mete data for model, including display text for properties. So as an option, you can keep the Display attribute intact and instead, create a new model metadata provider and return property metadata from a different source.
To do so, you can create a new metadata provider by deriving from DataAnnotationsModelMetadataProvider. Then override GetMetadataForProperty and call base, to get metadata. Then change DisplayName based on your logic by reading from your text manager.
You also need to register the new metadata provider as ModelMetadataProviders.Current in App_Start.
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
public class MyCustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor,
Type containerType,
PropertyDescriptor propertyDescriptor)
{
var metadata = base.GetMetadataForProperty(modelAccessor,
containerType, propertyDescriptor);
var display = propertyDescriptor.Attributes
.OfType<DisplayAttribute>().FirstOrDefault();
if (display != null)
{
metadata.DisplayName = TextManager.GetValue(display.Name);
}
return metadata;
}
}
And then register it in Application_Start():
ModelMetadataProviders.Current = new MyCustomModelMetadataProvider();
For more information take a look at DataAnnotationsModelMetadataProvider.cs source code in ASP.NET MVC sources.
This approach is useful when you want to change the way that you provide metadata for model. For example when you want to load display name and description from an external file rather than resources, without changing existing attributes.
Option 2 - Create new Metadata attributes
Another standard solution for creating metadata-aware attributes is creating an attribute and implementing IMetadataAware interface. Then in implementation of OnMetadataCreated you can easily set properties of metadata.
This approach doesn't need to register a new meta data provider. This approach is supported by the default metadata provider and is useful for creating new metadata-aware attributes:
using System;
using System.Web.Mvc;
public class CustomMetadataAttribure : Attribute, IMetadataAware
{
public string Key { get; set; }
public CustomMetadataAttribure(string key) => this.Key = key;
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.DisplayName = TextManager.GetValue(this.Key);
}
}
This approach is useful when you want to extend metadata attributes and add a few more attributes. For example when you want to add some attributes to control rendering. You can set ModelMetadata properties or add some new values to its AdditionalValues dictionary.
Maybe your DisplaySiteText attribute could inherit from the Display attribute and set the name using your helper class. Something like this:
public class DisplaySiteTextAttribute : DisplayAttribute
{
public DisplaySiteTextAttribute(string key)
{
Name = TextManager.GetValue(key);
}
}
I have a ViewModel that I can decorate with the [Required] attribute (see below). I've come to the point where I need to let the client control which fields are required or not. They can configure this trough XML and all this info is stored in the Model when it's first created. Now I have fields that are not decorated with [Required] but still need to get validated (as per "user settings") before submitting (for example the Phone field).
public class MyBusinessObjectViewModel
{
[Required]
public string Email { get; set; } //compulsory
public string Phone { get; set; } //not (yet) compulsory, but might become
}
If the user will not enter the Phone number, the data will still get posted. Wanting not to mess with custom validators, I just add the "data-val" and "data-val-required" attributes to the Html, like this:
Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add("data-val", "true");
dict.Add("data-val-required", "This field is required.");
#Html.TextBoxFor(x => x, dict);
This forces the client side validation for all the properties that are dynamically set as required. Is this good practice? What kind of side effects can I expect?
You should look into extending the meta model framework with your own metadata provider to do the actual binding between your site's configuration and the model metadata. You can actually set the required property flag to true on the property model metadata during the metadata creation process. I can't remember for sure whether this causes the built in editor templates to generate the attribute, but I think it does. Worst case scenario you can actually create and attach a new RequiredAttribute to the property, which is a tad bit kluggy, but works pretty well in certain scenarios.
You could also do this with IMetadataAware attributes, especially if Required is the only metadata aspect your users can customize, but the implementation really depends on what you're trying to do.
One major advantage of using a custom ModelMetadataProvider in your specific case is that you can use dependency injection (via ModelMetadataProviders) to get your customer settings persistence mechanism into scope, whereas with the data attribute you only get to write an isolated method that runs immediately after the metadata model is created.
Here is a sample implementation of a custom model metadata provider, you'd just have to change the client settings to whatever you wanted to use.
UPDATED but not tested at all
public class ClientSettingsProvider
{
public ClientSettingsProvider(/* db info */) { /* init */ }
public bool IsPropertyRequired(string propertyIdentifier)
{
// check the property identifier here and return status
}
}
public ClientRequiredAttribute : Attribute
{
string _identifier;
public string Identifier { get { return _identifer; } }
public ClientRequiredAttribute(string identifier)
{ _identifier = identifier; }
}
public class RequiredModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
ClientSettings _clientSettings;
public RequiredModelMetadataProvider(ClientSettings clientSettings)
{
_clientSettings = clientSettings;
}
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
// alternatively here is where you could 'inject' a RequiredAttribute into the attributes list
var clientRequiredAttribute = attributes.OfType<ClientRequiredAttribute>().SingleOrDefault();
if(clientRequiredAttribute != null && _clientSettings.IsPropertyRequired(clientRequiredAttribute.Identifier))
{
// By injecting the Required attribute here it will seem to
// the base provider we are extending as if the property was
// marked with [Required]. Your data validation attributes should
// be added, provide you are using the default editor templates in
// you view.
attributes = attributes.Union(new [] { new RequiredAttribute() });
}
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
// REMOVED, this is another way but I'm not 100% sure it will add your attributes
// Use whatever attributes you need here as parameters...
//if (_clientSettings.IsPropertyRequired(containerType, propertyName))
//{
// metadata.IsRequired = true;
//}
return metadata;
}
}
USAGE
public class MyModel
{
[ClientRequired("CompanyName")]
public string Company { get; set; }
}
public class MyOtherModel
{
[ClientRequired("CompanyName")]
public string Name { get; set; }
public string Address { get; set; }
}
Both of these models would validate the string "CompanyName" against your client settings provider.
Not wanting to mess with custom validators, so you messed in the View obfuscating the logic of your validation by removing it from the place where it is expected to be found.
Really, don't be afraid of creating a custom attribute validator. What you are doing right now is getting a technical debt.
I have MVC project that relies on webservices to provide data and those webservices are based on CMIS specification with custom functionality. I have several classes used as DataContracts, which were created by Visual Studio when I added references to services I am calling. I am using that class as a model to ensure I am able to send instances to the service and process correctly those sent back to me.
I also have views to edit instances of those classes and I would like to use DataAnnotations to validate the forms (usually [Required] atribute and sometimes display name change).
I do not want to put those atributes in service reference files because updating the reference would mean I will loose those atributes (at least I could not be sure everything is still the same after reference update).
My thought was to create child class that would only serve as tool to introduce DataAnnotations to atributes I know for sure I will be using (those that will not dissapear from DataContract class for sure). How would I accomplish such inheritance with code?
Example - I have this class created by VS in reference.cs file:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="LibraryRequest", Namespace="http://schemas.datacontract.org/2004/07/Agamemnon.Models")]
[System.SerializableAttribute()]
public partial class LibraryRequest : DocuLive.RepositoryServiceExt.Library {
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string PasswordField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string ServerField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private bool UseDefaultField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string UserNameField;
[System.Runtime.Serialization.DataMemberAttribute()]
public string Password {
get {
return this.PasswordField;
}
set {
if ((object.ReferenceEquals(this.PasswordField, value) != true)) {
this.PasswordField = value;
this.RaisePropertyChanged("Password");
}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public string Server {
get {
return this.ServerField;
}
set {
if ((object.ReferenceEquals(this.ServerField, value) != true)) {
this.ServerField = value;
this.RaisePropertyChanged("Server");
}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public bool UseDefault {
get {
return this.UseDefaultField;
}
set {
if ((this.UseDefaultField.Equals(value) != true)) {
this.UseDefaultField = value;
this.RaisePropertyChanged("UseDefault");
}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public string UserName {
get {
return this.UserNameField;
}
set {
if ((object.ReferenceEquals(this.UserNameField, value) != true)) {
this.UserNameField = value;
this.RaisePropertyChanged("UserName");
}
}
}
}
I want to make sure that no matter what changes in reference.cs file (even that class itself), I will always have Username, Password and Server marked as [Required] in my "Edit" and "Delete" forms.
Thanks in advance
Honza
I would stay away from inheriting an autogenerated class. It would not solve your problem with the attributes - you would have to override every single property so you can add attributes to it.
One solution is to use hand-coded datacontracts instead of autogenerated references. You will have full control over when they change, and you can put the attributes you need in them.
Another solution is wrapping the contract in your view model. Like this:
public class LibraryRequestViewModel {
private LibraryRequest request;
public LibraryRequestViewModel(LibraryRequest request){
this.request = request;
}
[Required]
public string Password {
get { return this.request.Password; }
set { this.request.Password = value; }
}
// do this for all fields you need
}
I've defined a custom DataAnnotation attribute similar to this one that goes on the class but ensures that at least one property is populated. It works correctly and adds an error message to the model's ValidationSummary. However, I want to be able to associate the error message with a particular property (or any string, really) so that I can display it in a particular place on my view.
Thus, if my custom attribute is used like this:
[RequireAtLeastOne(GroupId = 0, ErrorMessage = "You must specify at least one owner phone number.")]
public class UserViewModel: User {
...
}
then I want to be able to say something like:
[RequireAtLeastOne(GroupId = 0, ErrorMessage = "You must specify at least one owner phone number.", ValidationErrorKey = "my_key")]
public class UserViewModel: User {
...
}
...and use it in a view like this:
#Html.ValidationMessage("my_key")
It would also be fine if I had to associate the error message with a particular property on my model instead of an arbitrary string. How can I accomplish this?
Using ryudice's answer and this question as the starting point, I was able to solve this problem using IValidatableObject. For anyone interested, here is the full code I ended up with:
1. Define a custom validation attribute, RequireAtLeastOneAttribute
This attribute goes on the class to indicate that validation should check for property groups and ensure that at least one property from each group is populated. This attribute also defines the error message and an ErrorMessageKey, which will be used to keep track of the error messages and display them in the view instead of using the general-purpose ValidationSummary collection.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RequireAtLeastOneAttribute: ValidationAttribute {
/// <summary>
/// This identifier is used to group properties together.
/// Pick a number and assign it to each of the properties
/// among which you wish to require one.
/// </summary>
public int GroupId { get; set; }
/// <summary>
/// This defines the message key any errors will be associated
/// with, so that they can be accessed via the front end using
/// #Html.ValidationMessage(errorMessageKey).
/// </summary>
public string ErrorMessageKey { get; set; }
public override bool IsValid(object value) {
// Find all properties on the class having a "PropertyGroupAttribute"
// with GroupId matching the one on this attribute
var typeInfo = value.GetType();
var propInfo = typeInfo.GetProperties();
foreach (var prop in propInfo) {
foreach (PropertyGroupAttribute attr in prop.GetCustomAttributes(typeof(PropertyGroupAttribute), false)) {
if (attr.GroupId == this.GroupId
&& !string.IsNullOrWhiteSpace(prop.GetValue(value, null).GetString())) {
return true;
}
}
}
return false;
}
}
2. Define a custom attribute PropertyGroupAttribute
This will be used to define which property groups need to have at least one value filled in.
[AttributeUsage(AttributeTargets.Property)]
public class PropertyGroupAttribute : Attribute {
public PropertyGroupAttribute(int groupId) {
this.GroupId = groupId;
}
public int GroupId { get; set; }
}
3. Attach the attributes to the model and properties
Group properties together using the "GroupId" integer (which can be anything, as long as it's the same for all properties among which at least one must be filled in).
[RequireAtLeastOne(GroupId = 0, ErrorMessage = "You must specify at least one owner phone number.", ErrorMessageKey = "OwnerPhone")]
[RequireAtLeastOne(GroupId = 1, ErrorMessage = "You must specify at least one authorized producer phone number.", ErrorMessageKey = "AgentPhone")]
public class User: IValidatableObject {
#region Owner phone numbers
// At least one is required
[PropertyGroup(0)]
public string OwnerBusinessPhone { get; set; }
[PropertyGroup(0)]
public string OwnerHomePhone { get; set; }
[PropertyGroup(0)]
public string OwnerMobilePhone { get; set; }
#endregion
#region Agent phone numbers
// At least one is required
[PropertyGroup(1)]
public string AgentBusinessPhone { get; set; }
[PropertyGroup(1)]
public string AgentHomePhone { get; set; }
[PropertyGroup(1)]
public string AgentMobilePhone { get; set; }
#endregion
}
4. Implement IValidatableObject on the model
public class User: IValidatableObject {
...
#region IValidatableObject Members
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
var results = new List<ValidationResult>();
// This keeps track of whether each "RequireAtLeastOne" group has been satisfied
var groupStatus = new Dictionary<int, bool>();
// This stores the error messages for each group as defined
// by the RequireAtLeastOneAttributes on the model
var errorMessages = new Dictionary<int, ValidationResult>();
// Find all "RequireAtLeastOne" property validators
foreach (RequireAtLeastOneAttribute attr in Attribute.GetCustomAttributes(this.GetType(), typeof(RequireAtLeastOneAttribute), true)) {
groupStatus.Add(attr.GroupId, false);
errorMessages[attr.GroupId] = new ValidationResult(attr.ErrorMessage, new string[] { attr.ErrorMessageKey });
}
// For each property on this class, check to see whether
// it's got a PropertyGroup attribute, and if so, see if
// it's been populated, and if so, mark that group as "satisfied".
var propInfo = this.GetType().GetProperties();
bool status;
foreach (var prop in propInfo) {
foreach (PropertyGroupAttribute attr in prop.GetCustomAttributes(typeof(PropertyGroupAttribute), false)) {
if (groupStatus.TryGetValue(attr.GroupId, out status) && !status
&& !string.IsNullOrWhiteSpace(prop.GetValue(this, null).GetString())) {
groupStatus[attr.GroupId] = true;
}
}
}
// If any groups did not have at least one property
// populated, add their error messages to the
// validation result.
foreach (var kv in groupStatus) {
if (!kv.Value) {
results.Add(errorMessages[kv.Key]);
}
}
return results;
}
#endregion
}
5. Use the validation messages in the view
The validation messages will be saved as whatever ErrorMessageKey you specified in the RequireAtLeastOne attribute definition - in this example, OwnerPhone and AgentPhone.
#Html.ValidationMessage("OwnerPhone")
Caveats
The built-in validation also adds an error message to the ValidationSummary collection, but only for the first attribute defined on the model. So in this example, only the message for OwnerPhone would show up in ValidationSummary, since it was defined first on the model. I haven't looked for a way around this because in my case it didn't matter.
You can implement IValidatableObject on your model and do the custom logic there, it will let you add the message using whichever key you want.