ASP.NET MVC 3 HtmlHelper Exception не распознает ModelMetadata на унаследованном интерфейсе

После обновления до RTM MVC 3 я получаю исключение, где он ранее работал.

Вот сценарий. У меня есть несколько объектов, которые используют одни и те же базовые интерфейсы 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) %>

Однако он выдает исключение. Он не может найти AuthorId в ModelMetadata.

Я предполагаю, что в предыдущей версии он рассмотрел реализованные интерфейсы 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

Ответ 1

В 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, а позже, когда среда пытается найти свойство, с которым он падает.

Это причина. Что касается резолюции, я бы подождал официального заявления от Microsoft. Я не могу найти соответствующую информацию об этом в документе Notes Notes. Это ошибка или какая-то функция, которая должна быть устранена здесь?

Теперь вы можете либо использовать нежестко типизированный помощник Html.Hidden("AuthorId"), либо указать IOwned как тип вашего элемента управления (я знаю, как сосать).

Ответ 2

От команды MVC:

К сожалению, код был фактически используя исправленную ошибку, где контейнер выражения для Цель ModelMetadata - непреднамеренно задается объявлением тип вместо содержащего типа. Эта ошибка должна была быть исправлена ​​из-за потребности виртуальных объектов и метаданные проверки/модели.

Наличие интерфейсных моделей не то, что мы поощряем (и, учитывая ограничения, налагаемые исправлением ошибок, может реально поддерживать). Переключение к абстрактным базовым классам будет проблема.

Ответ 3

Благодаря бурсефалю, ответ на который указал мне в правильном направлении

Вы можете создать 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();

Ответ 4

f Вы заинтригованы, я немного поработал над этим в своем приложении. Когда я проходил через исходный код MVC, я обнаружил, что метод FromLambdaExpression, названный ниже, будет вызывать MetaDataProvider, который является переопределяемым синглтоном. Таким образом, мы могли бы просто реализовать этот класс, который на самом деле попытается выполнить inhereted 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;
        }              
    }
}

а затем просто замаскируйте реализацию MetaDataProvider в global.asax Application_Start()

ModelMetadataProviders.Current = new MyMetaDataProvider();

Это не лучший код, но он выполняет эту работу.

Ответ 5

Попробуйте использовать typecast. Он работает для моего проекта, хотя 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);
    }
}