CHAPTER 8
The code model is an API provided by Visual Studio that allows us to essentially get much of the same data at design time that reflection provides at run time. This opens up some intriguing possibilities for code generation without having to wait for compiled code.
The code model has a reputation for being difficult to work with. In part this is because it’s not very well documented. It also is generally only used when creating plug-ins and extensions for Visual Studio.
We will keep things simple here, following the same path we followed when exploring reflection. Drawing parallels between the two APIs will help us keep things straight.
In this chapter we will work our way through accessing the various pieces of metadata made available. We will look at getting the active project, getting a list of types, and finding all of the details about the types discovered.
Once we have finished this survey of the code model, we will pull all of the pieces together by using the code model to scan the active project, looking for any MVC controllers. For each controller found, we will search for the containing actions and generate JavaScript code to simplify calling the various actions that we find.
Throughout this chapter we will add functionality to a new class called CodeModelHelper that we can use to simplify the process of accessing code model data. Go ahead and create a new class called CodeModelHelper in the T4Utilities project that we created in Chapter 4.
Let’s start with the constructor for the CodeModelHelper. To do anything with the code model, we will need the DTE that we looked at in Chapter 4. To simplify access to this object, we will create a constructor that will require that the host be provided when the helper is created.
public DTE VisualStudio { get; set; } public Project ActiveProject { get; set; } public CodeModelHelper (ITextTemplatingEngineHost host ) { IServiceProvider serviceProvider = (IServiceProvider)host ; VisualStudio = serviceProvider .GetService(typeof(EnvDTE.DTE)) as DTE; ActiveProject = VisualStudio.ActiveDocument .ProjectItem.ContainingProject; } |
Constructor for the CodeModelHelper call
The last line is a subtle way to get the active project. In this case the active document will be the template itself. If you need to work with a different project, you can use the methods outlined in Chapter 4 for navigating a solution.
Getting a list of types is a more involved process than with reflection. One complication is that getting a list of classes is completely separate from getting a list of interfaces. Let’s start with a list of classes.
public IList<CodeClass> GetTypes () { return GetCodeItemsInProject<CodeClass> (ActiveProject.ProjectItems); } |
Get a List of Types from the Code Model
This works very similar to the GetTypes that we defined in ReflectionHelper. We can filter by any property in the CodeClass interface.
public IList<CodeClass> GetTypesInNamespace(string nameSpace) { return GetCodeItemsInProject<CodeClass> (ActiveProject.ProjectItems) .Where(t=>t.Namespace.Name == nameSpace).ToList(); } |
Filtering Types by NameSpace
Unfortunately, IsInterface is not one of the properties.
We will need a new method to get the interfaces. Fortunately, it should look familiar:
public IList<CodeInterface> GetInterfaces() { return GetCodeItemsInProject<CodeInterface> (ActiveProject.ProjectItems); } |
Get a List of Interfaces from the Code Model
The only difference between GetTypes and GetInterfaces is the generic parameter passed to the GetCodeItemsInProject method.
Let’s delve into the implementation of our GetCodeItemsInProject method to see how this is used.
private List<T> GetCodeItemsInProject<T>(ProjectItems items) where T : class { var classes = new List<T>(); foreach (ProjectItem item in items) { if (item.ProjectItems != null && item.ProjectItems.Count > 0) { classes.AddRange(GetCodeItemsInProject<T> (item.ProjectItems)); } if (item.FileCodeModel != null) { classes.AddRange(GetCodeItemsInCodeModel<T> (item.FileCodeModel.CodeElements)); } } return classes; } |
Get Code Items in Project
This method is not really as complicated as it initially looks. The recursive call to GetCodeItemsInProject is to handle cases where a project item may have nested projected items. This happens with the code-behind for a web form or the output of a template. This also happens when we add a folder to the project.
The true magic happens in the GetCodeItemsInCodeModel method.
private List<T> GetCodeItemsInCodeModel<T>(CodeElements elements) where T : class { var classes = new List<T>(); foreach (CodeElement element in elements) { if (typeof(T).Name == "CodeClass") if (element.Kind == vsCMElement.vsCMElementClass) classes.Add(element as T); if (typeof(T).Name == "CodeInterface") { if (element.Kind == vsCMElement.vsCMElementInterface) classes.Add(element as T); } var members = GetCodeElementMembers(element); if (members != null) classes.AddRange(GetCodeItemsInCodeModel<T>(members)); } return classes; } |
GetCodeItemsInCodeModel
The if statements in the middle can be expanded if you are searching for anything other than a CodeInterface or CodeClass. As written, these are the only type generic arguments suitable for the call to GetCodeItemsInProject.
We won’t have to worry so much about recursion and looping to find all the places where code might hide in the project beyond this point. From here on out we will be dealing in the context of a CodeInterface or a CodeClass. Unfortunately, in the code model API, there is not a common base class for these two entities, even though their interfaces are nearly identical. So we will need overloads for each of the methods we are about to discuss. One for CodeClass, and one for CodeInterface. Through the remainder of this chapter we will talk about types. This should not be confused with System.Type, which we used in reflection, but is used instead to collectively refer to interfaces and classes in CodeModel.
Getting the properties of a type is very straightforward.
public IList<CodeProperty> GetProperties(CodeClass name) { return name.Members.OfType<CodeProperty>().ToList(); } |
Code 84: Getting the Properties of a Type
Once we get the properties, we have access to some standard properties that we will need:
Name and Type will work pretty much as you would expect. Access is based on an enum that you may need a special decoder ring to make sense out of.
Member name | Description |
|---|---|
vsCMAccessAssemblyOrFamily | Element has assembly or family access; proposed new language feature |
vsCMAccessDefault | Won’t show up as a code reference; only provided to support adding code to a project |
vsCMAccessPrivate | Regular private |
vsCMAccessProject | Regular internal |
vsCMAccessProjectOrProtected | Combination of protected and internal |
vsCMAccessProtected | Regular protected |
vsCMAccessPublic | Regular public |
vsCMAcessWithEvents | Indicates that a variable declaration will respond to raised events |
We may also often be interested in whether or not the property has a getter or setter. To check this, we check to see if the Getter or Setter properties are null. The actual value for these properties will be a CodeFunction that implements the get or set. We aren’t really interested in how these functions are implemented, only whether or not they exist.
The GetMethods method is very similar to the GetPropeties method.
public IList<CodeFunction> GetMethods(CodeClass name) { return name.Members.OfType<CodeFunction>().ToList(); } |
Get the Methods of a Type
We have access to the same data about methods that we could get about properties. We can also get the parameters and a list of overloaded functions.
The Parameters property exposes a CodeElements object holding all of the associated parameters.
We can easily get the parameters:
public IList<CodeParameter> GetParameters (CodeFunction name) { return name.Parameters.OfType<CodeParameter>().ToList(); } |
Get the Parameters to a Method
Or, we can format the list of parameters as a string.
public string GetParameterString(CodeFunction action) { var paramList = ""; var parameters = GetParameters(action); foreach (var paramater in parameters) { paramList += paramater.Name; paramList += ", "; } char[] charsToTrim = { ',' }; paramList.TrimEnd(charsToTrim); return paramList; } |
Get the Parameters to a Method Formatted to be Inserted to Generated Code
We can easily determine if any of these code structures has a particular attribute.
public bool HasAttribute(CodeElements attributes, string attr) { return attributes.OfType<CodeAttribute>() .Any(attribute => attribute.Name == attr); } |
Determine whether or not a Group of CodeElements has a Given Attribute
This code can be called like this:
var getActions = actions.Where( a => code.HasAttribute(a.Attributes, "HttpGet")); |
Note: We are not passing the actual name of the attribute—just what would be added in code to add the attribute.
The only problem here is that this does not easily allow us to retrieve attributes that were added at higher levels in the inheritance hierarchy. If your design relies on inheriting attributes from base classes, you will need to extend this method to walk the inheritance tree.
We can get a list of all of the base classes like this:
public List<CodeClass> GetBaseClasses(CodeElements bases) { var allBases = new List<CodeClass>(); foreach (var b in bases.OfType<CodeClass>()) { allBases.Add(b as CodeClass); allBases.AddRange(GetBaseClasses ((b as CodeClass).Bases)); } return allBases; } |
Get all of the Bases Class for a Type
Once we have the list of base classes, we will need to know where we are getting the attributes from to know what to check at each level.
The easiest case is to check to see if a class has a particular attribute.
public bool ClassHasAttribute (CodeClass currentClass, string attr) { var baseClasses = GetBaseClasses(currentClass.Bases); baseClasses.Add(currentClass); var result = baseClasses.Any (s => HasAttribute(s.Attributes, attr)); return result; } |
Check to See if a Class has an Attribute
We can craft PropertyHasAttribute and MethodHasAttribute following the same pattern, except these methods will also need to know which property or method to check
public bool PropertyHasAttribute (CodeClass currentClass, CodeProperty currentProperty, string attr) { var baseClasses = GetBaseClasses(currentClass.Bases); var properties = new List<CodeProperty>(); baseClasses.ForEach(b => GetProperties(b) .Where(p => p.Name == currentProperty.Name)); return properties.Any(p => HasAttribute(p.Attributes, attr)); } |
Check to See if a Property has an Attribute
And:
public bool MethodHasAttribute(CodeClass currentClass, CodeFunction currentProperty, string attr) { var baseClasses = GetBaseClasses(currentClass.Bases); var methods = new List<CodeFunction>(); baseClasses.ForEach(b => GetMethods(b) .Where(p => p.Name == currentProperty.Name)); return methods.Any(p => HasAttribute(p.Attributes, attr)); } |
Check to See if a Method has an Attribute
In addition to walking the inheritance tree, we may often find that we need to simply know if a given class has a particular base class. We can write a HasBaseClass method like this:
public bool HasBaseClass(CodeElements bases, string baseClass) { return GetBaseClasses(bases) .Any (b => b.Name == baseClass); } |
Determine if a Class has a Particular Base Class
Let’s go back to the MVC API controller that we created in Chapter 7. You may recall that we used the scaffolding to create an EmployeeController using the template to create read/write actions with Entity Framework.
You will often need to call these actions through JavaScript from the client side. Often this will be done using jQuery. While this is not an overly complex task, it is one where the best practices can often change. Since we can easily discover all the potential methods that can be called, this makes it a good candidate for code generation.
Let’s start by adding a new template to the book.MVC.Api project that we created in Chapter 7. Name this template controllerscript.tt.
We start by adding the supporting directives.
<#@ template debug="true" hostSpecific="true" #> <#@ output extension=".js" #> <#@ assembly Name="System.Core.dll" #> <#@ assembly name="System.Web.Mvc" #> <#@ assembly name="EnvDTE" #> <#@ assembly name="EnvDTE100" #> <#@ import namespace="EnvDTE" #> <#@ import namespace="EnvDTE100" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Web.Mvc" #> <#@ assembly name="$(SolutionDir)T4Utilities\bin\Debug\T4Utilities.dll" #> <#@ import namespace = "T4Utilities" #> |
Assembly and Import Directives for the controllerscript Template
The assembly and import directives for EnvDTE are key to being able to use the code model. Also note that we added the T4Utilities to get the CodeModelHelper that we have been working on.
A good deal of this template will be static boiler plate code with some dynamic code in the middle. Let’s add the static header code next.
var Controllers = { relativePath: "", SetRelativePath : function(relPath){ Controllers.relativePath = relPath; } |
Static Header Code
Now we need to initialize the CodeModelHelper and add the calls to generate the dynamic code.
<# code = new CodeModelHelper(this.Host); CallActions(); #> |
The code property will be defined a little later in the feature block, along with the CallActions method.
The CallActions method will generate code that will be added to the Controllers object that we are building up by looping through the controllers and their associated actions, and providing a handy method to call the discovered method.
After we have declared the Controllers object, we have some more static JavaScript code to define.
(function (AjaxRequest, undefined) { AjaxRequest.Post = function (jsonData, url, successCallback, errorCallback) { $.ajax({ url: url, type: "POST", data: jsonData, datatype: "json", contentType: "application/json charset=utf-8", success: function (data) { successCallback(data); }, error: function (jqXHR, textStatus, errorThrown) { if (errorCallback) { errorCallback(); } } }); }; AjaxRequest.Get = function (jsonData, url, successCallback, errorCallback) { $.ajax({ url: url, type: "GET", data: jsonData, datatype: "json", contentType: "application/json charset=utf-8", success: function (data) { successCallback(data); }, error: function (jqXHR, textStatus, errorThrown) { if (errorCallback) { errorCallback(); } } }); }; }(window.AjaxRequest = window.AjaxRequest || {})); |
More Definition
Note: A full explanation of what this JavaScript code does is outside the scope of this book, but you can get all the details you need by downloading and reading the JavaScript Succinctly and jQuery Succinctly books..
Now we are ready to add the feature block. We will start by declaring the code property and the CallActions method.
public CodeModelHelper code{get ; set;} public void CallActions() { PushIndent(" "); var classes = code.GetTypes() .Where (c=> code.HasBaseClass(c.Bases, "ApiController")); if(classes.Count() > 0) WriteLine(CurrentIndent+ ","); foreach (var currentClass in classes) { var controllerName = currentClass.Name.Replace("Controller",""); WriteLine(CurrentIndent+ "'" + controllerName +"' :"); WriteLine(CurrentIndent+ "{"); PushIndent(" "); var actions = code.GetMethods(currentClass) .Where(func => func.Type.CodeType.Name == "ActionResult" || code.HasBaseClass(func.Type.CodeType.Bases, "ActionResult")).ToList();; foreach (var action in actions) { Write(CurrentIndent+ "'{1}URL' : '{0}/{1}'", controllerName, action.Name); if(action != actions.Last()) WriteLine(CurrentIndent+ ","); } ProcessActions (actions, currentClass, "Get"); ProcessActions(actions, currentClass, "Post"); PopIndent(); WriteLine("\r\n"+CurrentIndent+ "}"); } PopIndent(); WriteLine(CurrentIndent+ "};"); } |
Add Feature Block
In the CallActions method, we start by getting a list of all of the classes that are derived from controller. We ignore all other classes in the project. We strip off the word “Controller” from the name of the class, because this will not actually be included in references to the class.
Now that we have our list of controllers, we loop them, looking for actions. In this case, we are looking for the methods that are return types of any type derived from ActionResult.
We will process this list of actions in two steps. In the foreach loop, we build up a list of URLs to the various actions.
After the loop, the two calls to the ProcessActions method handle generating the method calls.
public void ProcessActions (IList<CodeFunction> actions, CodeClass controller, string verb) { var filteredActions = actions.Where( a => code.HasAttribute(a.Attributes, "Http" + verb)); if(filteredActions.Any()) WriteLine(CurrentIndent+ ","); foreach (var action in filteredActions) { WriteAjaxWrapper(verb,action, controller); if(action != filteredActions.Last()) WriteLine(CurrentIndent+ ","); } } |
Calling the ProcessActions Method
Here we filter based on which methods have the appropriate attribute for the requested verb. Each action should have either an HttpGet or an HttpPost attribute.
The WriteAjaxWrapper method makes the call to Ajax:
public void WriteAjaxWrapper(string method, CodeFunction action, CodeClass controller) { var paramList = code.GetParameterString(action); var parameters = code.GetParameters (action); WriteLine(CurrentIndent+ "'{0}{1}' : " +"function({2}successCallback, errorCallback) {{", action.Name, method,paramList); PushIndent(" "); WriteLine(CurrentIndent+ "AjaxRequest.{0}({1}" +"Controllers.relativePath + Controllers.{2}.{3}URL, " +"successCallback, errorCallback);", method, string.IsNullOrEmpty(paramList) ? "undefined,": paramList, controller.Name.Replace("Controller",""), action.Name); PopIndent(); Write(CurrentIndent+ "}"); } |
The WriteAjaxWrapper Method
Given the EmployeeController that we generated in Chapter 7, this template output will look like this:
|
var Controllers = { relativePath: "", SetRelativePath : function(relPath){ Controllers.relativePath = relPath; } , 'Employee' : { 'IndexURL' : 'Employee/Index' , 'DetailsURL' : 'Employee/Details' , 'CreateURL' : 'Employee/Create' , 'CreateURL' : 'Employee/Create' , 'EditURL' : 'Employee/Edit' , 'EditURL' : 'Employee/Edit' , 'DeleteURL' : 'Employee/Delete' , 'DeleteConfirmedURL' : 'Employee/DeleteConfirmed' , 'IndexGet' : function(successCallback, errorCallback) { AjaxRequest.Get(undefined, Controllers.relativePath + Controllers.Employee.IndexURL, successCallback, errorCallback); } , 'DetailsGet' : function(id, successCallback, errorCallback) { AjaxRequest.Get(id, Controllers.relativePath + Controllers.Employee.DetailsURL, successCallback, errorCallback); } , 'CreateGet' : function(successCallback, errorCallback) { AjaxRequest.Get(undefined, Controllers.relativePath + Controllers.Employee.CreateURL, successCallback, errorCallback); } , 'EditGet' : function(id, successCallback, errorCallback) { AjaxRequest.Get(id, Controllers.relativePath + Controllers.Employee.EditURL, successCallback, errorCallback); } , 'CreatePost' : function(employeeModel, successCallback, errorCallback) { AjaxRequest.Post(employeeModel, Controllers.relativePath + Controllers.Employee.CreateURL, successCallback, errorCallback); } , 'EditPost' : function(employeeModel, successCallback, errorCallback) { AjaxRequest.Post(employeeModel, Controllers.relativePath + Controllers.Employee.EditURL, successCallback, errorCallback); } , 'DeletePost' : function(id, successCallback, errorCallback) { AjaxRequest.Post(id, Controllers.relativePath + Controllers.Employee.DeleteURL, successCallback, errorCallback); } , 'DeleteConfirmedPost' : function(id, successCallback, errorCallback) { AjaxRequest.Post(id, Controllers.relativePath + Controllers.Employee.DeleteConfirmedURL, successCallback, errorCallback); } } }; (function (AjaxRequest, undefined) { AjaxRequest.Post = function (jsonData, url, successCallback, errorCallback) { $.ajax({ url: url, type: "POST", data: jsonData, datatype: "json", contentType: "application/json charset=utf-8", success: function (data) { successCallback(data); }, error: function (jqXHR, textStatus, errorThrown) { if (errorCallback) { errorCallback(); } } }); }; AjaxRequest.Get = function (jsonData, url, successCallback, errorCallback) { $.ajax({ url: url, type: "GET", data: jsonData, datatype: "json", contentType: "application/json charset=utf-8", success: function (data) { successCallback(data); }, error: function (jqXHR, textStatus, errorThrown) { if (errorCallback) { errorCallback(); } } }); }; }(window.AjaxRequest = window.AjaxRequest || {})); |
Template Output
Having such a script can be very helpful with projects with a large collection of controllers. It is also much easier to change the template generating this code than it is to change the generated code as best practices change.
In this chapter we have covered most topics related to accessing data through the code model. We saw an easy way to get to the project hosting the template. We explored how to get and filter the types contained in this project.
We explored retrieving and filtering the basic components for these types that would commonly be used in code generation. Visual Studio provides a wealth of information for us about the code being written. While the code model is more commonly used to manipulate existing code in a project, and is intended to be used for plug-ins and extensions, we have seen how it can be used to provide the data we need to generate code.