Skip to content
This repository has been archived by the owner on Dec 19, 2018. It is now read-only.

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

Closed
alexaku-zz opened this issue Mar 18, 2016 · 41 comments

Comments

@alexaku-zz
Copy link

The @Helper directive was removed from Razor (MVC 6) and it was not given any simple replacement. See #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;
}

}

@alexaku-zz alexaku-zz changed the title Add an HTML-formatted constructions support into Razor instead the removed @helper directive. Add an HTML-formatted constructions support into Razor instead of the removed @helper directive. Mar 18, 2016
@Eilon
Copy link
Member

Eilon commented Mar 18, 2016

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.

@Eilon Eilon added this to the Backlog milestone Mar 18, 2016
@alexaku-zz
Copy link
Author

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
Copy link
Member

@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
Copy link
Author

  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
Copy link
Member

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
Copy link
Author

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
Copy link

@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
Copy link
Member

Eilon commented Mar 23, 2016

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

@alexaku-zz
Copy link
Author

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
Copy link
Member

Eilon commented Mar 24, 2016

Ah, that looks fine to me, then.

@alexaku-zz
Copy link
Author

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

@Eilon
Copy link
Member

Eilon commented Mar 24, 2016

@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
Copy link
Author

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

@Eilon
Copy link
Member

Eilon commented Mar 24, 2016

@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
Copy link
Author

@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
Copy link

tboby commented Mar 24, 2016

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

  • Editor Templates: Don't accept multiple models
  • Partial views: Lose model metadata/attributes
  • Code helper: String/tag builders for the vast majority of common html structures, difficult to maintain

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
Copy link
Member

Eilon commented Mar 24, 2016

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
Copy link
Author

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

@alexaku-zz
Copy link
Author

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

@AlekseyMartynov
Copy link

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:
😭

@Eilon
Copy link
Member

Eilon commented Jul 7, 2016

@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
Copy link

binki commented Jan 16, 2017

@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
Copy link

grokky1 commented Feb 28, 2017

@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
Copy link
Member

rynowak commented May 5, 2017

We have no plans to do this

@ghost
Copy link

ghost commented Jul 13, 2017

@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
Copy link

atifaziz commented Sep 8, 2017

@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
Copy link

atifaziz commented Sep 8, 2017

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
Copy link

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

@NTaylorMullen
Copy link
Member

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

@rynowak rynowak removed this from the Backlog milestone Dec 17, 2017
@jahanalem
Copy link

jahanalem commented Jan 10, 2018

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
Copy link
Member

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

@johnhargrove
Copy link

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.

  • ViewComponents are not a replacement. Writing HTML inside of a bunch of string concatenations buried inside some C# classes well away from the view is a step backward, and honestly it should be avoided when possible.
  • Partials aren't easily parameterized. Other limitations are already well-covered in this thread.
  • The various suggested workarounds above are syntactic nightmares.

@frogcrush
Copy link

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
Copy link

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

@plus1319
Copy link

plus1319 commented Sep 12, 2018

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
Copy link

rjgotten commented Nov 26, 2018

@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 added a commit to m0sa/Mvc that referenced this issue Nov 26, 2018
@m0sa
Copy link

m0sa commented Nov 26, 2018

@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 -
~/Views/HelperExtensions.cshtml 27.61 us 4.825 us 4.955 us 24.96 us 36,225.1 -
~/Views/HelperPartialAsync.cshtml 317.32 us 6.490 us 19.034 us 308.42 us 3,151.4 0.4883
~/Views/HelperPartialSync.cshtml 332.87 us 6.622 us 12.274 us 326.67 us 3,004.2 -
~/Views/HelperPartialTagHelper.cshtml 466.97 us 7.049 us 5.886 us 466.81 us 2,141.5 -
~/Views/HelperTyped.cshtml 38.57 us 1.544 us 4.504 us 36.01 us 25,924.5 -

@duncansmart
Copy link

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
Copy link

rjgotten commented Nov 26, 2018

@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
Copy link

Yep, not bad 👍

image

Still want @helper back though :-)

@rjgotten
Copy link

rjgotten commented Nov 27, 2018

Still want @helper back though :-)

Don't we all? :-)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests