Tipo anônimo dinâmico no Razor causa RuntimeBinderException


156

Estou recebendo o seguinte erro:

'object' não contém uma definição para 'RatingName'

Quando você olha para o tipo dinâmico anônimo, ele claramente possui RatingName.

Captura de tela do erro

Sei que posso fazer isso com uma tupla, mas gostaria de entender por que a mensagem de erro ocorre.

Respostas:


240

Tipos anônimos com propriedades internas é uma péssima decisão de design de estrutura .NET, na minha opinião.

Aqui está uma extensão rápida e agradável para corrigir esse problema, ou seja, convertendo o objeto anônimo em um ExpandoObject imediatamente.

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

É muito fácil de usar:

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

Obviamente, na sua opinião:

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

2
+1 Eu estava procurando especificamente por HtmlHelper.AnonymousObjectToHtmlAttributes Eu sabia que isso já precisava ser resolvido e não queria reinventar a roda com código manuscrito semelhante.
Chris Marisic

3
Qual é o desempenho disso, comparado a simplesmente criar um modelo de suporte fortemente tipado?
GONeale

@DotNetWise, Por que você usaria HtmlHelper.AnonymousObjectToHtmlAttributes quando você pode apenas fazer IDictionary <string, objeto> anonymousDictionary = new RouteDictionary (object)?
Jeremy Boyd

Eu testei HtmlHelper.AnonymousObjectToHtmlAttributes e funciona conforme o esperado. Sua solução também pode funcionar. Use o que parece mais fácil :)
Adaptabi

Se você deseja que seja uma solução permanente, você também pode substituir o comportamento do seu controlador, mas isso requer mais algumas soluções alternativas, como identificar tipos anônimos e criar o dicionário de string / objeto a partir do tipo. Se você fizer isso, porém, você pode substituí-lo em: protected override System.Web.Mvc.ViewResult Ver (string viewName, corda mastername, modelo de objeto)
Johny Skovdal

50

Encontrei a resposta em uma pergunta relacionada . A resposta está especificada na postagem do blog de David Ebbo Passando objetos anônimos para visualizações do MVC e acessando-os usando dinâmica

A razão para isso é que o tipo anônimo está sendo passado no controlador em interno, portanto, ele só pode ser acessado de dentro do assembly em que foi declarado. Como as visualizações são compiladas separadamente, o fichário dinâmico reclama que não pode ultrapassar esse limite de montagem.

Mas se você pensar bem, essa restrição do fichário dinâmico é realmente bastante artificial, porque se você usar a reflexão privada, nada o impedirá de acessar esses membros internos (sim, até funciona com confiança Média). Portanto, o fichário dinâmico padrão está se esforçando para impor regras de compilação C # (onde você não pode acessar membros internos), em vez de permitir que você faça o que o tempo de execução do CLR permite.


Bata comigo :) Eu encontrei este problema com o meu Razor Engine (o precursor do razorengine.codeplex.com ) #
Buildstarted

Esta não é realmente uma resposta, não estamos dizendo mais sobre a "resposta aceita"!
Adaptabi

4
@DotNetWise: Explica por que o erro ocorre, e essa foi a pergunta. Você também terá o meu upvote para fornecer uma solução alternativa nice :)
Lucas

FYI: esta resposta agora está muito desatualizada - como o autor se diz em vermelho no início da postagem do blog referenciado
Simon_Weaver

@ Simon_Weaver Mas a atualização posterior não explica como deve funcionar no MVC3 +. - Encontrei o mesmo problema no MVC 4. Algum ponteiro sobre a maneira atualmente 'abençoada' de usar dinâmica?
Cristian Diaconescu 8/08/13

24

Usar o método ToExpando é a melhor solução.

Aqui está a versão que não requer a montagem 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
É uma resposta melhor. Não tenho certeza se o que o HtmlHelper faz com sublinhados na resposta alternativa.
Den

+1 para resposta de propósito geral, este é útil fora da ASP / MVC
codenheim

