5

I am applying validation using DataAnnotations to an MVC ViewModel which is a composite of several entity framework objects and some custom logic. The validation is already defined for the entity objects in interfaces, but how can I apply this validation to the ViewModel?

My initial idea was to combine the interfaces into one and apply the combined interface to the ViewModel, but this didn't work. Here's some sample code demonstrating what I mean:

// interfaces containing DataAnnotations implemented by entity framework classes
public interface IPerson
{
    [Required]
    [Display(Name = "First Name")]
    string FirstName { get; set; }

    [Required]
    [Display(Name = "Last Name")]
    string LastName { get; set; }

    [Required]
    int Age { get; set; }
}
public interface IAddress
{
    [Required]
    [Display(Name = "Street")]
    string Street1 { get; set; }

    [Display(Name = "")]
    string Street2 { get; set; }

    [Required]
    string City { get; set; }

    [Required]
    string State { get; set; }

    [Required]
    string Country { get; set; }
}

// partial entity framework classes to specify interfaces
public partial class Person : IPerson {}
public partial class Address : IAddress {}

// combined interface
public interface IPersonViewModel : IPerson, IAddress {}

// ViewModel flattening a Person with Address for use in View
[MetadataType(typeof(IPersonViewModel))] // <--- This does not work. 
public class PersonViewModel : IPersonViewModel
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public int Age { get; set; }

    public string Street1 { get; set; }

    public string Street2 { get; set; }

    public string City { get; set; }

    public string State { get; set; }

    public string Country { get; set; }
}

My real-world problem involves about 150 properties on the ViewModel, so it's not as trivial as the sample and retyping all the properties seems like a horrible violation of DRY.

Any ideas on how to accomplish this?

dhochee
  • 412
  • 4
  • 11
  • please tell me you found an elegant solution to this - i'm scratching my head with this one too – benpage Aug 07 '13 at 04:29

3 Answers3

8

In order for this to work you will need to manually associate the interfaces as metadata for your concrete classes.

I expected to be able to add multiple MetadataType attributes but that is not permitted.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] // Notice AllowMultiple
public sealed class MetadataTypeAttribute : Attribute

Therefore, this gives a compilation error:

[MetadataType(typeof(IPerson))] 
[MetadataType(typeof(IAddress))] // <--- Duplicate 'MetadataType' attribute 
public class PersonViewModel : IPersonViewModel

However, it works if you only have one interface. So my solution to this was to simply associate the interfaces using a AssociatedMetadataTypeTypeDescriptionProvider and wrap that in another attribute.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class MetadataTypeBuddyAttribute : Attribute
{
    public MetadataTypeBuddyAttribute(Type modelType, Type buddyType)
    {
        TypeDescriptor.AddProviderTransparent(
           new AssociatedMetadataTypeTypeDescriptionProvider(
               modelType,
               buddyType
           ),
           modelType);
    }
}

In my situation (MVC4) the data annotation attributes on my interfaces already worked. This is because my models directly implement the interfaces instead of having multi-level inheritance. However custom validation attributes implemented at the interface level do not work.

Only when manually associating the interfaces all the custom validations work accordingly. If I understand your case correctly this is also a solution for your problem.

[MetadataTypeBuddy(typeof(PersonViewModel), typeof(IPerson))] 
[MetadataTypeBuddy(typeof(PersonViewModel), typeof(IAddress))]
public class PersonViewModel : IPersonViewModel
Ericvf
  • 191
  • 1
  • 6
  • Cool idea. Never considered replacing the MetadataType attribute. I know it's been a long time but this may still prove useful as we still have redundant metadata models that could benefit from this. – dhochee Apr 13 '16 at 19:34
1

based on answer here, I couldn't somehow make that MetadataTypeBuddy attribute works. I'm sure that we must set somewhere that MVC should be calling that attribute. I managed to get it work when I run that attribute manually in Application_Start() like this

new MetadataTypeBuddyAttribute(typeof(PersonViewModel), typeof(IPerson));
new MetadataTypeBuddyAttribute(typeof(PersonViewModel), typeof(IAddress));
Ariwibawa
  • 627
  • 11
  • 23
1

The MetadataTypeBuddy attribute did not work for me.
BUT adding "new" MetadataTypeBuddyAttribute in the "Startup" did work BUT it can lead to complex code where the developer is not aware to add this in the "Startup" for any new classes.

NOTE: You only need to call the AddProviderTransparent once at the startup of the app per class.

Here is a thread safe way of adding multiple Metadata types for a class.

    [AttributeUsage(AttributeTargets.Class)]
    public class MetadataTypeMultiAttribute : Attribute
    {
        private static bool _added = false;
        private static readonly object padlock = new object();

        public MetadataTypeMultiAttribute(Type modelType, params Type[] metaDataTypes)
        {
            lock (padlock)
            {
                if (_added == false)
                {
                    foreach (Type metaDataType in metaDataTypes)
                    {
                        System.ComponentModel.TypeDescriptor.AddProviderTransparent(
                            new AssociatedMetadataTypeTypeDescriptionProvider(
                                modelType,
                                metaDataType
                            ),
                            modelType);
                    }

                    _added = true;
                }
            }
        }
    }
goroth
  • 2,510
  • 5
  • 35
  • 66