web-dev-qa-db-fra.com

Le type anonyme dynamique dans Razor provoque une exception RuntimeBinderException

Je reçois l'erreur suivante:

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

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

Screenshot of Error

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

154
JarrettV

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

Voici un rapide et extension Nice 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 facile à 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>
}
237
Adaptabi

J'ai trouvé la réponse dans une question liée . La réponse est spécifiée sur le blog de David Ebbo Passage d'objets anonymes aux vues MVC et accès via des objets dynamiques

La raison en est que le type anonyme étant passé dans le contrôleur en interne, donc il ne peut que être accessible depuis l’Assemblée dans lequel il est déclaré. Depuis les vues être compilé séparément, la dynamique Le classeur se plaint de ne pas pouvoir dépasser cette limite de l'Assemblée.

Mais si vous y réfléchissez, ceci la restriction du liant dynamique est en fait assez artificiel, parce que si vous utilisez la réflexion privée, rien n’est vous empêchant d'accéder à ceux membres internes (oui, cela fonctionne même en Confiance moyenne). Donc, la dynamique par défaut liant est hors de son chemin à 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 CLR le runtime permet.

50
JarrettV

Utiliser ToExpando method est la meilleure solution.

Voici la version que ne nécessite pas System.Web Assembly:

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

Au lieu de créer un modèle à partir d'un type anonyme et d'essayer ensuite de convertir l'objet anonyme en un ExpandoObject comme ceci ...

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

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

Vous pouvez simplement créer la ExpandoObject directement:

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

return View(model);

Ensuite, dans votre vue, définissez le type de modèle sur dynamique @model dynamic et vous pourrez accéder directement aux propriétés:

@Model.Profile.Name
@Model.Foo

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

16
Simon_Weaver

Vous pouvez utiliser le cadre impromptu interface pour envelopper un type anonyme dans une interface.

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

5
jbtule

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 suivant:

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 Assembly à partir des arguments d'entrée et utiliserait Mono.Cecil pour modifier l'accessibilité interne au 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 en chinois à ce sujet mais je crois que vous pouvez simplement lire le code et les instantanés. :)

4
Jeffrey Zhao

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

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 en tant que modèle et il fonctionnera comme prévu.

2
yoel halb

Maintenant à la 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
Matas Vaitkevicius

L'utilisation de l'extension ExpandoObject fonctionne, mais elle est interrompue 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, je l'utilise.

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 ().

Pour obtenir l’objet anonyme entier, il suffit d’ajouter ".AnonValue" à la fin.

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

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

En vous référant à answer @DotNetWise et Liaisons de vues avec une collection de types anonymes dans ASP.NET MVC ,

Tout d'abord, créer 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), cela 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>[email protected], [email protected]</div>
}

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

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

0
V-SHY

Je vais voler un peu de https://stackoverflow.com/a/7478600/37055

Si vous installez le paquet dynamitey vous pouvez faire ceci:

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

Et les paysans se réjouissent.

0
Chris Marisic