aspnet / Razor

[Archived] Parser and code generator for CSHTML files used in view pages for MVC web apps. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
883 stars 225 forks source link

Add an HTML-formatted constructions support into Razor instead of the removed @helper directive. #715

Closed alexaku-zz closed 7 years ago

alexaku-zz commented 8 years ago

The @helper directive was removed from Razor (MVC 6) and it was not given any simple replacement. See https://github.com/aspnet/Razor/issues/281. For example, if I have define HTML code <a src="http://www.youtube.com/watch?v=4EGDxkWoUOY" title="click me"><b>MVC 6</b> documentation</a> and I would like to use it in many places of my Razor-compatible web-page, then I must create a new file - a "partial view". The entire file for one HTML-formatted string? Furthermore, this separated file is for a single web-page only. What if there are many chunks of HTML-code, that I need to reuse in a single web-page? How many "partial view" files should I spawn? How convenient to accompany this zoo? The answer is obvious.

MVC developers have provided us with TagHelpers that are going to be added to ASP.NET MVC 6. Each tag helper is a new separate file (class). Furthermore, each of these separate file can't contain an HTML markup (Razor). So, it is not a replacement for @helpers. If you remember ViewComponents, then you understand that each new ViewComponent will add two new files - "class" and "view". It is not a replacement for @helpers too.

How can I reuse some portion of the simple HTML code in my "Razor file" without adding a bunch of extra files?

Dear MVC-developers, if you retain the @functions directive in the Razor in spite of deletion of the @helper directive, then you must add HTML markup feature in their bodies (@functions). See below two @functions that could produce the identical result (if you will develop the interpretator of inlined HTML blocks).

