Question

I've looked for a while for a definitive solution to this but have yet to come to a conclusion. I would like to specify data annotations just once on the data model class and have these be seen by the UI from a view model class without specifying them again. To illustrate my point suppose I have a UserAccount Class as such...

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; }
}

now I would like to specify a View Model which contains a password confirmation field that wouldn't be stored in the database and potentially other data models but I don't want to have to specify all the data annotation attributes again. This is about trying to find best practice, but also multiple declarations need maintaining and at some point one will get modified and the others won't.

I've looked at an Interfaces/Inheritance solution but that doesn't really cut it fully for various reasons. One other potential solution might be to have an Attribute on the View Model class (or properties) to say inherit attributes from... but I can't find anything suitable as yet for this.

Has anyone got any bright ideas or implemented this in a suitable fashion?

Was it helpful?

Solution 4

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...

  1. I feel that current practice of repeating DataAnnotations in viewmodels as well as the datamodels isn't great for maintainability or reuse.

  2. Using MetadataType is also inflexible, it's all or nothing and you can't include multiple MetadataType attributes on a single ViewModel.

  3. 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.

OTHER TIPS

You can decorate your viewmodel class with this atttribute:

[MetadataType(typeof(YourModelClass))]
public class YourViewModelClass
{
   // ...
}

Assuming that both classes have the same properties, the annotations will be correctly inhertied.

See: MetadataTypeAttribute Class in MSDN for more info.

Note: in MDSN remarks they explain the use for adding extra metadata for an existing model class, but this will work for your case. In fact, you can do it all the way round: annotate your ViewModel, and apply it to a partial class of your model. Or even create a single buddy class which is applied to both the entity in the model and the viewmodel. With any of this options, any class can "define" its own annotations and "inherit" others from the buddy class.

IMPORTANT NOTE: this solution is very different from the accepted one by Hoots in the sense that this solution is recognized by frameworks like EF or MVC, to create the model or to provide automatic data validation. The solution by Hoots must be used by hand, because the framework doesn't automatically use it.

I've looked at an Interfaces/Inheritance solution but that doesn't really cut it fully for various reasons.

Can you enlighten us on why the inheritance does not work for you?

The following is an example of the inheritance solution:

public class UserAccount : PasswordModel
{
    [Display(Name = "Username", Prompt = "Login As"), Required()]
    public string UserName { get; set; }

    [Display(Name = "Password", Prompt = "Password")]
    [StringLength(255, MinimumLength = 7, ErrorMessage = "The password must be at least 7 characters")]
    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }
}

public class ResetPasswordViewModel : PasswordModel
{
    [Display(Name = "Retype Password", Prompt = "Password")]
    [Required]
    [DataType(DataType.Password)]
    [CompareAttribute("Password", ErrorMessage = "Password does not match")]
    string RetypePassword { get; set; }
}

UPDATE:

From M-V-C, you can extend the design pattern to M-VM-V-C

The ViewModel

VM stands for ViewModels. These guys are like Models, but they don't represent the domain and they are not persisted. They represent the UI.

More about ViewModels

Based on the example earlier, the ResetPasswordViewModel is a ViewModel that represents the UI. If you are worried about persisting the RetypePassword, then don't add the ViewModel in EF.

Another approach

You can replace inheritance with composition. See the example below.

public class ResetPasswordViewModel
{
    public UserAccount UserAccount { get; set; }

    [Display(Name = "Retype Password", Prompt = "Password")]
    [Required]
    [DataType(DataType.Password)]
    public string RetypePassword { get; set; }
}

With the ViewModel above, you get the reusability of the Password property. You would access it like:

@Model.UserAccount.Password
@Model.ResetPassword

I may want to combine different data models within the same view

This is exactly what ViewModels are for. You create them specifically for the View.

You're asking for mutually exclusive things... You want to inherit your metadata, but you don't want to inherit your metadata in arbitrary ways... and that is just not typically a useful scenario. You're going to spend far more time trying to find some magic bullet scenario than you will ever spend maintaining separate models, and you will likely still end up with a sub-par solution that you aren't 100% happy with.

I know your frustration.. you shouldn't have to maintain the same data in multiple places.. you're violating the DRY principle.. Don't repeat yourself...

The problem is, any solution you create will be violating other principles, such as the open closed principle, or single responsibility principle. Often times these principles are at odds with each other, and you have to find a balance somewhere.

Simply put, the best practice is to maintain separate view and domain models, with separate data attributes, because your domain and your view are separate concerns and they should be kept separate. If they did not have separate concerns, you could simply use the same model for both, and there would be no need to jump through these hoops.

View Models are separate precisely because your views usually have different requirements than your domain. Trying to mix the two is going to lead to nothing but pain.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top