Le type anonyme dynamique dans Razor provoque RuntimeBinderException


156

J'obtiens l'erreur suivante:

'objet' ne contient pas de définition pour 'RatingName'

Lorsque vous regardez le type dynamique anonyme, il a clairement RatingName.

Capture d'écran de l'erreur

Je réalise que je peux le faire avec un Tuple, mais j'aimerais comprendre pourquoi le message d'erreur se produit.

Réponses:


240

À mon avis, les types anonymes ayant des propriétés internes sont une mauvaise décision de conception du framework .NET.

Voici une extension rapide et agréable pour résoudre ce problème, c'est-à-dire en convertissant immédiatement l'objet anonyme en 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;
}

C'est très simple à utiliser:

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

Bien sûr, à votre avis:

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

2
+1 Je cherchais spécifiquement HtmlHelper.AnonymousObjectToHtmlAttributes Je savais que cela devait absolument être déjà intégré et je ne voulais pas réinventer la roue avec un code similaire.
Chris Marisic

3
Quelle est la performance à ce sujet, par rapport à la simple création d'un modèle de support fortement typé?
GONeale

@DotNetWise, pourquoi utiliseriez-vous HtmlHelper.AnonymousObjectToHtmlAttributes alors que vous pouvez simplement faire IDictionary <string, object> anonymousDictionary = new RouteDictionary (object)?
Jeremy Boyd

J'ai testé HtmlHelper.AnonymousObjectToHtmlAttributes et fonctionne comme prévu. Votre solution peut également fonctionner. Utilisez ce qui vous semble le plus facile :)
Adaptabi

Si vous voulez que ce soit une solution permanente, vous pouvez également simplement remplacer le comportement de votre contrôleur, mais cela nécessite quelques solutions de contournement supplémentaires, comme pouvoir identifier les types anonymes et créer vous-même le dictionnaire chaîne / objet à partir du type. Si vous faites cela, vous pouvez le remplacer dans: protected override System.Web.Mvc.ViewResult View (string viewName, string masterName, object model)
Johny Skovdal

50

J'ai trouvé la réponse dans une question connexe . La réponse est spécifiée sur le billet de blog de David Ebbo Passer des objets anonymes aux vues MVC et y accéder en utilisant des

La raison en est que le type anonyme étant passé dans le contrôleur en interne, il n'est donc accessible qu'à partir de l'assembly dans lequel il est déclaré. Étant donné que les vues sont compilées séparément, le classeur dynamique se plaint de ne pas pouvoir dépasser cette limite d'assemblage.

Mais si vous y réfléchissez bien, cette restriction du classeur dynamique est en fait assez artificielle, car si vous utilisez la réflexion privée, rien ne vous empêche d'accéder à ces membres internes (oui, cela fonctionne même en confiance moyenne). Ainsi, le classeur dynamique par défaut fait tout son possible pour appliquer les règles de compilation C # (où vous ne pouvez pas accéder aux membres internes), au lieu de vous laisser faire ce que le runtime CLR permet.


Battez-moi :) J'ai rencontré ce problème avec mon moteur Razor (le précurseur de celui sur razorengine.codeplex.com )
Buildstarted

Ce n'est pas vraiment une réponse, je n'en dis pas plus sur la "réponse acceptée"!
Adaptabi

4
@DotNetWise: Cela explique pourquoi l'erreur se produit, qui était la question. Vous obtenez également mon vote positif pour avoir fourni une belle solution de contournement :)
Lucas

Pour info: cette réponse est désormais très dépassée - comme l'auteur le dit lui-même en rouge au début du billet de blog référencé
Simon_Weaver

@Simon_Weaver Mais la mise à jour de l'article n'explique pas comment cela devrait fonctionner dans MVC3 +. - J'ai rencontré le même problème dans MVC 4. Des pointeurs sur la manière actuellement «bénie» d'utiliser dynamique?
Cristian Diaconescu

24

L' utilisation de la méthode ToExpando est la meilleure solution.

Voici la version qui ne nécessite pas l' assemblage 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
C'est une meilleure réponse. Je ne sais pas si ce que HtmlHelper fait avec les traits de soulignement dans la réponse alternative.
Den

+1 pour une réponse à usage général, cela est utile en dehors de ASP / MVC
codenheim

qu'en est-il des propriétés dynamiques imbriquées? ils continueront à être dynamiques ... par exemple: `{foo:" foo ", nestedDynamic: {blah:" blah "}}
sports le

