Динамічний анонімний тип у Razor викликає RuntimeBinderException


156

Я отримую таку помилку:

'object' не містить визначення для 'RatingName'

Якщо ви подивитеся на анонімний динамічний тип, він, очевидно, має рейтинг.

Знімок екрана помилки

Я усвідомлюю, що можу це зробити за допомогою кортежу, але хотів би зрозуміти, чому виникає повідомлення про помилку.

Відповіді:


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

2
+1 Я спеціально шукав HtmlHelper.AnonymousObjectToHtmlAttributes, я знав, що це абсолютно потрібно запекти вже і не хотів винаходити колесо з аналогічним керованим кодом.
Кріс Марісіч

3
Яка ефективність роботи в порівнянні з простою силою набраної моделі резервної копії?
GONeale

@DotNetWise, чому б ти використовував HtmlHelper.AnonymousObjectToHtmlAttributes, коли ти можеш просто зробити IDictionary <string, object> anonymousDictionary = new RouteDictionary (object)?
Джеремі Бойд

Я перевірив HtmlHelper.AnonymousObjectToHtmlAttributes і працює, як очікувалося. Ваше рішення також може спрацювати. Використовуйте те, що здається легше :)
Adaptabi

Якщо ви хочете, щоб це було постійним рішенням, ви також можете просто змінити поведінку у своєму контролері, але для цього потрібні ще декілька обхідних шляхів, як-от можливість ідентифікувати анонімні типи та створити словник / словник об’єктів із типу самостійно. Якщо ви це зробите, ви можете змінити це в: захищеному перезаписі System.Web.Mvc.ViewResult View (рядок viewName, string masterName, об'єктна модель)
Johny Skovdal

50

Я знайшов відповідь у спорідненому питанні . Відповідь вказана в публікації блогу Девіда Еббо Передача анонімних об’єктів до MVC-переглядів та доступ до них за допомогою динаміки

Причиною цього є те, що анонімний тип передається у контролер у внутрішній частині, тому доступ до нього можна отримати лише в межах збірки, в якій він оголошений. Оскільки перегляди складаються окремо, динамічне в'яжуче скаржиться, що воно не може перейти цю межу складання.

Але якщо ви задумаєтесь, це обмеження від динамічного в'яжучого насправді є досить штучним, тому що якщо ви використовуєте приватне відображення, ніщо не заважає вам отримати доступ до цих внутрішніх членів (так, це працює навіть у середовищі довіри середнього рівня). Таким чином, динамічне в'яжуче за замовчуванням виходить із шляху, щоб застосувати правила компіляції C # (де ви не можете отримати доступ до внутрішніх членів), а не дозволяти виконувати те, що дозволяє час виконання CLR.


Побийте мене в цьому :) Я зіткнувся з цією проблемою з моїм Razor Engine (попередником цієї на razorengine.codeplex.com )
Buildstarted

Це насправді не відповідь, не сказати більше про "прийняту відповідь"!
Адаптабі

4
@DotNetWise: Це пояснює, чому виникає помилка, в чому полягало питання. Ви також отримаєте мою нагороду за надання хорошого рішення :)
Лукас

FYI: ця відповідь зараз дуже застаріла - як автор говорить самим червоним кольором на початку
посиланого

@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
codenheim

як щодо вкладених динамічних властивостей? вони будуть продовжувати динамічно ... напр .: `{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

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


@yohal ви, звичайно, могли - я думаю, це особисті переваги. Я вважаю за краще використовувати ViewBag для різних даних сторінки, які взагалі не стосуються моделі сторінки - можливо, стосуються шаблону і зберігають Model як основну модель
Simon_Weaver

2
До речі, вам не доведеться додавати @model динамично, оскільки це за замовчуванням
yoel halb

саме те, що мені було потрібно, впроваджуючи метод перетворення anon objs в об’єкти експандо, забирав занадто багато часу ...... дякую купи
h-rai

5

Ви можете використовувати імпровізований інтерфейс рамки, щоб обернути анонімний тип в інтерфейсі.

Ви просто повернете IEnumerable<IMadeUpInterface>і в кінці свого Linq використовувати .AllActLike<IMadeUpInterface>();це працює, оскільки він викликає анонімний ресурс за допомогою DLR з контекстом збірки, яка оголосила анонімний тип.


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

Причина RuntimeBinderException викликана, я думаю, що в інших публікаціях є хороша відповідь. Я просто зосереджуюсь, щоб пояснити, як я насправді змушую це працювати.

Звернівшись до відповідей @DotNetWise та прив'язування поглядів із колекції типу Anonymous в 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);
    }

У View, @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, який відповідає новим полям, також може стати способом передачі результату перегляду.


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 працює, але розривається при використанні вкладених анонімних об'єктів.

Як от

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 до 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>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.