Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

asp.net mvc 3 razor view -> strongly typed List of tuple problem

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

like image 928
Oskar Kjellin Avatar asked May 23 '11 14:05

Oskar Kjellin


3 Answers

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)

like image 30
Andrew Stanton-Nurse Avatar answered Nov 13 '22 10:11

Andrew Stanton-Nurse


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

Bottom line

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.

Final Update - or, I had the bit between my teeth :)

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.

like image 109
Andras Zoltan Avatar answered Nov 13 '22 10:11

Andras Zoltan


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.

like image 30
Travis J Avatar answered Nov 13 '22 08:11

Travis J