Customizing the Template
Because the input to any code-generation solution controls the output, it's time to consider what the input for this code-generation solution looks like. I assume that the config file for the application contains a ConnectionStrings element, like this:
<connectionStrings> <add name="MainDB" connectionString="..." providerName="..."/> </connectionStrings>
The solution should generate a class that looks like this:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MyProject { class ConnectionManager { string MainDB { get { return System.Configuration.ConfigurationManager. ConnectionStrings["MainDB"].ConnectionString; } } } }
Unfortunately, the result of adding the template for a new class in a non-ASP.NET project looks like this:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MyConsoleProject.Generated_Code { class ConnectionManager { } }
In a projectless web application, the class looks like this:
using System; using System.Collections.Generic; using System.Linq; using System.Web; /// <summary> /// Summary description for ConnectionManager /// </summary> public class ConnectionManager { public ConnectionManager() { // // TODO: Add constructor logic here // } }
A number of differences exist between the template file and the class file for which I'm aiming. To get the class file I want, I must do the following:
- Simplify the namespace. For many projects, I will have added the class to a subfolder named Generated Code. By default, in a C# project, the folder name will be included in the class's namespace (e.g., MyProject.Generated_Code). I'd prefer not to force developers to have to drill down through the Generated_Code namespace; instead, I will have the ConnectionManager be in the project's root namespace.
- Make the class static/shared. Making this change allows the developer to call properties on the class without having to instantiate it.
- Delete the constructor. Static/shared classes are not allowed to have constructors.
- Make the class a partial class. Because this is created as a partial class, developers can customize ConnectionManager's behavior by adding code to a separate file.
In addition, I want to ensure that the project has a reference to the System.Configuration DLL. Web projects will have this reference by default but other types of project won't.
Had I used a custom template (as described in Chapter 8, "Other Tools: Templates, Attributes, and Custom Tools," and demonstrated in the case study in Chapter 10), I could omit much of the following code. However, using custom templates does make your code-generation solution dependent on having the right template installed on the developer's computer. Although the following solution requires more code, it does mean that my solution is more self-contained.
Fixing the Namespace
To simplify the Namespace, I first retrieve the FileCodeModel for the class file. If the project is a "projectless" website, I must cast the ProjectItem as a VSWebProjectItem and call its Load method before I can access its FileCodeModel. For other project types, I can just access the FileCodeModel; therefore, once again, the code checks to see if this is an ASP.NET project and does the right thing:
FileCodeModel fcm; if (prj.Kind == "{E24C65DC-7377-472b-9ABA-BC803B73C61A}") { VsWebSite.VSWebProjectItem tmpWPI; tmpWPI = (VsWebSite.VSWebProjectItem) pji.Object; tmpWPI.Load(); fcm = tmpWPI.ProjectItem.FileCodeModel; } else { fcm = ConnMgr.FileCodeModel; }
Once the FileCodeModel is retrieved, I iterate through the top-level items until I find the Namespace. Once I find the Namespace, I set it to the project's DefaultNamespace, which I retrieved from the Project's Properties collection. For Visual Basic projects, a Namespace typically isn't included in the file, but that's not a problem—if the Namespace isn't found, the code does nothing:
CodeElement2 codeClass; foreach (CodeElement2 ce in fcm.CodeElements) { if (ce.Kind == vsCMElement.vsCMElementNamespace) { ce.Name = prj.Properties.Item( "DefaultNamespace").Value.ToString();
Because my code resets the Namespace's name, there's a very real possibility that my reference to the Namespace may be corrupted after the change. So, after changing the Namespace's name, I use this code to reacquire the reference to the Namespace:
CodeElement2 ceNamespace = (CodeElement2) fcm.CodeElements.Item (prj.Properties.Item("DefaultNamespace").Value.ToString());
Modifying the Class
To modify the class, I now find the Class by iterating through the CodeElements collection within the Namespace I just changed and store the reference in a variable named codeClass:
foreach (CodeElement2 ceClass in ceNamespace.Children) { { if (ce.Kind == vsCMElement.vsCMElementClass) { codeClass = ce; } }
If there is no Namespace in the Class (the typical scenario for a Visual Basic file), the code acquires the reference to the Class and puts it in codeClass inside the loop that looks for the Namespace:
if (ce.Kind == vsCMElement.vsCMElementClass) { codeClass = ce; }
With the Namespace corrected (if present) and a reference to the class held in the codeClass variable, I now look for the class's constructor inside codeClass's Children collection and delete it. Because I've added a C# file, I can identify the constructor by looking for a function with the same name as the class ("ConnectionManager"). For a Visual Basic application, I'd be looking for a method named New:
foreach (CodeElement2 ce in codeClass.Children) { if (ce.Kind == vsCMElement.vsCMElementFunction && ce.Name == "ConnectionManager") { fcm.Remove(ce); } }
I also need to modify the class's definition to make the class partial and shared/static. A CodeClass2 object has the necessary functionality to make those changes. Because the code has already retrieved a reference to the class as a CodeElement, all that I have to do is to cast my codeClass reference to a CodeClass2 object to get the functionality I need:
CodeClass2 cc = (CodeClass2) codeClass;
Now that I have a reference to a CodeClass2 object, I make the class a partial class by setting its ClassKind property and a static/shared class by setting its IsShared property:
cc.ClassKind = EnvDTE80.vsCMClassKind.vsCMClassKindPartialClass; cc.IsShared = true;
Adding a Reference
In order to access the ConnectionStrings element in the application's configuration file, non-web projects will need a reference to the System.Configuration assembly (website projects already have the necessary reference). To add this reference, the first step is to cast the reference to the Project object to a VSLangProj.VSProject type. Once the project is cast, a reference to the System.Configuration assembly can be added by name using the References collection's Add method (if the reference is already present, no error is raised):
VSLangProj.VSProject vsPrj; vsPrj = (VSLangProj.VSProject) prj.Object; vsPrj.References.Add("System.Configuration");