Search code examples
c#asp.net-corerazorasp.net-core-mvc

Dynamically include Partial views on Razor pages/partials


In aspnetcore, is it possible to dynamically include partial views on a given view and/or partial view?

Scenario:

I make use of webpack to bundle client side assets. One aspect of this is that if webpack finds a .ts file that has the same name as a .cshtml file in the same folder, it will generate another .cshtml that can be included on the page via a Html.Partial(). The generated .cshtml contains code to include the compiled output of the .ts file.

e.g.

|- + Views
   |-  Index.cshtml
   |-  Index.ts
   |-  Index.scriptbundle.cshtml  <-- generated by a webpack build

Example content of .scriptbundle.cshtml:

@Html.RequireScript(@<script src='@Url.Content("~/dist/js/Index.js")' asp-append-version="true"></script>)

Where .RequireScript() is an extension method that gathers script includes and outputs them in a block in the final output.

At the moment the Index.scriptbundle.cshtml is automatically added to the parent page (Index.cshtml) by some logic in the layout file. This all works fine, but only works for full-page rendering. It does not work for the rendering of Partial views as there is no layout file involved.

I'm looking for a way to move the automatic scriptbundle inclusion logic from the layout file to somewhere more intrinsic so that this pattern works for full page renders and partial page renders too.

I had this working in ASP.NET by using a customised base class for the page that inherited from System.Web.Mvc.WebViewPage, then overriding ExecutepageHierarchy() so that it added a call Html.Partial() as part of the view rendering if a scriptbundle.cshtml file existed. The way views are rendered has completely changed however, and the same extension points are not available.

I've been trying to find a way to influence the code generation for Razor views, but I can't find where this is done. My theory is that I could change the way code is generated from Razor views such that, if a scriptbundle.cshtml file is found, a call to Html.PartialAsync() can be added to the generated output. I've found referneces to ICompilationService and DefaultRoslynCompilationService[1][2], but I can't find where these classes/interfaces are in .NET (I'm using v8).

[1] https://whosnailaspnetcoredocs.readthedocs.io/ko/latest/mvc/views/razor.html

[2] https://stackoverflow.com/a/43004742


Solution

  • To achieve your automatic include of the script bundle without requiring developers to remember to add specific code to the view you could try to us view components and view filters. below is the sample code:

    First, create a view component that checks if a corresponding .scriptbundle.cshtml file exists and then renders it if it does.

    ScriptBundleViewComponent.cs:

    using Microsoft.AspNetCore.Mvc;
    
    namespace DynamicScriptBundleDemo.Components
    {
        public class ScriptBundleViewComponent : ViewComponent
        {
            public IViewComponentResult Invoke(string viewName)
            {
                var scriptBundlePath = Path.Combine(Directory.GetCurrentDirectory(), "Views", viewName + ".scriptbundle.cshtml");
                if (System.IO.File.Exists(scriptBundlePath))
                {
                    var relativePath = $"~/Views/{viewName}.scriptbundle.cshtml";
                    return View(relativePath);
                }
                return Content(string.Empty);
            }
    
        }
    }
    

    You need to create a view filter that automatically injects the script bundle by calling the view component.

    ScriptBundleViewFilter.cs:

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.ViewComponents;
    using Microsoft.AspNetCore.Mvc.ViewEngines;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using Microsoft.Extensions.DependencyInjection;
    using System.IO;
    using System.Text.Encodings.Web;
    using System.Threading.Tasks;
    
    namespace DynamicScriptBundleDemo.Filters
    {
        public class ScriptBundleViewFilter : IAsyncResultFilter
        {
            private readonly IServiceProvider _serviceProvider;
    
            public ScriptBundleViewFilter(IServiceProvider serviceProvider)
            {
                _serviceProvider = serviceProvider;
            }
    
            public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
            {
                var response = context.HttpContext.Response;
    
                if (context.Result is ViewResult viewResult)
                {
                    var viewName = viewResult.ViewName ?? context.ActionDescriptor.RouteValues["action"];
                    if (!string.IsNullOrEmpty(viewName))
                    {
                        var viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
    
                        var viewContext = new ViewContext(
                            context,
                            new DummyView(),
                            viewResult.ViewData,
                            context.HttpContext.RequestServices.GetRequiredService<ITempDataDictionaryFactory>().GetTempData(context.HttpContext),
                            TextWriter.Null,
                            new HtmlHelperOptions()
                        );
    
                        (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
    
                        var scriptBundleContent = await viewComponentHelper.InvokeAsync("ScriptBundle", new { viewName });
    
                        // Convert the ViewBuffer (scriptBundleContent) to a string
                        using (var writer = new StringWriter())
                        {
                            scriptBundleContent.WriteTo(writer, HtmlEncoder.Default);
                            var renderedContent = writer.ToString();
    
                            await next(); // Allow the view to render first
    
                            if (!string.IsNullOrEmpty(renderedContent))
                            {
                                // Write the script bundle content at the end of the response
                                await response.WriteAsync(renderedContent);
                            }
                        }
    
                        return; // Skip the next invocation since we have already processed the response
                    }
                }
    
                await next(); // Proceed normally if no script bundle is involved
            }
    
            private class DummyView : IView
            {
                public string Path => string.Empty;
    
                public Task RenderAsync(ViewContext context)
                {
                    return Task.CompletedTask;
                }
            }
        }
    }
    

    Program.cs:

    using DynamicScriptBundleDemo.Filters;
    using Microsoft.AspNetCore.Mvc.ViewComponents;
    using Microsoft.AspNetCore.Mvc;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddControllersWithViews(options =>
    {
        options.Filters.Add<ScriptBundleViewFilter>();
    });
    
    builder.Services.AddScoped<IViewComponentHelper, DefaultViewComponentHelper>();
    
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    
    app.Run();
    

    HomeController.cs:

    using DynamicScriptBundleDemo.Models;
    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    
    namespace DynamicScriptBundleDemo.Controllers
    {
        public class HomeController : Controller
        {
            public IActionResult Index()
            {
                return View();
            }
    
            public IActionResult _IndexPartial()
            {
                return PartialView();
            }
        }
    
    }
    

    The script bundle is automatically included based on the view being rendered. So, developers don't have to manually include script tags in each view.

    enter image description here