ASP.NET MVC 3 HtmlHelper Exception не распознает ModelMetadata на унаследованном интерфейсе - PullRequest
17 голосов
/ 16 января 2011

После обновления до 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) %>

Однако, это вызывает исключение. Он не может найти 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

Ответы [ 6 ]

14 голосов
/ 30 марта 2011

От команды MVC:

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

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

14 голосов
/ 16 января 2011

В 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. Я не могу найти соответствующую информацию об этом в документе Примечания к выпуску. Это ошибка или какая-то функция , которую необходимо обойти здесь?

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

7 голосов
/ 19 октября 2011

Благодаря 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();
6 голосов
/ 06 сентября 2011

Если вы заинтересованы, я реализовал небольшую работу для этого в своем приложении. В процессе работы с исходным кодом 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;
        }              
    }
}

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

ModelMetadataProviders.Current = new MyMetaDataProvider();

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

4 голосов
/ 21 марта 2011

Попробуйте использовать typecast. Это работает для моего проекта, хотя resharper выделяет его как избыточное.

Для вашего кода решение будет

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IActivity>" %>
<%: Html.HiddenFor(item => ((IOwned)item).AuthorId) %>
0 голосов
/ 04 апреля 2013

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