Dynamiczny typ anonimowy w Razor powoduje RuntimeBinderException

156

Otrzymuję następujący błąd:

„obiekt” nie zawiera definicji dla „RatingName”

Kiedy patrzysz na anonimowy typ dynamiczny, wyraźnie ma on RatingName.

Zrzut ekranu błędu

Zdaję sobie sprawę, że mogę to zrobić za pomocą krotki, ale chciałbym zrozumieć, dlaczego pojawia się komunikat o błędzie.

JarrettV
źródło

Odpowiedzi:

240

Moim zdaniem anonimowe typy posiadające właściwości wewnętrzne to kiepska decyzja dotycząca projektowania frameworka .NET.

Oto szybkie i przyjemne rozszerzenie, które rozwiązuje ten problem, np. Poprzez natychmiastową konwersję anonimowego obiektu na 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;
}

Jest bardzo łatwy w użyciu:

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

Oczywiście Twoim zdaniem:

@foreach (var item in Model) {
     <div>x = @item.x, y = @item.y</div>
}
Adaptabi
źródło
2
+1 Szukałem konkretnie HtmlHelper.AnonymousObjectToHtmlAttributes Wiedziałem, że to absolutnie musiało się już upiec i nie chciałem wymyślać koła na nowo z podobnym ręcznie wyrzucanym kodem.
Chris Marisic
3
Jaka jest wydajność w tym przypadku w porównaniu do prostego tworzenia silnie wpisanego modelu podkładowego?
GONeale
@DotNetWise, Dlaczego miałbyś używać HtmlHelper.AnonymousObjectToHtmlAttributes, skoro możesz po prostu zrobić IDictionary <string, object> anonymousDictionary = new RouteDictionary (object)?
Jeremy Boyd
Przetestowałem HtmlHelper.AnonymousObjectToHtmlAttributes i działa zgodnie z oczekiwaniami. Twoje rozwiązanie również może działać. Użyj tego, co wydaje się łatwiejsze :)
Adaptabi,
Jeśli chcesz, aby było to trwałe rozwiązanie, możesz również po prostu nadpisać zachowanie w kontrolerze, ale wymaga to kilku dodatkowych obejść, takich jak możliwość samodzielnego identyfikowania typów anonimowych i tworzenia słownika ciągów / obiektów na podstawie typu. Jeśli to zrobisz, możesz to zmienić w: protected override System.Web.Mvc.ViewResult View (string viewName, string masterName, object model)
Johny Skovdal
50

Znalazłem odpowiedź w pokrewnym pytaniu . Odpowiedź znajduje się w poście na blogu Davida Ebbo. Przekazywanie anonimowych obiektów do widoków MVC i uzyskiwanie do nich dostępu przy użyciu dynamiki

Przyczyną tego jest to, że typ anonimowy jest przekazywany w kontrolerze wewnętrznie, więc można uzyskać do niego dostęp tylko z poziomu zestawu, w którym jest zadeklarowany. Ponieważ widoki są kompilowane osobno, spinacz dynamiczny skarży się, że nie może przekroczyć tej granicy montażu.

Ale jeśli się nad tym zastanowić, to ograniczenie ze strony spoiwa dynamicznego jest w rzeczywistości dość sztuczne, ponieważ jeśli używasz refleksji prywatnej, nic nie stoi na przeszkodzie, aby uzyskać dostęp do tych wewnętrznych członków (tak, działa nawet w średnim zaufaniu). Tak więc domyślny spinacz dynamiczny robi wszystko, co w jego mocy, aby wymusić reguły kompilacji C # (gdzie nie można uzyskać dostępu do wewnętrznych elementów członkowskich), zamiast pozwalać na robienie tego, na co pozwala środowisko uruchomieniowe CLR.

JarrettV
źródło
Pokonaj mnie :) Natknąłem się na ten problem z moim silnikiem Razor (prekursorem tego na razorengine.codeplex.com )
Buildstarted
To nie jest tak naprawdę odpowiedź, nie mówiąc więcej o „zaakceptowanej odpowiedzi”!
Adaptabi
4
@DotNetWise: Wyjaśnia, dlaczego pojawia się błąd, o który był pytany. Otrzymujesz również moje poparcie za zapewnienie fajnego obejścia :)
Lucas
Do Twojej wiadomości: ta odpowiedź jest teraz bardzo nieaktualna - jak mówi sam autor na czerwono na początku odnośnego wpisu na blogu
Simon_Weaver
@Simon_Weaver Ale aktualizacja post nie wyjaśnia, jak to powinno działać w MVC3 +. - Mam ten sam problem w MVC 4. Czy są jakieś wskazówki dotyczące obecnie „błogosławionego” sposobu używania dynamiki?
Cristian Diaconescu
24

Korzystanie ToExpando metoda jest najlepszym rozwiązaniem.

