Enforcing Unobtrusive JavaScript

Unobtrusive JavaScript is a concept that’s been around for quite a while, but it’s now finally starting to make its way into the mainstream. The basic idea is that you should separate your JavaScript from your HTML – so instead of;

<a href="Default.aspx" onclick="alert('Sorry, that is unavailable'); return false;" >Click Here</a>

You should have something like this;

<a id="ClickHereLink" href="Default.aspx">Click Here</a>

... with some matching JavaScript in another file somewhere;

function pageLoad() {

    var link = $get('ClickHereLink');

    $addHandler(link, 'click', preventClick);

}

function preventClick() {

    alert('Sorry, that is unavailable');

    return false;

}

I’m not here to tell you why this is so great, but the best discussion and walkthrough of issues I’ve found is this article on onlinetools.org, so check it out if you’re interested.

How do I enforce it?

Recently I found myself cursing some code I’d written a long time ago, when I found it littered with onclick and onload handlers. It was frankly a pain to work out how it works. It was at this point that I thought “I should write something to shout at me when I write don’t write unobtrusive JavaScript”.

Coincidentally I have also been doing some work on HTML parsing, and I have to say I’ve found the XPath-like features in the HTML Agility Pack to be incredibly useful. If you’ve never used it I’d recommend taking a look.

It occurred to me that this was the perfect tool to do what I wanted – so I set about creating a simple MSBuild task that uses the HTML Agility Pack to scan ASPX and ASCX files for Obtrusive JavaScript – basically a form of static code analysis. If you’re interested in customising MSBuild, check this out.

The implementation is very simple;

· It is derived from Microsoft.Build.Utilities.Task.

· There is a property that is an array of ITaskItems, flagged with a [Required] attribute, as a way to pass in a list of files to check.

· There’s a hard-coded (for now) list of validation XPath expressions, with matching explanations.

· In the Execute method override, it loops through all the files.

· For each file, it opens the HTML and applies every XPath to it.

· If there are any matches for the XPath, it creates a build failure.

The key to success here is the simplicity with which the HTML Agility Pack lets me use XPath to find elements. The following is the basics of my code;

HtmlDocument doc = new HtmlDocument();

doc.Load(path);

HtmlNodeCollection results =

    doc.DocumentNode.SelectNodes(

        validator.SearchXPath);

Easy huh? The XPath expressions themselves mostly look like this;

//*[@onkeypress]

This will match any HTML element that has an onkeypress attribute... which of course means that the developer has specified a JavaScript event handler in the HTML, and broken the rules of unobtrusivitiy (hey, if that’s not a word I’m having first dibs on inventing it J).

Remembering that we’re actually scanning ASP.NET mark-up, not pure HTML, we can also check for ASP.NET attributes that actually create JavaScript event handlers in HTML too;

//*[@onclientclick]

Again, this is not difficult. The tricky one appears when you realise that it is valid to have OnClick in ASP.NET code! Of course, an <asp:button> can have an OnClick attribute referring to the server-side event handler. I wondered about saying “match any with an onclick that don’t have a runat attribute”, but this isn’t quite accurate (HtmlControls can have runat=”server” too remember).

In the end I went for explicitly excluding the ASP.NET controls that I knew were OK to use OnClick;

//*[@onclick and not(

name()='asp:button' or name()='asp:bulletedlist' or

name()='asp:imagebutton' or name()='asp:imagemap' or

name()='asp:linkbutton')]

Finally, I wanted to check that there was no JavaScript inside the page HTML itself... even between <script> tags. In other words, the only valid <script> tags should reference an external file, as follows;

<script language="javascript" type="text/javascript" src="Default.js"></script>

[Of course, you could also use the <asp:ScriptManager> control if you wanted to; in fact I’d prefer it]

To achieve this I check that there is nothing other than whitespace between the start and end script tags;

//script[string-length(normalize-space(text()))>1]

... and that’s it! As usual, the source code for this MSBuild Task is attached to this post, along with a demonstration Web App.

Wiring up the Task

Now we have a simple MSBuild task, all that is left to do is add it to a build. I created a new ASP.NET Web Application, right clicked the project file and chose “unload”, and then edited the XML manually.

The only bit I needed to add was as follows;

<UsingTask

   AssemblyFile="..\JsBuildTasks.dll"

    TaskName="ValidateUnobtrusive" />

<Target Name="BeforeBuild">

   <ValidateUnobtrusive

        SourceFiles="@(Content)" />

</Target>

First I import my task (I’ve copied a Release build of the assembly to the parent directory above my Web App project). Next, I override the BeforeBuild target, and add an instance of my ValidateUnobtrusive task. I’ve passed in an expansion of all the Content files in the project. The task filters for *.aspx and *.ascx at the moment – if you wanted to add *.html that would be trivial.

Save the file, and right click it to choose Reload Project. At this point Visual Studio 2008 warns you about a customised MSBuild project file – just choose “Load Project Normally” and you’re away.

From this point on, whenever you build a project containing Obtrusive JavaScript you’ll see errors like these;

Error Use of the 'onclick' JavaScript event on 'a' tag in markup causes Obtrusive Javascript. You should attach this handler in script instead.

Error Failed on 'script' tag . A JavaScript region containing something other than white space was found. You should link to related JavaScript resources files rather than embed script in a page. Conclusion

I think this works really well so far (although I’m at the early stages of trialling it); if you try it do let me know how you get on. One of the particularly cool things (that I haven’t tried yet as I’m working isolated on my laptop most of the time right now, and I’m short of time) is that this could be wired up into a Continuous Integration build, under Team Build – I guess that would be a good plan if you didn’t like running the validation as part of your local build, or if it starts to perform badly due to the complexity or size of your pages.

I also think it raises other questions and possibilities. For example; how about some static analysis of “.js” files themselves? Could this help check for Directly Accessible Scripts, for example?

Originally posted by Simon Ince on 10 July 2009 here.