【问题标题】:ASP.NET MVC 3 HtmlHelper Exception does not recognize ModelMetadata on inherited interfaceASP.NET MVC 3 HtmlHelper 异常无法识别继承接口上的 ModelMetadata
【发布时间】:2011-06-09 19:17:51
【问题描述】:

升级到 MVC 3 RTM 后,我遇到了以前工作的异常。

这里是场景。我有几个使用相同底层接口 IActivity 和 Iowned 的对象。

IActivity implements IOwned (another interface)

public interface IActivity:IOwned {...}

public interface IOwned 
{
    int? AuthorId {get;set;}
}

我有一个局部视图,它使用 IActivity 从其他具体局部重用。

这里是Activity Partial的定义。

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IActivity>" %>
<%: Html.HiddenFor(item => item.AuthorId) %>

但是,它会引发异常。在 ModelMetadata 中找不到 AuthorId。

我猜在以前的版本中它着眼于 IActivity 实现的接口。

除了在所有地方复制类似的界面之外,有什么想法、建议吗?

复制了下面的堆栈跟踪。

[ArgumentException: The property IActivity.AuthorId could not be found.]
   System.Web.Mvc.AssociatedMetadataProvider.GetMetadataForProperty(Func`1 modelAccessor, Type containerType, String propertyName) +498313
   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) +393
   System.Web.Mvc.Html.InputExtensions.HiddenFor(HtmlHelper`1 htmlHelper, Expression`1 expression, IDictionary`2 htmlAttributes) +57
   System.Web.Mvc.Html.InputExtensions.HiddenFor(HtmlHelper`1 htmlHelper, Expression`1 expression) +51
   ASP.views_shared_activity_ascx.__Render__control1(HtmlTextWriter __w, Control parameterContainer) in c:\Users\...\Documents\Visual Studio 2010\Projects\ngen\trunk\...\Views\Shared\Activity.ascx:3
   System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +109
   System.Web.UI.Control.RenderChildren(HtmlTextWriter writer) +8
   System.Web.UI.Control.Render(HtmlTextWriter writer) +10
   System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +27
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25
   System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +208
   System.Web.UI.Control.RenderChildren(HtmlTextWriter writer) +8
   System.Web.UI.Page.Render(HtmlTextWriter writer) +29
   System.Web.Mvc.ViewPage.Render(HtmlTextWriter writer) +43
   System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +27
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3060

【问题讨论】:

  • 您是否尝试过单步执行框架代码?
  • 不,我还没有逐步完成框架代码。我希望不要;)我认为现在我可以通过切换到 来解决它,但是这看起来像是 FX 恕我直言的一个重大变化。

标签: asp.net asp.net-mvc-3 html-helper modelmetadata


【解决方案1】:

来自 MVC 团队:

不幸的是,代码实际上是 利用已修复的错误,其中 表达式的容器 ModelMetadata 的用途是 无意中设置为声明 类型而不是包含类型。 这个错误必须修复,因为 虚拟财产的需求和 验证/模型元数据。

拥有基于接口的模型不是 我们鼓励的东西(也不,鉴于 错误修复施加的限制, 可以实际支持)。交换 抽象基类将修复 问题。

【讨论】:

  • 感谢您回答“为什么”。我可以看到这种变化对很多人都有影响。我想不出任何其他情况,其中传入 lambda 并由于接口继承而导致属性未找到异常。
  • 如果我们能证明这是一件大事并影响到许多人,我们可以提出一个案例来修复/更改它。
  • 接口的原因是 EF。除了立即升级到 EF 4.1(代码优先)Magic Unicorns ;)。 EF 使用EntityObject 占用基类。覆盖 EF 代码生成,即使它使用 T4 似乎也需要大量工作,接口似乎是一种实用的多态方法。解决方法很好,特别是如果它可以防止其他问题。
  • “我们不鼓励使用基于接口的模型”这句话给我留下了深刻的印象。我应该把 SOLID 中的 I 扔出窗外吗?
  • 我同意@DavidAlpert。我倾向于使用很多部分视图,它们使用接口作为视图模型。原因是它有助于重用视图,否则如果没有大量视图模型之间的映射,您将无法做到这一点
【解决方案2】:

System.Web.Mvc.ModelMetadata. FromLambdaExpression 方法中的 ASP.NET MVC 3 中有一个重大更改/错误,它解释了您遇到的异常:

ASP.NET MVC 2.0:

...
case ExpressionType.MemberAccess:
{
    MemberExpression body = (MemberExpression) expression.Body;
    propertyName = (body.Member is PropertyInfo) ? body.Member.Name : null;
    containerType = body.Member.DeclaringType;
    flag = true;
    break;
}
...

ASP.NET MVC 3.0

...
case ExpressionType.MemberAccess:
{
    MemberExpression body = (MemberExpression) expression.Body;
    propertyName = (body.Member is PropertyInfo) ? body.Member.Name : null;
    containerType = body.Expression.Type;
    flag = true;
    break;
}
...

注意containerType 变量是如何被赋予不同的值的。因此,在您的情况下,在 ASP.NET MVC 2.0 中,它被分配了 IOwned 的值,这是 AuthorId 属性的正确声明类型,而在 ASP.NET MVC 3.0 中,它被分配给 IActivity 和后来的框架试图找到它崩溃的属性。

这就是原因。就决议而言,我会等待微软的一些官方声明。我在发行说明文档中找不到任何相关信息。是错误还是某些功能需要在这里解决?

现在您可以使用非强类型的 Html.Hidden("AuthorId") 助手或指定 IOwned 作为控件的类型(我知道两者都很糟糕)。

【讨论】:

  • 我同意。是的,我对使用非 lambda 助手得出了相同的结论。并感谢您对源代码进行比较。再次感谢,非常有帮助。
  • @Dax70,我已经提交了一张票。你可以在这里关注它:connect.microsoft.com/VisualStudio/feedback/details/636341/…我希望我们能得到一些官方声明或解决方法。
  • 哇!这种改变没有帮助。它阻止我们升级到 MVC 3。希望它尽快得到修复!
【解决方案3】:

感谢 Burcephal,他的回答为我指明了正确的方向

您可以创建一个 MetaDataProvider 来解决此问题,此处的代码添加到基类中的代码中,检查模型的已实现接口上的属性,该模型本身就是一个接口。

public class MyMetadataProvider
    : EmptyModelMetadataProvider {

    public override ModelMetadata GetMetadataForProperty(
        Func<object> modelAccessor, Type containerType, string propertyName) {

        if (containerType == null) {
            throw new ArgumentNullException("containerType");
        }
        if (String.IsNullOrEmpty(propertyName)) {
            throw new ArgumentException(
                "The property &apos;{0}&apos; cannot be null or empty", "propertyName");
        }

        var property = GetTypeDescriptor(containerType)
            .GetProperties().Find(propertyName, true);
        if (property == null
            && containerType.IsInterface) {
            property = (from t in containerType.GetInterfaces()
                        let p = GetTypeDescriptor(t).GetProperties()
                            .Find(propertyName, true)
                        where p != null
                        select p
                        ).FirstOrDefault();
        }

        if (property == null) {
            throw new ArgumentException(
                String.Format(
                    CultureInfo.CurrentCulture,
                    "The property {0}.{1} could not be found",
                    containerType.FullName, propertyName));
        }

        return GetMetadataForProperty(modelAccessor, containerType, property);
    }
}

如上所述,将您的提供程序设置为 global.asax Application_Start

ModelMetadataProviders.Current = new MyMetaDataProvider();

【讨论】:

  • 这解决了我们应用程序中的问题。如果我们遇到任何不良的副作用,我一定会回到这里发表评论。非常感谢!
  • 我知道这是一个旧答案,但我们一开始发现它很有帮助。不幸的是,我们发现我们的验证警告丢失了使用 DisplayAttribute 提供的“友好”名称。因此,他们可能会看到“需要字段 MyPropertyName”,而不是看到“字段位置是必需的”。不确定解决方法是什么,所以我们将使用抽象类而不是接口。只是提醒未来的访客。
  • @DuncanMack 你能详细说明你的评论吗?我已经尝试修补 MetaDataProvider 以解决接口问题,它似乎与 DisplayAttribute 一起工作(尽管我没有抱怨!)。
  • @PhilCooper 我必须回去设置一个测试项目来重新审视这个。可能它已在后续更新中得到解决(特别是针对 MVC 或更广泛地针对运行时)。不知道我会解决它,但如果我这样做了,我会告诉你的。
  • @DuncanMack 是的,这是可能的。虽然答案和 cmets 很好,但我已经实现了这个解决方法,并且运行良好。
【解决方案4】:

如果您感兴趣,我已经在我的应用程序中实现了一些解决方法。 当我浏览 MVC 源代码时,我发现下面命名的 FromLambdaExpression 方法将调用 MetaDataProvider,它是一个可覆盖的单例。所以我们可以只实现那个类,如果第一个接口不起作用,它实际上会尝试继承的接口。 它也会爬上接口树。

