El tipo anónimo dinámico en Razor provoca RuntimeBinderException

156

Recibo el siguiente error:

'objeto' no contiene una definición para 'RatingName'

Cuando observa el tipo dinámico anónimo, claramente tiene RatingName.

Captura de pantalla de error

Me doy cuenta de que puedo hacer esto con una Tupla, pero me gustaría entender por qué aparece el mensaje de error.

JarrettV
fuente

Respuestas:

240

Los tipos anónimos que tienen propiedades internas es una mala decisión de diseño de marco .NET, en mi opinión.

Aquí hay una extensión rápida y agradable para solucionar este problema, es decir, convirtiendo el objeto anónimo en un Objeto Expando de inmediato.

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 muy fácil de usar:

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

Por supuesto en tu opinión:

@foreach (var item in Model) {
     <div>x = @item.x, y = @item.y</div>
}
Adaptabi
fuente
2
+1 Estaba buscando específicamente HtmlHelper.AnonymousObjectToHtmlAttributes. Sabía que esto ya tenía que ser horneado y no quería reinventar la rueda con un código similar.
Chris Marisic
3
¿Cómo es el rendimiento en esto, en comparación con simplemente hacer un modelo de respaldo fuertemente tipado?
GONeale
@DotNetWise, ¿por qué usarías HtmlHelper.AnonymousObjectToHtmlAttributes cuando solo puedes hacer IDictionary <string, object> anonymousDictionary = new RouteDictionary (objeto)?
Jeremy Boyd
He probado HtmlHelper.AnonymousObjectToHtmlAttributes y funciona como se esperaba. Tu solución también puede funcionar. Use lo que parezca más fácil :)
Adaptabi
Si desea que sea una solución permanente, también puede anular el comportamiento en su controlador, pero requiere algunas soluciones alternativas, como poder identificar tipos anónimos y crear el diccionario de cadenas / objetos a partir del tipo usted mismo. Sin embargo, si lo hace, puede anularlo en: anulación protegida System.Web.Mvc.ViewResult View (string viewName, string masterName, modelo de objeto)
Johny Skovdal
50

Encontré la respuesta en una pregunta relacionada . La respuesta se especifica en la publicación del blog de David Ebbo. Pasar objetos anónimos a vistas de MVC y acceder a ellos utilizando dinámicas.

La razón de esto es que el tipo anónimo se pasa en el controlador interno, por lo que solo se puede acceder desde el ensamblado en el que se declara. Como las vistas se compilan por separado, la carpeta dinámica se queja de que no puede superar ese límite de ensamblaje.

Pero si lo piensa, esta restricción del aglutinante dinámico es en realidad bastante artificial, porque si utiliza la reflexión privada, nada le impide acceder a esos miembros internos (sí, incluso funciona en la confianza media). Por lo tanto, el aglutinante dinámico predeterminado está haciendo todo lo posible para hacer cumplir las reglas de compilación de C # (donde no puede acceder a los miembros internos), en lugar de permitirle hacer lo que permite el tiempo de ejecución de CLR.

JarrettV
fuente
Golpeame :) Me encontré con este problema con mi Razor Engine (el precursor del que está en razorengine.codeplex.com )
Buildstart comenzó
¡Esto no es realmente una respuesta, no dice más sobre la "respuesta aceptada"!
Adaptabi
44
@DotNetWise: Explica por qué ocurre el error, que era la pregunta. También recibiste mi voto positivo por proporcionar una buena solución :)
Lucas
FYI: esta respuesta ahora está muy desactualizada, como dice el autor en rojo al comienzo de la publicación de blog referenciada
Simon_Weaver,
@Simon_Weaver Pero la actualización posterior no explica cómo debería funcionar en MVC3 +. - Llegué al mismo problema en MVC 4. ¿Algún indicador sobre la forma actualmente 'bendecida' de usar la dinámica?
Cristian Diaconescu
24

Usar el método ToExpando es la mejor solución.

