ASP.NET MVC 3 HtmlHelper异常无法识别继承接口上的ModelMetadata

时间:2011-01-15 22:24:22

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

升级到MVC 3 RTM后,我得到一个以前工作的例外。

以下是该方案。我有几个使用相同底层接口IActivity和IOwned的对象。

IActivity implements IOwned (another interface)

public interface IActivity:IOwned {...}

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

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

以下是活动部分的定义。

<%@ 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

6 个答案:

答案 0 :(得分:14)

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中,它被赋值给{{1}以及当框架试图找到它崩溃的属性时。

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

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

答案 1 :(得分:14)

来自MVC团队:

  

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

     

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

答案 2 :(得分:7)

感谢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();

答案 3 :(得分:6)

如果你有兴趣,我已经在我的应用程序中实现了一些工作。 当我浏览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();

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

答案 4 :(得分:4)

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

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

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

答案 5 :(得分:0)

通过 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);
    }
}