Mvc: ViewComponent and @Section

Created on 4 Aug 2015  路  52Comments  路  Source: aspnet/Mvc

The @section scripts don't render in a ViewComponent, view component should have the ability to include scripts within the @section unlike partital views as components may be reused across many views and the component is responsible for it's own functionality.

Most helpful comment

I agree with others that without SOME kind of inherent support for adding JS and CSS files from within ViewControllers they're a halfbaked concept. I can't believe that this isn't on the roadmap.

All 52 comments

@grahamehorner can you explain a bit more what scenario you have? Does the ViewComponent have its own layout page and view, and an @section for scripts? Or is the ViewComponent trying to have an @section for scripts for the _calling view's_ layout page?

I have the need to use a view component that has a view component layout and uses a view model; the layout includes a @section for scripts; at present the view component layout is rendering but fails to include the @section script at the end of the rendered page/output; this results in the view component not having the required functionality when rendered to the client.

I have forked the code and updated the default.cshtml in the Views\Shared\Components\Tags folder to show what I believe to be an issue; as the @section scripts fails to render in the pages that make use of the ViewComponent.

@grahamehorner nothing shows up as changed in that forked repo. What branches should we compare?

Might work better to post a standalone example.

@dougbu apologies the sync failed and I had not realised; the simple update should now be invisble

@grahamehorner thanks I've confirmed the behaviour you've described. The ViewViewComponentResult for Default.cshtml executes independently of the main ViewResult and its Layout value is null by default. Without a layout, no sections are rendered. The same behaviour occurs in display or editor templates and partial views.

The most obvious issue here is views which have Layout == null and contain @section do not encounter InvalidOperationExceptions because the section is not rendered. Such an Exception would have caught the oddly-placed / unrendered section up front.

The remainder appears to be By Design...

If you added a layout for your view component, it could render the "scripts" section. Rendering should be conditional in your scenario because view components should be independent enough to be used multiple times in the same page. But that condition correctly leads to errors because layouts confirm all defined sections are rendered.

With respect to defining a section in a view component that the main page's layout renders, note view components (and partials and templates...) should work in any page. Defining a section in any of these views with the expectation its name is known to the page or page layout creates an unnatural coupling.

why would a View or a ViewComponent ever have a Layout of null? unless explicitly set? shouldn't they inherit from the _ViewStart.cshtml layout or the layout of the parent/hosting cshtml?

I understand the 'unnatural coupling' comment and agree; but how could we achieve the following:-
a view component that requires some scripts for it's functionality (following best practice) needs to inject javascript at a given location (the end of the rendered html but prior to the closing body tag) but only when the ViewComponent is including in the View rendering ;)

or should the ViewComponent be able to be supplied a options object for the View where it can then use this to inject style/script/html or hook into dom elements of the View ?

I needed something similar and made: https://github.com/pebezo/Portal

The idea is that you can send content from a view or partial view (not ViewComponent , but somewhat similar) to a particular spot in the layout. The implementation is rather hacky; it works, but you have to worry about the order in which you place the in/out points.

To make it work properly, Razor would have to support at least two runs: one pre-render run where you could register what you want and the actual render. I brought up this idea over jabber, but it was shut down by @davidfowl because it would be too much like the page life cycle from WebForms. Since the view can now be sent over the wire before the entire page is rendered, the two-runs over the view may not be possible.

I'm thinking about the possible creation of a ViewComponentScriptInject and/or ViewComponentStyleInjection middleware; this would allow ViewComponent to have custom attributes eg. ViewCompnentScriptAttribute/ViewCompnentStyleAttribute that are used to registered style and script that needs to be injected into the rendered output at a given location.

@dougbu I know the current behaviour is by design, and workarounds do exist, but this is a particularly irksome design which, to my nose, smells of implementation details dictating the functional design.

Any chances for this to be implemented?

@leus we haven't seen what @grahamehorner put together.
@grahamehorner could you open a PR with an up-to-date version of your changes?