Aquí está la versión que no requiere el ensamblaje de 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;
}
alexey
fuente
1
Es una mejor respuesta. No estoy seguro si le gusta lo que HtmlHelper hace con guiones bajos en la respuesta alternativa.
Den
+1 para respuesta de propósito general, esto es útil fuera de ASP / MVC
codenheim
¿Qué pasa con las propiedades dinámicas anidadas? continuarán siendo dinámicos ... por ejemplo: `{foo:" foo ", nestedDynamic: {blah:" blah "}}
deportes
16

En lugar de crear un modelo a partir de un tipo anónimo y luego tratar de convertir el objeto anónimo en algo ExpandoObjectasí ...

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

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

Simplemente puede crear el ExpandoObjectdirectamente:

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

return View(model);

Luego, en su vista, establece el tipo de modelo como dinámico @model dynamicy puede acceder a las propiedades directamente:

@Model.Profile.Name
@Model.Foo

Normalmente recomendaría modelos de vista fuertemente tipados para la mayoría de las vistas, pero a veces esta flexibilidad es útil.

Simon_Weaver
fuente
@yohal ciertamente podrías, supongo que es una preferencia personal. Prefiero usar ViewBag para datos de página misceláneos generalmente no relacionados con el modelo de página, tal vez relacionado con la plantilla y mantener el modelo como el modelo principal
Simon_Weaver
2
Por cierto, no tiene que agregar @model dynamic, ya que es el predeterminado
yoel halb
exactamente lo que necesitaba, implementar el método para convertir anon objs para expandir objetos tomaba demasiado tiempo ... gracias, gracias
h-rai
5

Puede usar la interfaz improvisada de framework para envolver un tipo anónimo en una interfaz.

Simplemente devolvería un IEnumerable<IMadeUpInterface>y al final de su uso de Linq .AllActLike<IMadeUpInterface>();esto funciona porque llama a la propiedad anónima usando el DLR con un contexto del ensamblado que declaró el tipo anónimo.

jbtule
fuente
1
Impresionante truco :) No sé si es mejor que una simple clase con un montón de propiedades públicas, al menos en este caso.
Andrew Backer
4

Escribió una aplicación de consola y agregue Mono.Cecil como referencia (ahora puede agregarlo desde NuGet ), luego escriba el fragmento de código:

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

El código anterior obtendría el archivo de ensamblaje de los argumentos de entrada y usaría Mono.Cecil para cambiar la accesibilidad de interna a pública, y eso resolvería el problema.

Podemos ejecutar el programa en el evento Post Build del sitio web. Escribí una publicación de blog sobre esto en chino, pero creo que puedes leer el código y las instantáneas. :)

Jeffrey Zhao
fuente
2

Basado en la respuesta aceptada, he anulado en el controlador para que funcione en general y detrás de escena.

Aquí está el código:

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

Ahora puede pasar un objeto anónimo como modelo y funcionará como se espera.

Yoel Halb
fuente
0

La razón de la activación de RuntimeBinderException, creo que hay una buena respuesta en otras publicaciones. Solo me concentro en explicar cómo lo hago funcionar.

Por referirse a responder @DotNetWise y encuadernación con vistas colección de tipo anónimo en ASP.NET MVC ,

En primer lugar, crear una clase estática para extensión

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

En el controlador

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

En View, @model IEnumerable (dinámico, no una clase de modelo), esto es muy importante ya que vamos a vincular el objeto de tipo anónimo.

@model IEnumerable<dynamic>

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

El tipo en foreach, no tengo ningún error, ya sea usando var o dynamic .

Por cierto, crear un nuevo ViewModel que coincida con los nuevos campos también puede ser la forma de pasar el resultado a la vista.

V-SHY
fuente
0

Ahora en sabor recursivo

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;
    }
Matas Vaitkevicius
fuente
0

Usar la extensión ExpandoObject funciona pero se rompe cuando se usan objetos anónimos anidados.

Como

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

var workitem = WorkBL.Get(id);

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

Para lograr esto, uso esto.

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

El uso en el controlador es el mismo, excepto que usa ToRazorDynamic () en lugar de ToExpando ().

En su vista para obtener el objeto anónimo completo, simplemente agregue ".AnonValue" al final.

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

Probé el ExpandoObject pero no funcionó con un tipo complejo anónimo anidado como este:

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

Entonces, mi solución fue devolver un modelo JObject to View:

return View(JObject.FromObject(model));

y convertir a dinámico en .cshtml:

@using Newtonsoft.Json.Linq;
@model JObject

@{
    dynamic model = (dynamic)Model;
}
<span>Value of child is: @model.child.value</span>
Guilherme Muniz
fuente