Der dynamische anonyme Typ in Razor verursacht eine RuntimeBinderException


156

Ich erhalte die folgende Fehlermeldung:

'Objekt' enthält keine Definition für 'RatingName'

Wenn Sie sich den anonymen dynamischen Typ ansehen, hat er eindeutig RatingName.

Screenshot des Fehlers

Mir ist klar, dass ich dies mit einem Tupel tun kann, aber ich möchte verstehen, warum die Fehlermeldung auftritt.

Antworten:


240

Anonyme Typen mit internen Eigenschaften sind meiner Meinung nach eine schlechte Entscheidung für das .NET Framework-Design.

Hier ist eine schnelle und nette Erweiterung , um dieses Problem zu beheben, dh indem Sie das anonyme Objekt sofort in ein ExpandoObject konvertieren.

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

Es ist sehr einfach zu bedienen:

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

Natürlich aus Ihrer Sicht:

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

2
+1 Ich suchte speziell nach HtmlHelper.AnonymousObjectToHtmlAttributes Ich wusste, dass dies unbedingt bereits eingebrannt werden musste und wollte das Rad nicht mit ähnlichem handgerolltem Code neu erfinden.
Chris Marisic

3
Wie ist die Leistung dabei im Vergleich zur einfachen Erstellung eines stark typisierten Hintergrundmodells?
GONeale

@DotNetWise, warum sollten Sie HtmlHelper.AnonymousObjectToHtmlAttributes verwenden, wenn Sie nur IDictionary <string, object> anonymDictionary = new RouteDictionary (object) ausführen können?
Jeremy Boyd

Ich habe HtmlHelper.AnonymousObjectToHtmlAttributes getestet und funktioniert wie erwartet. Ihre Lösung kann auch funktionieren. Verwenden Sie, was einfacher erscheint :)
Adaptabi

Wenn Sie möchten, dass es sich um eine dauerhafte Lösung handelt, können Sie das Verhalten in Ihrem Controller auch einfach überschreiben. Es sind jedoch einige weitere Problemumgehungen erforderlich, z. B. die Möglichkeit, anonyme Typen zu identifizieren und das Zeichenfolgen- / Objektwörterbuch aus dem Typ selbst zu erstellen. Wenn Sie dies jedoch tun, können Sie es überschreiben in: protected override System.Web.Mvc.ViewResult View (Zeichenfolge viewName, Zeichenfolge masterName, Objektmodell)
Johny Skovdal

50

Ich fand die Antwort in einer verwandten Frage . Die Antwort finden Sie in David Ebbos Blogbeitrag Übergeben anonymer Objekte an MVC-Ansichten und Zugriff auf diese mithilfe von Dynamic

Der Grund dafür ist, dass der anonyme Typ im Controller intern übergeben wird, sodass nur innerhalb der Assembly zugegriffen werden kann, in der er deklariert ist. Da Ansichten separat kompiliert werden, beschwert sich der dynamische Ordner, dass er diese Baugruppengrenze nicht überschreiten kann.

Wenn Sie jedoch darüber nachdenken, ist diese Einschränkung des dynamischen Ordners tatsächlich ziemlich künstlich, denn wenn Sie private Reflexion verwenden, hindert Sie nichts daran, auf diese internen Mitglieder zuzugreifen (ja, es funktioniert sogar in mittlerer Vertrauenswürdigkeit). Der dynamische Standardordner ist also sehr bemüht, C # -Kompilierungsregeln durchzusetzen (bei denen Sie nicht auf interne Mitglieder zugreifen können), anstatt Sie das tun zu lassen, was die CLR-Laufzeit zulässt.


Schlagen Sie mich dazu :) Ich bin auf dieses Problem mit meiner Razor Engine gestoßen (dem Vorläufer der auf razorengine.codeplex.com )
Buildstarted

Dies ist nicht wirklich eine Antwort, nicht mehr über die "akzeptierte Antwort" zu sagen!
Adaptabi

4
@DotNetWise: Es erklärt, warum der Fehler auftritt, was die Frage war. Sie erhalten auch meine Gegenstimme für die Bereitstellung einer guten Problemumgehung :)
Lucas

Zu Ihrer Information: Diese Antwort ist jetzt sehr veraltet - wie der Autor am Anfang des referenzierten Blogposts in Rot sagt
Simon_Weaver

@Simon_Weaver Das Post-Update erklärt jedoch nicht, wie es in MVC3 + funktionieren soll. - Ich habe das gleiche Problem in MVC 4 festgestellt. Gibt es Hinweise auf die derzeit "gesegnete" Art der Verwendung von Dynamik?
Cristian Diaconescu

24

Die Verwendung der ToExpando- Methode ist die beste Lösung.

Hier ist die Version, für die keine System.Web- Assembly erforderlich ist :

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
Es ist eine bessere Antwort. Ich bin mir nicht sicher, ob HtmlHelper die Unterstreichung in der alternativen Antwort mag.
Den

+1 für allgemeine Antwort, dies ist nützlich außerhalb von ASP / MVC
Codenheim

