ASP.NET MVC 3 HtmlHelper 异常无法识别继承接口上的 ModelMetadata
-
11-10-2019 - |
题
升级到 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
解决方案
在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
作为您的控制类型(我知道两者都很烂)。
其他提示
来自 MVC 团队:
不幸的是,该代码实际上是在利用固定的错误,在该错误中,用于模型的目的的表达式容器被无意中设置为声明类型而不是包含类型。由于虚拟属性和验证/模型元数据的需求,必须修复此错误。
我们鼓励使用基于接口的模型(鉴于错误修复所施加的限制,也可以实际支持)。切换到抽象基类将解决该问题。
感谢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();
如果您被认为,我已经在我的应用程序中实施了一些工作。当我浏览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()中罐头实现元数据
ModelMetadataProviders.Current = new MyMetaDataProvider();
这不是有史以来最好的代码,但它可以完成工作。
尝试使用Typecast。它适用于我的项目,尽管Resharper强调了它是多余的。
对于您的代码,解决方案将是
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IActivity>" %>
<%: Html.HiddenFor(item => ((IOwned)item).AuthorId) %>
进一步 安东尼·约翰斯顿(Anthony Johnston) 答案,您可能会发现使用DataAnnotations时会得到异常 AssocedValidatorProvider.getValidatorsforProperty() 方法将尝试将继承接口用作容器类型而不是基本类型,因此未能再次找到属性。
这是从getValidatorsforpoperty方法中的反射代码(这是导致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");
}
}
如果是这样,我相信以下代码可能会有所帮助,因为它可以确保将contreType设置为属性所打开的类型,而不是视图模型的类型。
免责声明:它似乎可以正常工作,但是我尚未完全测试它,因此它可能具有不希望的效果!我也知道这不是写完美的书写,但是我试图使该格式保持类似于以前的答案,以便于比较。 :)
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);
}
}