asp.net-mvc-4asp.net-optimization

MVC4 StyleBundle doesn't render the bundle in the correct order


I'm trying to render a bundle of css files, but the output is in wrong order. I've tried the solution @ MVC4 Beta Minification and Bundling: Ordering files and debugging in browser , but it didn't help. Here is the bundle:

bundles.Add(new StyleBundle("~/stylesheet")
    .Include("~/css/main.css")
    .Include("~/css/mvc.css")
    .Include("~/js/jquery.thickbox.css")
    .Include("~/js/jquery.rating.css")
    .Include("~/css/ProductListing.css")
    .Include("~/css/dropdown/dropdown.css")
    .Include("~/css/dropdown/dropdown.vertical.css")
    .Include("~/js/fancybox/jquery.fancybox-1.3.1.css")
    .Include("~/css/scartpopup.css")
    .Include("~/css/ShoppingCart.css")
    .Include("~/css/ceebox.css")
    .Include("~/css/tooltip.css")
    .Include("~/css/recent_blog_posts.css")
    .Include("~/css/ProductDetail.css")
    .Include("~/css/jquery-ui-1.7.3.custom.css")
    .Include("~/css/filter_box.css")
    .Include("~/css/custom_page.css")
    .Include("~/css/Checkout.css")
    .Include("~/css/CheckoutButton.css")
);

And here is the result, as you can see the jquery-ui comes to top.

<link href="/css/jquery-ui-1.7.3.custom.css" rel="stylesheet"/>
<link href="/css/main.css" rel="stylesheet"/>
<link href="/css/mvc.css" rel="stylesheet"/>
<link href="/js/jquery.thickbox.css" rel="stylesheet"/>
<link href="/js/jquery.rating.css" rel="stylesheet"/>
<link href="/css/ProductListing.css" rel="stylesheet"/>
<link href="/css/dropdown/dropdown.css" rel="stylesheet"/>
<link href="/css/dropdown/dropdown.vertical.css" rel="stylesheet"/>
<link href="/js/fancybox/jquery.fancybox-1.3.1.css" rel="stylesheet"/>
<link href="/css/scartpopup.css" rel="stylesheet"/>
<link href="/css/ShoppingCart.css" rel="stylesheet"/>
<link href="/css/ceebox.css" rel="stylesheet"/>
<link href="/css/tooltip.css" rel="stylesheet"/>
<link href="/css/recent_blog_posts.css" rel="stylesheet"/>
<link href="/css/ProductDetail.css" rel="stylesheet"/>
<link href="/css/filter_box.css" rel="stylesheet"/>
<link href="/css/custom_page.css" rel="stylesheet"/>
<link href="/css/Checkout.css" rel="stylesheet"/>
<link href="/css/CheckoutButton.css" rel="stylesheet"/>

How can I make sure that the stylesheets are rendered in correct order?


