3

I have interface of a model declared, with class implementing it:

public interface IMyModel
{
    [Range(1, 1000)]
    [Display(Name = "ModelProp From Interface")]
    int MyIntProperty { get; set; }
    IMySubModel SubModel { get; }
}

public interface IMySubModel
{
    [Range(1,1000)]
    [Display(Name = "SubModelProp From Interface")]
    int MyIntSubProperty { get; set; }
}

I also have model implementation, with different Metadata:

public class MyModelImplementation:IMyModel
{
    [Display(Name = "ModelProp From Class")]
    [Range(1, 15)]
    public int MyIntProperty
    {
        get;
        set;
    }
    public IMySubModel SubModel { get; set; }
    public MyModelImplementation()
    {
        SubModel = new MySubModelImplementation();
    }
}

public class MySubModelImplementation: IMySubModel
{
    [Display(Name = "SubModelProp From Class")]
    [Range(1, 15)]
    public int MyIntSubProperty
    {
        get;
        set;
    }
}

I have View, where I use this model interface:

@model MvcApplicationModelInterface.Models.IMyModel
@using (Html.BeginForm())
{
    <p>Using Lambda Expression:</p>

    @Html.DisplayNameFor(m=>m.MyIntProperty)
    @Html.EditorFor(m=>m.MyIntProperty)
    @Html.ValidationMessageFor(m=>m.MyIntProperty)
    <br/>
    @Html.DisplayNameFor(m=>m.SubModel.MyIntSubProperty)
    @Html.EditorFor(m=>m.SubModel.MyIntSubProperty)
    @Html.ValidationMessageFor(m=>m.SubModel.MyIntSubProperty)
    <br/>

    <p>Using String Expression:</p>

    @Html.DisplayName("MyIntProperty")
    @Html.Editor("MyIntProperty")
    @Html.ValidationMessage("MyIntProperty")
    <br/>
    @Html.DisplayName("SubModel.MyIntSubProperty")
    @Html.Editor("SubModel.MyIntSubProperty")
    @Html.ValidationMessage("SubModel.MyIntSubProperty")
    <br/>
    @Html.ValidationSummary(true)
    <br/>
    <input type="submit" name="btnSubmit" value="btnSubmit" />
}

And I have controller that does proper binding and initialization of the model:

    [HttpGet]
    public ActionResult Index()
    {
        ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
        var model = new MyModelImplementation();
        model.MyIntProperty = 111;
        model.SubModel.MyIntSubProperty = 222;
        return View(model);
    }

    [ActionName("Index"), HttpPost]
    public ActionResult Save([ModelBinder(typeof(MyModelBinder))]IMyModel model)
    {
        return View(model);
    }

    public class MyModelBinder : DefaultModelBinder
    {
        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            return new MyModelImplementation();
        }
    }

The problem:

If in controller's action returns view with NOT NULL model (return View(new MyModelImplementation(){...});), results I see on the screen are:

enter image description here

However if controller's action returns results with null model (return View(null);), then results are:

enter image description here

As you can see, the behavior for Lambda html helpers and string expression helpers is different, and none of those is consistent at all and some behavior looks like a bug:

  • (expected) For Lambda helpers DisplayName is taken from IMyModel
  • (unexpected) For String expression all metadata is taken from MyModelImplementation class (Another words, if you use Html.DisplayNameFor(m=>m.MyIntProperty) - it shows metadata from Interface (model type Declared in view), however if you use Html.DisplayName("MyIntProperty") it uses metadata of model.GetType()).
  • (unexpected) ALL Validation rules and strings are ALWAYS taken from MyModelImplementation (model.GetType()) metadata, instead of declared model type (IMyModel).
  • (expected) For string expression helper Model's property is taken from Interface only in case model passed to a view is NULL
  • (unexpected) For string expression helper SubModel's property metadata is not retrieved/respected at all, in case model passed to view is NULL

Question:

What is the best workaround for this ASP.NET MVC Bug/Feature? How to force Html extensions always use metadata from model type Declared in a View by default? I tried to use MetadataTypeAttribute, but in this case the one who implements model will get freedom of overwritting original metadata specified in interface, which I don't want to allow. So I'm rather looking for some custom ModelMetadataProvider implementation. Also some metadata attributes are not respected in case of MetadataType, for example RequiredAttribute.

Philipp Munin
  • 5,610
  • 7
  • 37
  • 60

1 Answers1

0

I do not believe the difference between Editor() and EditorFor() is a bug. I think this is because EditorFor() has access to the type information of the strongly typed model, whereas Editor has to use reflection to get the type, which returns the actual type and not the interface type used.

I think what you are seeing is that the strongly typed versions have more context and can get at the interface information rather than simply having to call GetType().

Validation is working correctly because you told the model binder to bind to MyModelBinder which binds to MyModelImplementation. Validation is based on the method you are posting to and the arguments it takes.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • that's not true. Both of Editor and EditorFor use ViewData.Metadata, and ExpressionHelper, to get metadata, so they both have access to property ContainerType of Metadata object pointing to interface. But for some reason in stead of ContainerType it uses model.GetType() – Philipp Munin Apr 12 '13 at 22:53
  • @Philipp - No, both use `ViewData.ModelMetadata`, but the way it gets the metadata is different. They both get there via a TemplateHelper object, and TemplateHeler calls `ModelMetadata.FromLambdaExpression` for strongly typed and `ModelMetadata.FromStringExpression` for string types. The Lambda version looks up the container type based on the expression, while the string version doesn't have that info so must look up the container using `GetType()`. In any event, it's not a bug because the string versions simply don't have access to the expression data. – Erik Funkenbusch Apr 13 '13 at 20:48
  • This statement is also not accurate: as you see if you pass Model = null - for string expression it retrieves metadata from IModel.Property, however it does not do the same for ISubModel.SubProperty (Model.SubModel.SubProperty). So at least behavior is inconsistent. – Philipp Munin Apr 14 '13 at 01:13
  • @Philipp - You're passing nulls, of course you're going to see that behavior because, as I said, the `TemplateHelper` has to use `GetType()` for string expressions and it can't do that on a null object reference. My guess is that there are some null reference exceptions being thrown there, and some default behavior is occurring because of it – Erik Funkenbusch Apr 14 '13 at 04:10
  • Then why it takes correct metadata of IMyModel.MyProperty for string expression when I pass null? Different behavior for IMyModel.MyProperty and IMyModel.Submodel.SubProperty - looks like a bug for me. – Philipp Munin Apr 14 '13 at 21:13
  • @Philipp - As I said, it's most likely because an exception is being thrown somewhere and default behavior is being observed. I would imagine passing null is an undefined situation, and as is the case in most of these things, undefined can do anything. – Erik Funkenbusch Apr 14 '13 at 23:49
  • I understand, and I called it bug behavior, regardless what's the low-level cause :) Anyways thanks for your opinion. I'm still looking for some way to force Views to use metadata from Interfaces, instead of actual models' object type, on GLOBAL LEVEL (like MetadataProviders.Current) without giving model class implementer to control metadata. – Philipp Munin Apr 15 '13 at 17:52