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

发布于 2024-10-11 21:58:25 字数 2719 浏览 4 评论 0原文

升级到 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

After upgrading to MVC 3 RTM I get an exception where it previously worked.

Here is the scenario. I have several objects that use the same underlying interfaces IActivity and IOwned.

IActivity implements IOwned (another interface)

public interface IActivity:IOwned {...}

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

I have a partial view that uses IActivity for reuse from other concrete partials.

Here is the definition of the Activity Partial.

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

However, it throws an exception. It can not find AuthorId in the ModelMetadata.

I am guessing that in the previous version it looked at the interfaces IActivity implemented.

Any ideas, suggestions, short of duplicating similar interfaces everywhere?

Copied the stack trace below.

[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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

初与友歌 2024-10-18 21:58:25

来自 MVC 团队:

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

拥有基于接口的模型并不是
我们鼓励的事情(也不考虑到
错误修复带来的限制,
可以实际支持)。交换
抽象基类将修复
问题。

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.

财迷小姐 2024-10-18 21:58:25

ASP.NET MVC 3 的 System.Web.Mvc.ModelMetadata 中出现了重大更改/错误。 FromLambdaExpression 方法解释了您收到的异常:

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 作为控件的类型(我知道两者都很糟糕)。

There has been a breaking change/bug in ASP.NET MVC 3 in the System.Web.Mvc.ModelMetadata. FromLambdaExpression method which explains the exception you are getting:

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;
}
...

Notice how the containerType variable is assigned a different value. So in your case in ASP.NET MVC 2.0 it was assigned the value of IOwned which is the correct declaring type of the AuthorId property whereas in ASP.NET MVC 3.0 it is assigned to IActivity and later when the framework tries to find the property it crashes.

That's the cause. As far as the resolution is concerned I would wait for some official statement from Microsoft. I can't find any relevant information about this in the Release Notes document. Is it a bug or some feature which needs to be workarounded here?

For now you could either use the non-strongly typed Html.Hidden("AuthorId") helper or specify IOwned as type for your control (I know both suck).

喜爱纠缠 2024-10-18 21:58:25

感谢 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 '{0}' 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();

With thanks to Burcephal, whos answer pointed me in the right direction

You can create a MetaDataProvider which will get around this issue, the code here adds to the code in the base class checking for the property on implemented interfaces of a model which is itself an interface.

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 '{0}' 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);
    }
}

and as above, set your provider the global.asax Application_Start

ModelMetadataProviders.Current = new MyMetaDataProvider();
风启觞 2024-10-18 21:58:25

如果您有兴趣,我已经在我的应用程序中为此实施了一些解决方案。
当我查看 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();

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

f you are interrested, I have implemented a little work around for it in my app.
As I was going through the MVC source code, I found that the FromLambdaExpression method named below will be calling the MetaDataProvider which is an overridable singleton. So we could just implement that class that will actually try the inhereted intefaces if the first one does not work.
It will also go up the tree of intefaces.

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;
        }              
    }
}

and then, just cange the implementation of the MetaDataProvider in the global.asax Application_Start()

ModelMetadataProviders.Current = new MyMetaDataProvider();

It is not the best code ever, but it does the job.

庆幸我还是我 2024-10-18 21:58:25

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

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

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

Try using typecast. It works for my project although resharper highlights it as redundant.

For your code the solution would be

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IActivity>" %>
<%: Html.HiddenFor(item => ((IOwned)item).AuthorId) %>
蹲在坟头点根烟 2024-10-18 21:58:25

进一步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 '{0}' 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);
    }
}

Furthering Anthony Johnston's answer, you might find that you get exceptions when using DataAnnotations, as the AssociatedValidatorProvider.GetValidatorsForProperty() method will try to use the inheriting interface as the container type rather than the base one and therefore fail to find the property again.

This is the reflected code from the GetValidatorsForProperty method (it is the second line which causes the propertyDescriptor variable to be null and thus the exception to be thrown) :

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");
    }
}

If so, I believe that the following code may help, as it makes sure that the ContainerType is set to the type which the property is on, not the type of the view model.

Disclaimer: It seems to work fine, but I have not fully tested it yet so it might have undesired effects! I also understand it is not written perfectly, but I was trying to keep the format similar to the previous answer for ease of comparison. :)

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 '{0}' 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);
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文