Guidance Automation Toolkit: Bow before your master!
I just wanna say thank you VERY MUCH to the Patterns & Practices Team for putting together the Guidance Automation Toolkit. It´s an invaluable tool for an architect job of enforcing Guidelines.
Besides being an active guideline enforcer, GAT is a real productivity enhancer for specific application domains.
I´m building now the NMVP Software Factory using GAT as you can see here. I was going to wait before its first release to announce it, but I´m so thrilled with it that I felt the need to tell you guys how great it is shaping so far.
The purpose of this factory is to enable Model-View-Presenter applications based on NMVP Framework easy to build, while observing best practices and tier separation (check my article here).
One main concern I have today when managing the architecture team in the company I work for is testability of our applications. It´s really hard to enforce this key aspect of Object Oriented Programming without the proper tools. The excuse is always the same: lose productivity. This is due to the repetitive nature of unit tests. Several attempts to reduce this effect have been made as mocking and stubbing for example(ok they are not just attempts to improve productivity).
Now, if at the moment you generate your Business Entity, based on a database table, the tool automatically generates unit tests for that entity for you, then people would hardly have an excuse not to unit test their entities, right? That´s one of my goals with this Software Factory. Automatically enforce testing. This way you can have change requests and make sure everything is working.
In later releases I plan on building recipes for automatic generation of MVP-Based UIs, like data lists, filter screens and CRUDs. Since these are pretty common you could just adjust the Factory to your company´s standards. Please notice that the generated code is nothing but ASP.Net, so each application can be fully customized after generation. Pretty much in the same way you´d do if you had built it from scratch.
Now let´s get some code, since it´s been a while since I´ve shown some code! While doing the Factory, I found an issue in GAT. Well not really an issue, more like something that is not implemented out of the box. It´s really hard to add a Solution Item in a solution folder. I always use as a best practice a solution folder called External, with subfolders containing all referenced assemblies I´m using at that particular Application, like NMVP, NHibernate and such.
An action in GAT is pretty much what its name says. It´s a code that will get executed at some time in your recipe. So actions have two particularly useful methods: Execute and Undo. By overriding those you are free to do anything you´d like.
Since it wasn´t implemented in GAT already, what do we programmers do? Do it ourselves! So here it comes the AddSolutionItem and AddSolutionFolder classes. Notice that they´re draft, and by all means I could get them better worked, but I don´t have the time nor the interest right now, since I´m running to get Release 0.1.0 out of the oven.
Both actions depend on a class called ProjectFinder that helps them finding the correct folder to copy the files to.
Another thing to notice is that VS 2005 has a known bug that happens when you add solution items: it opens them with the default editor. This is really messy if you are adding several files. I found a workaround in the web though: closing the currently active window after adding the file. This way it opens the window and then closes.
Well let´s check the code for the AddSolutionItem action:
/// <summary>
/// Adds a new solution item.
/// </summary>
/// <remarks>This action was developed by Bernardo Heynemann(heynemann@gmail.com)
/// and is part of the NMVP Software Factory (http://www.codeplex.com/nmvpfactory).</remarks>
public class AddSolutionItem : ConfigurableAction {
#region Fields
private string from;
private string to;
#endregion Fields
#region Properties
/// <summary>
/// The file to be added.
/// </summary>
[Input(Required = true)]
public string From {
get { return from; }
set { from = value; }
}
/// <summary>
/// The path to add the file to.
/// </summary>
[Input(Required = true)]
public string To {
get { return to; }
set { to = value; }
}
#endregion Properties
#region Execute and Undo
/// <summary>
/// Executes the action.
/// </summary>
public override void Execute() {
Assembly execAsm = Assembly.GetExecutingAssembly();
DTE dte = GetService<DTE>();
string solutionDirectory = Path.GetDirectoryName((string)dte.Solution.Properties.Item("Path").Value);
string fromPath = Path.Combine(Path.GetDirectoryName(execAsm.Location), From);
string toPath = Path.Combine(solutionDirectory, To);
if (!File.Exists(fromPath)) {
throw new InvalidOperationException(string.Format("From file does not exist at path {0}", fromPath));
}
File.Copy(fromPath, toPath);
if (!File.Exists(toPath)) {
throw new InvalidOperationException(string.Format("To file does not exist at path {0}", toPath));
}
Project prj = ProjectFinder.GetProject(dte, Path.GetDirectoryName(to));
if (prj == null) {
throw new InvalidOperationException(string.Format("The project was not found at folder {0}", Path.GetDirectoryName(to)));
}
prj.ProjectItems.AddFromFile(toPath);
// The AddExistingItem operation also shows the item in a new window, close that.
dte.ActiveWindow.Close(EnvDTE.vsSaveChanges.vsSaveChangesNo);
//dte.ItemOperations.AddExistingItem(toPath);
}
/// <summary>
/// Undo the action.
/// </summary>
public override void Undo() { }
#endregion Execute and Undo
}
A few key things here:
- The From and To properties are to be used in the recipe that uses this action so the Package developer can tell what file needs to be copied from where to where.
- The execute method here has two steps.
- First it needs to find the location of the file to copy. Since all files in Packages must have build type of 'Content' and Copy to Output Directory set as 'Copy Always', we know that this file must be in the same folder as our package assembly (or at least under this folder), so we get this path from the executing assembly (our package) using the Assembly.GetExecutingAssembly() method. Our next job here is to get the Solution folder. This one is pretty tricky since we must get this from DTE using this code: (string)dte.Solution.Properties.Item("Path").Value. This is because at this time the solution may not be built yet.
- After that it has to locate the directory in the solution that the item must be added to. That´s when the ProjectFinder comes in handy. After locating the project (the directory in the solution) it has to copy the file to, is simply a matter of adding the file.
- We don´t implement the undo method since we don´t need it.
The AddSolutionFolder action is analogous so I´ll skip its comments. Here is the code:
/// <summary>
/// Adds a new solution item.
/// </summary>
/// <remarks>This action was developed by Bernardo Heynemann(heynemann@gmail.com)
/// and is part of the NMVP Software Factory (http://www.codeplex.com/nmvpfactory).</remarks>
public class AddSolutionFolder : ConfigurableAction {
#region Fields
private string from;
private string to;
#endregion Fields
#region Properties
/// <summary>
/// The folder to be added.
/// </summary>
[Input(Required = true)]
public string From {
get { return from; }
set { from = value; }
}
/// <summary>
/// The path to add the folder to.
/// </summary>
[Input(Required = true)]
public string To {
get { return to; }
set { to = value; }
}
#endregion Properties
#region Execute and Undo
/// <summary>
/// Executes the action.
/// </summary>
public override void Execute() {
Assembly execAsm = Assembly.GetExecutingAssembly();
DTE dte = GetService<DTE>();
string solutionDirectory = Path.GetDirectoryName((string)dte.Solution.Properties.Item("Path").Value);
string fromPath = Path.Combine(Path.GetDirectoryName(execAsm.Location), From);
string toPath = Path.Combine(solutionDirectory, To);
if (!Directory.Exists(fromPath)) {
throw new InvalidOperationException(string.Format("From folder does not exist at path {0}", fromPath));
}
if (!Directory.Exists(toPath)){
try {
Directory.CreateDirectory(toPath);
}
catch {
throw new InvalidOperationException(string.Format("Unable to create folder {0}", toPath));
}
}
foreach (string filePath in Directory.GetFiles(fromPath)) {
string fromFile = Path.Combine(fromPath, Path.GetFileName(filePath));
string toFile = Path.Combine(toPath, Path.GetFileName(filePath));
try {
File.Copy(fromFile, toFile);
}
catch {
throw new InvalidOperationException(string.Format("Unable to copy file {0} to file {1}", fromFile, toFile));
}
}
Project prj = ProjectFinder.GetProject(dte, to);
if (prj == null) {
throw new InvalidOperationException(string.Format("The project was not found at folder {0}", Path.GetDirectoryName(to)));
}
foreach (string filePath in Directory.GetFiles(toPath)) {
prj.ProjectItems.AddFromFile(Path.Combine(toPath, Path.GetFileName(filePath)));
// The AddFromFile operation also shows the item in a new window, close that.
dte.ActiveWindow.Close(EnvDTE.vsSaveChanges.vsSaveChangesNo);
}
}
public override void Undo() { }
#endregion Execute and Undo
}
Here is an example on using each one:
Add Solution Folder
<Action Name="AddNMVP"
Type="NMVP.Factory.Actions.AddSolutionFolder, NMVP.Factory"
From="Templates\Solutions\External\NMVP"
To="External\NMVP" />
Add Solution Item
<Action Name="AddNMVP"
Type="NMVP.Factory.Actions.AddSolutionItem, NMVP.Factory"
From="Templates\Solutions\External\NMVP\nmvp.dll"
To="External\NMVP\nmvp.dll" />
Conclusion
This post rapidly became a large one. You can see that I´m really excited about the Software Factories approach, using GAT. I hope I can deliver all that I´m expecting in a few weeks. In the meantime I´ll keep writing about my experience with it.

#101