Was ist mit verschachtelten dynamischen Eigenschaften? Sie werden weiterhin dynamisch sein ... zB: `{foo:" foo ", nestedDynamic: {blah:" blah "}}
Sport

16

Anstatt ein Modell aus einem anonymen Typ zu erstellen und dann zu versuchen, das anonyme Objekt in ein solches zu konvertieren ExpandoObject...

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

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

Sie können einfach die ExpandoObjectdirekt erstellen :

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

return View(model);

Anschließend legen Sie in Ihrer Ansicht den Modelltyp als dynamisch fest @model dynamicund können direkt auf die Eigenschaften zugreifen:

@Model.Profile.Name
@Model.Foo

Normalerweise würde ich für die meisten Ansichten stark typisierte Ansichtsmodelle empfehlen, aber manchmal ist diese Flexibilität praktisch.


@yohal könnten Sie sicherlich - ich denke, es ist persönliche Präferenz. Ich bevorzuge es, ViewBag für verschiedene Seitendaten zu verwenden, die im Allgemeinen nicht mit dem Seitenmodell zusammenhängen - möglicherweise im Zusammenhang mit der Vorlage, und Modell als primäres Modell
beizubehalten

2
Übrigens müssen Sie @model dynamic nicht hinzufügen, da dies die Standardeinstellung ist
yoel halb

genau das, was ich brauchte, die Implementierung der Methode zum Konvertieren von anon objs in expando-Objekte nahm zu viel Zeit in Anspruch ...... danke Haufen
h-rai

5

Sie können die Framework Impromptu-Schnittstelle verwenden , um einen anonymen Typ in eine Schnittstelle einzubinden.

Sie würden einfach eine zurückgeben IEnumerable<IMadeUpInterface>und am Ende Ihrer Linq-Verwendung .AllActLike<IMadeUpInterface>();funktioniert dies, da die anonyme Eigenschaft über das DLR mit einem Kontext der Assembly aufgerufen wird, die den anonymen Typ deklariert hat.


1
Fantastischer kleiner Trick :) Ich weiß nicht, ob es besser ist als nur eine einfache Klasse mit einer Reihe von öffentlichen Eigenschaften, zumindest in diesem Fall.
Andrew Backer

4

Schrieb eine Konsolenanwendung und fügte Mono.Cecil als Referenz hinzu (Sie können es jetzt über NuGet hinzufügen ). Schreiben Sie dann den Code:

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

Der obige Code würde die Assembly-Datei von den Eingabeargumenten abrufen und Mono.Cecil verwenden, um die Zugänglichkeit von intern zu öffentlich zu ändern. Dadurch würde das Problem behoben.

Wir können das Programm im Post Build-Event der Website ausführen. Ich habe einen Blog-Beitrag darüber auf Chinesisch geschrieben, aber ich glaube, Sie können einfach den Code und die Schnappschüsse lesen. :) :)


2

Basierend auf der akzeptierten Antwort habe ich den Controller überschrieben, damit er im Allgemeinen und hinter den Kulissen funktioniert.

Hier ist der Code:

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

Jetzt können Sie einfach ein anonymes Objekt als Modell übergeben, und es funktioniert wie erwartet.



0

Der Grund für die ausgelöste RuntimeBinderException ist meiner Meinung nach in anderen Beiträgen gut zu beantworten. Ich konzentriere mich nur darauf zu erklären, wie ich es tatsächlich zum Laufen bringe.

Weitere Informationen finden Sie unter Beantworten von @ DotNetWise- und Bindungsansichten mit der Sammlung anonymer Typen in ASP.NET MVC .

Erstellen Sie zunächst eine statische Klasse für die Erweiterung

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

In der Steuerung

    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 (dynamisch, keine Modellklasse) ist dies sehr wichtig, da wir das anonyme Typobjekt binden werden.

@model IEnumerable<dynamic>

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

Bei der Eingabe von foreach habe ich weder bei der Verwendung von var noch bei dynamic einen Fehler .

Wenn Sie ein neues ViewModel erstellen, das mit den neuen Feldern übereinstimmt, können Sie das Ergebnis auch an die Ansicht übergeben.


0

Jetzt im rekursiven Geschmack

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

Die Verwendung der ExpandoObject-Erweiterung funktioniert, funktioniert jedoch nicht, wenn verschachtelte anonyme Objekte verwendet werden.

Sowie

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

var workitem = WorkBL.Get(id);

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

Um dies zu erreichen, benutze ich dies.

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

Die Verwendung im Controller ist dieselbe, außer dass Sie ToRazorDynamic () anstelle von ToExpando () verwenden.

Um das gesamte anonyme Objekt abzurufen, fügen Sie am Ende einfach ".AnonValue" hinzu.

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

0

Ich habe das ExpandoObject ausprobiert, aber es funktionierte nicht mit einem verschachtelten anonymen komplexen Typ wie dem folgenden:

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

Meine Lösung bestand also darin, ein JObject to View-Modell zurückzugeben:

return View(JObject.FromObject(model));

und in .cshtml in dynamisch konvertieren:

@using Newtonsoft.Json.Linq;
@model JObject

@{
    dynamic model = (dynamic)Model;
}
<span>Value of child is: @model.child.value</span>
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.