12

UPDATE: Problem Solved.

Darin Dimitrov's answer contains a link to another, related question. One of the proposed solutions on that page ended up working for us.


Original Question

The code sample below works in MVC 2 but throws an exception in MVC 3. Does anyone know why MVC 3 introduced this breaking change? Is there a way to get this working in MVC 3 while still allowing me to describe the viewmodel as an Interface from within the view?

There is another related question on StackOverflow, but it doesn't give me any info as to why there is a difference between MVC 2 and MVC 3.

Model

public interface IPerson {
    string Name { get; set; }
}

public interface ISpy : IPerson {
    string CodeName { get; set; }
}

public class Spy : ISpy {
    public string Name { get; set; }
    public string CodeName { get; set; }
}

Controller Action

public ActionResult Index() {
    var model = new Spy { Name = "James Bond", CodeName = "007" };
    return View(model);
}

MVC2 View (Works perfectly)

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcApp.Models.ISpy>" %>
<p>CodeName: <%: Html.TextBoxFor(x => x.CodeName) %></p>
<p>Name: <%: Html.TextBoxFor(x => x.Name) %></p>

MVC3 View (Throws exception)

@model MvcApp.Models.ISpy
<p>Name: @Html.TextBoxFor(x => x.Name)</p>
<p>CodeName: @Html.TextBoxFor(x => x.CodeName)</p>

MVC3 Exception Information

I am showing the relevant exception info below, but you can view the entire output of the error page here: https://gist.github.com/1443750.

Runtime Exception
System.ArgumentException: The property **`MvcApp.Models.ISpy.Name`** could not be found.

Source Error:

Line 1:  @model MvcApp.Models.ISpy
Line 2:  <p>CodeName: @Html.TextBoxFor(x => x.CodeName)</p>
Line 3:  <p>Name: @Html.TextBoxFor(x => x.Name)</p>


Stack Trace:

[ArgumentException: The property MvcApp.Models.ISpy.Name could not be found.]
   System.Web.Mvc.AssociatedMetadataProvider.GetMetadataForProperty(Func`1 modelAccessor, Type containerType, String propertyName) +502169
   System.Web.Mvc.ModelMetadata.GetMetadataFromProvider(Func`1 modelAccessor, Type modelType, String propertyName, Type containerType) +101
   System.Web.Mvc.ModelMetadata.FromLambdaExpression(Expression`1 expression, ViewDataDictionary`1 viewData) +421
   System.Web.Mvc.Html.InputExtensions.TextBoxFor(HtmlHelper`1 htmlHelper, Expression`1 expression, IDictionary`2 htmlAttributes) +58
   System.Web.Mvc.Html.InputExtensions.TextBoxFor(HtmlHelper`1 htmlHelper, Expression`1 expression) +50
   ASP._Page_Views_Home_Index_cshtml.Execute() in c:\Projects\MvcApplication1\MvcApplication1\Views\Home\Index.cshtml:3

Help us Darin Dimitrov, you're our only hope.

Community
  • 1
  • 1
jessegavin
  • 74,067
  • 28
  • 136
  • 164

3 Answers3

11

I have already brought this issue to the attention of Microsoft. Unfortunately the response I got was: sorry that's by design. Personally the conclusion I draw from such an answer is that the MVC team is designing a buggy product if that's by design. And they are not to be blamed for taking such a design decision, they are for blaming for not mentioning it anywhere in the breaking changes section of the release notes document. Had they mentioned it, then it would have been our fault when we took the decision for migrating even if we deliberately knew about the breaking change.

Anyway, here's a related question.

And here's an answer from the MVC team:

Unfortunately, the code was actually exploiting a bug that was fixed, where the container of an expression for ModelMetadata purposes was inadvertently set to the declaring type instead of the containing type. This bug had to be fixed because of the needs of virtual properties and validation/model metadata.

Having interface-based models is not something we encourage (nor, given the limitations imposed by the bug fix, can realistically support). Switching to abstract base classes would fix the issue.

Community
  • 1
  • 1
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Thanks for your answer Darin. Unfortunately this is the one roadblock preventing our team from upgrading to MVC3. :( – jessegavin Dec 11 '11 at 21:58
  • UPDATE: One of the answers in the related question you linked to solved the problem for us. That answer is located here: http://stackoverflow.com/a/7826906/5651 – jessegavin Dec 12 '11 at 16:55
  • I fail to understand why using `IPerson` as the `@model` is ok and `ISpy` not. In this example the problem is present when an interface is inheriting from an other and all all descriptions on why is this happening are about properties on interfaces not having the same attributes as implementations. – vinczemarton Oct 06 '15 at 09:30
1

I found a pathetic workaround which allows you to use this model structure:

@model MvcApp.Models.IPerson
<p>Name: @Html.TextBoxFor(x => x.Name)</p>
@Html.Partial('_SpyBox')

And in _SpyBox.cshtml

@model MvcApp.Models.ISpy
<p>CodeName: @Html.TextBoxFor(x => x.CodeName)</p>

Meh.

vinczemarton
  • 7,756
  • 6
  • 54
  • 86
0

Change this line:

@model MvcApp.Models.ISpy

To

@model MvcApp.Models.Spy
Ryand.Johnson
  • 1,906
  • 2
  • 16
  • 22
  • 1
    I need the view to be reusable by any viewmodel which implements ISpy. While your suggestion "works", it doesn't actually solve the problem or answer my question. – jessegavin Dec 09 '11 at 19:15
  • 1
    So, in that case you would be passing your viewmodel and not ISpy or Spy. It would be @model MvcApp.Models.SpyViewModel or whatever you named it. Then in the view it would be x.spy.Name where spy is a property of the viewmodel. – Ryand.Johnson Dec 09 '11 at 19:30
  • and you never stated the question? I solved your exception. You make no mention of using a viewmodel in your post. – Ryand.Johnson Dec 09 '11 at 19:31
  • You're right. I hadn't really asked a question. I have updated. – jessegavin Dec 09 '11 at 19:55
  • You're interface does not contain a property called Name. It contains a property called CodeName. If you take the line in the view that references Name out and just have a reference to codename does it work? – Ryand.Johnson Dec 09 '11 at 20:04