By the way - a feature needed for this is the ability to specify if a section block must execute once per tag helper in the default context, or every time the tag helper is executed.

once per tag helper

@leus if you need conditional execution of @RenderSection(), wrap it with a C# condition. Could also do something more complicated e.g. define a <once/> tag helper and use that to wrap the @RenderSection().

That sounds like a good workaround. I'm just wondering if this feature is added, what should be the default behaviour for view components.

<div class="mycomp">My Component</div>
@section scripts {
    <script src="ginormous-javascript-file.js"></script>
    <script>
    $(function() {
        $(".mycomp").someVeryExpensiveJavascriptFunction();
    });
    </script>
}

Would adding something like @section(once) complicate things much?

I think we should have a RazorTagHelper that removes the scripts from the initial render/output, buffering them
in a session script cache of some kind, and when middleware detects the tag the session script cache is
injected prior to the tag thus placing the view-component scripts at the end of the page with other scripts
and in the order the session script cache was populated.

The session script cache would prevent the same script from getting injected multiple times

Are there plans for enabling this functionality?

I'd appreciate an update aswell. Would the section blocks also work within Item/Edit tempates? Also the ability to only render the contents once is important as the same component/template may be used multiple times.

One issue I can think of is that ViewComponents are an Mvc feature and not a Razor feature. Consequently you could invoke a Razor view component from a Spark view page (for instance). Unless we make defining sections a feature of views in general, this doesn't work very well.

Similar issue in my project. I'm using this workaround: https://github.com/aspnet/Docs/issues/687#issuecomment-168711927

I've got a similar problem.

I have a view component rendering an address block of five textboxes that I want to use in other views requiring address input. I would like to drop the "invoke" in and just apply the current values to the textboxes via the invoke method if its an "Edit" scenario. By creating an address view component I was aiming to avoid having to create the state select list and suburb autocomplete array every time I wanted an address to be created in a respective view.

The address view component has a model with both state and suburbs lists and here's the rub. I need to load the autocomplete jquery script I've written *_after *_ the the jquery-ui script which is at the bottom of the _Layout page. By loading this into a @section I could do that. Without it the script renders halfway down the page and before the jquery-ui so does not work. I could of course move the jquery-scripts to the head of the _Layout but then I face possible render blocking issues.

If I had the option of @section I could make the address view component only rely on the values for the addresses from the main view and avoid having to load up the state select list and the suburb list every time I have to add the address boxes. Right now I am between a rock and a hard place with bits of code all over the place which defeats the purpose of neat low coupled code. Incidentally Taghelpers suffer the same problem. No @section option and the resulting need to create two taghelpers - one for rendering markup and one for rendering the script.. which is placed in... thats right... a @section block in the main view.. to my mind this is ugly and the tag helper should be one not two hence the attempt at making a view component to do the same thing and hitting ther same problem.

+1 Need a solution to this. Any non trivial ViewComponent can expect to have javascript / css files. Have been using @SimonOrdo's solution here: https://github.com/aspnet/Docs/issues/687#issuecomment-168711927 in RC1, but now hitting an exception with this in RC2

Any news on this issue yet?

I think we should have a RazorTagHelper that removes the scripts from the initial render/output, buffering them
in a session script cache of some kind, and when middleware detects the tag the session script cache is
injected prior to the tag thus placing the view-component scripts at the end of the page with other scripts
and in the order the session script cache was populated.

The session script cache would prevent the same script from getting injected multiple times

I like the sound of this suggestion! How would it determine script equality though - would it be by the id attribute? It's nice to give the developer the option over whether the script will ultimately be included a just once, or once per instance of their VC.

I am also assuming that a TagHelper based approach wouldn't be able to include a script higher up on the page for example within the Head section. So if a VC needed to include a css file in the HEAD this approach wouldn't work (unless the VC was also being rendered in the HEAD)

