Andras Zoltan
Andras Zoltan

Reputation: 42333

Dynamic view of anonymous type missing member issue - MVC3

I have an MVC3 site that I've setup for testing another site - most of it has been quick and dirty, and so I've not gone to town creating model and view model types for all the views - only where input is requried from the user.

Okay so I have a controller method that projects a Linq sequence and sets it into ViewBag.

ViewBag.SomeData = Enumerable.Range(1,10).Select(i=> new { Value = i });

In my view (Razor C#) I then want to read this - quite simple:

@foreach(dynamic item in ViewBag.SomeData)
{
  @:Number: @item.i
}

Except, of course, I get a RuntimeBinderException because the anonymous type created in the controller is internal to the web project's output assembly and the actual Razor code here will be running in a different assembly generated by the build manager so, all in all DENIED!

Obviously a 'proper' model type would solve the issue - but let's say I simply don't want to do that because that's my prerogative(!) - how best to keep the code to a minimum and retain the dynamic-ness here?

Upvotes: 4

Views: 1813

Answers (6)

Andras Zoltan
Andras Zoltan

Reputation: 42333

This is what I've done (and I stress this only really addresses the issue adequately for anonymous types); lifting the members out and pushing them into an ExpandoObject.

My initial change was to make the projection a multi-statement that returns an ExpandoObject:

ViewBag.SomeData = Enumerable.Range(1,10).Select(i=> {
  var toReturn = new ExpandoObject();
  toReturn.Value = i;
  return toReturn;
});

Which is almost as short as the anonymous type just not as clean.

But then I wondered if I could grab the publicly readable members out of the anonymous type (the type is internal, but the properties it generates are public):

public static class SO7429957
{
  public static dynamic ToSafeDynamic(this object obj)
  {
    //would be nice to restrict to anonymous types - but alas no.
    IDictionary<string, object> toReturn = new ExpandoObject();
  
    foreach (var prop in obj.GetType().GetProperties(
      BindingFlags.Public | BindingFlags.Instance)
      .Where(p => p.CanRead)) // watch out for types with indexers 
    {
      toReturn[prop.Name] = prop.GetValue(obj, null);
    }

    return toReturn;
  }
}

Which means I can then use my original code - but with a little extension method call tagged on the end:

ViewBag.SomeData=Enumerable.Range(1,10).Select(i=> new { Value = i }.ToSafeDynamic());

It's not efficient, and it should definitely not be used for serious production code. But it's a good time saver.

Upvotes: 4

Matt Robb
Matt Robb

Reputation: 11

If you're just going for pure quick'n'dirty, you can always use Tuples in situations like this.

ViewBag.SomeData = Enumerable.Range(1,10).Select(i=> new Tuple<int>(i);

-

@foreach(dynamic item in ViewBag.SomeData)
{
    @:Number: @item.Item1
}

Upvotes: 1

jbtule
jbtule

Reputation: 31799

I think ToSafeDynamic is a good solution. But I'd like to share some other options using the open source ImpromptuInterface (in nuget) that would work well in this situation.

One option, being a DynamicObject based proxy, ImpromptuGet which will just forward calls to the annonymous type, similar to using dynamic (it uses the same api's except it sets the context to the anonymous type's self so the internal access doesn't matter).

ViewBag.SomeData = Enumerable.Range(1,10)
                           .Select(i => ImpromptuGet.Create(new { Value = i }));

This option doesn't look as clean as ToSafeDynamic, but it has a small distinction in that it is only calling the properties when they are used, rather than copying all the data upfront.

However, a better solution from ImpromptuInterface is it's quick syntax for creating prototype dynamic objects.

ViewBag.SomeData = Enumerable.Range(1,10).Select(i => Build.NewObject(Value:i));

that will create an expando-like object, but you also have the option to create literal ExpandoObjects (which in my testing offer the same performance on getters as POCO objects cast to dynamic).

ViewBag.SomeData = Enumerable.Range(1,10)
                           .Select(i => Build<ExpandoObject>.NewObject(Value:i));

Also the creation setup portion can be stored in a temporary variable or fields to shorten the lambda even more.

var Expando =Build<ExpandoObject>.NewObject;
ViewBag.SomeData = Enumerable.Range(1,10).Select(i => Expando(Value:i));

Upvotes: 1

takepara
takepara

Reputation: 10433

ASP.NET MVC dynamic view sections

How about this?

Upvotes: -1

Darin Dimitrov
Darin Dimitrov

Reputation: 1038730

Obviously a 'proper' model type would solve the issue - but let's say I simply don't want to do that because that's my prerogative(!)

You cannot have such prerogative. Sorry, there is absolutely no excuse for this :-) Not to mention that what you are trying to achieve is impossible because dynamic types are internal to the declaring assembly. Razor views are compiled into a separate dynamic assembly at runtime by the ASP.NET engine.

So back to the topic: never pass anonymous objects as models to views. Always define use view models. Like this:

public class MyViewModel
{
    public int Value { get; set; }
}

and then:

public ActionResult Index()
{
    var model = Enumerable.Range(1, 10).Select(i => new MyViewModel { Value = i });
    return View(model);
}

and then use a strongly typed view:

@model IEnumerable<MyViewModel>
@Html.DisplayForModel()

and inside the corresponding display template which will automatically be rendered for each element of the collection so that you don't have to write any loops in your views (~/Views/Shared/DisplayTemplates/MyViewModel.cshtml):

@model MyViewModel
@:Number: @Html.DisplayFor(x => x.Value)

Things that we have improved from the initial version:

  • We have strongly typed views with Intellisense (and if you activate compilation for views even compile time safety)
  • Usage of strongly typed view models adapted to the specific requirements of your views.
  • Getting rid of ViewBag/ViewData which imply weak typing
  • Usage of display templates which avoid you writing ugly loops in your views => you rely on conventions and the framework does the rest

Upvotes: 1

Jon Skeet
Jon Skeet

Reputation: 1500225

Do you know the name of the assembly generated by the build manager? If so, you should be able to apply InternalsVisibleTo in the controller assembly, and it will all work fine.

EDIT: One possible solution: create a public type extending DynamicObject in the model assembly which proxies any requests for properties down to your anonymous type via reflection. It would be ugly, but I think it should work... effectively making the anonymous types public.

Upvotes: 0

Related Questions