Last Updated: 9/9/2015

It was at Microsoft’s TechEd North America 2013 conference in New Orleans where I first heard the announcement that Visual Studio’s “One ASP.NET” templates would begin including Bootstrap. A single slide in Damian Edwards’ “Microsoft ASP.NET, Web, and Cloud Tools Preview” session summarized it nicely. Boot what?!? At least that was my initial reaction. What’s this newfangled styling and layout framework? More importantly, why should I care as an ASP.NET developer? I returned to the office the following week and began researching it. As a developer whose strong certainly isn’t CSS, the value-add became evident almost immediately.

Fast forward a year or so, and I’m using Bootstrap for large-scale enterprise web development. All projects on which I was working involved development teams of varying skill levels, and none of us bared much confidence in our Bootstrap abilities. Needless to say, there was a lot of habitual misuse of the framework. Surely, one could mentor the developers to prevent these mistakes and inadvertent exploitation, but who was experienced enough to perform that role commendably? In short, nobody. To compound the dilemma, additional developers were being procured and were expected to get up-to-speed in a reasonable amount of time.

This got me thinking. Could the development workflow be enhanced such that some mode of static analysis occurs during each and every build? Such a tool could partially fill the void caused by lack of expertise, which could expedite formal code reviews and make the review process feel much less intimidating. It turns out that the Bootstrap team had already solved this problem with a Node-based tool. Enter Bootlint, an HTML5 linter with an intimate comprehension of Bootstrap jargon and best practices.

Assumptions & prerequisites

To preface things, this blog post assumes that Bootstrap is being used inside of MVC Razor views. This post also intends to be forward-looking. ASP.NET Core 1.0 is the focus; although, keep in mind that Bootlint may still be used with older versions of ASP.NET. The Beta 7 release bits were used here, so certain pieces may still be in flux. See below for specific instructions to upgrade your environment from Beta 6 to Beta 7. If that doesn’t pertain to you, it’s safe to bypass the remainder of this section.

With Visual Studio 2015

A separate Beta 7 download will be required to update the DNVM and tooling available in Visual Studio 2015.

With Visual Studio Code

Validate that the latest version of the DNVM is being used by asking it to update itself:

dnvm update-self

Next, download the latest DNX by issuing the following command:

dnvm upgrade

After doing so, run the following command to ensure that one of the Beta 7 CLR options is marked as the default:

dnvm list

Take note of the default indicator in the screenshot below.
dnvm list console output

Finally, make sure Node.js is installed with NPM.

How to install Bootlint

The Bootlint CLI must be installed at the system level using NPM:

npm install -g bootlint

Once that’s done, integrate it into your development workflow. With ASP.NET Core 1.0, chances are that your development is being done in either Visual Studio 2015 or Visual Studio Code. The installation process differs a bit between the two products. See the specifics documented below.

With Visual Studio 2015

The default template in Visual Studio 2015 introduces Gulp as the JavaScript task runner of choice. Open the package.json file found in the project’s root, and manually add a key-value pair within the devDependencies section. Here’s an example:

"gulp-bootlint": "^0.6.4"

Now the package.json file might look like this:


{
"name": "ASP.NET",
"version": "0.0.0",
"devDependencies": {
"gulp": "3.8.11",
"gulp-bootlint": "^0.6.4",
"gulp-concat": "2.5.2",
"gulp-cssmin": "0.1.7",
"gulp-uglify": "1.2.0",
"rimraf": "2.2.8"
}
}

view raw

package.json

hosted with ❤ by GitHub

Solution Explorer - gulp-bootlint installed
Don’t fret about locating the correct version number. Intellisense is provided within the IDE to assist with that portion of the key-value pair. Save the package.json file, and the dependency will be installed automatically via NPM. Expand the Dependencies/npm node in Solution Explorer to verify that it was actually installed. See the highlighted section in the screenshot to the right.

With Visual Studio Code

The OmniSharp project’s aspnet Yeoman generator is one popular option for scaffolding the ASP.NET Core 1.0 application. After scaffolding, install the gulp-bootlint NPM module locally in your MVC Core 1.0 project:

npm install --save-dev gulp-bootlint

How to configure Bootlint

Next, open gulpfile.js, and create a new task to execute the Bootstrap linting process. In its most basic form, that task could look something like this:


var gulp = require('gulp');
var bootlint = require('gulp-bootlint');
gulp.task('bootlint', function(){
gulp.src('Views/**/*.cshtml')
.pipe(bootlint());
});

view raw

gulpfile.js

hosted with ❤ by GitHub

This new “bootlint” task will traverse the Views folder hierarchy recursively, analyzing each CSHTML view file it encounters by employing the gulp-bootlint plugin.

How to suppress superficial errors & warnings

When problems are encountered, the console output will reference problem codes to uniquely identify each rule violation. Run this task, and you’ll soon find that the default configuration isn’t suitable for this type of application. Since a master layout, full Razor views, and partial Razor views are being melded together, confusion sets in for Bootlint. Here’s a screenshot of what’s likely to happen with the default configuration:
Bootlint no disabled IDs

Consequently, there are a few rules which make sense to suppress right away, namely:

  1. E001 <– validates that an HTML5 DOCTYPE declaration exists
  2. E007 <– validates that only 1 copy of Bootstrap’s JavaScript has been included on the page

To understand why these rules must be suppressed, refer to the master layout file contents provided below.


