Il tipo anonimo dinamico in Razor causa RuntimeBinderException


156

Ricevo il seguente errore:

'oggetto' non contiene una definizione per 'RatingName'

Quando guardi il tipo dinamico anonimo, ha chiaramente RatingName.

Schermata di errore

Mi rendo conto di poterlo fare con una Tupla, ma vorrei capire perché si verifica il messaggio di errore.

Risposte:


240

I tipi anonimi con proprietà interne sono una cattiva decisione di progettazione del framework .NET, secondo me.

Ecco un'estensione rapida e piacevole per risolvere questo problema, ad esempio convertendo subito l'oggetto anonimo in 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;
}

È molto facile da usare:

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

Naturalmente a tuo avviso:

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

2
+1 Stavo specificatamente cercando HtmlHelper.AnonymousObjectToHtmlAttributes Sapevo che questo doveva già essere inserito e non volevo reinventare la ruota con un codice simile a quello manuale.
Chris Marisic,

3
Qual è la prestazione su questo, rispetto alla semplice creazione di un modello di supporto fortemente tipizzato?
GONeale,

@DotNetWise, Perché dovresti usare HtmlHelper.AnonymousObjectToHtmlAttributes quando puoi semplicemente eseguire IDictionary <stringa, oggetto> anonymousDictionary = new RouteDictionary (oggetto)?
Jeremy Boyd,

Ho testato HtmlHelper.AnonymousObjectToHtmlAttributes e funziona come previsto. La tua soluzione può anche funzionare. Usa quello che sembra più semplice :)
Adaptabi

Se vuoi che sia una soluzione permanente, potresti anche ignorare il comportamento nel tuo controller, ma richiede alcune soluzioni alternative, come essere in grado di identificare i tipi anonimi e creare il dizionario stringa / oggetto dal tipo da solo. Se lo fai però, puoi sovrascriverlo in: override protetto System.Web.Mvc.ViewResult View (string viewName, string masterName, modello oggetto)
Johny Skovdal,

50

Ho trovato la risposta in una domanda correlata . La risposta è specificata sul post del blog di David Ebbo Passare oggetti anonimi alle viste MVC e accedervi utilizzando la dinamica

La ragione di ciò è che il tipo anonimo viene passato nel controller all'interno, quindi è possibile accedervi solo all'interno dell'assembly in cui è dichiarato. Poiché le viste vengono compilate separatamente, il raccoglitore dinamico si lamenta del fatto che non può oltrepassare quel limite di assieme.

Ma se ci pensate, questa restrizione dal raccoglitore dinamico è in realtà abbastanza artificiale, perché se usate la riflessione privata, nulla vi impedisce di accedere a quei membri interni (sì, funziona anche con fiducia Media). Quindi il raccoglitore dinamico predefinito fa di tutto per imporre le regole di compilazione C # (dove non è possibile accedere ai membri interni), invece di lasciarti fare ciò che il runtime CLR consente.


Sconfiggimi :) Ho riscontrato questo problema con il mio Razor Engine (il precursore di quello su razorengine.codeplex.com )
Buildstarted

Questa non è davvero una risposta, non dire di più sulla "risposta accettata"!
Adaptabi,

4
@DotNetWise: spiega perché si è verificato l'errore, qual era la domanda. Ottieni anche il mio voto per aver fornito una bella soluzione :)
Lucas

Cordiali saluti: questa risposta è ormai
superata

@Simon_Weaver Ma l'aggiornamento post non spiega come dovrebbe funzionare in MVC3 +. - Ho riscontrato lo stesso problema in MVC 4. Qualche suggerimento sul modo attualmente "benedetto" di usare la dinamica?
Cristian Diaconescu,

24

L' uso del metodo ToExpando è la soluzione migliore.

Ecco la versione che non richiede l' assemblaggio di 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
È una risposta migliore. Non sono sicuro se mi piace quello che fa HtmlHelper con i trattini bassi nella risposta alternativa.
Den

+1 per la risposta di uso generale, questo è utile al di fuori di ASP / MVC
codenheim

