VB.NET's @@<text> tags in Razor pages issues

Viewed 490

I'm updating an old VB.Net Aspnet MVC (razor pages) web site. Updating .NET Framework version, Entity Framework, MVC.

It works pretty well, so far, except for a couple of pages that use a @@ operator. I seen that before, to escape Razor's @, in C#.

But in this case, in VB.NET, it returns an Anonymous method that takes an object and returns a System.Web.WebPages.HelperResult.

The following code :

@Code
    Dim myString As String
    myString = "mystring"
End Code

Double-At : @String.Format("{0}", @@<text>@myString</text>)
<br />
Double-At execution : @String.Format("{0}", (@@<text>Some @myString and then some</text>)(Nothing))
<br />

Outputs :

Double-At : VB$AnonymousDelegate_0`2[System.Object,System.Web.WebPages.HelperResult] 
Double-At execution : Somesome mystring and then some 

Since I'm not too used to VB.Net, I tried to find the equivalent in C# so that i can play around with it and understand it better but wasn't able to find anything similar.

Googling for VB.Net Razor Double at signs hasn't yield any meaningful answers and i still don't know what that thing is.

Right now, the website I am updating fails with generated code that doesn't compile and that generated code comes from those Double At-Signs fonctions.

And for reference, in case someone has an idea, the generated code that fails to compile, it fails because the @ I'm using inside the @@ get split up on multiple line, even tho they are a single instruction.

For example :

@(Html.Kendo().Window() _
                    .Name("SomeWindow") _
                    .Draggable() _
                    .Modal(True) _
                    .Title("") _
                    .Width(300) _
                    .Content(@@<text>
    SomeValue : @LocalizedResources.SomeValue
</text>))

Crashes at runtime compilation (when hitting the page) with the following code:

WriteTo(__razor_template_writer, 

        #ExternalSource("C:\TestProject\TestPage.vbhtml",141)


        #End ExternalSource

        ##ExternalSource("C:\TestProject\TestPage.vbhtml",141)
             LocalizedResources.SomeValue)


        #End ExternalSource

The fact that the WriteTo function's second parameter comes on another line makes the compiler spit out an error, BC30205 : End of statement expected.

Any idea what that @@ is? Any idea what it's C# equivalent would be ? Or even suggestions to get my Razor pages to generate code that actually compiles ?

2 Answers

The feature mentioned in question is known as templated Razor delegate or inline Razor template, which generate Func<T, TResult> (or Func(Of T, TResult) in VB.NET, see also What is Func, how and when is it used) and allows inserting snippet with @<tag>...</tag> format.

The common way to create templated Razor delegate uses syntax like these:

C#

@{
    string myString = "mystring";
    Func<object, HelperResult> helper = @<text>@myString</text>;
}

@String.Format("{0}", helper(myString))

VB.NET

@Code
    Dim myString As String = "mystring"
    Dim helper = (@@<text>@myString</text>)
@End Code

@String.Format("{0}", helper(myString))

The VB.NET definition actually has lambda expression like this (by applying GetType() method we can know underlying type behind the lambda):

Dim helper = Function(o As Object) 
    @<text>@myString</text>
End Function

The difference between C# and VB.NET delegates implementation is VB.NET utilizes relaxed delegate conversion by default which enables ability to assign functions (including lambdas) to delegates for non-identical parameters (even with anonymous delegate), but only available if Option Strict setting turned off. If you add @Option Strict On on the Razor view with expression like above, you will see this error:

Option Strict On requires each lambda expression parameter to be declared with an 'As' clause if its type cannot be inferred.

In short, you cannot use anonymous delegate with Option Strict On, and forced to use Func(Of Object, HelperResult) instead.

This feature is not exist in C#, an explicit conversion with Func<T, TResult> must be used to create instance from the lambda with type TResult:

var result = (Func<object, HelperResult>)(@<text>@myString</text>);

If you use var result = (@<text>@myString</text>); directly, this exception will occur because the compiler doesn't know if it should be converted to delegate:

Cannot assign lambda expression to an implicitly-typed local variable

Therefore, the same rule also apply for String.Format() method, which contains object as second parameter:

// Error: cannot convert lambda expression to type 'object' because it is not a delegate type
@String.Format("{0}", ((@<text>@myString</text>)))
@String.Format("{0}", ((@<text>@myString</text>))(null))

// Correct
@String.Format("{0}", ((Func<object, HelperResult>)(@<text>@myString</text>)))
@String.Format("{0}", ((Func<object, HelperResult>)(@<text>@myString</text>))(null))

Regarding Kendo helper problem, one of the Content() method overload inside Kendo UI WindowBuilder contains Func<object, object> delegate type as parameter:

namespace Kendo.Mvc.UI.Fluent
{
    public class WindowBuilder : WidgetBuilderBase<Window, WindowBuilder>, IHideObjectMembers
    {
        // other methods

        public WindowBuilder Content(Func<object, object> value)
        {
            base.Component.Template.InlineTemplate = value; // this is the inline template
            return this;
        }

        // other methods
    }
}

where InlineTemplate property declared as Func<T, object>:

public Func<T, object> InlineTemplate
{
    get
    {
        return this.inlineTemplate;
    }
    set
    {
        this.inlineTemplate = value;
        this.binder = delegate(T dataItem, IHtmlNode node)
        {
            // skipped for brevity 
        };
    }
}

Therefore you can pass lambda expression as parameter in CSHTML page:

@(Html.Kendo().Window()
              .Name("SomeWindow")
              .Draggable()
              .Modal(true)
              .Title("")
              .Width(300)
              .Content(@<text>SomeValue : @LocalizedResources.SomeValue</text>))

Notes:

1) VB$AnonymousDelegate and Func(Of T, TResult) are not the same type.

2) Any helpers that accepts templated Razor delegate must have TResult parameter defined as HelperResult, hence the @@ with inline template refers to Func(Of T, HelperResult).

Reference:

Templated Razor delegates

I thought maybe double-@ is required because without it, VB would think it's an inline XML document, but that didn't turn out to be the case. Anyway, @@<text>...</text> in VB Razor is the same as @<text>...</text> in C# Razor.

The VB version is creating an anonymous delegate type and casting the lambda to that type so that you don't have to write the DirectCast expression yourself. On the other hand, C# won't create an anonymous delegate type so it requires an explicit cast before it can call the lambda.

The equivalent CSHTML would be this (I'll reuse your text labels so as not to introduce noise):

Double-At : @String.Format("{0}", (Func<object,HelperResult>)(@<text>@myString</text>))
<br />
Double-At execution : @String.Format("{0}", ((Func<object,HelperResult>)(@<text>@myString</text>))(null))

Assigning a lambda to a compatibly typed variable, on the other hand, will implicitly cast. You could start by refactoring it like this:

@{
    Func<object, HelperResult> func = @<text>@myString</text>;
}

Double-At execution : @string.Format("{0}", func(null))

And then you can take it one step further to make it reusable by having it use that object parameter. If such a function were being passed as the column template for a grid component, for example, the data item for that row would be passed in the object parameter and would be called item.

@{
    Func<object, HelperResult> func = @<text>@item</text>;
}

Double-At execution(1) : @string.Format("{0}", func(myString))
<br />
Double-At execution(2) : @string.Format("{0}", func(myOtherString))

As for the Kendo component, VB definitely balks on the multi-line content but C# won't. Just un-double the @ and it should be fine in C#. For example, I couldn't do this in VB but it worked perfectly in C#:

@{
    Func<object, HelperResult> func = @<text>
...
@item
...
</text>;
}
Related