开发者

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 ass开发者_如何学运维embly 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?


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.


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


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


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
}


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.


ASP.NET MVC dynamic view sections

How about this?

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