Here's a solution I've come up with after considering the other possible solutions posed. It's based on finding the following article...
http://jendaperl.blogspot.co.uk/2010/10/attributeproviderattribute-rendered.html
My reasons for this over others are stated at the bottom of this post.
Having an existing Data Model class of...
public class UserAccount
{
[Display(Name = "Username", Prompt = "Login As"), Required()]
string UserName { get; set; }
[Display(Name = "Password", Prompt = "Password"), Required(), DataType(DataType.Password), StringLength(255, MinimumLength = 7, ErrorMessage = "The password must be at least 7 characters")]
string Password { get; set; }
}
wouldn't it be great if we could just copy the property attributes into a view that use it, maybe override them if we need to so that the views that use this class don't need to be revisted on a simple attribute change. Here's my solutions. Create a new attribute as below...
using System.ComponentModel;
namespace MyApp.ViewModels
{
public class AttributesFromAttribute : AttributeProviderAttribute
{
public AttributesFromAttribute(Type type, string property)
: base(type.AssemblyQualifiedName, property)
{
}
public T GetInheritedAttributeOfType<T>() where T : System.Attribute
{
Dictionary<string,object> attrs = Type.GetType(this.TypeName).GetProperty(this.PropertyName).GetCustomAttributes(true).ToDictionary(a => a.GetType().Name, a => a);
return attrs.Values.OfType<T>().FirstOrDefault();
}
}
}
now you can just add the following to the relevant property in the view model class...
[AttributesFrom(typeof(MyApp.DataModel.UserAccount), "UserName")]
e.g...
public class RegisterViewModel
{
public UserAccount UserAccount { get; set; }
public RegisterViewModel()
{
UserAccount = new UserAccount();
}
[AttributesFrom(typeof(MyApp.DataModel.UserAccount), "UserName")]
string UserName { get; set; }
[AttributesFrom(typeof(MyApp.DataModel.UserAccount), "Password")]
string Password { get; set; }
[AttributesFrom(typeof(MyApp.DataModel.UserAccount), "Password")]
[Display(Name = "Confirm Password", Prompt = "Confirm Password"), Compare("Password", ErrorMessage = "Your confirmation doesn't match.")]
public string PasswordConfirmation { get; set; }
}
This then gives copying of attributes that can be overridden (as with PasswordConfirmation above) allowing for multiple data models in the same viewmodel and if you need to access the inherited attributes from code, you can do so using the GetInheritedAttributeOfType method. For example...
public static class AttrHelper
{
public static T GetAttributeOfType<T>(this ViewDataDictionary viewData) where T : System.Attribute
{
var metadata = viewData.ModelMetadata;
var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
var attrs = prop.GetCustomAttributes(false);
// Try and get the attribute directly from the property.
T ret = attrs.OfType<T>().FirstOrDefault();
// If there isn't one, look at inherited attribute info if there is any.
if(ret == default(T))
{
AttributesFromAttribute inheritedAttributes = attrs.OfType<AttributesFromAttribute>().FirstOrDefault();
if (inheritedAttributes != null)
{
ret = inheritedAttributes.GetInheritedAttributeOfType<T>();
}
}
// return what we've found.
return ret;
}
}
This can be called from an Editor Template for example...
var dataTypeAttr = AttrHelper.GetAttributeOfType<DataTypeAttribute>(ViewData);
which will first look at the viewmodel's property attributes directly but if nothing's found it will look at the inherited attributes with it's call to GetInheritedAttributeOfType.
This works best for me because...
I feel that current practice of repeating DataAnnotations in viewmodels as well as the datamodels isn't great for maintainability or reuse.
Using MetadataType is also inflexible, it's all or nothing and you can't include multiple MetadataType attributes on a single ViewModel.
Encapsulating the datamodels in the viewmodels without Properties is lacking as it also doesn't have the flexability. You have to include the entire encapsulated object so cannot populate the DataModel over multiple views.