e as propriedades dinâmicas aninhadas? eles continuarão dinâmicos ... por exemplo: `{foo:" foo ", nestedDynamic: {blah:" blah "}}
sports

16

Em vez de criar um modelo a partir de um tipo anônimo e tentar converter o objeto anônimo para um ExpandoObjectcomo este ...

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

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

Você pode apenas criar o ExpandoObjectdiretamente:

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

return View(model);

Em sua opinião, você define o tipo de modelo como dinâmico @model dynamice pode acessar as propriedades diretamente:

@Model.Profile.Name
@Model.Foo

Normalmente, eu recomendaria modelos de visualização com tipos fortes para a maioria das visualizações, mas às vezes essa flexibilidade é útil.


@ yhal você certamente poderia - eu acho que é preferência pessoal. Eu prefiro usar ViewBag para dados página Diversos geralmente não relacionadas com o modelo de página - talvez relacionadas com o modelo e mantê-modelo como o modelo primário
Simon_Weaver

2
BTW você não tem que adicionar dinâmica @model, como é o padrão
halb yoel

exatamente o que eu precisava, implementando o método para Anon convertido objs para expando objetos estava tomando muito tempo ...... graças montes
h-rai

5

Você pode usar a interface de improviso da estrutura para agrupar um tipo anônimo em uma interface.

Você retornaria um IEnumerable<IMadeUpInterface>e, no final do Linq, usará .AllActLike<IMadeUpInterface>();isso porque chama a propriedade anônima usando o DLR com um contexto do assembly que declarou o tipo anônimo.


1
Truque impressionante :) Não sei se é melhor do que apenas uma classe simples com várias propriedades públicas, pelo menos nesse caso.
Andrew Backer

4

Escreva um aplicativo de console e adicione Mono.Cecil como referência (agora você pode adicioná-lo no NuGet ) e depois escreva o trecho 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
    });
}

O código acima obteria o arquivo de montagem das entradas args e usaria o Mono.Cecil para alterar a acessibilidade de interna para pública, e isso resolveria o problema.

Podemos executar o programa no evento Post Build do site. Eu escrevi um post sobre isso em chinês, mas acredito que você pode apenas ler o código e os instantâneos. :)


2

Com base na resposta aceita, substituí o controlador para fazê-lo funcionar em geral e nos bastidores.

Aqui está o 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");
       }
    }
}

Agora você pode simplesmente passar um objeto anônimo como modelo, e ele funcionará conforme o esperado.



0

O motivo de RuntimeBinderException acionado, acho que tem uma boa resposta em outras postagens. Eu apenas me concentro em explicar como eu realmente faço isso funcionar.

Consulte as respostas às visualizações @DotNetWise e Binding com coleção de tipos anônimos no ASP.NET MVC ,

Em primeiro lugar, crie uma classe estática para extensão

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

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

Em View, @model IEnumerable (dinâmico, não uma classe de modelo), isso é muito importante, pois vamos vincular o 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>
}

O tipo no foreach, não tenho erro usando var ou dinâmico .

A propósito, criar um novo ViewModel que corresponda aos novos campos também pode ser o caminho para passar o resultado para a visualização.


0

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

0

O uso da extensão ExpandoObject funciona, mas é interrompido ao usar objetos anônimos aninhados.

Tal como

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

var workitem = WorkBL.Get(id);

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

Para conseguir isso, eu uso isso.

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

O uso no controlador é o mesmo, exceto que você usa ToRazorDynamic () em vez de ToExpando ().

Na sua opinião, para obter todo o objeto anônimo, basta adicionar ".AnonValue" ao final.

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

0

Eu tentei o ExpandoObject, mas não funcionou com um tipo complexo anônimo aninhado como este:

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

Portanto, minha solução foi retornar um modelo JObject para View:

return View(JObject.FromObject(model));

e converta para dinâmico em .cshtml:

@using Newtonsoft.Json.Linq;
@model JObject

@{
    dynamic model = (dynamic)Model;
}
<span>Value of child is: @model.child.value</span>
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.