With that in mind, I was also thinking of an attribute based approach like this:

    [HeadInclude("/somefile.css")]
    [BodyInclude("/somefile.js", BodyLocation.End, InstanceType.Single )]
    [BodyInclude("/perinstance.js", BodyLocation.End, InstanceType.PerInvocation)]
    public class SomeViewComponent : ViewComponent
    {

    }

These attributes could be evaluated at registration time of the ViewComponent (i.e with the application parts manager), and MVC could handle injecting the right VC scripts into the right page locations. In fact, to tell it the location it might make more sense to use an attribute like this:

    [SectionInclude("scripts", "/somefile.js", InstanceType.Single)]
    public class SomeViewComponent : ViewComponent
    {

    }

That way you only need one type of attribute to include stuff into a named section, and then that section could be placed anywhere making it a bit more flexible.

I think this idea has some problems too though, mainly that you couldn't conditionally include scripts. Sometimes a VC might not want to include a script if for example, the parameters being passed to it with the invoke() don't require it. This is where the TagHelper approach is a bit better, but they could both potentially be used in combination.

There are still no plans to implement this. However, it does sound more like something you could expect to see a solution for in https://github.com/OrchardCMS/Orchard2, which @sebastienros is working on.

I have actually finished implementing the resource manager, which can be used in any ASP.NET Core/MVC app.

Projects:
https://github.com/OrchardCMS/Orchard2/tree/master/src/Orchard.ResourceManagement
https://github.com/OrchardCMS/Orchard2/tree/master/src/Orchard.ResourceManagement.Abstractions

Here is a view with all tag helpers you can use to define required resources from a view:
https://github.com/OrchardCMS/Orchard2/blob/master/src/Orchard.Web/Modules/Orchard.Demo/Views/Home/Index.cshtml#L4

You can also directly use the service from any code path to do the same:
https://github.com/OrchardCMS/Orchard2/blob/master/src/Orchard.ResourceManagement.Abstractions/IResourceManager.cs

And inject the results of what was required during a request like this:
https://github.com/OrchardCMS/Orchard2/blob/master/src/Orchard.Web/Themes/TheTheme/Views/Layout.cshtml#L13

Creating an attribute should be quite trivial, look at the Tag Helpers implementation for some example.

@sebastienros - thank you for the above. I am using something similar based on this: https://github.com/aspnet/Docs/issues/687#issuecomment-168711927

Could you clarify how this works when registering resources within ViewComponents? For example if you have several instances of the same ViewComponent within a single View, and you register a resource within that ViewComponent - does that work - and do you end up with multiple resources being output in the layout in the order they were registered, or just a single one - or can you control that? Also, if VC is invoked during RenderBody() - can it still add resources retrospectively into the head? If you have any ViewComponent's using this system that would be good to look at!

For example, a VC that needs to ensure that a meta tag is included in the current pages Head - you would probably only want to register that once, but how do you deal with that if there can be multiple instances of that VC on the page?

The way I have been dealing with this at the moment is by doing something like this:

In my View I call the VC once, with an argument to tell it to render in a special mode, which basically
registers the js / css resources that the VC needs.

