Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
366 views
in Technique[技术] by (71.8m points)

asp.net mvc 3 - Model inheritance possible when using strongly-typed view in MVC3?

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>
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...