Closed alexaku-zz closed 7 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:
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.
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?
@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)
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?
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)
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)
@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:
I'll let @NTaylorMullen answer that, as he's the resident Razor expert.
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>{...})
Ah, that looks fine to me, then.
To @Eilon. But it is less convenient than the @helper directive. Will we see the @helper directive in the MVC 6 RTM?
@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.
@Eilon,
What's the point to re-implement the @helper directive? You can incorporate @<text>...</text>
in the @function directive.
@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.
@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)
@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.
@helper
s seem to be the only way to make html presentation templates without losing metadata or the benefits of using razor completely!
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.
@tboby, I do not quite understand you too. May you give an example of the code?
In the end, I propose to call it the "ASP.NET Core 0.6 MVC" until the old functionality will be restored.
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:
@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.
@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…
@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.
We have no plans to do this
@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.
Use a view component Pros: None (over @Helper) Cons: An extra CSHTML file for each set of HTML
Use an HTML partial view Pros: None (over @Helper) Cons: No strongly-typed parameters, HORRIBLY slow (100x slower than @Helper)
Use an HTML helper or Razor tag helper Pros: None (over @Helper) Cons: Everything is C# code, so no Razor syntax within the helper
@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.
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.
What are the extensibility points for the community to add @helper
support?
@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.
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>
}
}
}
@jahanalem's, @atifaziz suggestions should be enough to massage your code back into a similar looking state.
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.
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.
@NTaylorMullen
Your solution is so ugly, that I believe that We should pretend it doesn't exist. Especially compared to so nice @helper
.
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>
@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)
@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 |
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
@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.
Yep, not bad 👍
Still want @helper
back though :-)
Still want
@helper
back though :-)
Don't we all? :-)
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;
}
}