Introduction
When your site grows in complexity, it makes sense to keep it maintainable by using components, such as partial views, DisplayTemplates and EditorTemplates.
The problem is that while you can organize your HTML this way, your CSS, JavaScript and images are still stored in their own directories - with no clear indication exactly what CSS, etc. is used by which partials. Essentially, the implementation of each partial is spread over up to 4 directories.
That makes it very easy to break a partial when changing a CSS or JavaScript file - because the dependencies between CSS, JavaScript and HTML are not always clear. That also makes it hard to reuse a partial somewhere else.
You can solve this to some extent by using MVC areas, but those are used mainly to create sub-sites.
The solution is to co-locate all assets (CSS, JavaScript and images) that support some HTML with that HTML, in the same directory. Assets that support all pages in a controller can go in the view directory for that controller. And assets that are shared throughout the site can go into a top level directory.
This can lead to lots of small, focused CSS and JavaScript files. That's a good thing, but it makes creating your MVC bundles harder - your pages still have to load the right CSS and JavaScript in the right order. To help with this, Dynamic Bundles generates the bundles for you when the page loads. Caching keeps overhead to a minimum.
Key benefits
Dynamic Bundles is an extension of the Razor view engine and MVC bundles. It greatly improves maintainability and code reuse of ASP.NET MVC sites:
- Co-locate the HTML, CSS, JavaScript and images that make up a page or component in the same directory, instead of organizing these files in separate directories by type. This clearly exposes dependencies.
- Auto generate CSS and JavaScript bundles that contain the right files in the right order, based on their file organisation. No need to recompile when files are added or deleted. Caching keeps CPU usage and disk accesses minimal.
- Provides the same minification and file combining as standard MVC bundles.
File Structure
Lets compare the file structure of a classic MVC site with one that uses Dynamic Bundles.
Classic MVC
- HTML, CSS, JavaScript and image files organized in separate directories by type.
- CSS and JavaScript that support different pages and components often put in the same physical files, creating hidden dependencies.
- Long brittle urls from CSS files to background images.
- Unclear what CSS, JavaScript and images are required for a given component, making reuse harder.
Dynamic Bundles
- HTML, CSS, JavaScript and image files that belong together sit in the same directory.
- The view engine included in Dynamic Bundles lets you put partial views and layout files in their own sub directories.
- Splitting CSS, JavaScript by component encourages developers to keep these files small and focussed.
- Short simple image background image urls in your CSS.
- Co-locating all assets that make up a component makes reuse much easier.
Bundles
Lets compare the way that bundles are created with a classic MVC site with one that uses Dynamic Bundles.
Classic MVC
- You have to create and maintain bundles yourself.
- You have to make sure to include the correct files, and in the right order.
- When CSS and JavaScript files are added or deleted, the site needs to be recompiled.
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/shared/js").Include(
"~/Scripts/SharedCode.js",
"~/Scripts/VariousCode.js"));
bundles.Add(new ScriptBundle("~/bundles/pile/js").Include(
"~/Scripts/PileOfCode.js"));
bundles.Add(new StyleBundle("~/Content/shared/css").Include(
"~/Content/Reset.css",
"~/Content/Site.css"));
bundles.Add(new StyleBundle("~/Content/account/css").Include(
"~/Content/Account.css"));
}
@Styles.Render("~/Content/shared/css")
@Styles.Render("~/Content/account/css")
...
@RenderBody()
...
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/shared/js")
@Scripts.Render("~/bundles/pile/js")
Dynamic Bundles
- Bundles are auto generated. No need to create bundles yourself.
- Ensures only required CSS and JavaScript files are loaded, and in the right order.
- Optimizes client side caching, by combining files into bundles by area, controller, shared and layout.
- When CSS and JavaScript files are added or deleted, new bundles are automatically generated without recompilation.
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
}
}
@*Nominate where to load the bundles.
The bundles themselves are automatically generated.*@
@DynamicBundlesTopRender()
...
@RenderBody()
...
@DynamicBundlesBottomRender()
Installation
- Install Dynamic Bundles
- Add view engine to global.asax
- Add layout container
- Update Web.config for views
- Co-locate assets
- Create explicit dependencies
1. Install Dynamic Bundles
Install the DynamicBundles package from NuGet:
Install-Package DynamicBundles
2. Add view engine to global.asax
Update your global.asax.cs or global.asax.vb, to add the DynamicBundles view engine:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ViewEngines.Engines.Add(new DynamicBundles.DynamicBundlesViewEngine());
}
...
}
3. Add layout container
In classic MVC sites, pages sit within a _Layout.cshtml or _Layout.vbhtml file, which contains shared headers, footers, etc.
The problem when introducing Dynamic Bundles is that you want to separate CSS, JavaScript and pictures that are specific to the layout from those that are shared by the entire site.
To make this separation happen, create a new file _LayoutContainer.cshtml (you'll see the content in a moment). This and the _Layout.cshtml go into their own directory. The result looks like this:
Classic MVC
Dynamic Bundles
Contents of _LayoutContainer.cshtml
<!DOCTYPE html>
<html>
@*Nominate where to load the bundles. The bundles themselves are automatically generated.*@
@DynamicBundlesTopRender()
@RenderBody()
@DynamicBundlesBottomRender()
</html>
Changes to _Layout.cshtml
- Set Layout to _LayoutContainer.cshtml, which acts as the overall container of the site.
- Remove the doctype and html tags.
- Remove all style and script rendering, including rendering of script sections.
@{
Layout = "../_LayoutContainer/_LayoutContainer.cshtml";
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Dynamic Bundles for ASP.NET MVC</title>
@Styles.Render("~/Content/shared/css")
@Styles.Render("~/Content/account/css")
</head>
<body>
@RenderBody()
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/shared/js")
@Scripts.Render("~/bundles/pile/js")
@RenderSection("scripts", required: false)
</body>
</html>
4. Update Web.config for views
In addition to the Web.config file in the root directory of your site, each MVC site also has a Web.config file in its Views directory. If your site uses areas, each area has a Views directory as well, with its own Web.config file.
The Web.config files in the Views directories need to be updated to:
- Install the Dynamic Bundles page base type. This gets each view to register the assets it needs, so bundles with the right files can be generated.
- Allow the web server to serve CSS, JavaScript and image files from the Views directory.
<configuration>
<system.web.webPages.razor>
<pages pageBaseType="System.Web.Mvc.WebViewPageDynamicBundles.WebViewPage">
...
</pages>
</system.web.webPages.razor>
<system.webServer>
<!---->
<handlers>
<remove name="BlockViewHandler"/>
<!---->
<add name="BlockViewHandler" path="*.cshtml" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>
</configuration>
5. Co-locate assets
Finally co-locate your asset files (CSS, JavaScript, images) with the views where they are used:
- Move all assets that are shared throughout the site in the _LayoutContainer directory.
- Move all assets that are shared by all views for a controller into that controller's directory.
- If there are assets specific to a single view file, create a sub directory for that view file and put all assets (including the view itself) into that sub directory. Be sure to name the sub directory the same as the view file, without the extension.
- Dynamic Bundles will add both the assets in the sub directory and those in its parent directory(s), less specific assets first. In the example below, when /Product/List is loaded, first the assets in ~/Views/Product and then those in ~/Views/Product/List are added.
This works for both controller specific views and shared (partial) views. Dynamic Bundles makes sure that assets are only ever added once to a bundle.
6. Create explicit dependencies
You may have dependencies from one directory on another. For example, a JavaScript file assumes that a file in another directory has been loaded.
You can specify these dependencies with .nuspec files. These have the same structure as their NuGet counterparts (definition).
If Dynamic Bundles finds a nuspec file in a directory, it will find the directories specified in that nuspec file and add the assets in those directories to the bundles. If those directories have nuspec files themselves, Dynamic Bundles processes those nuspec files as well, etc. This is a fully recurrent process.
To create a dependency from a directory X to some other directories, include a .nuspec file in directory X that looks like this:
="1.0"
<package >
<metadata>
<dependencies>
<dependency id="../AccountDetailsAssets" />
<dependency id="~/Views/Shared/DetailsAssets" />
</dependencies>
</metadata>
</package>
Note that:
- The name of the .nuspec file doesn't matter, as long as it has the extension .nuspec.
- You can specify root relative paths (starting with ~/) and paths relative to the .nuspec file, but not absolute paths (such as C:\Dev\Views\Accounts).