@functions {

public HtmlString GetTableHeader1(String className) { HtmlString result = <text> <tr class="@className"> <td title="abc">one</td> <td title="xyz">two</td> </tr> </text>; return result; }

public HtmlString GetTableHeader2(String className) { HtmlString result = new HtmlString(@" <tr class=""" + Html.Encode(className) + @"""> <td title=""abc"">one</td> <td title=""xyz"">two</td> </tr> "); return result; }

}

Eilon commented 8 years ago

First, I should clarify that View Components were designed as a replacement for MVC 5.x's Child Actions, it is not at all meant to be a replacement for @helper functions.

It's also worth noting that @helper was never well-supported in MVC because the code that was generated for them was designed for ASP.NET Web Pages, not ASP.NET MVC. This meant that all the HTML helpers, context types, etc. that were available from an @helper function were not compatible with MVC. E.g. you couldn't use Model State or render an MVC HTML helper from one of them.

I think in the meantime there are several alternatives to @helper functions that are available in ASP.NET Core MVC, each with some pros and cons:

  1. Use a view component
    • Pros: Supports strongly-typed parameters
    • Cons: If using a Razor CSHTML file, it means an extra CSHTML file for each set of HTML
  2. Use an HTML partial view
    • Pros: Each view is a single file, can mix code + Razor/HTML markup in the same file
    • Cons: No strongly-typed parameters
  3. Use an HTML helper or Razor tag helper
    • Pros: Get rich Intellisense in Visual Studio; tag helpers can compose with each other and control allowed markup in the parser
    • Cons: Everything is code, so no Razor syntax within the helper

In summary, there are several rich alternatives, though there aren't any that are an exact replacement for @helper functions.

That's certainly a feature we will look at bringing back, but without the limitations that @helper functions had in MVC 5.x. At this time we simply don't have the time to design and implement this feature and bring it to the necessary quality level to ship it in this version.

alexaku-zz commented 8 years ago

I have not asked about recovery of the @helper directive. I asked how reuse a chunk of HTML (Razor) code without adding a new file? It is impossible in MVC 6 RC, isn't it?

NTaylorMullen commented 8 years ago

@alexaku you can do:

Func<dynamic, IHtmlContent> foo = @<p>Some HTML</p>;

@foo(null)

Which generates (behind the scenes):

Func<dynamic, IHtmlContent> foo = item => new HelperResult(async(__razor_template_writer) => {
  WriteLiteralTo(__razor_template_writer, "<p>Some HTML</p>");
});

If you want to get fancy and have it take a value by utilizing the generated item property and do:

Func<int, IHtmlContent> foo = @<p>The number you entered is: @item</p>;

@foo(1234)
alexaku-zz commented 8 years ago

1) Thank you, @NTaylorMullen. IHtmlContent is a new part of the ASP.NET 5, but I don't found any documentation about this techniques of Razor markup. It is necessary to bring this information to the general public. 2) This still does not solve the problem of a parameterized Razor code. What if there are two or three parameters of different types? In these cases, I have to use Tuple<...>, isn't it?

NTaylorMullen commented 8 years ago

Thank you, @NTaylorMullen. IHtmlContent is a new part of the ASP.NET 5, but I don't found any documentation about this techniques of Razor markup. It is necessary to bring this information to the general public.

Our documentation is a work in progress but rest assured it's being worked on. These are definitely a less known part of Razor.

This still does not solve the problem of a parameterized Razor code. What if there are two or three parameters of different types?

As unfortunate as it might be you'd need to provide some sort of poco object in that case. A potential, less clean solution would be using variables from outside of the @<p>...</p>:

var firstName = "John";
var lastName = "Doe";

Func<int, IHtmlContent> person = @<p>@firstName @lastName is @item years old.</p>;

@person(30)
alexaku-zz commented 8 years ago

Thank you for your patience, @NTaylorMullen. What do you think about the ability of implementation of this Razor syntax in the @functions directive?

@functions {

IHtmlContent person(String firstName, String lastName, Int32 age) { Func<dynamic, IHtmlContent> result = @<p>@firstName @lastName is @age years old.</p>; return result(null); }

}

@person("John", "Doe", 30)

crbranch commented 8 years ago

@Eilon Which of the alternatives you listed (if any) would be most appropriate for rendering a recursive data structure (e.g., treeview). Example using the old @helper syntax here:

http://stackoverflow.com/a/6423891/333127

Eilon commented 8 years ago

I'll let @NTaylorMullen answer that, as he's the resident Razor expert.

alexaku-zz commented 8 years ago

To @crbranch, @Eilon. @NTaylorMullen suggests using the following structure: @{ Func<IEnumerable<Foo>, IHtmlContent> ShowTree = @<text>@{ var foos = item; <ul> @foreach (var foo in foos) { <li> @foo.Title @if (foo.Children.Any()) { @ShowTree(foo.Children) } </li> } </ul> }</text>; }

@ShowTree(new List<Foo>{...})

Eilon commented 8 years ago

Ah, that looks fine to me, then.

alexaku-zz commented 8 years ago

To @Eilon. But it is less convenient than the @helper directive. Will we see the @helper directive in the MVC 6 RTM?

Eilon commented 8 years ago

@alexaku it will not be in ASP.NET Core 1.0 MVC at RTM (formerly known as MVC 6). But it's certainly a feature we will look to add back in the future.

alexaku-zz commented 8 years ago

@Eilon, What's the point to re-implement the @helper directive? You can incorporate @<text>...</text> in the @function directive.

Eilon commented 8 years ago

@alexaku sorry I might be confused. I thought you were asking for @helper to be added back, no?

One difference between @helper and stuff in @functions is that helpers are available in other files, but functions are local to the file they are in.

alexaku-zz commented 8 years ago

@Eilon, I need the @helper directive for a seamless project migration from MVC 5 to MVC 6 (ASP.NET Core 1.0). MVC 6 was made an incompatible with MVC 5. There are too many the @helper directive usages in my projects. After migration, I won't need the old Razor functionality and will be ready to use any new Razor constructions. I think that the use of the @helper functions in other files must be replaced with the use of the View Component. For this case, the "View Component" is most suitable. But, despite all the advantages, the design of the View Component invocation is weird, because only using 'nameof' helps keep your code valid when renaming definitions, and any parameters (of any type and any number of them) do not result in an error of the compiler. @Component.Invoke(nameof(SomeComponentClass), anyTypeVariable1, any)

tboby commented 8 years ago

@Eilon, My common usecase for @helper is where I need markup templates that cover more than one property of the model. Say I want to divide my entire form into sets of four, with controls and validation displayed for each set. Viewmodels are impracticable as they result in vast duplication of metadata.

And from my understanding of view components, they'd suffer from the same duplication of model metadata as simply using viewmodels with editor templates.

@helpers seem to be the only way to make html presentation templates without losing metadata or the benefits of using razor completely!

Eilon commented 8 years ago

Hmm, what model metadata gets lost? There's only one model metadata system and it should get picked up no matter whether it's a partial view, @helper, or anything else.

alexaku-zz commented 8 years ago

@tboby, I do not quite understand you too. May you give an example of the code?

alexaku-zz commented 8 years ago

In the end, I propose to call it the "ASP.NET Core 0.6 MVC" until the old functionality will be restored.

AlekseyMartynov commented 8 years ago

Same here.

We used @helper as a workaround for inability to nest @<text> tags.

Example:

What we wanted:

@Html.CoolStuff(@<text>

        @Html.CoolStuff(@<text>
            Nested
        </text>)

</text>);

What we did in MVC 5:

@helper NestedCoolStuff() {
    @Html.CoolStuff(@<text>
        Nested
    </text>)
}

@Html.CoolStuff(@<text>
   @NestedCoolStuff()
</text>);

What we do in Core: :sob:

Eilon commented 8 years ago

@DamianEdwards @rynowak @NTaylorMullen have been looking at some possible improvements in this area, so there is certainly a positive outlook that we'll see improvements here, though we don't yet have any commitment or final design.

binki commented 7 years ago

@alexaku You’re making it look like it’s impossible to pass parameters to functions. I don’t know how to use MVC Core, but with the real version I can get typed parameters this way:

@{
Func<string, int, IEnumerable<string>, HelperResult> showThing = (name, age, tags) => new Func<object, HelperResult>(@<text>
<div>
  <h3>@name</h3>
  <p>Age: @age</p>
  <ul>
    @foreach (var tag in tags)
    {
      <li>@tag</li>
    }
  </ul>
</div>
</text>)(null);
}

<div>
  @showThing("Name", 1, new[] { "a", "b", "c", })
</div>

Can someone check for me if this still works in Core? Thanks.

Now, I do have a library of @helper which would be annoying to rewrite. Also, I rely on the App_Code/MyHelpers.cshtml way of magically importing my helpers to multiple razor files. If that really went away in the Core version, it’ll be a pain to switch to the new way of sharing code. But maybe one of the strongly typed code reuse options listed earlier would be sufficient, would require refactoring my stuff, and it would probably result in me cleaning up a lot of sloppy stuff. However, it would certainly be easier for to update me if @helper and App_Code-style sharing were still available…

grokky1 commented 7 years ago

@NTaylorMullen @binki @alexaku Did you find a way to put these "helpers" in separate files?

I tried putting the helpers in the view's layout, but then the view can't see them.

rynowak commented 7 years ago

We have no plans to do this

ghost commented 7 years ago

@Eilon

@Helper had the benefit of being extremely fast, razor syntax, strongly typed, and as many helpers as you wanted in each file. The primary cons were having to put it in App_Code to share across views, having to create static classes for helpers like HtmlHelper and UrlHelper, and some oddities in formatting/indenting with Visual Studio.

The applications my company has created have hundreds of @Helper functions in App_Code. There is no way to have a high level of speed, razor syntax, and strongly typed calls without them. If they are removed we will have to move to a ton of compiled methods returning IHtmlString built from StringBuilders (or a custom type of chaining TagBuilders) which is truly a step down from @Helper.

I updated your comparison of other options to @Helper.

  1. Use a view component Pros: None (over @Helper) Cons: An extra CSHTML file for each set of HTML

  2. Use an HTML partial view Pros: None (over @Helper) Cons: No strongly-typed parameters, HORRIBLY slow (100x slower than @Helper)

  3. Use an HTML helper or Razor tag helper Pros: None (over @Helper) Cons: Everything is C# code, so no Razor syntax within the helper

atifaziz commented 7 years ago

@NTaylorMullen said:

This still does not solve the problem of a parameterized Razor code. What if there are two or three parameters of different types?

As unfortunate as it might be you'd need to provide some sort of poco object in that case. A potential, less clean solution would be using variables from outside of the @<p>...</p>:

The example given was:

var firstName = "John";
var lastName = "Doe";

Func<int, IHtmlContent> person = @<p>@firstName @lastName is @item years old.</p>;

@person(30)

But how about using a C# tuple to ship several values into the first argument? As in:

Func<(string FirstName, string LastName, int Age), IHtmlContent> person =
    @<p>@item.FirstName @item.LastName is @item.Age years old.</p>;

@person(("John", "Doe", 30))

The only odd looking bit may be the double parentheses needed at the call sites but that's far better than having to rely on closures for parameterization.

atifaziz commented 7 years ago

In addition to using C# tuples, as shown above, using local C# functions can also make it simpler to pass multiple arguments:

IHtmlContent Render<T>(Func<T, IHtmlContent> helper, T item = default(T)) =>
    helper(item);

Func<object, IHtmlContent> Person(string fn, string ln, int age) =>
    @<p>@fn @ln is @age years old.</p>;

@Render(Person("John", "Doe", 42))

Render above is only for cosmetics as @Person("John", "Doe", 42)(null) would look ugly.

duncansmart commented 6 years ago

What are the extensibility points for the community to add @helper support?

NTaylorMullen commented 6 years ago

@duncansmart take a look at how we built some of MVCs directives:

You'll run into some issues with getting the exact syntax of the old @helper directive but you'll be able to get somewhat close.

jahanalem commented 6 years ago

How can I compatible this code with asp.net core 2 ?

//Recursive function for rendering child nodes for the specified node

@helper CreateNavigation(int parentId, int depthNavigation, int currentPageId)
{
   @MyHelpers.Navigation(parentId, depthNavigation, currentPageId);
}

@helper Navigation(int parentId, int depthNavigation, int currentPageId)
{

   if ()
   {
       if ()
       {
         <ul style="">
            @foreach ()
            {
               if ()
                {
                   <li class="">
                      @Navigation(child.Id, depthNavigation, currentPageId)
                   </li>
                }
             }
         </ul>
      }
   }
}
NTaylorMullen commented 6 years ago

@jahanalem's, @atifaziz suggestions should be enough to massage your code back into a similar looking state.

johnhargrove commented 6 years ago

This issue should be re-opened as it is still not really addressed. I have been using Core for a couple years now and I run into this limitation all the time.

frogcrush commented 6 years ago

I agree. The limitation I have right now is you can't (sensibly) make recursive functions. Partials are simply too slow for this, especially if you're generating something like a menu or a tree view.

ShadowDancer commented 6 years ago

@NTaylorMullen Your solution is so ugly, that I believe that We should pretend it doesn't exist. Especially compared to so nice @helper.

plus1319 commented 6 years ago

how about this code in mvc5 , without @helper i can't implement this perfectly in core Recursive Category

@helper AddOption(int? parentId)
        {
            foreach(var item in Model.Where(p=> p.ParentId == parentiId).ToList())
            {
                <option value="@item.Id">@item.Name</option>
                AddOption(item.Id);
            }
        }

<select>
    <option value="">Main Category</option>
    @AddOption(null)
</select> 
rjgotten commented 5 years ago

@atifaziz

Nice idea. Let's improve on that a bit by eliminating the need for the separate Render call:

public static HelperExtensions
{
  public static Func<T1,IHtmlContent> Helper<T1>(
    this RazorPageBase page,
    Func<T1,Func<object,IHtmlContent>> helper
  ) => p1 => helper(p1)(null);

  public static Func<T1,T2,IHtmlContent> Helper<T1,T2>(
    this RazorPageBase page,
    Func<T1,T2,Func<object,IHtmlContent>> helper
  ) => (p1, p2) => helper(p1, p2)(null);

  public static Func<T1,T2,T3,IHtmlContent> Helper<T1,T2,T3>(
    this RazorPageBase page,
    Func<T1,T2,T3,Func<object,IHtmlContent>> helper
  ) => (p1, p2, p3) => helper(p1, p2, p3)(null);

  // etc. for as high as you want to take the # of parameters
}

Use like:

var Person = this.Helper((string fn, string ln, int age) =>
  @<p>@fn @ln is @age years old.</p>
);

@Person("John", "Doe", 42)
m0sa commented 5 years ago

@rjgotten nice idea... I've added it to my benchmarks:

https://github.com/m0sa/Mvc/tree/master/benchmarks/Microsoft.AspNetCore.Mvc.Performance.Views/Views

Looks like closing over the helper arguments performs a lot better than actually passing them in as the model to the template!

BenchmarkDotNet=v0.10.13, OS=Windows 10.0.17134
Intel Core i7-5960X CPU 3.00GHz (Broadwell), 1 CPU, 16 logical cores and 8 physical cores
Frequency=2928836 Hz, Resolution=341.4326 ns, Timer=TSC
.NET Core SDK=3.0.100-preview-009750
  [Host]     : .NET Core 3.0.0-preview1-26907-05 (CoreCLR 4.6.26907.04, CoreFX 4.6.26907.04), 64bit RyuJIT
  Job-CZVHYQ : .NET Core 3.0.0-preview1-26907-05 (CoreCLR 4.6.26907.04, CoreFX 4.6.26907.04), 64bit RyuJIT

Runtime=Core  Server=True  Toolchain=.NET Core 3.0
RunStrategy=Throughput
ViewPath Mean Error StdDev Op/s Gen 0 Allocated
~/Views/HelperDynamic.cshtml 42.77 us 4.324 us 4.441 us 40.48 us 23,378.4 - 22.61 KB
~/Views/HelperExtensions.cshtml 27.61 us 4.825 us 4.955 us 24.96 us 36,225.1 - 28.89 KB
~/Views/HelperPartialAsync.cshtml 317.32 us 6.490 us 19.034 us 308.42 us 3,151.4 0.4883 182.52 KB
~/Views/HelperPartialSync.cshtml 332.87 us 6.622 us 12.274 us 326.67 us 3,004.2 - 182.44 KB
~/Views/HelperPartialTagHelper.cshtml 466.97 us 7.049 us 5.886 us 466.81 us 2,141.5 - 215.99 KB
~/Views/HelperTyped.cshtml 38.57 us 1.544 us 4.504 us 36.01 us 25,924.5 - 22.61 KB
duncansmart commented 5 years ago

That's a nice workaround improvement @rjgotten. Some issues with it after trying it out. The helper vars need to be declared before they're used (with @helper you can declare them at the bottom of the view for example). Also the intellisense for the args suffers as they're just genetic Func<>s

image

rjgotten commented 5 years ago

@duncansmart Hmm.... in that case, I guess you could still go the local function route and pull the helper resolution part inside the definition of the local function.

E.g.

public static Helper
{
  public static IHtmlContent Body(Func<object, IHtmlContent> body)
  {
    return body(null);
  }
}

And then consume as

IHtmlContent Person(string fn, string ln, int age) => 
  Helper.Body(@<p>@fn @ln is @age years old.</p>)

@Person("John", "Doe", 42)

That should solve both the argument naming problem and the definition hoisting problem. (Also means you won't have to define a hell of a lot of extension methods for argument arity.)

Still saves you a bit of type declaration on the local functions return type vs the original by @atifaziz Namely a plain IHtmlContent vs a Func<object, IHtmlContent>. And calling the helper is totally clean this way.

duncansmart commented 5 years ago

Yep, not bad 👍

image

Still want @helper back though :-)

rjgotten commented 5 years ago

Still want @helper back though :-)

Don't we all? :-)