Bundling in MVC4
In today's world we tend to create sophisticated websites with rich content using multiple CSS or CSS3 files and jQuery plugins or JavaScript files that help us to display rich content on the webpage and it gives a better look and feel to the application pages that in turn increases the user's attraction towards the websites. But a problem arises when the user tries to request pages that are enriched with multiple CSS or scripts files or images. What happens behind the scenes is that for every stylesheet or script file or image used on the page the request from the browser to the server is made for downloading the item in the user's browser that decreases the performance of the page. For instance let's say that there are 5 CSS files and 7 scripts files used on the page. Then in this case there would be approximately 13 to 14 requests made for each CSS and scripts files and for accessing the page (12 requests for CSS and Script and 1 request for accessing the requested page and 1 more for parsing the requested page). Although the latest browser are now supporting caching features but the problem is that at least for the first request from the browser to the server it would use a lot of bandwidth on the server and if your application is used by multiple user's then in that case it would cause a serious performance issue. Using MVC we can reduce this unwanted number of calls from the browser to the server using the feature named “Bundling”.
Bundling helps us to manage our stylesheets and scripts files in a more elegant way. It also helps in reducing the network traffic by optimizing the request processing (in other words reducing the number of calls for downloading the scripts/CSS files) from the user to the server and vice versa. Bundling helps us to merge all the files in a single request from the server to the browser. As we all know now that when there are many requests and calls from the user to the server the performance of the server is reduced due to use of many server resources that in turn affects the performance of the application. This is where features like bundling acts as a “Boon” in the application development. Whenever you create a new project in MVC and you select the project template other than “Empty” you get many scripting files that consist of jQuery and its supporting files like validate, unobtrusive and so on. You may also find the script files exist in two flavors like jquery-{version}.js and jqyery-{version}.min.js and so on.
A JavaScript file is a scripting file that is mostly used in the development environment because it helps us to debug the file more efficiently then the minified version. It contains proper syntax with white spaces and comments and more user-friendly names given to variables/functions wherever required and it is very much easy to debug it like a normal JavaScript file. If you try to look at the minified version of the jquery-{version}.min.js file then in this case you may find a single line of entry stretched from left to right with no spaces or comments and no proper syntax because of which it becomes difficult for the developer to debug the minified script file. The advantage of using a minified version in the application is that it is lightweight (it causes less bandwidth and less download time on the server to deliver it to the client browser) that's why it used in the deployment process.
Let's proceed with the demo application. For this I'm selecting the project template as “Basic”.
When you create a new project you'll find many script files added to the application. Some of them may be a .js file and some them may be .min.js files. In the case of a knockout-{version}.js file the convention is a little bit different. You'll find the name as “knockout-2.1.0.debug.js” and “knockout-2.1.0.js” where the “knockout-2.1.0.debug.js” file is used in development and “knockout-2.1.0.js” is used in the deployment process.
Let's add a new model with the name “Employee.cs”; the following is the code snippet for it.
-
- [Bind(Exclude = "Id")]
- public class Employee
- {
- [Key]
- public int Id { get; set; }
-
- [Required(ErrorMessage = "Name is Required")]
- public string Name { get; set; }
-
- [Required(ErrorMessage = "Address is Required")]
- public string Address { get; set; }
-
- [Required(ErrorMessage = "Salary is Required")]
- public float Salary { get; set; }
- }
I've added two new folders to the application with the names “
Concrete” and “
Abstract” that will store the C# files and interfaces. I've added a new interface named “
IEmployeeRepository” within the Abstract folder. The following is the code snippet
“
IEmployeeRepository.cs”:
- public interface IEmployeeRepository
- {
- IQueryable<Employee> Employees { get; }
- void AddEmployee(Employee objEmployee);
-
- void DeleteEmployee(int eid);
- }
Inside the Concrete folder we've added two new classes. Here is the code snippet for it.
“DummyDbContext.cs”
-
-
-
-
- public class DummyDbContext
- {
- private IList<Employee> _lstEmployees;
-
- public DummyDbContext()
- {
- _lstEmployees = new List<Employee>();
- _lstEmployees.Add(new Employee() { Id = CreateIdentity(), Name = "Vishal Gilbile", Address = "Andheri", Salary = 25000 });
- _lstEmployees.Add(new Employee() { Id = CreateIdentity(), Name = "Rahul Bandekar", Address = "Bandra", Salary = 18000 });
- _lstEmployees.Add(new Employee() { Id = CreateIdentity(), Name = "Jack Fernandis", Address = "Santacruz", Salary = 30000 });
- _lstEmployees.Add(new Employee() { Id = CreateIdentity(), Name = "Lincy Pullan", Address = "Borivali", Salary = 45000 });
- _lstEmployees.Add(new Employee() { Id = CreateIdentity(), Name = "Sunny D'Souza", Address = "Vasai", Salary = 50000 });
- }
-
-
-
-
-
- private int CreateIdentity()
- {
- if (_lstEmployees.Count == 0)
- return 1;
- else
- {
- return _lstEmployees[_lstEmployees.Count - 1].Id + 1;
- }
- }
-
- public IQueryable<Employee> Employees
- {
- get
- {
- return _lstEmployees.AsQueryable();
- }
- }
-
-
- public void AddEmployee(Employee objEmployee)
- {
- objEmployee.Id = CreateIdentity();
- _lstEmployees.Add(objEmployee);
- }
-
- public void DeleteEmployee(int empID)
- {
- int idx = _lstEmployees.IndexOf(_lstEmployees.Where(x => x.Id == empID).FirstOrDefault());
- if (idx >= 0)
- {
- _lstEmployees.RemoveAt(idx);
- }
- }
- }
“EmployeeRepository.cs”
- public class EmployeeRepository : IEmployeeRepository
- {
- DummyDbContext objDbContext = new DummyDbContext();
- public IQueryable<Employee> Employees
- {
- get { return objDbContext.Employees; }
- }
-
- public void AddEmployee(Employee objEmployee)
- {
- objDbContext.AddEmployee(objEmployee);
- }
-
- public void DeleteEmployee(int eid)
- {
- objDbContext.DeleteEmployee(eid);
- }
- }
Now let's add a new controller to the application with the name “
Home”. Before moving further we've installed the
Unity.Mvc3 package within our application using the package console to get the benefit of “
Dependency Injection”.
We've updated the newly added file named BootStrapper.cs (that we get after installing the unity.mvc3 package). the following is the code snippet that we added to the BootStrapper.cs file.
- public static class Bootstrapper
- {
- public static void Initialise()
- {
- var container = BuildUnityContainer();
-
- DependencyResolver.SetResolver(new UnityDependencyResolver(container));
- }
-
- private static IUnityContainer BuildUnityContainer()
- {
- var container = new UnityContainer();
-
-
-
-
-
-
- container.RegisterType<IEmployeeRepository, EmployeeRepository>();
-
- return container;
- }
- }
Also we need to register the BootStarpper.cs file in the Global.asax file. The following is the line of code that you need to add to the Global.asax file inside the Application_Start function:
-
- Bootstrapper.Initialise();
“HomeController.cs”
- public class HomeController : Controller
- {
- EmployeeRepository _repository = new EmployeeRepository();
- private IEmployeeRepository _repository = null;
-
- public HomeController(IEmployeeRepository repository)
- {
- this._repository = repository;
- }
-
- public ActionResult Index()
- {
- return View(_repository.Employees);
- }
- }
Now let's try to create a view for the index function with scaffolding option selected to “List”.
- @model IEnumerable<BundlingDemo.Models.Employee>
- @{
- ViewBag.Title = "Index";
- }
- <h2>
- Index</h2>
- <link rel="Stylesheet" type="text/css" href="../../Content/MyStyle.css" />
- <link rel="Stylesheet" type="text/css" href="../../Content/Site.css" />
- <script type="text/javascript" src="../../Scripts/jquery-1.7.1.js"></script>
- <script type="text/javascript" src="../../Scripts/jquery.unobtrusive-ajax.js"></script>
- <script type="text/javascript" src="../../Scripts/jquery.validate.js"></script>
- <script type="text/javascript" src="../../Scripts/modernizr-2.5.3.js"></script>
- <script type="text/javascript" src="../../Scripts/jquery.validate.unobtrusive.js"></script>
-
- <p>
- @Html.ActionLink("Create New", "Create")
- </p>
- <div class="customTable">
- <table>
- <tr>
- <td>
- @Html.DisplayNameFor(model => model.Id)
- </td>
- <td>
- @Html.DisplayNameFor(model => model.Name)
- </td>
- <td>
- @Html.DisplayNameFor(model => model.Address)
- </td>
- <td>
- @Html.DisplayNameFor(model => model.Salary)
- </td>
- <td>
- </td>
- </tr>
- @foreach (var item in Model)
- {
- <tr>
- <td>
- @Html.DisplayFor(modelItem => item.Id)
- </td>
- <td>
- @Html.DisplayFor(modelItem => item.Name)
- </td>
- <td>
- @Html.DisplayFor(modelItem => item.Address)
- </td>
- <td>
- @Html.DisplayFor(modelItem => item.Salary)
- </td>
- <td>
- @Html.ActionLink("Delete", "Delete", new { id = item.Id })
- </td>
- </tr>
- }
- </table>
- </div>
Our index view is using a set of CSS classes that are a newly created file with the name myStyle.css and also some jQuery script files. The following is the code snippet for the MyStyle.css:
- .customTable
- {
- margin: 0px;
- padding: 0px;
- width: 100%;
- box-shadow: 10px 10px 5px #888888;
- border: 1px solid #000000;
- -moz-border-radius-bottomleft: 14px;
- -webkit-border-bottom-left-radius: 14px;
- border-bottom-left-radius: 14px;
- -moz-border-radius-bottomright: 14px;
- -webkit-border-bottom-right-radius: 14px;
- border-bottom-right-radius: 14px;
- -moz-border-radius-topright: 14px;
- -webkit-border-top-right-radius: 14px;
- border-top-right-radius: 14px;
- -moz-border-radius-topleft: 14px;
- -webkit-border-top-left-radius: 14px;
- border-top-left-radius: 14px;
- }
- .customTable table
- {
- border-collapse: collapse;
- border-spacing: 0;
- width: 100%;
- height: 100%;
- margin: 0px;
- padding: 0px;
- }
- .customTable tr:last-child td:last-child
- {
- -moz-border-radius-bottomright: 14px;
- -webkit-border-bottom-right-radius: 14px;
- border-bottom-right-radius: 14px;
- }
- .customTable table tr:first-child td:first-child
- {
- -moz-border-radius-topleft: 14px;
- -webkit-border-top-left-radius: 14px;
- border-top-left-radius: 14px;
- }
- .customTable table tr:first-child td:last-child
- {
- -moz-border-radius-topright: 14px;
- -webkit-border-top-right-radius: 14px;
- border-top-right-radius: 14px;
- }
- .customTable tr:last-child td:first-child
- {
- -moz-border-radius-bottomleft: 14px;
- -webkit-border-bottom-left-radius: 14px;
- border-bottom-left-radius: 14px;
- }
- .customTable tr:hover td
- {
- background-color: #ffaaaa;
- }
- .customTable td
- {
- vertical-align: middle;
- background-color: #ffffff;
- border: 1px solid #000000;
- border-width: 0px 1px 1px 0px;
- text-align: left;
- padding: 7px;
- font-size: 10px;
- font-family: Arial;
- font-weight: normal;
- color: #000000;
- }
- .customTable tr:last-child td
- {
- border-width: 0px 1px 0px 0px;
- }
- .customTable tr td:last-child
- {
- border-width: 0px 0px 1px 0px;
- }
- .customTable tr:last-child td:last-child
- {
- border-width: 0px 0px 0px 0px;
- }
- .customTable tr:first-child td
- {
- background: -o-linear-gradient(bottom, #ff5656 5%, #7f0000 100%);
- background: -webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ff5656), color-stop(1, #7f0000) );
- background: -moz-linear-gradient( center top, #ff5656 5%, #7f0000 100% );
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#ff5656", endColorstr="#7f0000");
- background: -o-linear-gradient(top,#ff5656,7f0000);
- background-color: #ff5656;
- border: 0px solid #000000;
- text-align: center;
- border-width: 0px 0px 1px 1px;
- font-size: 14px;
- font-family: Arial;
- font-weight: bold;
- color: #ffffff;
- }
- .customTable tr:first-child:hover td
- {
- background: -o-linear-gradient(bottom, #ff5656 5%, #7f0000 100%);
- background: -webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ff5656), color-stop(1, #7f0000) );
- background: -moz-linear-gradient( center top, #ff5656 5%, #7f0000 100% );
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#ff5656", endColorstr="#7f0000");
- background: -o-linear-gradient(top,#ff5656,7f0000);
- background-color: #ff5656;
- }
- .customTable tr:first-child td:first-child
- {
- border-width: 0px 0px 1px 0px;
- }
- .customTable tr:first-child td:last-child
- {
- border-width: 0px 0px 1px 1px;
- }
Try to run the application on Google Chrome. After the view is rendered press F12 and go to the Network tab and again refresh the browser's page. You'll see the following snapshot.
What we can observe is that for every CSS or a script file referenced on the view a call is made from the browser to the server for downloading it from the server to the browser. Also you could get the total number of bytes that were transferred from the browser to the server and vice versa for doing the preceding activity. In our case the number of CSS and script files were less but in a real-life scenario it would be more.
NOTE: After the page is cached in the user's browser the total number of bytes sent and received between the server and browser would be reduced. But for the initial request like in our case it would tend to increase the bandwidth on the server.
Now let's try to implement the Bundling feature. For this we've updated the index.cshtml, the update is for removing all the CSS links and script links from the view and wrapping those inside the BundleConfig.cs file. Please ensure you have removed all the CSS and script links from the index.cshtml view. Open your _Layout.cshtml file you'll find a link in the header section of the view by using the following code snippet:
- @Styles.Render("~/Content/css")
“~/Content/css” is the virtual path given to the StyleBundle class inside the BundleConfig.cs file under your App_Start folder. The BundleConfig.cs file is the file that is used for doing the bundling operation in MVC4. You can get this file inside your app_start folder.
The following is the default code snippet for the BundleConfig.cs file. It has a single function with the name RegisterBundles.
- public class BundleConfig
- {
-
- public static void RegisterBundles(BundleCollection bundles)
- {
- bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
- "~/Scripts/jquery-{version}.js"));
-
- bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
- "~/Scripts/jquery-ui-{version}.js"));
- bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
- "~/Scripts/jquery.unobtrusive*",
- "~/Scripts/jquery.validate*"));
-
-
-
- bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
- "~/Scripts/modernizr-*"));
-
- bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));
-
- bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
- "~/Content/themes/base/jquery.ui.core.css",
- "~/Content/themes/base/jquery.ui.resizable.css",
- "~/Content/themes/base/jquery.ui.selectable.css",
- "~/Content/themes/base/jquery.ui.accordion.css",
- "~/Content/themes/base/jquery.ui.autocomplete.css",
- "~/Content/themes/base/jquery.ui.button.css",
- "~/Content/themes/base/jquery.ui.dialog.css",
- "~/Content/themes/base/jquery.ui.slider.css",
- "~/Content/themes/base/jquery.ui.tabs.css",
- "~/Content/themes/base/jquery.ui.datepicker.css",
- "~/Content/themes/base/jquery.ui.progressbar.css",
- "~/Content/themes/base/jquery.ui.theme.css"));
- }
- }
From looking at the code we can say that “~/Content/css” is the virtual path and it points to “~/Content/site.css”, in other words a specific file. Now we need to add a new entry for the “MyStyle.css” because our index.cshtml view is using the style inside that stylesheet. To do this there are two possible approaches:
- You point the virtual path “~/content/css” to all the CSS files within the Content folder.
- You add a new entry in the BundleConfig file with a new virtual path pointing to the MyStyle.css file.
NOTE: “Virtual Path” are user-specific like how we name our classes and variables.
The problem with the first approach is that if your stylesheets follow a specific ordering then this way shouldn't be used. It will be better that you prefer to use the second approach because the first will not take care of ordering of the stylesheets it will randomly arrange the stylesheet and send it to the user's browser that may cause an undesired look and feel on the user's browser view.
Please comment all the code within the BundleConfig.RegisterBundles method. Do not comment the function signature. Now we'll try to update the RegisterBundles method. the following is the updated code snippet.
- public class BundleConfig
- {
-
- public static void RegisterBundles(BundleCollection bundles)
- {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/*.css"));
-
- bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
- "~/Scripts/jquery-{version}.js",
- "~/Scripts/jquery.unobtrusive*",
- "~/Scripts/jquery.validate*",
- "~/Scripts/modernizr-*"
- ));
- }
NOTE
- In the StyleBundle class we provided the virtual path the same, because we are referring to the same name inside the _Layout.cshtml file. But we have updated the Include function earlier it was pointing to a single file. Now it's pointing to all the CSS files within the Content folder. But as we said it will not take care of ordering of the CSS. So where the ordering of the CSS is important over there we shouldn't be using this approach. We should add a separate stylesheet entry as per the ordering required like in the case of script files how we did.
- In the case of the jquery-version.js file, at runtime the –version value will be updated with the version of jQuery file that resides inside your application. Also to take either the .js or .min.js file will take care of by reading the web.config file. The compilation element from the web.config file, if its debug attribute is set to “true” it will load the .js file and if it is set to “false” it will load the .min.js file. You can manually also force the MVC to load the minified version by setting the BundleTable.EnableOptimizations = true; inside your RegisterBundle Method.
- From the code we can also observe that for adding a stylesheet entry in the bundleconfig we make use of the StyleBundle Class and for adding an entry for JavaScript or jQuery files we make use of the ScriptBundle class.
- Inside the _Layout.cshtml file the @Style.Render(virtual path) method is used for rendering the stylesheets from the BundleConfig.cs file and the @Script.Render(vritual path) method is used for rendering the script from the BundleConfig.cs file.
- Also we need to comment the @Script.Render(~/bundles/modernizr) call within the head section of the _Layout.cshtml, because here we have commented the modernizr script in the BundleConfig.cs file.
- Your virtual path shouldn't be an actual path on the server.
Apart from these common script and CSS files, your view might be referring to some view specific script files. Let's use the example that we want our view to display the current date and time. For this we've created a new folder with the name “Home” inside the Script folder within the application and also added a new script file with the view name, in other words index.js. The following is the code snippet for it.
Index.js
- function GetCurrentDateAndTime() {
- var d = new Date();
- $("#myDiv").html("<b>Current Data And Time: " + d.toString() + "</b>");
- setInterval(GetCurrentDateAndTime, 1000);
- }
-
- $(document).ready(function () {
- GetCurrentDateAndTime();
- });
Now how do we add this script to the view? Because if we add it to the BundleConfig.cs file then it would be unnecessarily downloaded to the user's browser even though the user is not browsing the index.cshtml file. So for view-specific script files we need to add it to that specific view only. If you carefully look at the _Layout.cshtml file it provides a provision for it by creating an optional section within it. Here is the code snippet that you can find in your _Layout.cshtml file.
- @RenderSection("scripts", required: false)
We can make use of this section within those specific views where we need to add the view specific script files. Since the section is optional, on those views where there are no view-specific script files you can omit this section.
Let's update the index.cshtml to add this section. The following is the line of code that is before or after the H2 index element.
- @section scripts
- {
- <script type="text/javascript" src="~/Scripts/Home/Index.js"></script>
- }
NOTE: Ensure that if your view specific script files make use of some other script files or if they have a dependency on some other script files then in that case those script files should be rendered first before your dependent script files are rendered. This could be handled in the _Layout.cshtml file by shifting the optional script section below the @Scripts.Render(virtual path) method.
Now let's try to run the application and see the changes in the network profiling.
If you maximize the preceding image you'll find that the total number of bytes sent by the browser to the server and vice versa will be less as compared to the preceding screen shot. Also our index.js file is rendered after all the script files are rendered.
Now let's try to set the debug attribute to “false” and then check the changes.
You'll find that the total number of calls between the browser and server is more reduced now and there is a single entry for the CSS as well as for the script file where their names are encrypted. These files basically contain data from multiple CSS or script files merged into one and downloaded to the user's browser.
Our final output with Date and Time will be something. For this I've updated the index.cshtml file. You can get a copy by downloading it.