Model inheritance possible when using strongly-typ

2019-01-18 12:35发布

问题:

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>

回答1:

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.



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



回答3:

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.