问题
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?
回答1:
Okay sorry I just don't agree that this is horrible etc etc. Yes I would never consider doing this stuff on a production site - but for a prototyping, non-commercial, internal-only, for-testing-purposes I think using this approach is perfectly valid.
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 possibly get away with grabbing the publicly readable members out of the anonymous type (possibly relies on compiler internals - 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))
{
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());
And do you know what - I know it's not efficient, and most people will say it's ugly; but it'll saved me time and allow me to focus on writing features in my test site for my QA team to use in testing the real thing (whose code is, of course immaculate throughout :) ).
回答2:
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
回答3:
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 ExpandoObject
s (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));
回答4:
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.
回答5:
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
}
回答6:
ASP.NET MVC dynamic view sections
How about this?
来源:https://stackoverflow.com/questions/7429957/dynamic-view-of-anonymous-type-missing-member-issue-mvc3