Search code examples
asp.net-mvcbundling-and-minificationvirtualpathprovider

Including a MVC View or string in a javascript bundle using Bundling framework


I'm trying to bundle several javascript files together, but I also need to include a variable from app.config in the js.

My thought was to use a Controller to return a string to set the variable, so going to ~/Javascript/Index would return var foo = "bar"; This works fine.

But when I try to build a bundle, the static files are being included, but the string (or view) isn't showing up. In looking around I found that the Optimizing framework was limited to static files up until 1.1 when support for VirtualPathProviders was implemented.

I upgraded to the latest package, but I can't find any information on how to get a mix of static files and ones generated by a Controller/View to bundle. I guess I just want to use the MVC path provider?

My other thought was to try to use the bundling engine to build a string of the bundled static files and then just append my string and return it all to the browser. But, I can't find a method that allows me to use the bundling engine to return a result of the bundling process.


Solution

  • Integration of dynamic content into the bundling process requires the following steps:

    1. Writing the logic that requests / builds the required content. Generating content from Controller directly requires a bit of work:

      public static class ControllerActionHelper
      {
          public static string RenderControllerActionToString(string virtualPath)
          {
              HttpContext httpContext = CreateHttpContext(virtualPath);
              HttpContextWrapper httpContextWrapper = new HttpContextWrapper(httpContext);
      
              RequestContext httpResponse = new RequestContext()
              {
                  HttpContext = httpContextWrapper,
                  RouteData = RouteTable.Routes.GetRouteData(httpContextWrapper)
              };
      
              // Set HttpContext.Current if RenderActionToString is called outside of a request
              if (HttpContext.Current == null)
              {
                  HttpContext.Current = httpContext;
              }
      
              IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
              IController controller = controllerFactory.CreateController(httpResponse,
                  httpResponse.RouteData.GetRequiredString("controller"));
              controller.Execute(httpResponse);
      
              return httpResponse.HttpContext.Response.Output.ToString();
          }
      
          private static HttpContext CreateHttpContext(string virtualPath)
          {
              HttpRequest httpRequest = new HttpRequest(string.Empty, ToDummyAbsoluteUrl(virtualPath), string.Empty);
              HttpResponse httpResponse = new HttpResponse(new StringWriter());
      
              return new HttpContext(httpRequest, httpResponse);
          }
      
          private static string ToDummyAbsoluteUrl(string virtualPath)
          {
              return string.Format("http://dummy.net{0}", VirtualPathUtility.ToAbsolute(virtualPath));
          }
      }
      
    2. Implement a virtual path provider that wraps the existing one and intercept all virtual paths that should deliver the dynamic content.

      public class ControllerActionVirtualPathProvider : VirtualPathProvider
      {
          public ControllerActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
          {
              // Wrap an existing virtual path provider
              VirtualPathProvider = virtualPathProvider;
          }
      
          protected VirtualPathProvider VirtualPathProvider { get; set; }
      
          public override string CombineVirtualPaths(string basePath, string relativePath)
          {
              return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
          }
      
          public override bool DirectoryExists(string virtualDir)
          {
              return VirtualPathProvider.DirectoryExists(virtualDir);
          }
      
          public override bool FileExists(string virtualPath)
          {
              if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
              {
                  return true;
              }
      
              return VirtualPathProvider.FileExists(virtualPath);
          }
      
          public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies,
              DateTime utcStart)
          {
              AggregateCacheDependency aggregateCacheDependency = new AggregateCacheDependency();
      
              List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
      
              // Create CacheDependencies for our virtual Controller Action paths
              foreach (string virtualPathDependency in virtualPathDependenciesCopy.ToList())
              {
                  if (ControllerActionHelper.IsControllerActionRoute(virtualPathDependency))
                  {
                      aggregateCacheDependency.Add(new ControllerActionCacheDependency(virtualPathDependency));
                      virtualPathDependenciesCopy.Remove(virtualPathDependency);
                  }
              }
      
              // Aggregate them with the base cache dependency for virtual file paths
              aggregateCacheDependency.Add(VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy,
                  utcStart));
      
              return aggregateCacheDependency;
          }
      
          public override string GetCacheKey(string virtualPath)
          {
              return VirtualPathProvider.GetCacheKey(virtualPath);
          }
      
          public override VirtualDirectory GetDirectory(string virtualDir)
          {
              return VirtualPathProvider.GetDirectory(virtualDir);
          }
      
          public override VirtualFile GetFile(string virtualPath)
          {
              if (ControllerActionHelper.IsControllerActionRoute(virtualPath))
              {
                  return new ControllerActionVirtualFile(virtualPath,
                      new MemoryStream(Encoding.Default.GetBytes(ControllerActionHelper.RenderControllerActionToString(virtualPath))));
              }
      
              return VirtualPathProvider.GetFile(virtualPath);
          }
      
          public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
          {
              return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
          }
      
          public override object InitializeLifetimeService()
          {
              return VirtualPathProvider.InitializeLifetimeService();
          }
      }
      
      public class ControllerActionVirtualFile : VirtualFile
      {
          public CustomVirtualFile (string virtualPath, Stream stream)
              : base(virtualPath)
          {
              Stream = stream;
          }
      
          public Stream Stream { get; private set; }
      
          public override Stream Open()
          {
               return Stream;
          }
      }
      

      You also have to implement CacheDependency if you need it:

      public class ControllerActionCacheDependency : CacheDependency
      {
          public ControllerActionCacheDependency(string virtualPath, int actualizationTime = 10000)
          {
              VirtualPath = virtualPath;
              LastContent = GetContentFromControllerAction();
      
              Timer = new Timer(CheckDependencyCallback, this, actualizationTime, actualizationTime);
          }
      
          private string LastContent { get; set; }
      
          private Timer Timer { get; set; }
      
          private string VirtualPath { get; set; }
      
          protected override void DependencyDispose()
          {
              if (Timer != null)
              {
                  Timer.Dispose();
              }
      
              base.DependencyDispose();
          }
      
          private void CheckDependencyCallback(object sender)
          {
              if (Monitor.TryEnter(Timer))
              {
                  try
                  {
                      string contentFromAction = GetContentFromControllerAction();
      
                      if (contentFromAction != LastContent)
                      {
                          LastContent = contentFromAction;
                          NotifyDependencyChanged(sender, EventArgs.Empty);
                      }
                  }
                  finally
                  {
                      Monitor.Exit(Timer);
                  }
              }
          }
      
          private string GetContentFromControllerAction()
          {
              return ControllerActionHelper.RenderControllerActionToString(VirtualPath);
          }
      }
      
    3. Register your virtual path provider:

      public static void RegisterBundles(BundleCollection bundles)
      {
          // Set the virtual path provider
          BundleTable.VirtualPathProvider = new ControllerActionVirtualPathProvider(BundleTable.VirtualPathProvider);
      
          bundles.Add(new Bundle("~/bundle")
              .Include("~/Content/static.js")
              .Include("~/JavaScript/Route1")
              .Include("~/JavaScript/Route2"));
      }
      
    4. Optional: Add Intellisense support to your views. Use <script> tags within your View and let them be removed by a custom ViewResult:

      public class DynamicContentViewResult : ViewResult
      {
          public DynamicContentViewResult()
          {
              StripTags = false;
          }
      
          public string ContentType { get; set; }
      
          public bool StripTags { get; set; }
      
          public string TagName { get; set; }
      
          public override void ExecuteResult(ControllerContext context)
          {
              if (context == null)
              {
                  throw new ArgumentNullException("context");
              }
      
              if (string.IsNullOrEmpty(ViewName))
              {
                  ViewName = context.RouteData.GetRequiredString("action");
              }
      
              ViewEngineResult result = null;
      
              if (View == null)
              {
                  result = FindView(context);
                  View = result.View;
              }
      
              string viewResult;
      
              using (StringWriter viewContentWriter = new StringWriter())
              {
                  ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, viewContentWriter);
      
                  View.Render(viewContext, viewContentWriter);
      
                  if (result != null)
                  {
                      result.ViewEngine.ReleaseView(context, View);
                  }
      
                  viewResult = viewContentWriter.ToString();
      
                  // Strip Tags
                  if (StripTags)
                  {
                      string regex = string.Format("<{0}[^>]*>(.*?)</{0}>", TagName);
                      Match res = Regex.Match(viewResult, regex,
                          RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Singleline);
      
                      if (res.Success && res.Groups.Count > 1)
                      {
                          viewResult = res.Groups[1].Value;
                      }
                      else
                      {
                          throw new InvalidProgramException(
                              string.Format("Dynamic content produced by View '{0}' expected to be wrapped in '{1}' tag.", ViewName, TagName));
                      }
                  }
              }
      
              context.HttpContext.Response.ContentType = ContentType;
              context.HttpContext.Response.Output.Write(viewResult);
          }
      }
      

      Use an extension method or add an helper function to your controller:

      public static DynamicContentViewResult JavaScriptView(this Controller controller, string viewName, string masterName, object model)
      {
          if (model != null)
          {
              controller.ViewData.Model = model;
          }
      
          return new DynamicContentViewResult
          {
              ViewName = viewName,
              MasterName = masterName,
              ViewData = controller.ViewData,
              TempData = controller.TempData,
              ViewEngineCollection = controller.ViewEngineCollection,
              ContentType = "text/javascript",
              TagName = "script",
              StripTags = true
          };
      }
      

    The steps are similiar for other type of dynamic contents. See Bundling and Minification and Embedded Resources for example.

    I added a proof of concept repository to GitHub if you want to try it out.