4

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?

Hoots
  • 1,876
  • 14
  • 23

4 Answers4

4

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.

JotaBe
  • 38,030
  • 8
  • 98
  • 117
  • I think this is what I'm looking for, but need to check. It sounds similar to a solution I'm working on based on this blog... – Hoots Apr 30 '14 at 14:16
  • Had a quick look. It looks promising, but what would happen for example if the scenario was different, say a customer data object and an address data object that I wanted to combine onto a single view model. From what I've read you can only apply this attribute once and in this scenario I would want annotations against the customer and the address but I couldn't specify [MetadataType(typeof(Customer))] and [MetadataType(typeof(Address))] on the same view model. Have I understood it correctly? – Hoots Apr 30 '14 at 14:31
  • You can solve it by creating 3 classes fro the viewmodel: one for Customer, one for Address, and finally your ViewModel class which has a property of type Customer, and other property of type Address. In your view you can refer the "subproperties" and MVC handles them perfectly. MVC HtmlHelpers support this perfectly `Html.xxxFor(m => m.Address.City, ...)` of `Html.TextBox("Address.City", ...)`. Besides your mappings are easier to do: you can map a whole entity to each property in your model. – JotaBe Apr 30 '14 at 15:59
  • Yes, that's the conventional way of doing things. I'm being a bit nit picky here but using a model view object without direct properties does not have the validation up from. If you check ModelState.IsValid it will be true even if m.Address.City is 'Required' and hasn't been set, because it's a level removed from the Model View. I think I've come up with a solution which I will post in a second. I'll leave it for a few days to set one as the answer dependant on comments. – Hoots May 01 '14 at 08:53
  • You made me doubt it, but you're completely wrong. I've been using models like this in production apps and have never had a problem with properties being one or several levels away from the root View Model. As you made me doubt I did a small proof of concept and I can assure that using the class `Address` (with no attributes), `AddressMetadata` (with Required and other atributes), `[MetadataType(typeof(AddressMetadata))]` decorating `Address`, and a `PersonModel` class with a property of type `Address`, and `HtmlXxxFor(m => m.Address.yyy)` editor, display, validation and ModelState works fine – JotaBe May 03 '14 at 00:51
  • I've asked a question which hopefully will point to what I've been missing, maybe you could respond on it. I suspect my bind may be a problem... – Hoots May 07 '14 at 16:32
  • 1
    MetadataType is useful for abstracting out the Data Annotations but not really for reuse. As far as I can see it's all or nothing. What about a scenario where you want to fill in your DataModel properties on two different Screens i.e. wizard like with two different views. This question highlights some short commings http://stackoverflow.com/questions/19838772/how-can-i-ignore-missing-fields-from-the-metadatatype-attribute In the example on my other question adding MetadataType wouldn't add anything, you still need to fill in all Required Properties it wouldn't matter where the annotations are. – Hoots May 09 '14 at 14:24
2

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.

Community
  • 1
  • 1
Yorro
  • 11,445
  • 4
  • 37
  • 47
  • Firstly, I may want to combine different data models within the same view. Secondly, although not impossible, there are complications around persisting just the required data i.e. I wouldn't want to persist the retyped password confirmation. – Hoots Apr 30 '14 at 14:15
  • Hi Yorro, Yes it's the view model I'm talking about. Thanks for the update. See comments in JotaBe answer where talking about view model. Basically Inheritence won't work for two independant objects e.g. Employee and Department in the same view model. Encapsulation works to a point but ModelState.IsValid reports true based on the current object, it doesn't check encapsulated objects. I've put together a solution that I think does what I want. (See my current solution, comments welcome) – Hoots May 01 '14 at 09:38
1

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.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • I appreciate the comments and sentiment but I don't fully agree. Whilst it's OK to have repeated data or repeated code in some circumstances, what you're suggesting is akin to say having 3 objects a Person, Pupil and Teacher and coding them all seperately rather than having the Pupil and Teacher inherit from Person. View Models contain their own set of business rules in addition to the rules associated with the data it uses but that doesn't mean they shouldn't reuse code and data where available. – Hoots May 01 '14 at 09:06
  • @Hoots - I agree that certain kinds of metadata would be nice to inherit. Field length, for instance is a good example. There are very few use cases where you might want your UI to have a larger field length than your data model (but I can think of a few). But others, like Required are more fuzzy. Your data model might not require a field, but you might conditional situations where you do require it. Metadata is, unfortunately, an all or nothing proposition when you inherit it. Unfortunately, Attributes are a course grained solution to these problems. – Erik Funkenbusch May 01 '14 at 14:02
  • Required on a data model property is like using a not null in the database. If it's nullable you can still require the front end to require it. If it's not null, you must have the front end require it otherwise the insert operation would fail. The same goes for data model required to view model. Have you seen the solution I came up with along the attributes inheritance line? It doesn't require you to have the same annotations in your view model, but the option to inherit them and override is there if you want it. – Hoots May 01 '14 at 19:57
  • @Hoots - No, there are reasons why you might want your view to not have the field be required be your data model would make it required. For instance, you might leave it empty and have the value calculated when left empty, or a default value inserted if left empty (think current date/time). That's the point, view models have different requirements from data models, except in very simple cases. – Erik Funkenbusch May 01 '14 at 20:34
  • I think we agree and we're just saying the same thing just in different ways. – Hoots May 01 '14 at 21:34
  • Erik, Just wondering if you noticed the solution I posed for this which is more about copying attributes than inheriting them but I think does a decent job of allowing Annotation reuse. Any comments appreciated. – Hoots May 09 '14 at 14:34
1

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.

Hoots
  • 1,876
  • 14
  • 23
  • There is an important difference between this solution and the `MetaDataAttribute` one: the metadata attribute is recognized by the framework, for example to create automatic validation on an MVC view, while the other solution isn't recognized by the framework and must be used by hand, using the `GetAttributeOfType`. It depends on what you want to do to choose one or the other solution, but they work in a very different way. – JotaBe Apr 28 '15 at 07:53