Динамический анонимный тип в Razor вызывает RuntimeBinderException

156

Я получаю следующую ошибку:

'object' не содержит определения для RatingName

Когда вы смотрите на анонимный динамический тип, он явно имеет RatingName.

Скриншот ошибки

Я понимаю, что могу сделать это с помощью кортежа, но я хотел бы понять, почему появляется сообщение об ошибке.

JarrettV
источник

Ответы:

240

На мой взгляд, анонимные типы, имеющие внутренние свойства, - плохое решение для разработки .NET Framework.

Вот быстрое и приятное расширение для решения этой проблемы, т. Е. Путем немедленного преобразования анонимного объекта в ExpandoObject.

public static ExpandoObject ToExpando(this object anonymousObject)
{
    IDictionary<string, object> anonymousDictionary =  new RouteValueDictionary(anonymousObject);
    IDictionary<string, object> expando = new ExpandoObject();
    foreach (var item in anonymousDictionary)
        expando.Add(item);
    return (ExpandoObject)expando;
}

Это очень легко использовать:

return View("ViewName", someLinq.Select(new { x=1, y=2}.ToExpando());

Конечно, на ваш взгляд:

@foreach (var item in Model) {
     <div>x = @item.x, y = @item.y</div>
}
Adaptabi
источник
2
+1 Я специально искал HtmlHelper.AnonymousObjectToHtmlAttributes. Я знал, что это уже было необходимо, и не хотел изобретать велосипед с подобным кодом, управляемым вручную.
Крис Марисик
3
Какова производительность на этом, по сравнению с простым созданием строго типизированной модели поддержки?
GONeale
@DotNetWise, Зачем вам использовать HtmlHelper.AnonymousObjectToHtmlAttributes, когда вы можете просто сделать IDictionary <string, object> anonymousDictionary = new RouteDictionary (object)?
Джереми Бойд
Я проверил HtmlHelper.AnonymousObjectToHtmlAttributes и работает как ожидалось. Ваше решение также может работать. Используйте то, что кажется проще :)
Adaptabi
Если вы хотите, чтобы это было постоянное решение, вы также можете просто переопределить поведение в вашем контроллере, но это требует еще нескольких обходных путей, таких как возможность идентифицировать анонимные типы и создание словаря строк / объектов из типа самостоятельно. Однако, если вы сделаете это, вы можете переопределить это в: protected override System.Web.Mvc.ViewResult View (string viewName, string masterName, object model)
Джонни Сковдал
50

Я нашел ответ в связанном вопросе . Ответ указан в сообщении Дэвида Эббо в блоге. Передача анонимных объектов в представления MVC и доступ к ним с помощью динамического

Причина этого заключается в том, что анонимный тип передается во внутренний контроллер, поэтому к нему можно получить доступ только из сборки, в которой он объявлен. Поскольку представления компилируются отдельно, динамическое связующее устройство жалуется, что оно не может пересечь эту границу сборки.

Но если подумать, это ограничение динамического связывания на самом деле довольно искусственное, потому что, если вы используете личное отражение, ничто не мешает вам получить доступ к этим внутренним членам (да, это даже работает в Medium trust). Таким образом, динамическое связующее устройство по умолчанию делает все возможное для обеспечения соблюдения правил компиляции C # (когда вы не можете получить доступ к внутренним элементам), вместо того, чтобы позволить вам делать то, что позволяет среда выполнения CLR.

JarrettV
источник
Ударь меня в этом :) Я столкнулся с этой проблемой с моим Razor Engine (предшественник тому на razorengine.codeplex.com )
Buildstarted
Это не совсем ответ, не говоря уже о "принятом ответе"!
Адаптаби
4
@DotNetWise: Это объясняет, почему возникает ошибка, о которой и идет речь. Вы также получите мой отзыв за хороший обходной путь :)
Лукас
К вашему сведению: этот ответ сейчас сильно устарел - как говорит сам автор красным цветом в начале ссылки на блог
Simon_Weaver
@Simon_Weaver Но пост-обновление не объясняет, как оно должно работать в MVC3 +. - Я столкнулся с той же проблемой в MVC 4. Какие-нибудь указатели на «благословенный» в настоящее время способ использования динамических?
Кристиан
24

Использование ToExpando метода является лучшим решением.

Вот версия, которая не требует сборки System.Web :