<!doctype html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] – mvc_task_runners</title>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/lib/bootstrap-touch-carousel/dist/css/bootstrap-touch-carousel.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="hidden" asp-fallback-test-property="visibility" asp-fallback-test-value="hidden" />
<link rel="stylesheet" href="//ajax.aspnetcdn.com/ajax/bootstrap-touch-carousel/0.8.0/css/bootstrap-touch-carousel.css"
asp-fallback-href="~/lib/bootstrap-touch-carousel/css/bootstrap-touch-carousel.css"
asp-fallback-test-class="carousel-caption" asp-fallback-test-property="display" asp-fallback-test-value="none" />
<link rel="stylesheet" href="~/css/site.css" asp-file-version="true" />
</environment>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-controller="Home" asp-action="Index" class="navbar-brand">mvc_task_runners</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-controller="Home" asp-action="Contact">Contact</a></li>
</ul>
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; 2015 – mvc_task_runners</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/lib/hammer.js/hammer.js"></script>
<script src="~/lib/bootstrap-touch-carousel/dist/js/bootstrap-touch-carousel.js"></script>
</environment>
<environment names="Staging,Production">
<script src="//ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="//ajax.aspnetcdn.com/ajax/hammer.js/2.0.4/hammer.min.js"
asp-fallback-src="~/lib/hammer.js/hammer.js"
asp-fallback-test="window.Hammer">
</script>
<script src="//ajax.aspnetcdn.com/ajax/bootstrap-touch-carousel/0.8.0/js/bootstrap-touch-carousel.js"
asp-fallback-src="~/lib/bootstrap-touch-carousel/dist/js/bootstrap-touch-carousel.js"
asp-fallback-test="window.Hammer && window.Hammer.Instance">
</script>
<script src="~/js/site.js" asp-file-version="true"></script>
</environment>
@RenderSection("scripts", required: false)
</body>
</html>

view raw

_Layout.cshtml

hosted with ❤ by GitHub

Error code “E001” will appear for each and every view we decided to scan. Why? Remember that the file glob used in the Gulp task will capture all MVC views. As you probably already guessed, Bootlint believes that something is awry because the HTML DOCTYPE declaration appears only in the master layout.

Error code “E007” will be triggered because of how the master layout is using environment tag helpers. In other words, the Bootstrap JavaScript file is referenced in more than 1 location. See the gist above, starting at line 51.

There’s no wrongdoing in either case. Let’s dissect the reasoning behind the “E007” error code.

ASP.NET Core 1.0 uses an environment variable called ASPNET_ENV. For the purposes of this example, set this variable equal to “Development”, “Staging”, or “Production” within the application’s debugging profile. As an aside, this variable can take on any value. The screenshot below depicts how this variable is assigned a value in Visual Studio 2015.

ASPNET_ENV environment variable

In Visual Studio Code, the same can be accomplished via the commands section in project.json (see code snippet below). The “web-dev” command is Visual Studio Code’s equivalent to setting ASPNET_ENV equal to “Development”.

  "commands": {
    "kestrel": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.Kestrel --config hosting.ini",
    "web-dev": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --config hosting.ini /ASPNET_DEV=Development",
    "web-stage": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --config hosting.ini /ASPNET_DEV=Staging",
    "web-prod": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --config hosting.ini /ASPNET_DEV=Production"
  }

Open a command prompt, navigate to the project’s root directory, and instruct the DNX to execute the “web-prod” command:

dnx web-prod

Open the application in the browser, and notice that it is indeed running in “Production” mode:
Production mode

With this knowledge, it then makes sense that only one of the environment tag helpers’ contents will be rendered at runtime. Bootlint was never intended to be sophisticated enough to decode the melding of Razor views and tag helpers. One solution is to modify the Gulp task slightly to mimic this:


var gulp = require('gulp');
var bootlint = require('gulp-bootlint');
gulp.task('bootlint', function(){
gulp.src('Views/**/*.cshtml')
.pipe(bootlint({
disabledIds: ['E001', 'E007']
}));
});

view raw

gulpfile.js

hosted with ❤ by GitHub

The disabledIds option accepts an array of problem codes to be suppressed.

Putting Bootlint to the test

Now that Bootlint is configured to behave with an MVC Core 1.0 application, let’s test whether it’s working properly. As a preliminary smoke test, open one of the Razor views which uses Bootstrap’s 12-column grid system. Locate a div tag to which a col-* class has been affixed, and remove the trailing number. For example, change col-md-3 to col-md-. After making this exact change in my Views/Home/Index.cshtml file (see screenshot below) and then running the Gulp task, the following error detail is revealed:
Bootlint col-md-3 error

The error message contains a line number, 69, which points to the exact location in which the CSS class name was changed. Now, add the missing number back to the CSS class name, run the Gulp task again, and notice that the error has disappeared. That was easy!

Other noteworthy Bootlint options

There are two other configuration options worth noting:

  1. stoponerror
  2. stoponwarning

Both options are disabled by default. By setting stoponerror to true, the Gulp task in which Bootlint is executing will immediately halt when encountering an error (those problem codes prefixed with “E”). Likewise, stoponwarning will halt execution of a task when encountering a warning (those problem codes prefixed with “W”). This is useful when, for example, you don’t want to pipe into another potentially long-running Gulp operation if an error or a warning was encountered.

To enable both of the aforementioned settings, our Gulp task would now look as follows:


var gulp = require('gulp');
var bootlint = require('gulp-bootlint');
gulp.task('bootlint', function(){
gulp.src('Views/**/*.cshtml')
.pipe(bootlint({
disabledIds: ['E001', 'E007'],
stoponerror: true,
stoponwarning: true
}));
});

view raw

gulpfile.js

hosted with ❤ by GitHub

Conclusion

Bootlint is an indispensable linting tool to have in your web development arsenal. Whether conducting a brief code review, just learning Bootstrap, or attempting to improve quality via automated builds, this is the appropriate tool for the task. After all, it’s much easier than one might think to inadvertently drift from Bootstrap’s best practices. With the proper configuration, Bootlint will help developers avoid an excessive level of cognitive strain.

2 Comments »

Leave a comment