Description
ViewComponents are powerfull solutions that allows the developer to keep his code clean and reusable. Unfortunatelly it has the same functionality as all the Views and for the current version (.NET Core 2.2) there is no proper support of defining scripts and style tag in a view component but to render them in the Layout file at the @section area.
A ViewComponent can act like a view, you can add a layout and since the layout is what triggers the method to take whats in @section{…} and place it somewhere else, it will do so. But a ViewComponent is isolated and act independently, therefore it will take the mentioned actions in it’s own space. It will take the @section parts and it will inject then in the layout defined space using it’s own layout within it’s own container space.
See the example below (left image : layout for view component, right image : view component’s view)
The result is pretty clear. The script tags we added are enclosed in the main tag and not grouped lower. And most important, we will not have access to jquery or other frameworks we include in layout. We need to position our scripts below those tags
Solution
The solution is quite simple. At first I thought of using a middleware to grab and change the response body just before it’s being sent, but this is quite expensive in cpu terms and unnecessary. The solution implies 2 taghelpers:
- TagHelperScriptCut – this is used in ViewComponent and it’s role is to grab the script content, store it in HttpContext.Items (this object is accesible through the ViewContext and it’s shared) and to prevent rendering of the contents
- TagHelperScriptPaste – this is used to search the Items object and render all scripts with a certain ke
1. Folder Structure
2. Tag Helpers
namespace TagHelperScriptCutPaste.TagHelpers.Common { public class TagHelperCutPaste { public const string ItemsStorageKey = "a2b459c4-3c62-4a90-977a-5999eb5978c5"; // CutPasteKey identifies the appartenence of the cuted part to the paste section public string CutPasteKey { get; set; } // Cut script TagContent public TagHelperContent TagHelperContent { get; set; } // Attributes belonging to the cut script public List Attributes { get; set; } } }
[HtmlTargetElement("script", Attributes = "asp-cut-key")] public class TagHelperScriptCut : TagHelper { [HtmlAttributeName("asp-cut-key")] public string CutKey { get; set; } [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { List deferedScripts = new List(); if (this.ViewContext.HttpContext.Items.ContainsKey(TagHelperCutPaste.ItemsStorageKey)) { deferedScripts = this.ViewContext.HttpContext.Items[TagHelperCutPaste.ItemsStorageKey] as List; if (deferedScripts == null) { //conversion failed throw new ApplicationException("Duplicate Items key : " + TagHelperCutPaste.ItemsStorageKey); } } else { deferedScripts = new List(); this.ViewContext.HttpContext.Items.Add(TagHelperCutPaste.ItemsStorageKey, deferedScripts); } //solve content TagHelperContent result = await output.GetChildContentAsync(); //add content to the dictionary deferedScripts.Add(new TagHelperCutPaste { CutPasteKey = this.CutKey, TagHelperContent = result, //pass the attributes Attributes = context.AllAttributes.Where(x => x.Name != "asp-cut-key").ToList() }); //do not render content in this section output.Content.Clear(); return; } }
namespace TagHelperScriptCutPaste.TagHelpers { [HtmlTargetElement("script", Attributes = "asp-paste-key")] public class TagHelperScriptPaste : TagHelper { [HtmlAttributeName("asp-paste-key")] public string DeferDestinationId { get; set; } [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } private IHtmlGenerator Generator { get; set; } public TagHelperScriptPaste(IHtmlGenerator generator) { this.Generator = generator; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { await base.ProcessAsync(context, output); if (this.ViewContext.HttpContext.Items.ContainsKey(TagHelperCutPaste.ItemsStorageKey)) { //get list of script contents object storage = this.ViewContext.HttpContext.Items[TagHelperCutPaste.ItemsStorageKey]; List cutKeys = storage as List; if (cutKeys == null) { //the key was found but conversion failed throw new ApplicationException($"Conversion failed for item type {storage.GetType()} to type" + typeof(Dictionary<string, TagHelperCutPaste>)); } if (cutKeys.Count == 0) { return; } //get those items that match the key for this tag helper List componentsWithStorageKey = cutKeys.Where(x => x.CutPasteKey == DeferDestinationId).ToList(); if (componentsWithStorageKey == null || componentsWithStorageKey.Count == 0) { return; } //render first item in place of this script TagHelperCutPaste firstScript = componentsWithStorageKey.First(); output.Content.SetHtmlContent(firstScript.TagHelperContent.GetContent()); foreach (TagHelperAttribute attr in firstScript.Attributes) { output.Attributes.Add(attr); } //add the rest of script items after the current one if (componentsWithStorageKey.Count > 0) { for (int i = 1; i < componentsWithStorageKey.Count; i++) { TagHelperCutPaste script = componentsWithStorageKey[i]; TagBuilder builder = new TagBuilder("script"); builder.MergeAttributes(script.Attributes.ToDictionary(x => x.Name, x => x.Value)); builder.InnerHtml.AppendHtml(script.TagHelperContent.GetContent()); output.PostElement.AppendHtml(builder); } } } return; } } }
ciao ho provato il tuo esempio in una applicazione net core 5 ma non funziona
The example was created using .NET Core 2.2. Internals might have changed but the idea should be the same. The Cut tag helper should read the rendering content, store it and prevent rendering. The PasteTagHelper should inject the stored data. Data is passed using the Items dictionary.
Make sure to “RenderSection(“Scripts”, false)” in _Layout
hi i tried your example in a net core 5 application but it doesn’t work