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

ViewComponent and @Section #2910

Closed
grahamehorner opened this issue Aug 4, 2015 · 52 comments
Closed

ViewComponent and @Section #2910

grahamehorner opened this issue Aug 4, 2015 · 52 comments
Milestone

Comments

@grahamehorner
Copy link

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.

@Eilon
Copy link
Member

Eilon commented Aug 5, 2015

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

@danroth27 danroth27 added this to the 6.0.0-beta8 milestone Aug 14, 2015
@grahamehorner
Copy link
Author

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.

@grahamehorner
Copy link
Author

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

please see https://github.com/grahamehorner/Mvc

@dougbu
Copy link
Member

dougbu commented Aug 18, 2015

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

Might work better to post a standalone example.

@grahamehorner
Copy link
Author

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

@dougbu
Copy link
Member

dougbu commented Aug 20, 2015

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

@dougbu dougbu removed their assignment Aug 20, 2015
@grahamehorner
Copy link
Author

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 ;)

@grahamehorner
Copy link
Author

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 ?

@Eilon Eilon modified the milestones: 6.0.0-beta8, 6.0.0-rc1 Sep 24, 2015
@pebezo
Copy link

pebezo commented Sep 25, 2015

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.

@grahamehorner
Copy link
Author

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.

@leus
Copy link

leus commented Oct 6, 2015

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

@dougbu
Copy link
Member

dougbu commented Oct 6, 2015

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

@leus
Copy link

leus commented Oct 7, 2015

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.

@dougbu
Copy link
Member

dougbu commented Oct 7, 2015

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().

@leus
Copy link

leus commented Oct 8, 2015

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?

@grahamehorner
Copy link
Author

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

@Eilon Eilon modified the milestones: 6.0.0-rc1, 1.0.0-rc2 Oct 22, 2015
@sebastienros
Copy link
Member

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.

@dazinator
Copy link

dazinator commented Aug 9, 2016

@sebastienros - thank you for the above. I am using something similar based on this: dotnet/AspNetCore.Docs#687 (comment)

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!

@sebastienros
Copy link
Member

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.

@dazinator
Copy link

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

@benzhi2011
Copy link

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

thanks

@benzhi2011
Copy link

@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

@dazinator
Copy link

dazinator commented Sep 24, 2016

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

@benzhi2011
Copy link

Thank you for your concrete explanation @dazinator

@CodingSamurai
Copy link

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.

@Eilon
Copy link
Member

Eilon commented Dec 30, 2016

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

@Eilon Eilon closed this as completed Dec 30, 2016
@mxa0079
Copy link

mxa0079 commented Sep 9, 2017

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

@Eilon
Copy link
Member

Eilon commented Sep 11, 2017

@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: #2910 (comment)

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.

@dazinator
Copy link

dazinator commented Sep 11, 2017

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.

@sebastienros
Copy link
Member

sebastienros commented Sep 11, 2017

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

@MortenMeisler
Copy link

MortenMeisler commented Jan 9, 2018

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>

}

@JohnCOsborne
Copy link

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.

@gilm0079
Copy link

gilm0079 commented May 1, 2018

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?

@gilm0079
Copy link

gilm0079 commented May 1, 2018

@grahamehorner Did you find a solution for this?

@grahamehorner
Copy link
Author

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

@begrob
Copy link

begrob commented Jun 17, 2018

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 :) ! "

@alkex992
Copy link

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

@clockwiseq
Copy link

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

@dodyg
Copy link

dodyg commented Jun 26, 2018

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

@clockwiseq
Copy link

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

@dodyg
Copy link

dodyg commented Jun 26, 2018

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.

@begrob
Copy link

begrob commented Jun 27, 2018

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

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

No branches or pull requests