public class MyMetaDataProvider : EmptyModelMetadataProvider
{
    public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
    {
        try
        {
            return base.GetMetadataForProperty(modelAccessor, containerType, propertyName);
        }
         catch(Exception ex)
        {
            //Try to go up to type tree
            var types = containerType.GetInterfaces();              
            foreach (var container in types)
            {
                if (container.GetProperty(propertyName) != null)
                {
                    try
                    {
                        return GetMetadataForProperty(modelAccessor, container, propertyName);
                    }
                    catch
                    {
                        //This interface did not work
                    }
                }
            }               
            //If nothing works, then throw the exception
            throw ex;
        }              
    }
}

然后,只需在 global.asax Application_Start() 中更改 MetaDataProvider 的实现

ModelMetadataProviders.Current = new MyMetaDataProvider();

这不是有史以来最好的代码,但它可以完成工作。

【讨论】:

    【解决方案5】:

    尝试使用类型转换。它适用于我的项目,尽管 resharper 强调它是多余的。

    对于您的代码,解决方案是

    <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IActivity>" %>
    <%: Html.HiddenFor(item => ((IOwned)item).AuthorId) %>
    

    【讨论】:

      【解决方案6】:

      进一步Anthony Johnston的回答,您可能会发现在使用 DataAnnotations 时会出现异常,因为 AssociatedValidatorProvider.GetValidatorsForProperty() 方法将尝试使用继承接口作为容器类型而不是基本类型,因此无法再次找到该属性。

      这是来自 GetValidatorsForProperty 方法的反映代码(它是导致 propertyDescriptor 变量为 null 并因此引发异常的第二行):

      private IEnumerable<ModelValidator> GetValidatorsForProperty(ModelMetadata metadata, ControllerContext context)
      {
          ICustomTypeDescriptor typeDescriptor = this.GetTypeDescriptor(metadata.ContainerType);
          PropertyDescriptor propertyDescriptor = typeDescriptor.GetProperties().Find(metadata.PropertyName, true);
          if (propertyDescriptor != null)
          {
              return this.GetValidators(metadata, context, propertyDescriptor.Attributes.OfType<Attribute>());
          }
          else
          {
              object[] fullName = new object[] { metadata.ContainerType.FullName, metadata.PropertyName };
              throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, MvcResources.Common_PropertyNotFound, fullName), "metadata");
          }
      }
      

      如果是这样,我相信以下代码可能会有所帮助,因为它确保将 ContainerType 设置为属性所在的类型,而不是视图模型的类型。

      免责声明:它似乎工作正常,但我还没有完全测试它,所以它可能会产生不良影响!我也明白它写得并不完美,但为了便于比较,我试图保持与之前的答案相似的格式。 :)

      public class MyMetadataProvider : DataAnnotationsModelMetadataProvider
      {
          public override ModelMetadata GetMetadataForProperty(
              Func<object> modelAccessor, Type containerType, string propertyName)
          {
      
              if (containerType == null)
              {
                  throw new ArgumentNullException("containerType");
              }
              if (String.IsNullOrEmpty(propertyName))
              {
                  throw new ArgumentException(
                      "The property &apos;{0}&apos; cannot be null or empty", "propertyName");
              }
      
              var containerTypeToUse = containerType;
      
              var property = GetTypeDescriptor(containerType)
                  .GetProperties().Find(propertyName, true);
              if (property == null
                  && containerType.IsInterface)
              {
      
                  var foundProperty = (from t in containerType.GetInterfaces()
                              let p = GetTypeDescriptor(t).GetProperties()
                                  .Find(propertyName, true)
                              where p != null
                              select (new Tuple<System.ComponentModel.PropertyDescriptor, Type>(p, t))
                              ).FirstOrDefault();
      
                  if (foundProperty != null)
                  {
                      property = foundProperty.Item1;
                      containerTypeToUse = foundProperty.Item2;
                  }
              }
      
      
              if (property == null)
              {
                  throw new ArgumentException(
                      String.Format(
                          CultureInfo.CurrentCulture,
                          "The property {0}.{1} could not be found",
                          containerType.FullName, propertyName));
              }
      
              return GetMetadataForProperty(modelAccessor, containerTypeToUse, property);
          }
      }
      

      【讨论】:

      • 因为我遇到了这个问题,并认为分享一个潜在的解决方案很有用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-07
      • 2011-12-05
      相关资源
      最近更新 更多