Oto wersja, która nie wymaga montażu 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
źródło
1
To lepsza odpowiedź. Nie jestem pewien, czy podoba się to, co HtmlHelper robi z podkreśleniami w alternatywnej odpowiedzi.
Den
+1 dla odpowiedzi ogólnego przeznaczenia, jest to przydatne poza ASP / MVC
codenheim
a co z zagnieżdżonymi właściwościami dynamicznymi? nadal będą dynamiczne ... np .: `{foo:" foo ", nestedDynamic: {blah:" blah "}}
sport
16

Zamiast tworzyć model z anonimowego typu, a następnie próbować przekonwertować anonimowy obiekt na ExpandoObjectpodobny ...

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

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

Możesz po prostu utworzyć ExpandoObjectbezpośrednio:

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

return View(model);

Następnie w swoim widoku ustawiasz typ modelu jako dynamiczny @model dynamici masz bezpośredni dostęp do właściwości:

@Model.Profile.Name
@Model.Foo

Zwykle zalecam silnie wpisane modele widoków dla większości widoków, ale czasami ta elastyczność jest przydatna.

Simon_Weaver
źródło
@yohal z pewnością możesz - myślę, że to osobiste preferencje. Wolę używać ViewBag do różnych danych strony ogólnie niezwiązanych z modelem strony - być może związanych z szablonem i zachowaj Model jako model podstawowy
Simon_Weaver
2
BTW nie musisz dodawać dynamiki @model, ponieważ jest to ustawienie domyślne
yoel halb
dokładnie to, czego potrzebowałem, implementacja metody konwertowania obiektów anon na obiekty expando zajmowała zbyt dużo czasu ...... dzięki sterty
h-rai
5

Możesz użyć impromptu frameworka do zawijania anonimowego typu w interfejsie.

Po prostu zwróciłbyś IEnumerable<IMadeUpInterface>i na końcu swojego Linq użyj .AllActLike<IMadeUpInterface>();tego, co działa, ponieważ wywołuje anonimową właściwość przy użyciu DLR z kontekstem zestawu, który zadeklarował typ anonimowy.

jbtule
źródło
1
Niesamowita mała sztuczka :) Nie wiem, czy jest lepsza niż zwykła klasa z kilkoma publicznymi właściwościami, przynajmniej w tym przypadku.
Andrew Backer,
4

Napisałem aplikację konsolową i dodaj Mono.Cecil jako odniesienie (możesz teraz dodać go z NuGet ), a następnie napisz fragment kodu:

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

Powyższy kod pobierze plik zestawu z argumentów wejściowych i użyje Mono.Cecil do zmiany dostępności z wewnętrznej na publiczną, a to rozwiązałoby problem.

Program możemy uruchomić w ramach wydarzenia Post Build witryny. Napisałem o tym post na blogu po chińsku, ale wydaje mi się, że możesz po prostu przeczytać kod i migawki. :)

Jeffrey Zhao
źródło
2

W oparciu o zaakceptowaną odpowiedź zastąpiłem w kontrolerze, aby działał ogólnie i za kulisami.

Oto kod:

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

Teraz możesz po prostu przekazać anonimowy obiekt jako model i będzie działać zgodnie z oczekiwaniami.

yoel halb
źródło
0

Zamierzam trochę ukraść z https://stackoverflow.com/a/7478600/37055

Jeśli zainstalujesz pakiet dynamitey, możesz to zrobić:

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

A chłopi się cieszą.

Chris Marisic
źródło
0

Powód uruchomienia RuntimeBinderException, myślę, że w innych postach jest dobra odpowiedź. Skupiam się tylko na wyjaśnieniu, jak to właściwie działa.

Odwołaj się do odpowiedzi @DotNetWise i widoków Binding z kolekcją typu Anonymous w ASP.NET MVC ,

Po pierwsze, utwórz klasę statyczną do rozszerzenia

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

W kontrolerze

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

W widoku @model IEnumerable (dynamiczny, a nie klasa modelu) jest to bardzo ważne, ponieważ zamierzamy powiązać obiekt typu anonimowego.

@model IEnumerable<dynamic>

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

Typ w foreach, nie mam błędu ani przy użyciu var ani dynamic .

Nawiasem mówiąc, utwórz nowy ViewModel, który jest zgodny z nowymi polami, może być również sposobem przekazania wyniku do widoku.

V-SHY
źródło
0

Teraz w stylu rekurencyjnym

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
źródło
0

Korzystanie z rozszerzenia ExpandoObject działa, ale nie działa, gdy używane są zagnieżdżone obiekty anonimowe.

Jak na przykład

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

var workitem = WorkBL.Get(id);

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

Aby to osiągnąć, używam tego.

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

Użycie w kontrolerze jest takie samo, z wyjątkiem użycia ToRazorDynamic () zamiast ToExpando ().

Aby uzyskać cały anonimowy obiekt, po prostu dodaj na końcu „.AnonValue”.

var project = @(Html.Raw(JsonConvert.SerializeObject(Model.Project.AnonValue)));
var projectName = @Model.Project.Name;
Donny V.
źródło
0

Wypróbowałem ExpandoObject, ale nie działało to z zagnieżdżonym anonimowym typem złożonym, takim jak ten:

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

Więc moim rozwiązaniem było zwrócenie modelu JObject do View:

return View(JObject.FromObject(model));

i przekonwertuj na dynamiczny w .cshtml:

@using Newtonsoft.Json.Linq;
@model JObject

@{
    dynamic model = (dynamic)Model;
}
<span>Value of child is: @model.child.value</span>
Guilherme Muniz
źródło