The Build Events options in Visual Studio can make your life much easier. I use them primarily to copy files around after building projects, but we can use them to run any commands that we want. Let's take a look at the options, and then I'll show how I use them.
Build Events are part of the Project options, and they are often overlooked. If we open up the "Properties" for a project, we get the following screen:
This lets us run command-line commands either before the project is built (pre-build events) or after (post-build events). Although, we do get a drop-down to select if the post-build event runs or not. The default value "On successful build" is usually where I keep this value. This way, the post-build event will *not* run if there is a build failure. Other options include "Always" and "When the build updates the project output."
You may be curious about this last option. Visual Studio is lazy about compiling things, so it will only build if the source files have changed. If we select "When the build updates the project output", then the post-build event will only run if the compiler updates the output files on this project.
We can basically put anything in here that we could run from the command line. And Visual Studio provides macros to make it easier. So, even though we can type straight into these text boxes, it's much better to click the "Edit Pre-build..." or "Edit Post-build..." buttons to bring up the dialog.
The Build Event Dialog
The build event dialog is where things start to get interesting. Well, they aren't actually very interesting at first (this dialog is already populated with a command which we'll talk about in just a bit):
But, when we click the "Macros", we can see how Visual Studio can really help us out:
This shows us a number of macros that we can use in our commands. We can add the macros by simply double-clicking on the item. This will add a corresponding placeholder that has the format $(xxx). These macros will be replaced with actual values when the event runs.
Here's a complete list of the available macros (this is from Visual Studio 2013, but this dialog has not changed for quite a while):
Let's take a look at the "TargetDir" macro. In the value column, we see this expanded out to the full path of the output folder.
But here's why using macros is compelling: If I move this project to a different folder, this TargetDir macro will expand to the new location. If I change the configuration from "Release" to "Debug", the macro will expand to the corresponding output folder (bin\Debug\ vs. bin\Release\).
There are a number of other interesting items. And we can get various levels of detail, depending on our needs. For example, look at the various "Project" macros in the list above.
ProjectName is just the name of the project itself
ProjectFileName includes the project name, plus the file extension
ProjectPath includes the fully-qualified path to the project, and includes the project file name/extension
ProjectDir is the fully-qualified path to the project, but does *not* include the project file name
And we can see similar naming schemes for "Target" and "Solution". Lots of cool stuff.
How I Use Post-Build Events
I primarily use post-build events to copy files around. For example, a number of my sample applications use late-binding. This means that certain dlls are not present at compile time; instead, they are referenced dynamically at runtime.
The problem with this is that I would normally need to copy the late-bound dlls into the project output folder (the same folder as the executable) in order for the application to work. Instead of manually copying the files, I use a post-build event to automatically copy them over.
So, when my examples use dynamically-loaded repositories, I can simply copy over the already-compiled assemblies from a solution folder. Here's the "LateBindingRepositories" folder from one of my solutions:
In addition to the assemblies, this also has some additional files. The "People.txt" file is the data file used by the CSV repository (PersonRepository.CSV.dll). The SQL repository (PersonRepository.SQL.dll) uses Entity Framework and also pulls its data from a local database file (People.sdf).
I need to get these files into the output folder for my main WPF application. So, I have a post-build step in that project:
So, let's parse this command.
We're using "xcopy" to copy files from one folder to another. This is how we had to copy files back before Windows (now I feel old). This has 2 main parameters: the source and the destination.
For the source, we are asking for the solution folder with the $(SolutionDir) macro. If we look at the macro list above, we can see how this expands out, and notice that it includes the trailing path delimiter. Then we go to the "LateBindingRepositories" folder and ask for all files (the *.*). This will copy the assemblies (dlls), plus the other files in that folder.
For the destination, we use the output directory with $(TargetDir). This will be the same folder as our executable (in bin\Debug or bin\Release depending on the Solution Configuration).
The "/Y" switch tells xcopy to overwrite any existing files. This is here because xcopy will normally give you an overwrite prompt if it finds an existing file. We can't get that prompt from a build event.
Important Note About Paths and File Names
One important thing to notice about the source and destination paths: they are wrapped in quotes. If there are any spaces in the path or file name, then we need quotes around it so that xcopy knows that this is a single argument (a space delimits arguments on the command line).
I've run into this issue because I don't normally have spaces in my project paths. But then someone copied the project to their machine (which did have spaces in the path) and the post-build step stopped working.
Another way that I've used post-build events is for dynamically loaded modules. Prism (from the Microsoft Patterns & Practices team) makes it really easy to load up modules from a particular folder. This way, we don't need them to all be present at compile time.
So, in each of my modules, I have a post-build step to copy the output to $(SolutionDir)Modules. And then in my shell application (the one that loads the modules), I have a post-build that copies the modules from $(SolutionDir)Modules into the appropriate folder that the application is looking for.
And we can get more creative as well. I'm using post-build events to copy files around. But we can also use them to run applications such as testing tools, linting tools, validation tools, IL re-writers, and obfuscators. This can help us automate these processes on our developer machines (although we would want to put these into a formal process on our build server).
The Build Events can be a big time saver, especially if you find yourself copying files from one output folder to another. And the macros that are provided by Visual Studio make it really easy to use paths and file names that easily adapt when we move our projects or build with different configurations.