Model inheritance possible when using strongly-typed view in MVC3?
Asked Answered
G

3

10

I have the following setup in my model:

namespace QuickTest.Models
{
    public class Person
    {
        [Required]
        [Display(Name = "Full name")]
        public string FullName { get; set; }

        [Display(Name = "Address Line 1")]
        public virtual string Address1 { get; set; }
    }
    public class Sender : Person
    {
        [Required]
        public override string Address1 { get; set; }
    }
    public class Receiver : Person
    {
    }
}

and in my view:

@model QuickTest.Models.Person
@{
    ViewBag.Title = "Edit";
}
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    <fieldset>
        <legend>Person</legend>
        <div class="editor-label">
            @Html.LabelFor(model => model.FullName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.FullName)
            @Html.ValidationMessageFor(model => model.FullName)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Address1)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Address1)
            @Html.ValidationMessageFor(model => model.Address1)
        </div>

        <div class="errors">
            @Html.ValidationSummary(true)
        </div>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

Client-side validation is enabled. However, if I send an object of type Sender to the View, client-side validation does not detect that the Address1 field is required. Is there any way of making the client validation work in this scenario?

PS: I discovered that client validation works if I use the following to display the Address1 field in the view:

<div class="editor-field">
    @Html.Editor("Address1", Model.Address1)
    @Html.ValidationMessageFor(model => model.Address1)
</div>
Grimona answered 29/11, 2011 at 13:54 Comment(3)
Sender is a person, but person is not a sender, your view is strongly typed to person, therefore, it's never going to detect anything that has to do with sender.Uncourtly
Actually if you add this to the view: Model.GetType().ToString() you'll see that the following is displayed: QuickTest.Models.Sender which means that the type is known when the view is rendered.Grimona
However, EditorFor() is going to treat is as the type you have strong typed it to, which is person.Uncourtly
S
9

You can customize the validators and the metadata to come from your concrete class, but the solution has several moving parts, including two custom metadata providers.

First, create a custom Attribute to decorate each property of the base class. This is necessary as a flag for our custom providers, to indicate when further analysis is needed. This is the attribute:

[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class BaseTypeAttribute : Attribute { }

Next, create a custom ModelMetadataProvider inheriting from DataAnnotationsModelMetadataProvider:

public class MyModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(
        IEnumerable<Attribute> attributes,
        Type containerType,
        Func<object> modelAccessor,
        Type modelType,
        string propertyName)
    {
        var attribute = attributes.FirstOrDefault(a => a.GetType().Equals(typeof(BaseTypeAttribute))) as BaseTypeAttribute;
        if (attribute != null && modelAccessor != null)
        {
            var target = modelAccessor.Target;
            var containerField = target.GetType().GetField("container");
            if (containerField == null)
            {
                var vdi = target.GetType().GetField("vdi").GetValue(target) as ViewDataInfo;
                var concreteType = vdi.Container.GetType();
                return base.CreateMetadata(attributes, concreteType, modelAccessor, modelType, propertyName);
            }
            else
            {
                var container = containerField.GetValue(target);
                var concreteType = container.GetType();
                var propertyField = target.GetType().GetField("property");
                if (propertyField == null)
                {
                    concreteType = base.GetMetadataForProperties(container, containerType)
                        .FirstOrDefault(p => p.PropertyName == "ConcreteType").Model as System.Type;
                    if (concreteType != null)
                        return base.GetMetadataForProperties(container, concreteType)
                            .FirstOrDefault(pr => pr.PropertyName == propertyName);
                }
                return base.CreateMetadata(attributes, concreteType, modelAccessor, modelType, propertyName);
            }
        }
        return base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
    }
}

Then, create a custom ModelValidatorProvider inheriting from DataAnnotationsModelValidatorProvider:

public class MyModelMetadataValidatorProvider : DataAnnotationsModelValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {
        List<ModelValidator> vals = base.GetValidators(metadata, context, attributes).ToList();

        var baseTypeAttribute = attributes.FirstOrDefault(a => a.GetType().Equals(typeof(BaseTypeAttribute))) 
            as BaseTypeAttribute;

        if (baseTypeAttribute != null)
        {
            // get our parent model
            var parentMetaData = ModelMetadataProviders.Current.GetMetadataForProperties(context.Controller.ViewData.Model,
                metadata.ContainerType);

            // get the concrete type
            var concreteType = parentMetaData.FirstOrDefault(p => p.PropertyName == "ConcreteType").Model;
            if (concreteType != null)
            {
                var concreteMetadata = ModelMetadataProviders.Current.GetMetadataForProperties(context.Controller.ViewData.Model,
                    Type.GetType(concreteType.ToString()));

                var concretePropertyMetadata = concreteMetadata.FirstOrDefault(p => p.PropertyName == metadata.PropertyName);

                vals = base.GetValidators(concretePropertyMetadata, context, attributes).ToList();
            }
        }
        return vals.AsEnumerable();
    }
}

After that, register both custom providers in Application_Start in Global.asax.cs:

ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MvcApplication8.Controllers.MyModelMetadataValidatorProvider());
ModelMetadataProviders.Current = new MvcApplication8.Controllers.MyModelMetadataProvider();

Now, change your models like so:

public class Person
{
    public Type ConcreteType { get; set; }

    [Required]
    [Display(Name = "Full name")]
    [BaseType]
    public string FullName { get; set; }

    [Display(Name = "Address Line 1")]
    [BaseType]
    public virtual string Address1 { get; set; }
}

public class Sender : Person
{
    public Sender()
    {
        this.ConcreteType = typeof(Sender);
    }

    [Required]
    [Display(Name = "Address Line One")]
    public override string Address1 { get; set; }
}

public class Receiver : Person
{
}

Note that the base class has a new property, ConcreteType. This will be used to indicate which inheriting class has instantiated this base class. Whenever an inheriting class has metadata which overrides the metadata in the base class, the inheriting class' constructor should set the base class ConcreteType property.

Now, even though your view uses the base class, the attributes specific to any concrete inheriting class will appear in your view, and will affect the validation of the model.

In addition, you should be able to turn the View into a template for the Person type, and use the template for any instance using the base class or inheriting from it.

Stepdaughter answered 30/11, 2011 at 3:34 Comment(2)
var propertyField = target.GetType().GetField("property"); What this line is for?Landward
And what is for target.GetType().GetField("vdi")Landward
A
2

Hmm, this is a tricky one since the HtmlHelper<T>.EditorFor method uses the generic parameter of the HtmlHelper<T> to figure out which validation attributes are required.

I would suggest writing your own EditorFor extension method that delegates calls to the non-generic HtmlHelper.Editor method.

Amritsar answered 29/11, 2011 at 13:59 Comment(0)
J
1

Have you considered creating your own EditorTemplate for Person, Sender and Receiver? The EditorFor and DisplayFor look for a custom template that match the type of the object.

The internal method will look for a template that matches the type of the object. It will then look for a template that matches the base class and then on up the chain of inheritance.

Jew answered 29/11, 2011 at 14:56 Comment(1)
Yes I did, but in my case the differences between Sender and Receiver are not significant, so I don't think there's a point in having the same view twice just for the sake of changing the @model directive at the top of it.Grimona

© 2022 - 2024 — McMap. All rights reserved.