che dire delle proprietà dinamiche nidificate? continueranno ad essere dinamici ... ad esempio: `{foo:" foo ", nestedDynamic: {blah:" blah "}}
sports

16

Invece di creare un modello da un tipo anonimo e quindi provare a convertire l'oggetto anonimo in un ExpandoObjectsimile ...

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

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

Puoi semplicemente creare ExpandoObjectdirettamente:

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

return View(model);

Quindi, nella tua vista, imposti il ​​tipo di modello come dinamico @model dynamice puoi accedere direttamente alle proprietà:

@Model.Profile.Name
@Model.Foo

Normalmente consiglierei modelli di vista fortemente tipizzati per la maggior parte delle viste, ma a volte questa flessibilità è utile.


@Youhal certamente potresti - immagino sia una preferenza personale. Preferisco usare ViewBag per dati di pagina vari generalmente non correlati al modello di pagina - forse relativi al modello e mantenere Model come modello principale
Simon_Weaver

2
A proposito, non è necessario aggiungere la dinamica @model, dato che è l'impostazione predefinita
yoel halb

esattamente ciò di cui avevo bisogno, implementare il metodo per convertire anon objs in expando oggetti stava impiegando troppo tempo ...... grazie heap
h-rai

5

È possibile utilizzare l' interfaccia improvvisata del framework per racchiudere un tipo anonimo in un'interfaccia.

Restituiresti semplicemente un IEnumerable<IMadeUpInterface>e alla fine del tuo Linq utilizzi .AllActLike<IMadeUpInterface>();questo lavoro perché chiama la proprietà anonima utilizzando il DLR con un contesto dell'assembly che ha dichiarato il tipo anonimo.


1
Fantastico trucchetto :) Non so se è meglio di una semplice classe con un sacco di proprietà pubbliche, almeno in questo caso.
Andrew Backer,

4

Ha scritto un'applicazione console e aggiungi Mono.Cecil come riferimento (ora puoi aggiungerlo da NuGet ), quindi scrivi il pezzo di codice:

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

Il codice sopra otterrebbe il file assembly da input args e userebbe Mono.Cecil per cambiare l'accessibilità da interno a pubblico e ciò risolverebbe il problema.

Possiamo eseguire il programma nell'evento Post Build del sito Web. Ho scritto un post sul blog in cinese ma credo che tu possa leggere il codice e le istantanee. :)


2

Sulla base della risposta accettata, ho ignorato il controller per farlo funzionare in generale e dietro le quinte.

Ecco il codice:

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");
       }
    }
}

Ora puoi semplicemente passare un oggetto anonimo come modello e funzionerà come previsto.



0

Il motivo di RuntimeBinderException è stato attivato, penso che ci sia una buona risposta in altri post. Mi concentro solo per spiegare come lo faccio effettivamente funzionare.

Facendo riferimento a rispondere alle visualizzazioni @DotNetWise e Binding con raccolta di tipi anonimi in ASP.NET MVC ,

Innanzitutto, crea una classe statica per l'estensione

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

Nel controller

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

In View, @model IEnumerable (dinamico, non una classe di modello), questo è molto importante in quanto assoceremo l'oggetto di tipo anonimo.

@model IEnumerable<dynamic>

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

Digitando foreach, non ho alcun errore nell'usare var o dynamic .

A proposito, creare un nuovo ViewModel che corrisponda ai nuovi campi può anche essere il modo per passare il risultato alla vista.


0

Ora dal sapore ricorsivo

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

L'uso dell'estensione ExpandoObject funziona ma si interrompe quando si utilizzano oggetti anonimi nidificati.

Ad esempio

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

var workitem = WorkBL.Get(id);

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

Per ottenere questo, lo uso.

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

L'utilizzo nel controller è lo stesso tranne per l'uso di ToRazorDynamic () anziché ToExpando ().

Nella tua visione per ottenere l'intero oggetto anonimo devi solo aggiungere ".AnonValue" alla fine.

var project = @(Html.Raw(JsonConvert.SerializeObject(Model.Project.AnonValue)));
var projectName = @Model.Project.Name;

0

Ho provato ExpandoObject ma non ha funzionato con un tipo complesso anonimo nidificato come questo:

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

Quindi la mia soluzione era di restituire un JObject al modello View:

return View(JObject.FromObject(model));

e converti in dinamico in .cshtml:

@using Newtonsoft.Json.Linq;
@model JObject

@{
    dynamic model = (dynamic)Model;
}
<span>Value of child is: @model.child.value</span>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.