Creating the Code Generator
With the framework for calling my code-generation solution in place, I'm ready to start creating the code-generation code in my DatabaseUtilities class. The constructor for the class accepts the reference to the DTE2 object and moves it to a field in the class. The initial version of the class also contains the GenerateConnectionManager method that's called from my add-in:
using System; using System.Collections.Generic; using System.Text; using EnvDTE; using EnvDTE80; namespace ConnectionStringGenerator { class DatabaseUtiltiies { DTE2 applicationObject; public DatabaseUtiltiies(DTE2 ApplicationObject) { applicationObject = ApplicationObject; } public void GenerateConnectionManager() { } } }
At this point, I've got enough code to start testing my solution by adding a line of code to my GenerateConnectionManager method that writes to the status bar:
applicationObject.DTE.StatusBar.Text = "Code generator called.";
I can now check that the menu item appears (with all the text spelled correctly), that I can load my generation class, and that I can successfully call the generation method. If all that works, I'm ready to start thinking about what the solution will do.
Finding the Project
The first step in this code-generation project is to retrieve a reference to the project that the developer wants to modify. At this point, it's worthwhile to think about the problem from the point of view of the developer for whom you're generating the code. When the developer clicks the menu item that starts the code-generation process, what project will the developer expect your code to work with?
For a code-generation solution run from a button on a menu, my first choice is to work with the project for the currently open document. This code retrieves the project for that document:
Project prj = null; if (applicationObject.ActiveDocument != null) { prj = applicationObject.ActiveDocument. ProjectItem.ContainingProject; }
However, if there is no open document, my second choice is to work with the project for the item currently selected in Solution Explorer. This code checks to see if an item is selected in Solution Explorer and if the item has an associated ProjectItem. Then, if both of those conditions are true, it retrieves the associated Project:
else { if (applicationObject.SelectedItems.Count > 0) { if (applicationObject.SelectedItems.Item(1).ProjectItem != null) { prj = applicationObject.SelectedItems.Item(1). ProjectItem.ContainingProject; } else { prj = applicationObject.SelectedItems.Item(1).Project; } }
Unfortunately, there is a possibility that no document is open, that nothing is selected in Solution Explorer, or that the selected item doesn't return a Project reference. If I can't determine the Project, I give up and exit. However, the decent thing to do in that situation is to tell the developer that no code has been generated. It's tempting to pop up a form telling the developer that no code was generated, but when working through the Add-In Wizard, I promised never to display a modal dialog. So, instead I just update Visual Studio's status bar with code like this. (In the section "Notifying the Developer" later in this chapter, I enhance the messaging to use the TaskList for more serious messages.)
if (prj == null) { applicationObject.DTE.StatusBar.Text = "Please select a project item."; return; }
Assuming that I get a reference to the Project, I now get references to the Project's ProjectItems collection and the Solution it's part of—I'll need both of these objects later in the solution:
ProjectItems pjis = prj.ProjectItems; Solution2 sln = (Solution2) applicationObject.Solution;
Does Anything Need to be Done?
In the process I recommended in Chapter 1, "Introducing Code Generation," as part of reading your inputs you should determine whether any code needs to be generated. In this case, that means retrieving the web.config file and determining if it contains any connectionString elements.
This code attempts to retrieve the project's web.config file and, if that fails, the project's app.config file. If neither exists, the code exits:
ProjectItem cfg = null; try { cfg = pi.Project.ProjectItems.Item("web.config"); } catch { try { cfg = pi.Project.ProjectItems.Item("app.config"); } catch {} } if (cfg == null) { return; }
With a configuration file found, the code loads its contents into an XML document by passing the full pathname to the configuration file to an XmlDocument object. The code then uses an XPath expression to search the document for connectionString elements. If none are found, the code exits:
System.Xml.XmlDocument dom; dom = new System.Xml.XmlDocument(); dom.Load(@cfg.Properties.Item("FullPath").Value.ToString()); System.Xml.XmlNode ndCons = dom.SelectSingleNode("//connectionStrings"); if (ndCons == null || ndCons.ChildNodes.Count == 0) { return; }
Segregating Generated Code
I'm almost ready to start adding code, but I need to decide how to handle the files containing my generated code. Although many code generators attempt to hide generated classes from the developer, my preference is to leave the code visible. (Among other benefits, this makes it easier for me to check that I'm generating the right code during development and debugging.)
However, I do segregate my generated code into special folders. Using the reference to the ProjectItems collection, I can add that folder to hold my generated code using the AddFolder method. For most projects, I create a folder called "Generated Code" to put the class file in. However, for ASP.NET projects, I place the class file in the App_Code folder.
These folders may already be present. (Even my own Generated Code folder may already exist if the developer has run this add-in, or another one of my code-generation utilities, before.) Adding the folder a second time will raise an error; however, rather than check that the folder already exists, I just catch the error and discard it. I'll need to access the folder again, so after adding it I retrieve a reference to the new folder through the ProjectItems' Item method (unfortunately, the AddFolder method doesn't return a reference to the new folder) and store it.
I begin by declaring a field to hold the reference to the folder with the generated code:
ProjectItem codeFolder;
In the following code, I first check to see what kind of project I have by looking at the GUID in the Project object's Kind property. If it's an ASP.NET project, I attempt to add the App_Code folder. For any other kind of project, I add a folder named "Generated Code." As I noted before, if the folders already exist, I just catch the error and discard it. After attempting to add the folder, I get a reference to it:
if (prj.Kind == "{E24C65DC-7377-472b-9ABA-BC803B73C61A}") { try { pjis.AddFolder("App_Code", "{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}"); } catch { }; codeFolder = pjis.Item("App_Code"); } else { try { pjis.AddFolder("Generated Code", Constants.vsProjectItemKindPhysicalFolder); } catch {}; codeFolder = pjis.Item("Generated Code"); }
I could simplify the code required to add the App_Code folder by using the VsWebSite objects (described in Chapter 5, "Supporting Project-Specific Features"). However, for this case study, one of my goals is to use as few tools as possible, which means avoiding using the project-specific objects described in that chapter.
With the folder in place, I add the class file that will eventually contain my ConnectionManager code. At this point I have to decide how I want to handle regeneration when the developer is generating the code for the second (or subsequent) time. The simplest strategy for supporting regeneration is to find the file containing the code from the previous generation and delete it. The alternative is to attempt to reconcile the previously generated code against the current environment, a process that is both difficult to implement and error-prone. (One solution is demonstrated in the case study in Chapter 10, "Case Study: Generating Validation Code," where I selectively replace methods in a class to leave the developer's methods in place while replacing my generated methods.)
In a well-designed solution, you should only need to update an existing file occasionally. Typically, solutions end up having to reconcile old code with new code because the solution didn't provide a clean separation between the generated code and the developer's custom code. For this example, I keep most of the generated code in one file and provide a separate file for the developer's custom code.
For this solution, both of the files will have their names begin with "ConnectionManager." The file that holds the generated code will be named "ConnectionManager.Generation," the file holding the developer's code will be named "ConnectionManager.Customization." Initially, all I build into the solution is the ConnectionManager.Generation file.
In this solution, if the ConnectionManager.Generation file already exists, I don't want to try adding it again and catching the error: I always want to delete any existing version of the file in order to start generating the code from a blank slate. To ensure that I'm deleting the right file, I use the full pathname to the file by concatenating together the path to the project and name of the folder I added. The code looks like this:
string ProjectPath; ProjectPath = System.IO.Path.GetDirectoryName(prj.FullName); ProjectItem prji = sln.FindProjectItem(@ProjectPath + @"\" + codeFolder.Name + @"\ConnectionManager.Generation.cs"); if (prji != null) { prji.Delete(); }
Adding the Template
After ensuring that the file doesn't exist, I now add the Visual Studio template that provides the base for my generation class: a class file in C#. To get this file in the right folder, I use the reference to the folder where I'm going to keep my generated code, which I retrieved earlier. This example adds the template for a non-ASP.NET project:
string ItemTemplatePath = sln.GetProjectItemTemplate( "Class.zip", "CSharp"); ProjectItem pji = codeFolder.ProjectItems.AddFromTemplate (ItemTemplatePath,"ConnectionManager.Generation.cs");
To enable my add-in to support ASP.NET, I need to add a different template. This code checks the project's Kind property and, when the project is a website, adds the correct class. Revising the previous code to handle ASP.NET projects produces the following code:
string ItemTemplatePath; if (prj.Kind == "{E24C65DC-7377-472b-9ABA-BC803B73C61A}") { ItemTemplatePath = sln.GetProjectItemTemplate("Class.zip", @"Web\CSharp"); } else { ItemTemplatePath = sln.GetProjectItemTemplate("Class.zip", "CSharp"); } ProjectItem pji = codeFolder.ProjectItems.AddFromTemplate (ItemTemplatePath,"ConnectionManager.Generation.cs");
It's possible, for a number of reasons, that the AddFromTemplate method will successfully add the class file but not return a ProjectItem. (For instance, if the template is a wizard, you won't get a return value because wizards don't return ProjectItems.) So, after adding the item, I check to see if the reference is null; if it is, I use FindProjectItem to get a reference to the class file. (This also provides a check that the class file was successfully added.)
if (pji == null) { pji = sln.FindProjectItem(@ProjectPath + @"\" + codeFolder.Name + @"\ConnectionManager.Generation.cs"); } if (pji == null) { applicationObject.DTE.StatusBar.Text = "Unable to add class file."; return; }