ASP.NET MVC 3 Razor视图 -> 强类型元组列表问题

37

我在asp.net MVC razor视图中遇到了一个奇怪的问题。

我想要我的模型是一个List<Tuple<string, int, int, int, int>>,这在我的其他c#方法中完全有效。但当我把它粘贴到@model声明中时,它似乎只选择元组的字符串部分。因此我没有整数。只有item1。

如果我让它绑定到一个元组而不是列表,这个问题就不存在了。

看起来生成的代码是错误的,所以这可能是razor视图中的一个bug吗?

编译时出现的错误是:

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

为了分离这个问题,我做了以下的事情:

创建一个空的asp.net mvc项目。创建一个新视图。粘贴以下代码。

@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>
}

我知道我可以创建一个类或结构体来保存数据。只是出于好奇问一下。

编辑: 已解决并在这里向MVC团队报告 http://aspnet.codeplex.com/workitem/8652


6
真是一个好问题!这似乎是 Razor 编译器中的一个错误。我自己尝试了一下,发现生成的代码是 public class _Page_Views_Home_Index_cshtml : System.Web.Mvc.WebViewPage<global::System.Collections.Generic.List<global::System.Tuple<string {(我已经尝试使用 global:: 前缀来让它工作)。看起来 Razor 解析 @model 指令在第一个泛型参数后失败了。 - Andras Zoltan
@Andras 是的,我也是这么想的。看起来它在第五个元组参数处崩溃了。4可以工作。 - Oskar Kjellin
解决了问题。但并不意味着你可以使用五个参数的泛型,因为CodeTypeReference中存在一个错误 - 请参见我的更新(现在已经修剪过)的答案。 - Andras Zoltan
如果你的模型看起来像 @model List<Tuple<string, int, int, int, int>>,那么现在是时候使用视图模型类了,这样可以避免让其他程序员在理解你的代码时头疼不已!在这种情况下,元组是一种代码异味。 - Tim Abell
6个回答

36

编辑:我已经删除了一些正在进行中的评论-只需查看历史记录即可。

因此,您可以使用1、2、3或4个元组泛型参数使其工作,但是不适用于5个。 一旦您使用5个参数,它就会生成以下代码:

 public class _Page_Views_Home_Index_cshtml : 
   System.Web.Mvc.WebViewPage<List<System.Tuple<string {

我想知道是否有字符长度限制,因此我创建了一个类似这样的类:

namespace ASP{  //same namespace that the backend code for the page is generated
  public class T { } 
}

并更改了模型声明:

@model List<Tuple<T,T,T,T,T>>.

最终(请参阅历史记录),我到达了。
@inherits System.Web.Mvc.WebViewPage<Tuple<T,T,T,T,T>>

同样的问题!这不是与@model关键字有关的问题...

花了一些时间(阅读MVC3和Razor源代码,在该解决方案中添加了一些测试)-但是这里有一个测试,显示我们为什么会出现此错误:

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

所以,四个参数的版本通过了,但五个参数的版本没有通过。
而且,你猜怎么着?实际值是Tuple - 也就是一个被截断的泛型类型名称,截断方式与你在代码中观察到的完全相同
标准的Razor解析器和Mvc Razor解析器都在解析@inherits或@model关键字时使用CodeTypeReferenceCollection类型。以下是@inherits在代码生成期间的代码:
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.BaseTypesCodeTypeReferenceCollection类型的集合,而span.BaseClass是一个字符串。通过ILSpy追踪发现,有问题的方法应该是私有方法CodeTypeReference.Initialize(string typeName, CodeTypeReferenceOptions options)。我现在没有足够的时间去弄清楚为什么会出错,但这应该是Microsoft开发人员的工作。

底线

在Razor的@inherits@model语句中,不能使用具有4个以上参数的泛型(至少在C#中是如此,不知道VB)。看起来Razor解析器错误地使用了CodeTypeReference类型。

最终更新 - 或者说,我非常专注 :)

CodeTypeReference所做的一件事是使用CodeTypeReference.RipOffAssemblyInformationFromTypeName(string typeName)方法从传递的类型名称中剥离程序集名称信息。

当然,如果你想一想——Tuple<T,T,T,T,T>就像一个程序集限定的类型名称:类型名称=Tuple<T,程序集=T,版本=T,区域设置=T,公钥标记=T(如果你编写了一个非常糟糕的C#解析器!)。

果然,如果你把Tuple<T,T,T,T,T,T>作为类型名称传递进去,实际上你会得到一个Tuple<T,T>

深入代码,发现它准备接收一个语言中性的类型名称(处理'['但对于'<'没有任何处理),所以MVC团队不应该直接将我们源代码中的C#类型名称传递给它。

MVC团队需要改变它们生成基础类型的方式——他们可以使用一个新引用的public CodeTypeReference(string typeName, params CodeTypeReference[] typeArguments)构造函数(而不是依赖于.Add(span.BaseClass)创建它),并解析出泛型参数,因为他们知道类型名称将是C#/VB风格的——不是带括号等符号的语言中性的.NET风格的实际名称。


@Andreas Zoltan,厉害了!我应该向微软报告这个问题吗? - Oskar Kjellin
@Oskar - 需要向MVC3团队报告这个问题,他们没有正确地使用CodeTypeReference类... - Andras Zoltan
嘿@Oskar,谢谢。这是一个很好的问题,我拆解它时非常开心!天啊,我需要有自己的生活! :) - Andras Zoltan
1
@Andras 这就是生活。大多数人只是看不到。 - Oskar Kjellin
刚遇到了这个问题。很遗憾你必须在 CodePlex 上注册才能投票,我猜这会让我望而却步。 - lko
显示剩余2条评论

8
这个问题已经在内部讨论了几次,但很遗憾最终的结果是我们不能为Razor 2.0(MVC 4)修复这个问题。让我简单介绍一下背后的原因。
首先,需要注意的是Razor解析器故意尽可能少地解析C#代码。这样做的主要原因是为了给你自由地使用你想要的C#代码,而不是让我们阻碍你。因此(你可以通过查看代码自己验证!),我们不解析@inherits@model指令中的类型名称,我们只是运行到行末。我们还是Razor编辑器的解析引擎,这意味着我们必须支持部分完成的语句,例如@model Foo<Bar,虽然这是技术上无效的,但如果你正在输入@model Foo<Bar>,这将是一个预期的中间步骤,所以我们应该能够处理它。
现在,我们需要考虑如果我们决定改变代码生成方式会发生什么。正如CodeTypeReference文档所述,我们将不得不使用1[[ ... ]]语法来定义泛型。然而,我们仍然只是插入用户输入的内容,因此如果你输入@model Foo<Bar>,我们会告诉CodeDOM基础类型是类似于System.Web.Mvc.WebViewPage`1[[Foo<Bar>]]的东西。正如你所看到的,我们仍然得到了类型名称中的<>。因此,我们决定利用CodeDOM通常不会抱怨<>和VB中的(Of ...)语法来解决这个问题。
即使解析你提供的整个类型名称也很困难,因为我们必须处理不完整的语句,例如@model Foo<Bar, Baz。事实上,这也会使编辑器非常脆弱,因为编辑器实际上依赖于我们能够告诉它们Razor文本的哪个范围映射到了生成的C#/VB代码的哪个范围,如果我们引入了其他的翻译层(例如CodeDOM将使用的翻译[]或CodeTypeReference构造函数的其他重载),我们将无法向编辑器保证这些,你会看到奇怪的行为。
因此,我们只能采用解决方法,即避免使用这么多的通用参数。实际上,有很多理由避免使用Tuple,因为使用自定义模型类可以让您命名涉及的属性,并在添加属性时提供更大的灵活性(对于元组,当您想要将“属性”添加到元组时,必须更新Controller和View)。话虽如此,我们正在关注这个问题,并在思考如何在4.0后更好地处理它。现在我们已经开源了,我们很乐意听取您的建议,甚至接受您的代码!
如果您有任何问题,请随时通过我的SO个人资料上的电子邮件与我联系或在评论中继续讨论。我只是想为您提供背景信息,以表彰您在追踪此问题方面做出的出色工作!
-Andrew Nurse (Razor解析器的开发者)

2
我知道已经很久了,但是感谢这个更新,即使它不是我们想要听到的 :-) - Andras Zoltan
刚刚遇到了这个问题...真的那么难吗?Razor实际上是我见过的最聪明的解析器,却无法解析这个?我很震惊。 - Bart Calixto
我赞同建议转换为适当的视图模型。 - Tim Abell

1

今天我遇到了这个问题。我正在返回有关用户活动的一些信息,并尝试使用模型定义

@model List<Tuple<string,bool,DateTime,DateTime,DateTime>>
@* Name, Online, Created, Login, Active *@

原因是我已经厌倦了为视图模型创建单一使用类,所以我会为简单的用途这样做。我收到了与您相同的错误。我尝试通过在@model中使用不同组合的元组来绕过错误,但没有成功。
最终起作用的是简单地使用ViewBag。需要注意的是,model已经保存在ViewBag中,因此在此容量中使用它没有任何问题。
在我的actionresult方法中,我只需将元组列表分配给一个viewbag值即可。
ViewBag.listTuple = listOfTuples;

在视图中,我将其转换回来。
@{
    List<Tuple<string,bool,DateTime,DateTime,DateTime>> tuples = ViewBag.listTuple;
}

就是这样。运行得很好。我并不是说这是完美的解决方案,但它是一个可行的解决方法


0

只是想指出,如果您删除@model指令,您将没有智能感知,实际上谁在乎,但视图可以完美地处理任何数量的元组参数。


0
对于所有可能因为 CS1003 错误在 Razor 视图寻找源的人,CS1003 错误是 MVC 无法解析 Razor 语法的一般症状。这可能由许多问题引起,包括上面提到的通用元组问题。
我注意到的一件事是,如果您尝试在 Razor 视图中使用 C# 风格的单行注释来注释模型声明,则会出现此问题:
BAD:  @model Int32 // user ID

GOOD: @* user ID *@
      @model Int32

Visual Studio的语法高亮不会标记它为问题,但在运行时将失败。

这与问题所涉及的内容无关。 - Oskar Kjellin

0
我遇到了这个问题,因为我正在使用一个包含TuplesTuple
@model Tuple<Tuple<IEnumerable<int>, int, int>, Tuple<float, float, float>>

不用说,Razor不喜欢那样。无论如何,我认为有趣的是解析器在总类型数量而不是顶级对象类型数量上进行分解。

我的解决方法

最终,我采用了一个视图模型,它简单地为内部元组创建了占位符:

public class ViewModel
{
    public Tuple<IEnumerable<int>, int, int> Models { get; set; }
    public Tuple<float, float, float> Selected { get; set; }
}

我很高兴我创建了视图模型。在视图内部,它更易读(略微)且更容易操作和理解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接