@Component.Invoke("MyWidget", new { Options = new { Type: RenderType.ScriptIncludes } }

Then for actually displaying each instance of the VC in the View:

@Component.Invoke("MyWidget", new { Options = new { RenderType.Instance } }
@Component.Invoke("MyWidget", new { Options = new { RenderType.Instance } }
@Component.Invoke("MyWidget", new { Options = new { RenderType.Instance } }

But the consequences of this is that it requires the consumer of the VC to remember to place these additional 1 time invokes on the page, before then seperately invoking again to display each instance. I'd prefer a solution where this was all contained within the single VC invoke, and not left to the consumer to worry about.

Thanks for sharing your work with this so far!

Currently you can call this from a View Component's view only, as it doesn't target view components specifically.
If multiple views or services require the same resource during a request, a single one will be emitted in the rendered content, and any other dependent resource (bootstrap depends on jquery, so requiring bootstrap would also inject jquery).
If two scripts are required and have no relationships, they will be rendered in the order they were required.
In the example I linked, you can see there are two meta tags with the same name. The default behavior is to concatenate, and there are options on the service/taghelpger too.

It also lets you target Head and Foot for scripts, CSS, custom inline scripts, custom link tags, and handles script versions.

@sebastienros - Thanks - sounds very good. I will give it a whirl! - Can it also make my tea? :-)

@dazinator Would you please post an example of how to use this invoke and the full namespace of RenderType?

thanks

@sebastienros I am new to MVC 6, could you show me how to use script section on the view component that is used multi times on the home view.
I cannot find RenderType which @DamianEdwards @dazinator mentioned

@benzhi2011 - RenderType was just an enum that I created as an argument for the purpose of my example. You could easily do the same.

Perhaps you didn't understand so please, allow me to elaborate.

Rather than including script tags in the View for your ViewComponent i.e:

<div> <p> My VC </p> </div>
<script src="myscripts.js"></script>

Move your script tags into a seperate view file for your ViewComponent:

<script src="myscripts.js"></script>

And have just your content in the other View file:

<div> <p> My VC </p> </div>

You now have two seperate View files for your VC, one containing your actual content, and one that just contains the script includes.

Then in your page, when you Invoke the view component, You can invoke it with an argument telling it which View to render (i.e the scripts view, or the content view).

Inside your ViewComponent's Invoke() method, inspect the argument you passed it, and return the right view.

Now back in your page, in the scripts section of your page, you can invoke your ViewComponent with the argument that makes it render the script includes into that section of the view (i.e the scripts section where you want them).

Elsewhere on your page, where you want the actual VC content to be displayed, you can Invoke it so that it displays the View that just displays the content. You can then invoke that multiple times on your page wherever you want your VC to be displayed.

This isn't ideal, but it's a workaround I am using for the present.

Thank you for your concrete explanation @dazinator

I agree with others that without SOME kind of inherent support for adding JS and CSS files from within ViewControllers they're a halfbaked concept. I can't believe that this isn't on the roadmap.

Closing because there are no plans at this time to implement this in MVC.

@Eilon - by the reaction to your message I beleive it is clear that the community needs this. Could you ellaborate the reasoning for not adding this to the pipeline and simply closing the case?

The current response to this issue is not aligned with the "customer obsession" culture that Satya is building at Microsoft.

Thanks!

@mxa0079 I totally agree that there's a community need, and it was mentioned earlier that a solution already exists via some components that @sebastienros built for Orchard Core: https://github.com/aspnet/Mvc/issues/2910#issuecomment-238298882

We've gotten plenty of flack before (and deservedly so!) for trying to replicate perfectly good community solutions. This is a case where we've stayed back because the community has what we believe to be a great solution for resource (CSS+JS) management.

The orchard links above 404 for me.. did a quick search for orchard core resource management: yielded another link that 404's! https://github.com/OrchardCMS/Orchard2/tree/master/src/OrchardCore/Orchard.ResourceManagement

If relying on a community maintained solution to fill such a fundamental gap in the framework (lets face it, this makes or breaks the real world usefulness of view components and was raised as a problem from day one) is your chosen path forward - my personal opinion of that doesnt matter.. but may I ask there is some easy to find documentation for it, similar to the rest of the mvc docs? Only because it seems that this problem is forced onto anyone who uses ViewComponents, and could leave them thumbling in the dark, especially if they are new to asp.net core mvc, and they can't find any clear direction on the docs site for recommended ways to solve this problem.

Sorry, last week we renamed all the packages in preparation for a beta release on nuget.org, and I didn't remember these links were here. And yes, I intend to publish an article on how to use it, and also for some other important packages. The location remains to be defined.

In the meantime, you can find the package on this myget feed: https://www.myget.org/feed/Packages/orchardcore-preview

OrchardCore.ResourceManagement
OrchardCore.ResourceManagement.Abstractions

And some instructions:

https://orchardcore.readthedocs.io/en/latest/OrchardCore.Modules/OrchardCore.Resources/README/#usage

For those who just stumpled here (aka TL;DR) you just have to define a layout on the viewcomponent.

Example:

_LayoutViewComponent.cshtml:

@RenderBody()
<environment include="Development">
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
    <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"
            asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
            asp-fallback-test="window.jQuery"
            crossorigin="anonymous"
            integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk">
    </script>
    <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
            asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
            crossorigin="anonymous"
            integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
    </script>
    <script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)

YourViewComponentView.cshtml

@model YourModel
@{
    Layout = "_LayoutViewComponent";
}

//html stuff

@section scripts
    {
    <script type="text/javascript">
//your scripting
    </script>

}

That will work but since ViewComponent is being loaded inside a view and that view is probably using _Layout.cshtml which is already adding jquery.js, bootstrap.js, and site.js; this will cause you to duplicate the script files.

I'm just catching up on this long thread. I think I'm with the majority on this one as well. The component architecture should basically be a self-contained app for separation of concerns. HTML, MVC markup, JS and CSS all contained within the component definition.

I'm not so much worried about duplication of scripts or styles. That can be handled by a good layout of your application and/or using a script dependency framework like require.js if you are unsure about what scripts are being loaded.

My main concern is just ability to inject a section block upstream to a layout which should be native to the ViewComponent and shouldn't be community driven otherwise I would recommend removing ViewComponents entirely from the MS code-base as a half working implementation isn't helpful. If MS isn't looking to provide native support for this then I would rather see a full implementation from the community side vs. using the community to augment deficiencies in the core functionality.

If you have the structure of (outer-most HTML to inner-most) Layout -> View -> ViewComponent, I think it is typical for the Layout to render a section at the end of the head to inject styles from the view and render a section at the end of the body to inject scripts from the view. At the very least I would think the ViewComponent should be able to inject its section definitions up 2 levels to that Layout. Obviously there is some mechanism there already to inject the HTML markup from a ViewComponent into the parent View so could it just inject the other sections at that time as well?

@grahamehorner Did you find a solution for this?

@gilm0079 I opted to use a TagHelper extending the partial to have an optional flag

Imagine that in a VC, besides using the currently available @section to add to the immediate parent layout (to support VC as a self-contained unit), you could also use @@section (or some other suitable syntax) to add to the top-most layout (the main layout of the actual page being rendered)(to support VC as a unit that - besides being self-contained - also lives in the "outside" world of a page). The whole issue (I totally agree that this is almost a make/break for VC unless one resorts to unnatural workarounds) would be solved.

And to replicate gilm0079's comment - the pipeline obviously exists that connects the top-most layout to the bottom-most VC. So instead of passing just the immediate parent layout, if the top-most layout was also passed down to all VCs (possibly nested), that's what the @@section would refer to.

Probably an overkill to have a mechanism that would let you refer to ANY layout "above you" in the hierarchy. "Just a simple @@section please :) ! "

Why is this issue been closed? I do not see any "official solution", only incredible workarounds that only make projects more and more intricate

@alkex992, it boggles the mind to think that the workarounds on this thread are "acceptable". You are absolutely correct and I can't find another issue or thread regarding an open request for this basic and necessary feature.

@clockwiseq it really limits the usefulness of ViewComponent. Better not use it.

Exactly @dodyg , it has left me with very limited options as to how to create reusable components.

This issue is three years old. I don't think we'll see some official movement on this topic. The alternative is to fork the Razor template engine and figure this out ourselves. It will make an interesting side project just to see what's possible.

Is anyone interested to hook up for some pair programming to give this a shot ?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MariovanZeist picture MariovanZeist  路  4Comments

michaelbowman1024 picture michaelbowman1024  路  3Comments

Lutando picture Lutando  路  4Comments

hikalkan picture hikalkan  路  4Comments

CezaryRynkowski picture CezaryRynkowski  路  4Comments