16

Au lieu de créer un modèle à partir d'un type anonyme, puis d'essayer de convertir l'objet anonyme en un objet ExpandoObjectsimilaire ...

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

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

Vous pouvez simplement créer ExpandoObjectdirectement:

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

return View(model);

Ensuite, dans votre vue, vous définissez le type de modèle comme dynamique @model dynamicet vous pouvez accéder directement aux propriétés:

@Model.Profile.Name
@Model.Foo

Je recommande normalement des modèles de vue fortement typés pour la plupart des vues, mais cette flexibilité est parfois pratique.


@yohal vous pourriez certainement - je suppose que c'est une préférence personnelle. Je préfère utiliser ViewBag pour les données de page diverses généralement sans rapport avec le modèle de page - peut-être lié au modèle et garder Model comme modèle principal
Simon_Weaver

2
BTW, vous n'avez pas besoin d'ajouter @model dynamic, car c'est la valeur par défaut
yoel halb

exactement ce dont j'avais besoin, implémenter la méthode pour convertir des objets anon en objets expando prenait trop de temps ...... merci beaucoup
h-rai

5

Vous pouvez utiliser l' interface impromptue du framework pour envelopper un type anonyme dans une interface.

Vous venez de retourner un IEnumerable<IMadeUpInterface>et à la fin de votre utilisation Linq, .AllActLike<IMadeUpInterface>();cela fonctionne car il appelle la propriété anonyme en utilisant le DLR avec un contexte de l'assembly qui a déclaré le type anonyme.


1
Super petit truc :) Je ne sais pas si c'est mieux qu'une simple classe avec un tas de propriétés publiques, du moins dans ce cas.
Andrew Backer

4

A écrit une application console et ajoutez Mono.Cecil comme référence (vous pouvez maintenant l'ajouter à partir de NuGet ), puis écrivez le morceau de 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
    });
}

Le code ci-dessus obtiendrait le fichier d'assembly à partir des arguments d'entrée et utiliserait Mono.Cecil pour modifier l'accessibilité de interne à public, ce qui résoudrait le problème.

Nous pouvons exécuter le programme dans l'événement Post Build du site Web. J'ai écrit un article de blog à ce sujet en chinois, mais je pense que vous pouvez simplement lire le code et les instantanés. :)


2

Sur la base de la réponse acceptée, j'ai remplacé le contrôleur pour le faire fonctionner en général et dans les coulisses.

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

Maintenant, vous pouvez simplement passer un objet anonyme comme modèle, et cela fonctionnera comme prévu.



0

La raison de RuntimeBinderException déclenchée, je pense qu'il y a une bonne réponse dans d'autres articles. Je me concentre simplement pour expliquer comment je le fais fonctionner.

En vous référant à la réponse @DotNetWise et vues de liaison avec collection de types anonyme dans ASP.NET MVC ,

Tout d'abord, créez une classe statique pour l'extension

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

Dans le contrôleur

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

Dans View, @model IEnumerable (dynamique, pas une classe de modèle), c'est très important car nous allons lier l'objet de type anonyme.

@model IEnumerable<dynamic>

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

Le type dans foreach, je n'ai aucune erreur en utilisant var ou dynamic .

En passant, créer un nouveau ViewModel qui correspond aux nouveaux champs peut également être le moyen de transmettre le résultat à la vue.


0

Maintenant en saveur récursive

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'utilisation de l'extension ExpandoObject fonctionne mais s'arrête lors de l'utilisation d'objets anonymes imbriqués.

Tel que

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

var workitem = WorkBL.Get(id);

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

Pour ce faire, j'utilise ceci.

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'utilisation dans le contrôleur est la même sauf que vous utilisez ToRazorDynamic () au lieu de ToExpando ().

À votre avis, pour obtenir l'intégralité de l'objet anonyme, ajoutez simplement ".AnonValue" à la fin.

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

0

J'ai essayé ExpandoObject mais cela ne fonctionnait pas avec un type complexe anonyme imbriqué comme celui-ci:

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

Ma solution était donc de renvoyer un modèle JObject to View:

return View(JObject.FromObject(model));

et convertir en dynamique en .cshtml:

@using Newtonsoft.Json.Linq;
@model JObject

@{
    dynamic model = (dynamic)Model;
}
<span>Value of child is: @model.child.value</span>
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.