Home > Articles > Programming > Windows Programming

  • Print
  • + Share This
This chapter is from the book

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");
  • + Share This
  • 🔖 Save To Your Account