Search code examples
asp.net-coreasp.net-core-tag-helpers

ASP.NET Core: Detect whether a tag helper is rendered with a parent element


In ASP.NET Core is it possible to detect whether a tag helper is rendered with a parent element?

The reason I ask is that I have a script tag helper which caches the HTML it would normally render and instead I use middleware to render the HTML before the closing body tag. However if the tag helper is called within a partial view then I'd like to simply render it in place.

My first thoughts was to add a body tag helper and then add a variable to the request cache, which I could detect within my script tag helper. However I found that since the body tag helper was within my layout page, it was rendered after my view (which executes the script tag helper).

Another solution I thought was to store a variable within the action, but I don't like this as I have to remember to do this every time I return a partial view.

I'd appreciate any help.

Edit: Here's some code for reference.

ScriptTagHelper:

public class ScriptTagHelper : TagHelper {
    private readonly IHtmlManager _htmlManager;

    public ScriptTagHelper(IHtmlManager htmlManager) {
        _htmlManager = htmlManager;
    }

    public string? At { get; set; }

    [HtmlAttributeNotBound, ViewContext]
    public ViewContext ViewContext { get; set; } = default!;

    public override void Process(TagHelperContext context, TagHelperOutput output) {
        if (!string.IsNullOrEmpty(At)) {
            output.SuppressOutput();

            var builder = new TagBuilder("script") {
                TagRenderMode = TagRenderMode.Normal
            };

            foreach (var attribute in output.Attributes) {
                builder.Attributes.Add(attribute.Name, attribute.Value.ToString());
            }

            _htmlManager.RegisterHtml(At, builder);
        }
    }
}

Here's my middleware:

public class RenderHtmlMiddleware {
    private readonly RequestDelegate _next;

    public RenderHtmlMiddleware(RequestDelegate next) {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext, IHtmlManager htmlManager) {
        var originBody = httpContext.Response.Body;

        using var body = new MemoryStream();
        httpContext.Response.Body = body;

        try {
            await _next(httpContext);
        } finally {
            httpContext.Response.Body = originBody;
        }

        if (body.Length > 0) {
            var content = Encoding.UTF8.GetString(body.ToArray());

            foreach (var position in Enum.GetValues<HtmlPosition>()) {
                var html = await htmlManager.GetHtmlAsync(position);
                var innerHtml = string.Join("", html.Select(h => h.ToHtmlString()));

                ... Code removed for brevity which handles different positions
                
                content = content.Replace("</body>", string.Concat(innerHtml, Environment.NewLine, "</body>"));
            }

            httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(content);

            await httpContext.Response.WriteAsync(content);
        }
    }
}

Now within my view/partial view I can simply say:

<script at="@HtmlPosition.BodyPostContent" src="foo"></script>

Solution

  • However if the tag helper is called within a partial view then I'd like to simply render it in place.

    If you just want to check if it was rendered in a partialview:

    you may add a property:

    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }
    

    You could access HttpContext/current view name with ViewContext

    a minimal example:

    [HtmlTargetElement("MyTag")]
        public class MyTag : TagHelper
        {
    
            [HtmlAttributeNotBound]
            [ViewContext]
            public ViewContext ViewContext { get; set; }
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                output.TagName = "label";
                //check if is partial
                var path = ViewContext.View.Path;
                var ispartial = path.Contains("Partial");
               
                if (!ispartial) 
                {
                    output.SuppressOutput();
                }
                
                
            }
        }
    

    In Home View:

    <partial name="MyPartial"/>
    <MyTag>Home</MyTag>
    

    In Partial View:

    <MyTag>Partial</MyTag>
    

    Result:

    enter image description here

    only <label>partial</label> was rendered

    Update: A possible solution for template:

    public class MyPartial : PartialTagHelper
        {
            public MyPartial(ICompositeViewEngine viewEngine, IViewBufferScope viewBufferScope) : base(viewEngine, viewBufferScope)
            {
            }
            
            
            public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 
            {
                this.ViewContext.HttpContext.Items.Add("IsPartial", true);
                await base.ProcessAsync(context, output);
                this.ViewContext.HttpContext.Items.Remove("IsPartial");
            }
        }
    
    
    [HtmlTargetElement("MyTag")]
        public class MyTag : TagHelper
        {
    
            [HtmlAttributeNotBound]
            [ViewContext]
            public ViewContext ViewContext { get; set; }
            
    
            public ViewLocationExpanderContext ViewLocationExpanderContext { get; set; }
            public override void Process(TagHelperContext context, TagHelperOutput output)
            {
                output.TagName = "label";
    
                var ispartial = ViewContext.HttpContext.Items.TryGetValue("IsPartial",out var partial);          
    
                //access httpcontext            
               
                if (!ispartial) 
                {
                    output.SuppressOutput();
                }
                
                
                
            }
        }
    

    In View:

    @model MyEntity
    <my-partial name="MyPartial"model="@Model"/>
    
    @Html.DisplayFor(x=>x.SomeEntity)
    

    In partial:

    @model MyEntity
    
    
    @Html.DisplayFor(x=>x.SomeEntity)
    

    Template:

    @model SomeEntity
    
    <dl>
        <dd>Id:@Model.Id</dd>
        <dd>Amount:@Model.Amount</dd>
        <dd>Rate:@Model.Rate</dd>    
    </dl>
    <MyTag>InPartial1</MyTag>
    <MyTag>InPartial2</MyTag>
    

    Even if taghelper was call in template,Only that in partial view was rendered: enter image description here