The
NetTutsMSBuildJs.csproj
file is just an XML file specially configured to handle the MSBuild process for this project. It is perfectly legitimate to create one of these manually or edit it to suit your project. Obviously, for purely .NET purposes it's much better to use the Visual Studio GUI to configure this file automatically for you, but the point of this tutorial is to show you how to add in a JavaScript build, which is not part of the standard .NET build.
In Visual Studio, you can't edit this project file unless you unload the project, and you can't load the project if there is a syntax error in the file! So, practice unloading and loading the project so that you can edit this key file. To unload the project, right-clickthe project and click the Unload Project item.
After unloading the project, all the folders and files disappear and you're left with just the solutions and projects in the Solution Explorer. Right-click the project and this time the context menu is very short. Choose Edit NetTutsMSBuildJs.csproj and the project configuration file opens.
Now, just to build your confidence and get used to dealing with those times when you can't load the project because of a syntax error in the project files, type a deliberate mistake near the beginning of the project file: just type a letter before the first tag outside the XML document structure. Save and close the file. Try to load the project from the context menu and you will get an error message saying the project can't be loaded. Yes, Visual Studio is very strict like this.
Re-open the project file, correct the error, save and close again. When you re-load the project, it should load smoothly. Now it's time to edit for real. We will only manually change one thing in the project file, and that will be to add an Import element which will import a file to perform the JavaScript build.
Step 2: Create a Build File for the JavaScript Build and Import It Into the Project File.
If you add an Import element to the project file for a file which doesn't exist, you won't be able to load the project, so create a new text file called
js.build
in the jsbuild folder. After you enter the necessary XML code, the IDE will recognise this file as an XML file. There will be no need to actually associate the .build
extension with the XML editor. Enter this starting code into jsbuild\js.build
, save and close.
1
2
| </ Project > |
Now, unload the project and edit the project file by adding this line to the end of the file just before the closing tag.
1
| < Import Project = "jsbuild\js.build" /> |
You should now be able to re-load the project.
Step 3: Hello Discworld!!!!!
Five exclamation marks, the sure sign of an insane mind. - Terry Pratchett, Reaper Man
I am a bit bored with saying "Hello World" at the beginning of every new IT tutorial. So this time, I'm going to say hello to Terry Pratchett's amazing Discworld.
Open
js.build
. The IDE should automatically notice that it is an XML file. If not, perhaps you have invalid XML. After adding the following code to set up a Hello Discworld message, the IDE should finally realise this is XML. Make sure thejs.build
file now contains the following XML. Don't forget the five exclamation marks to get the right flavour of insanity for the Discworld!!!!!
1
2
3
4
5
6
7
8
| < Target Name = "HelloDiscworld" > < Message Text = "Hello Discworld!!!!!" Importance = "high" ></ Message > </< Target > < Target Name = "AfterBuild" > < CallTarget Targets = "HelloDiscworld" ></ CallTarget > < Target > </ Project > |
When you right click on the project and run build, you should see the message in the output window.
Like Ant, MSBuild uses the idea of targets to perform groups of tasks. The AfterBuild target is run automatically by MSBuild after everything else has been successfully built. I'm tacking the JavaScript build onto the end of the .NET build so the AfterBuild extension point seems the best place to put this. Notice how AfterBuild is run automatically and within AfterBuild we call our Target HelloDiscworld. I've set the Importance of the message to high because otherwise it might not appear in the output window.
Step 4: Sort Out Paths
Right. We went a little bit mad in the Discworld with too many exclamation marks, but at least our JavaScript build file seems to be working! OK. Joking aside, we now have to get the most crucial thing in a build routine right: paths.
As with Ant, I have always had trouble understanding absolute and relative paths in these configuration files, so I want to tread carefully. Add a PropertyGroup element to the top of the
js.build
file, just below the Project tag and add two properties like this.
1
2
3
4
| < PropertyGroup > < ConcatDir >debug-js\concat</ ConcatDir > < MinDir >debug-js\min</ MinDir > </ PropertyGroup > |
Now, alter the message so we can see what these properties contain:
1
| < Message Text = "Hello Discworld!!!!! from $(ConcatDir)" Importance = "high" ></ Message > |
Now clean and build the project again or just choose rebuild. The message appears in the output like this:
Hello Discworld!!!!! from debug-js\concat
Step 5: Create Clean and Init Targets
Lovely. We've got our environment, our source files and we've got properties in the build file containing relative paths pointing to the directories we need to work with. Now we can add a CleanJs Target and an InitJs Target to Remove and Make the concat and min directories. I have a habit of putting little "hello" messages in to these targets when developing these files just to re-assure myself they're actually running or checking property values. I find increasing the output verbosity in MSBuild tends to give me a flood of information that I don't need, though it's great when I can't figure out where I've made a mistake.
MSBuild uses simple relative paths from the root folder of the whole project. If you have a folder called js in your project, you can use the valuejs
in a named Property within a PropertyGroup without further complication.
01
02
03
04
05
06
07
08
09
10
11
12
| < Target Name = "CleanJs" > < Message Text = "Hello from CleanJs" Importance = "high" ></ Message > < RemoveDir Directories = "$(ConcatDir)" Condition = "Exists('$(ConcatDir)')" > < Output PropertyName = "ConcatDirRemoved" TaskParameter = "RemovedDirectories" /> </ RemoveDir > < RemoveDir Directories = "$(MinDir)" Condition = "Exists('$(MinDir)')" ></ RemoveDir > < Message Text = "Hello from removed dirs $(ConcatDirRemoved)" Importance = "high" ></ Message > </ Target > < Target Name = "InitJs" > < MakeDir Directories = "$(ConcatDir)" Condition = "!Exists('$(ConcatDir)')" ></ MakeDir > < MakeDir Directories = "$(MinDir)" Condition = "!Exists('$(MinDir)')" ></ MakeDir > </ Target > |
To run these targets add
CallTarget
elements to the AfterBuild
target.
1
2
| < CallTarget Targets = "CleanJs" ></ CallTarget > < CallTarget Targets = "InitJs" ></ CallTarget > |
Step 6: Concatenating the Files
You're probably getting used to editing the
js.build
file by now. You may have noticed an annoying error message linked to text underlined with wiggly blue lines, like this:
This is an annoying bug in Visual Studio which has been there for quite a while. PropertyGroup elements and ItemGroup elements can be populated with any value you like. The problem is Visual Studio wrongly reports an error for the first Property or Item you define in one of these groups. As you've seen, ConcatDir works when you build the project, and there is no problem loading the project. Just ignore these distracting invalid child element errors.
At last, some real build work. We add a new target to concatenate the files we want. Unlike Ant and NAnt, there is no built-in Concat task, so we have to roll our own with the ReadLinesFromFile task
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
| < Target Name = "ConcatenateJsFiles" > < ItemGroup > < ConcatFiles Include=" js\jquery-1.8.2.min.js; js\jquery-ui-1.9.2.custom.min.js; debug-js\src\jquery.ui.menubar.js; js\knockout-2.1.0.js; debug-js\src\default-src.js "/> </ ItemGroup > < ReadLinesFromFile File = "%(ConcatFiles.Identity)" > < Output TaskParameter = "Lines" ItemName = "ConcatLines" /> </ ReadLinesFromFile > < WriteLinesToFile File = "debug-js\concat\default-concat.js" Lines = "@(ConcatLines)" Overwrite = "true" /> </ Target > |
Add a new
CallTarget
element to the AfterBuild
target in js.build
callingConcatenateJsFiles
. Rebuild the project as usual and lo and behold, a file calleddefault-concat.js
magically gets created in the debug-js\concat
directory. You will probably have to refresh the Solution Explorer to see it.
Now add a new Web form page called
Default-concat.aspx
to the debug
folder, linking it to the Main.Master
page. This is very short and slightly different from theDefault-src.aspx
page. This time, all the JavaScript we need has been concatenated into one file, so you only need one script tag link to default-concat.js
.
1
2
3
4
5
6
| <%@ Page Title="Default concat" Language="C#" MasterPageFile="~/Main.Master" AutoEventWireup="true" CodeBehind="Default-src.aspx.cs" Inherits="NetTutsMsBuildJs.debug.Default_src" %> < asp:Content ID = "Content1" ContentPlaceHolderID = "head" runat = "server" > </ asp:Content > < asp:Content ID = "Content2" ContentPlaceHolderID = "JsScripts" runat = "server" > < script src = "/debug-js/concat/default-concat.js" ></ script > </ asp:Content > |
To try this out, open the
Default-concat.aspx
page in the IDE window and run the project again in Debug mode. You should see the fully functioning menubar in your browser with the title Debug concat appearing in the title bar of the browser.Step 7: Final Stage - Minifying
The final target, target!!!!!
Our menubar seems to be working and when we concatenate the files we seem to have got the right order and everything's going smoothly in the
Debug-concat.aspx
page. It's now finally time to minify the source files default-src.js
andjquery.ui.menubar.js
and concatenate them with the professional release files in the correct order. This is slightly more complicated because now we need to bring in an external dependency which, so far, we haven't needed: the YUI compressor. There is a.NET port of this being developed but I'm so used to the Java version, I prefer to use my old favorite. Create a new target called MinifyJsFiles like this:
01
02
03
04
05
06
07
08
09
10
11
12
13
| < Target Name = "MinifyJsFiles" > < ItemGroup > < MinFiles Include=" debug-js\src\jquery.ui.menubar.js; debug-js\src\default-src.js "/> < Compressor Include = "jsbuild\yuicompressor-2.4.7.jar" ></ Compressor > </ ItemGroup > < Message Text = "Hello Compressor.Fullpath: %(Compressor.Fullpath)" Importance = "high" ></ Message > < Exec Command = "java -jar %(Compressor.Fullpath) debug-js\src\default-src.js --type js -o debug-js\min\default-min.js" /> < Exec Command = "java -jar %(Compressor.Fullpath) debug-js\src\jquery.ui.menubar.js --type js -o debug-js\min\jquery.ui.menubar-min.js" /> </ Target > |
Notice the property Compressor. Here you just have to define the relative path from the
project
folder, but the jar file, run by the Java process, will need the full path. Luckily, MSBuild provides an easy way to convert a relative path into a full path. You use the %
syntax and invoke the Fullpath property. This is an example of MSBuild Well-known Item Metadata.
Add yet another
CallTarget
element to the AfterBuild
element to call theMinifyJsFiles
target.
Now our final target, target. We have to take all the professional release files and concatenate them with the minified version of our sources and concatenate them into one file.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
| < Target Name = "ConcatenateMinFiles" > < ItemGroup > < ConcatMinFiles Include=" js\jquery-1.8.2.min.js; js\jquery-ui-1.9.0.custom.min.js; debug-js\min\jquery.ui.menubar-min.js; js\knockout-2.1.0.js; debug-js\min\default-min.js "/> </ ItemGroup > < ReadLinesFromFile File = "%(ConcatMinFiles.Identity)" > < Output TaskParameter = "Lines" ItemName = "ConcatMinFilesLines" /> </ ReadLinesFromFile > < Message Text = "We are concatenating these minified files %(ConcatMinFiles.Identity)" Importance = "high" ></ Message > < WriteLinesToFile File = "debug-js\min\default.js" Lines = "@(ConcatMinFilesLines)" Overwrite = "true" /> </ Target > |
You have to be careful with this ItemName property in the build files. Property and item instances are stored in a global context in MSBuild. If you use the same name for
ItemName
in two different concatenated targets, you end up concatenating all the files from both targets.
Rebuild the project and you should now see two new files in the
debug-js\min
folder:default-min.js
and jquery.ui.menubar-min.js
. The debug-js
folder should now look like this after re-building and refreshing the Solution Explorer:
Create a new Web form page called
Default-min.aspx
linked to the Main.Master
page and put it into the debug
folder.
1
2
3
4
5
6
| <%@ Page Title="Default min" Language="C#" MasterPageFile="~/Main.Master" AutoEventWireup="true" CodeBehind="Default-src.aspx.cs" Inherits="NetTutsMsBuildJs.debug.Default_src" %> < asp:Content ID = "Content1" ContentPlaceHolderID = "head" runat = "server" > </ asp:Content > < asp:Content ID = "Content2" ContentPlaceHolderID = "JsScripts" runat = "server" > < script src = "/debug-js/min/default-min.js" ></ script > </ asp:Content > |
Conclusion
We walked through the steps required in Visual Studio Express 2012 for Web, to create a JavaScript project that marries Knockout with jQuery UI to create a menubar and then integrate a JavaScript build into the overall project build in Visual Studio.
In this tutorial we walked through the steps required in Visual Studio Express 2012 for Web, to create a JavaScript project that marries Knockout with jQuery UI to create a menubar from a JSON definition file and then integrate a JavaScript build of the source files into the .NET MSBuild process. The final result was that we had a web page with only one script tag containing all the complex JavaScript needed to run the page.
I think you can see how easy it would be to adapt this example to a very large, complex JavaScript library running in a .NET project. It should also be reasonably straightforward to develop these ideas to include tasks suitable for a release version. The obvious next step is to copy the fully minified and concatenated
default.js
file to the js
folder, then include that in a definitive Default.aspx
file in the root directory. With this example as a starting point, you should be able to explore the MSBuild documentation and develop a fully working build file to automate every part of your build process.
I also use this kind of approach for CSS files. In this particular case, the jQuery UI CSS files are so well optimized it hardly seemed worth minifying them, but in other projects it might be important for performance. A more sophisticated next step for you grunters out there would be to create a
js.build
file that runs a grunt file with an MSBuild Exec task. That way, you could easily include linting and testing to the build process.Further reading
For further reading about Visual Studio, this excellent Nettuts+ Visual Studio: Web Dev Bliss will guide you on how to integrate Web Essentials and add code checking to your build process, but unfortunately, Web Essentials is not available for the Express edition. See Mads Kristensen's answer here: "...unfortunately Express doesn't allow third party extensions to be installed". This tutorial is aimed at users of the Express edition and I hope it has given you a starting point for creating your own integrated JavaScript build, all within the Visual Studio Express environment.
No comments:
Post a Comment