I'm having an odd problem with asp.net MVC razor view.
I want my model to be a List<Tuple<string, int, int, int, int>>
which is perfectly valid in my other c# methods. But when I paste it into the @model
declaration it seems to only pick out the string part of the tuple. So I have no ints. Only item1.
This problem is not present if I make it bind to a Tuple instead of the List.
Seems like the generated code is wrong, so perhaps this is a bug in razor view?
The error I get at compilation is:
Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.
Compiler Error Message: CS1003: Syntax error, '>' expected
Source Error:
Line 27:
Line 28:
Line 29: public class _Page_Views_Dashboard_DestinationStatistics_cshtml : System.Web.Mvc.WebViewPage<List<Tuple<string {
Line 30:
Line 31: #line hidden
To isolate this problem I did the following thing:
Create an empty asp.net mvc project. Create a new view. Past the following code.
@model List<Tuple<string, int, int, int, int>>
@foreach (var stat in Model)
{
<tr>
<td>
@stat.Item1
</td>
<td>
@stat.Item2
</td>
<td>
@stat.Item3
</td>
<td>
@stat.Item4
</td>
<td>
@stat.Item5
</td>
</tr>
}
I know I can just create a class or a struct to hold the data instead. Just asking out of curiosity
EDIT: Solved and reported to the MVC team here http://aspnet.codeplex.com/workitem/8652
This has been through a few waves of discussion internally now and I'm afraid the final product is that we're not going to be able to fix this for Razor 2.0 (MVC 4). Let me give a little background on the reasoning.
First, it's important to note that the Razor parser intentionally parses the C# as little as possible. The main reason for this is to give you the freedom to the C# you want without us getting in the way. As a result (and you can verify this yourself now by checking out the code!) we do not parse type names in the @inherits
and @model
directives, we simply run until the end of the line. We are also the parsing engine behind the Razor editor, which means we have to support partially complete statements like @model Foo<Bar
which is technically invalid but if you were typing @model Foo<Bar>
this would be an expected intermediate step, so we should be able to handle it.
Now, we need to consider what would happen in we decided to change the way we generate code. As the CodeTypeReference documentation states, we would have to use 1[[ ... ]]
syntax to define the generic. However, we would still just be inserting whatever the user typed, so if you typed @model Foo<Bar>
we'd tell CodeDOM that the base type was something like System.Web.Mvc.WebViewPage`1[[Foo<Bar>]]
. As you can see, we still end up with <>
in the type name. As a result, we made the decision to use the fact that CodeDOM generally doesn't complain about <>
and (Of ...)
(in VB) syntax to hack our way around this problem.
Even parsing the whole type name you provided would be difficult given that we'd have to handle incomplete statements like @model Foo<Bar, Baz
. In fact, it would also make the editor very brittle, since the editor actually depends on us being able to tell them exactly what range of Razor text maps to what range of C#/VB generated code, and if we introduce additional translation layers (such as the translation CodeDOM will be doing if we use []
or even the other overloads of the CodeTypeReference constructor) we can no longer make those assurances to the editor and you'll see strange behavior
So that leaves us with the workaround, which is to simply avoid using this many generic arguments. There are actually a number of reasons to avoid using Tuple in this way since using a custom model class would allow you to name the properties involved, and would give you greater flexibility when adding properties (with a tuple, you have to update the Controller and View when you want to add a "property" to your tuple). Having said that, we are keeping an eye on this issue and looking at how we can do a better job with this after 4.0. And now that we're open-source, we'd be happy to hear your suggestions and even accept your code!
Please don't hesitate to contact me (email is on my SO profile) or continue discussing this in comments if you have questions. I just wanted to give you the background context you deserve for having put so much excellent work into tracking this down!
-Andrew Nurse (Dev on Razor parser)
EDIT I've trimmed out some of the in-progress comments here - just view the history to see.
So you can make this work with 1, 2, 3 or 4 tuple generic parameters but it doesn't work with 5. As soon as you use 5 parameters it generates code like this:
public class _Page_Views_Home_Index_cshtml :
System.Web.Mvc.WebViewPage<List<System.Tuple<string {
I wanted to just find out if it's a character-length limitation, so I generated a class like this:
namespace ASP{ //same namespace that the backend code for the page is generated
public class T { }
}
And changed the model declaration:
@model List<Tuple<T,T,T,T,T>>.
In the end (see the history) I got to
@inherits System.Web.Mvc.WebViewPage<Tuple<T,T,T,T,T>>
Same problem! It's not a problem with the @model keyword...
It took a while (reading through the MVC3 and Razor source, adding a couple of tests to that solution) - but here's a test that shows the why we get this error:
[TestMethod]
public void TestMethod()
{
System.CodeDom.CodeTypeReferenceCollection c =
new CodeDom.CodeTypeReferenceCollection();
c.Add("Tuple<T,T,T,T>");
c.Add("Tuple<T,T,T,T,T>");
//passes
Assert.AreEqual("Tuple<T,T,T,T>", c[0].BaseType);
//fails
Assert.AreEqual("Tuple<T,T,T,T,T>", c[1].BaseType);
}
So - the four-parameter version passes, but not the 5 parameter version.
And guess what- the actual value is Tuple<T
- i.e. a truncated generic type name truncated exactly the same way that you've observed in your code.
Both the standard Razor parser and the Mvc Razor parser use the CodeTypeReferenceCollection
type when parsing either the @inherits
or @model
keyword. Here's the code for @inherits
during code generation:
protected internal virtual void VisitSpan(InheritsSpan span) {
// Set the appropriate base type
GeneratedClass.BaseTypes.Clear();
GeneratedClass.BaseTypes.Add(span.BaseClass);
if (DesignTimeMode) {
WriteHelperVariable(span.Content, InheritsHelperName);
}
}
GeneratedClass.BaseTypes
is a CodeTypeReferenceCollection
- and span.BaseClass
is a string. Following that through in ILSpy, the offending method must be the private method CodeTypeReference.Initialize(string typeName, CodeTypeReferenceOptions options)
. I've not enough time now to figure out why it breaks - but then that's a Microsoft developer's job I think :) Update below - couldn't resist. I now know where it's wrong
You can't use generics with more than 4 parameters in either Razor @inherits
or @model
statements (at least in C# - don't know about VB). It appears that the Razor parser is incorrectly using the CodeTypeReference
type.
One of the things that CodeTypeReference
does is strip off assembly name information from a passed type name with a call to the method CodeTypeReference.RipOffAssemblyInformationFromTypeName(string typeName)
.
And of course, if you think about it - Tuple<T,T,T,T,T>
is just like an assembly-qualified type name: With the type name = Tuple<T
, Assembly = T
, Version=T
, Culture=T
, PublicKeyToken=T
(if you write a really BAD C# parser!).
Sure enough - if you pass in Tuple<T,T,T,T,T,T>
as the type name - you actually get a Tuple<T,T>
.
Looking deeper into the code, it's primed to receive a language-neutral typename (handles '[' but nothing for '<', for example) so, actually, the MVC team shouldn't just be handing the C# typename from our source straight through.
The MVC team needs to change how they generate the base type - They could use the public CodeTypeReference(string typeName, params CodeTypeReference[] typeArguments)
constructor for a new reference (instead of just relying on the .Add(span.BaseClass)
creating it), and parse the generic parameters themselves since they know that the type name will be C#/VB style - not language-neutral .Net style with brackets etc as part of the actual name.
I came across this problem today. I was returning some information about user activity, and had attempted to use a model definition of
@model List<Tuple<string,bool,DateTime,DateTime,DateTime>>
@* Name, Online, Created, Login, Active *@
The reason is that I am kind of tired of making single use classes for viewmodels, so that is why I do this for simple use. I received the same error as you. I attempted to bypass the error by using different combinations of tuples in the @model
but to no avail.
What did end up working was to simply use the ViewBag
. To note, the model
is held in the ViewBag
anyway so there isn't any issue using it in this capacity.
In my actionresult method, I simply assigned the list of tuples to a viewbag value
ViewBag.listTuple = listOfTuples;
and then in the view I cast i back
@{
List<Tuple<string,bool,DateTime,DateTime,DateTime>> tuples = ViewBag.listTuple;
}
And that was that. Ran just fine. I am not saying this is the perfect solution but it is a working workaround.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With