public static ExpandoObject ToExpando(this object anonymousObject)
{
    IDictionary<string, object> expando = new ExpandoObject();
    foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(anonymousObject))
    {
        var obj = propertyDescriptor.GetValue(anonymousObject);
        expando.Add(propertyDescriptor.Name, obj);
    }

    return (ExpandoObject)expando;
}
алексей
источник
1
Это лучший ответ. Не уверен, нравится ли то, что HtmlHelper делает с подчеркиванием в альтернативном ответе.
День
+1 для ответа общего назначения, это полезно вне ASP / MVC
коденхайм
как насчет вложенных динамических свойств? они будут продолжать быть динамическими ... например: `{foo:" foo ", nestedDynamic: {blah:" blah "}}
спорт
16

Вместо того, чтобы создавать модель из анонимного типа, а затем пытаться преобразовать анонимный объект в ExpandoObjectподобное:

var model = new 
{
    Profile = profile,
    Foo = foo
};

return View(model.ToExpando());  // not a framework method (see other answers)

Вы можете просто создать ExpandoObjectнепосредственно:

dynamic model = new ExpandoObject();
model.Profile = profile;
model.Foo = foo;

return View(model);

Затем, по вашему мнению, вы устанавливаете тип модели как динамический, @model dynamicи вы можете получить доступ к свойствам напрямую:

@Model.Profile.Name
@Model.Foo

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

Simon_Weaver
источник
@ Йохал, ты, конечно, мог бы - я думаю, это личное предпочтение. Я предпочитаю использовать ViewBag для разных данных страницы, как правило, не связанных с моделью страницы - возможно, связанных с шаблоном и сохраняющих Модель в качестве основной модели
Simon_Weaver
2
Кстати, вам не нужно добавлять динамический @model, так как это по умолчанию
yoel halb
именно то, что мне было нужно, реализация метода преобразования anon objs в объекты расширения занимала слишком много времени ...... спасибо, куча
h-rai
5

Вы можете использовать импровизированный интерфейс Framework, чтобы обернуть анонимный тип в интерфейс.

Вы просто вернете IEnumerable<IMadeUpInterface>и в конце вашего Linq используйте .AllActLike<IMadeUpInterface>();это работает, потому что он вызывает анонимное свойство, используя DLR с контекстом сборки, которая объявила анонимный тип.

jbtule
источник
1
Удивительный маленький трюк :) Не знаю, лучше ли это, чем простой класс с множеством открытых свойств, по крайней мере, в этом случае.
Эндрю Бакер
4

Написал консольное приложение и добавил Mono.Cecil в качестве ссылки (теперь вы можете добавить его из NuGet ), затем напишите фрагмент кода:

static void Main(string[] args)
{
    var asmFile = args[0];
    Console.WriteLine("Making anonymous types public for '{0}'.", asmFile);

    var asmDef = AssemblyDefinition.ReadAssembly(asmFile, new ReaderParameters
    {
        ReadSymbols = true
    });

    var anonymousTypes = asmDef.Modules
        .SelectMany(m => m.Types)
        .Where(t => t.Name.Contains("<>f__AnonymousType"));

    foreach (var type in anonymousTypes)
    {
        type.IsPublic = true;
    }

    asmDef.Write(asmFile, new WriterParameters
    {
        WriteSymbols = true
    });
}

Приведенный выше код получит файл сборки из входных аргументов и использует Mono.Cecil, чтобы изменить доступность с внутреннего на общедоступный, и это решит проблему.

Мы можем запустить программу в событии Post Build на сайте. Я написал в блоге об этом на китайском языке, но я верю, что вы можете просто прочитать код и снимки. :)

Джеффри Чжао
источник
2

Основываясь на принятом ответе, я переопределил в контроллере, чтобы он работал вообще и за кулисами.

Вот код:

protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
    base.OnResultExecuting(filterContext);

    //This is needed to allow the anonymous type as they are intenal to the assembly, while razor compiles .cshtml files into a seperate assembly
    if (ViewData != null && ViewData.Model != null && ViewData.Model.GetType().IsNotPublic)
    {
       try
       {
          IDictionary<string, object> expando = new ExpandoObject();
          (new RouteValueDictionary(ViewData.Model)).ToList().ForEach(item => expando.Add(item));
          ViewData.Model = expando;
       }
       catch
       {
           throw new Exception("The model provided is not 'public' and therefore not avaialable to the view, and there was no way of handing it over");
       }
    }
}

Теперь вы можете просто передать анонимный объект в качестве модели, и он будет работать как положено.

Йоэль Хэлб
источник
0

Я собираюсь сделать немного воровство с https://stackoverflow.com/a/7478600/37055

Если вы устанавливаете пакет- динамит, вы можете сделать это:

return View(Build<ExpandoObject>.NewObject(RatingName: name, Comment: comment));

И крестьяне радуются.

Крис Марисик
источник
0

Причина RuntimeBinderException сработала, я думаю, что есть хороший ответ в других сообщениях. Я просто пытаюсь объяснить, как я на самом деле заставляю это работать.

По ссылке на ответ @DotNetWise и Binding view с коллекцией анонимных типов в ASP.NET MVC ,

Во-первых, создайте статический класс для расширения

public static class impFunctions
{
    //converting the anonymous object into an ExpandoObject
    public static ExpandoObject ToExpando(this object anonymousObject)
    {
        //IDictionary<string, object> anonymousDictionary = new RouteValueDictionary(anonymousObject);
        IDictionary<string, object> anonymousDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(anonymousObject);
        IDictionary<string, object> expando = new ExpandoObject();
        foreach (var item in anonymousDictionary)
            expando.Add(item);
        return (ExpandoObject)expando;
    }
}

В контроллере

    public ActionResult VisitCount()
    {
        dynamic Visitor = db.Visitors
                        .GroupBy(p => p.NRIC)
                        .Select(g => new { nric = g.Key, count = g.Count()})
                        .OrderByDescending(g => g.count)
                        .AsEnumerable()    //important to convert to Enumerable
                        .Select(c => c.ToExpando()); //convert to ExpandoObject
        return View(Visitor);
    }

В представлении @model IEnumerable (динамический, а не класс модели) это очень важно, так как мы собираемся связать объект анонимного типа.

@model IEnumerable<dynamic>

@*@foreach (dynamic item in Model)*@
@foreach (var item in Model)
{
    <div>x=@item.nric, y=@item.count</div>
}

Тип в foreach, у меня нет ошибок ни с помощью VAR, ни динамического .

Кстати, создание новой ViewModel, которая соответствует новым полям, также может быть способом передачи результата в представление.

V-SHY
источник
0

Теперь в рекурсивном аромате

public static ExpandoObject ToExpando(this object obj)
    {
        IDictionary<string, object> expandoObject = new ExpandoObject();
        new RouteValueDictionary(obj).ForEach(o => expandoObject.Add(o.Key, o.Value == null || new[]
        {
            typeof (Enum),
            typeof (String),
            typeof (Char),
            typeof (Guid),

            typeof (Boolean),
            typeof (Byte),
            typeof (Int16),
            typeof (Int32),
            typeof (Int64),
            typeof (Single),
            typeof (Double),
            typeof (Decimal),

            typeof (SByte),
            typeof (UInt16),
            typeof (UInt32),
            typeof (UInt64),

            typeof (DateTime),
            typeof (DateTimeOffset),
            typeof (TimeSpan),
        }.Any(oo => oo.IsInstanceOfType(o.Value))
            ? o.Value
            : o.Value.ToExpando()));

        return (ExpandoObject) expandoObject;
    }
Матас Вайткявичюс
источник
0

Использование ExpandoObject Extension работает, но прерывается при использовании вложенных анонимных объектов.

Такие как

var projectInfo = new {
 Id = proj.Id,
 UserName = user.Name
};

var workitem = WorkBL.Get(id);

return View(new
{
  Project = projectInfo,
  WorkItem = workitem
}.ToExpando());

Для этого я использую это.

public static class RazorDynamicExtension
{
    /// <summary>
    /// Dynamic object that we'll utilize to return anonymous type parameters in Views
    /// </summary>
    public class RazorDynamicObject : DynamicObject
    {
        internal object Model { get; set; }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (binder.Name.ToUpper() == "ANONVALUE")
            {
                result = Model;
                return true;
            }
            else
            {
                PropertyInfo propInfo = Model.GetType().GetProperty(binder.Name);

                if (propInfo == null)
                {
                    throw new InvalidOperationException(binder.Name);
                }

                object returnObject = propInfo.GetValue(Model, null);

                Type modelType = returnObject.GetType();
                if (modelType != null
                    && !modelType.IsPublic
                    && modelType.BaseType == typeof(Object)
                    && modelType.DeclaringType == null)
                {
                    result = new RazorDynamicObject() { Model = returnObject };
                }
                else
                {
                    result = returnObject;
                }

                return true;
            }
        }
    }

    public static RazorDynamicObject ToRazorDynamic(this object anonymousObject)
    {
        return new RazorDynamicObject() { Model = anonymousObject };
    }
}

Использование в контроллере такое же, за исключением того, что вы используете ToRazorDynamic () вместо ToExpando ().

По вашему мнению, чтобы получить весь анонимный объект, вы просто добавляете «.AnonValue» в конец.

var project = @(Html.Raw(JsonConvert.SerializeObject(Model.Project.AnonValue)));
var projectName = @Model.Project.Name;
Донни В.
источник
0

Я пробовал ExpandoObject, но он не работал с вложенным анонимным сложным типом, подобным этому:

var model = new { value = 1, child = new { value = 2 } };

Поэтому я решил вернуть модель JObject to View:

return View(JObject.FromObject(model));

и преобразовать в динамический в .cshtml:

@using Newtonsoft.Json.Linq;
@model JObject

@{
    dynamic model = (dynamic)Model;
}
<span>Value of child is: @model.child.value</span>
Гильерме Мунис
источник