Solution

  • Bundling is not supposed to render the CSS files in the exact same order, it follows a different logic. If you need to render them as defined, then you should create a custom IBundleOrderer and set it to the bundle as the required Orderer:

    public class AsDefinedBundleOrderer : IBundleOrderer
    {
        public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
        {
            return files;
        }
    }
    

    And

    var bundle = new StyleBundle("~/stylesheet");
    bundle.Orderer = new AsDefinedBundleOrderer();
    bundles.Add(bundle);
    

    Then this will do nothing with the list so Render will render them exactly in the same order.

    Update on default ordering

    Bundling uses the concept of IBundleOrderer to sort the items within a Bundle. The Bundle class has it's Orderer property which looks like this:

    public IBundleOrderer Orderer
    {
      get
      {
        if (this._orderer == null)
          return (IBundleOrderer) DefaultBundleOrderer.Instance;
        else
          return this._orderer;
      }
      set
      {
        this._orderer = value;
        this.InvalidateCacheEntries();
      }
    }
    

    So the default orderer is actually a DefaultBundleOrderer until you overwrite it with your custom orderer.

    The IBundleOrderer has the following signature:

    public interface IBundleOrderer
    {
      IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files);
    }
    

    The DefaultBundleOrderer implementation of this orders the files by the BundleContext, here is a snippet from the implementation of OrderFiles:

      foreach (BundleFileSetOrdering ordering in (IEnumerable<BundleFileSetOrdering>) context.BundleCollection.FileSetOrderList)
        DefaultBundleOrderer.AddOrderingFiles(ordering, (IEnumerable<FileInfo>) list, fileMap, foundFiles, result);
    

    So the different result happens because this. This is of course not a random sort algorithm :) The rules are defined in the BUndleCollection class:

    public static void AddDefaultFileOrderings(IList<BundleFileSetOrdering> list)
    {
      if (list == null)
        throw new ArgumentNullException("list");
      BundleFileSetOrdering bundleFileSetOrdering1 = new BundleFileSetOrdering("css");
      bundleFileSetOrdering1.Files.Add("reset.css");
      bundleFileSetOrdering1.Files.Add("normalize.css");
      list.Add(bundleFileSetOrdering1);
      BundleFileSetOrdering bundleFileSetOrdering2 = new BundleFileSetOrdering("jquery");
      bundleFileSetOrdering2.Files.Add("jquery.js");
      bundleFileSetOrdering2.Files.Add("jquery-min.js");
      bundleFileSetOrdering2.Files.Add("jquery-*");
      bundleFileSetOrdering2.Files.Add("jquery-ui*");
      bundleFileSetOrdering2.Files.Add("jquery.ui*");
      bundleFileSetOrdering2.Files.Add("jquery.unobtrusive*");
      bundleFileSetOrdering2.Files.Add("jquery.validate*");
      list.Add(bundleFileSetOrdering2);
      BundleFileSetOrdering bundleFileSetOrdering3 = new BundleFileSetOrdering("modernizr");
      bundleFileSetOrdering3.Files.Add("modernizr-*");
      list.Add(bundleFileSetOrdering3);
      BundleFileSetOrdering bundleFileSetOrdering4 = new BundleFileSetOrdering("dojo");
      bundleFileSetOrdering4.Files.Add("dojo.*");
      list.Add(bundleFileSetOrdering4);
      BundleFileSetOrdering bundleFileSetOrdering5 = new BundleFileSetOrdering("moo");
      bundleFileSetOrdering5.Files.Add("mootools-core*");
      bundleFileSetOrdering5.Files.Add("mootools-*");
      list.Add(bundleFileSetOrdering5);
      BundleFileSetOrdering bundleFileSetOrdering6 = new BundleFileSetOrdering("prototype");
      bundleFileSetOrdering6.Files.Add("prototype.js");
      bundleFileSetOrdering6.Files.Add("prototype-*");
      bundleFileSetOrdering6.Files.Add("scriptaculous-*");
      list.Add(bundleFileSetOrdering6);
      BundleFileSetOrdering bundleFileSetOrdering7 = new BundleFileSetOrdering("ext");
      bundleFileSetOrdering7.Files.Add("ext.js");
      bundleFileSetOrdering7.Files.Add("ext-*");
      list.Add(bundleFileSetOrdering7);
    }
    

    So when you call this from Application_Start:

    BundleConfig.RegisterBundles(BundleTable.Bundles);
    

    Actually you pass the default BundleCollection defined in the library.

    So we have the BundleFileSetOrdering instances passed one-by-one into:

    private static void AddOrderingFiles(BundleFileSetOrdering ordering, IEnumerable<FileInfo> files, Dictionary<string, HashSet<FileInfo>> fileMap, HashSet<FileInfo> foundFiles, List<FileInfo> result)
    {
      foreach (string key in (IEnumerable<string>) ordering.Files)
      {
        if (key.EndsWith("*", StringComparison.OrdinalIgnoreCase))
        {
          string str = key.Substring(0, key.Length - 1);
          foreach (FileInfo fileInfo in files)
          {
            if (!foundFiles.Contains(fileInfo) && fileInfo.Name.StartsWith(str, StringComparison.OrdinalIgnoreCase))
            {
              result.Add(fileInfo);
              foundFiles.Add(fileInfo);
            }
          }
        }
        else if (fileMap.ContainsKey(key))
        {
          List<FileInfo> list = new List<FileInfo>((IEnumerable<FileInfo>) fileMap[key]);
          list.Sort((IComparer<FileInfo>) FileInfoComparer.Instance);
          foreach (FileInfo fileInfo in list)
          {
            if (!foundFiles.Contains(fileInfo))
            {
              result.Add(fileInfo);
              foundFiles.Add(fileInfo);
            }
          }
        }
      }
    }
    

    Conclusion

    If we want to simplify the process we can say that the library prefers some kind of files and makes some sorting on the other files if multiple possibilities found. This is the expected behavior most of the time but as you can see it is easly overridable with the AsDefinedBundleOrderer so it does nothing with the given file set so the order remains the original.