|
Chapter 12: The Code Model
12 The Code ModelThe Visual Studio .NET code model promises to be nothing less than the programmer's Universal Translator, the macro writer's Babel fish, the hacker's Esperanto. The idea is simpledefine a single API that captures the essence of the most common programming constructs, and have each of the languages in Visual Studio .NET implement that API in its native tongue. The result is a single set of objectsthe code modelthat a programmer can use to read or write code in any of the languages in Visual Studio .NET.
Discovering CodeOne of the basic uses of the code model is to find code that's already there. The code model gives you the tools to enumerate all the code constructs in a project as well as zero in on a code construct in a specific source file at the user's request.
A Quick Tour Through a Source FileLet's look at an example file to see how the code model represents it. Listing 12-1 shows a (somewhat) typical C# source file.
namespace CMNamespace Listing 12-1 An example C# source file The code in Listing 12-1 defines a namespace that holds a menagerie of code constructs, including a delegate, a structure, an enumeration, an interface, and a class. The interface and the class each define members of their own: the interface defines a method, and the class defines a member variable, a method, and a property. The code model gives you different ways of looking at these constructs, depending on your needs. We'll begin with the most basic representation: the CodeElement. The CodeElement object exposes a number of properties that allow you to determine the specific kind of code construct being represented. Figure 12-1 shows how the code model wraps each of the code constructs in Listing 12-1. Figure 12-1 The CodeElement objects that the code model generates for Listing 12-1 Figure 12-1 illustrates the hierarchical relationship between the different CodeElement objects, starting with CMNamespace. The dotted lines are to remind you that the CodeElement objects have no direct connections linking them togetheryou'll need a more refined view before you can navigate the hierarchy.
Navigating the Hierarchy Before you can navigate the hierarchy shown in Figure 12-1, you need access to the top-level CodeElement objects. The FileCodeModel object represents a source file and its code constructs, and the FileCodeModel.CodeElements property holds the collection of top-level CodeElement objects that we want. In our example, the only top-level object is CMNamespace, so you'd expect to find only one CodeElement object in the FileCodeModel.CodeElements collection. (Alternatively, you could use the CodeModel.CodeElements collection, which holds the top-level CodeElement objects for an entire project, but then you'd have to search the items in the collection to find the one representing CMNamespace.) To get you started on your code model journey, Listing 12-2 provides functions that return the FileCodeModel object or the CodeModel object associated with the active window.
Function GetFileCodeModel() As FileCodeModel Return fcm Listing 12-2 The GetFileCodeModel and GetCodeModel functions We established already that you can't travel directly from one CodeElement object to another, so how do you find the rest of the CodeElement objects from the CMNamespace CodeElement object? The answer is that you query the CodeElement object for the interface that corresponds to the underlying code construct and then use that interface to find the related CodeElement objects. The code model defines interfaces for each of the major code constructs: CodeNamespace, CodeStruct, CodeInterface, CodeClass, CodeEnum, CodeVariable, CodeDelegate, CodeProperty, CodeAttribute, CodeFunction, and CodeParameter. Each of these interfaces offers properties and methods specific to its underlying code construct; for example, CodeFunction has a Parameters collection that contains a CodeParameter object for each formal parameter of the underlying function. The CodeElement.Kind property returns a value from the vsCMElement enumeration that indicates the specific type of the underlying code construct. For the CodeElement that wraps CMNamespace, the Kind property returns vsCMElementNamespace, which means you can retrieve a CodeNamespace interface from that CodeElement object. Once you have the CodeNamespace interface, you can retrieve its children in the hierarchy by iterating through its Members collection. Most of the interfaces also contain a Members collection, which allows you to access their children. Navigating the code hierarchy, then, requires successive iterations of querying the CodeElement object for a specific interface and then finding the child CodeElement objects through the Members property of the interface. Figure 12-2 shows a more detailed view of our example hierarchy. The solid lines in Figure 12-2 represent the child code elements reachable through the Members collections. Figure 12-2 A detailed view of the example hierarchy Listing 12-3 shows one way of traversing the hierarchy. You pass in a CodeElements collection to the RecurseCodeElements routine, which iterates through the collection, writing the names of each item to the Output window and calling itself recursively whenever the current item sports a Members collection.
Sub TestRecurseCodeElements() Case vsCMElement.vsCMElementClass Listing 12-3 Navigating the hierarchy recursively The GetMembers function in Listing 12-3 determines the type of the CodeElement passed to it, assigns the CodeElement to a variable of the correct type, and returns the type's Members collection. In Visual Basic, you could just as easily return Members from the CodeElement variable itself, assuming that the underlying object also implemented a Members property. However, strongly typed languages require that you explicitly cast the CodeElement variable to the correct type (or QueryInterface for the correct interface), so we tried to use code comparable to that used by such languages. The GetMembers function illustrates some of the complexities involved with managing the code model interfaces. To help manage this complexity, the code model defines a generic interface named CodeType, which you can retrieve from any object that also supports one of the following interfaces: CodeClass, CodeStruct, CodeInterface, CodeEnum, and CodeDelegate. (Incidentally, CodeType defines the Members property, which is why you can find this property on objects that support the previous interfaces.) If you have a CodeElement object, you can check for the availability of the CodeType interface directly by using the CodeElement.IsCodeType property. So, CodeType gives us yet another way to view our example hierarchy, as shown in Figure 12-3; CodeType also simplifies the logic needed to traverse the code hierarchy, as shown in Listing 12-4. The one "gotcha" in the CodeType approach is that CodeNamespace objects don't support the CodeType interface. In the RecurseCodeElementsByCodeType macro, solving this "gotcha" requires an extra If branch to check for CodeNamespace elements specifically. Figure 12-3 A CodeType view of the example hierarchy We're almost at the end. You can see from Figure 12-2 and Figure 12-3 that the Members collections let you reach all CodeElement objects except the attribute on the class and the parameters in the delegate and the class member function. The CodeClass interface defines an Attributes collection that holds the CodeAttribute objects that apply to the class, and the CodeDelegate and CodeFunction interfaces define a Parameters collection of CodeParameter objects; iterating through those collections allows you to complete the journey through the code hierarchy.
Sub TestRecurseCodeElementsByCodeType() Listing 12-4 Using CodeType to recurse through the code hierarchy
Getting a CodeElement from a Point ObjectYou've seen how to start at the top of a CodeElement hierarchy and visit every child. The code model also allows you to find a CodeElement from a point object in a source file. This ability enables you to create interactive features that respond to the programmer's input.The code model offers two ways of retrieving a CodeElement object from a point: the CodeElementFromPoint method of the FileCodeModel object and the CodeElement property of the TextPoint, EditPoint, and VirtualPoint objects. Here's the prototype for CodeElementFromPoint:
CodeElement CodeElementFromPoint(TextPoint Point, vsCMElement Scope); The CodeElementFromPoint method takes a TextPoint object that specifies a location in a source file and a vsCMElement value that determines which of the enclosing code elements to return. For example, in Listing 12-1, if you had a TextPoint located on the param parameter of CMCallbackFunction, calling CodeElementFromPoint with a vsCMElement value of vsCMElementParameter would return the CodeElement representing param; calling CodeElementFromPoint with a vsCMElement value of vsCMElementClass would return the CodeElement representing CMClass. The CodeElement property takes a vsCMElement value that serves the same purpose as the Scope parameter of CodeElementFromPoint. You might wonder why the code model would bother with CodeElementFromPoint when point objects already have a way to get a CodeElement. The answer is that the CodeElement property is implemented in terms of CodeElementFromPoint and is just a concise way of calling xxxPoint.Parent.Parent.ProjectItem.FileCodeModel.CodeElementFromPoint.
Generating CodeThe other main use of the code model is to generate source code programmatically. This aspect of the code model reveals most clearly the promise of a universal programming language: the same AddClass method that generates a Visual C# class when run against a .cs file will generate a Visual C++ class when run against a .cpp file, and will generate a Visual J# class when run against a .jsl file. In this section, we'll show how to generate the source file in Listing 12-1 by using the code model. Note that the following example provides only the briefest of introductions to this subjectfor details about the objects and methods used in this section, please refer to the Appendix.
Building a Source FileAll of the code model methods that generate code begin with Add, as in AddNamespace, AddClass, AddVariable, and so on. By calling an Add method on a CodeElement, you create a new code construct within the CodeElement. Note that CodeElement objects can't adopt other existing CodeElement objectsthey can have children only by bearing their own; a consequence of this is that you have to create your code hierarchy from the top down. The top-most element in Listing 12-1 is CMNamespace, so you begin by creating a new namespace, as shown in the following code:
Sub CreateListing_12_1() The FileCodeModel.AddNamespace method generates the source code for a new top-level namespace and returns a reference to its corresponding CodeNamespace object. The call to AddNamespace takes place within a Try/Catch block because Add methods throw an exception if they're unable to create the requested code element. Assuming all goes well, you'll have a reference with which to create the namespace's child elements. The first child element to create is CMDelegate. CMDelegate defines an integer parameter, and you can create both the delegate and the parameter in the same Try/Catch block:
Try AddDelegate and AddParameter both take a parameter that specifies the code element's type; the vsCMTypeRef.vsCMTypeRefVoid value represents a void type, and the vsCMTypeRef.vsCMTypeRefInt value represents an integer type. The previous code doesn't declare a variable to store AddParameter's return value because nothing further needs to be done with the delegate's parameter. Next you create the structure and its field:
Try By now the rhythm should be familiar: define a variable for the new code element, assign the return value of the Add method to the variable, and call methods on the variable to alter the new code element. The -1 parameter to AddStruct tells the method to insert the source code for the new structure after every other sibling code element; all the Add methods accept this optional parameter. Here's the code for creating the enumeration and the interface:
Try The second parameter to AddFunction lets you specify what kind of function to create, such as a constructor, a destructor, a pure virtual function, and so on; the value of vsCMFunction.vsCMFunctionFunction for CMInterfaceMethod creates a vanilla function. Finally, here's the code that creates the class, its attribute, and all its members:
Try Now that you're done writing the code that writes the code, it's time for some bad news: none of the language implementations will generate a complete simulacrum of Listing 12-1 from this code, either because the language doesn't support a particular code construct or because the language hasn't yet implemented a particular Add method. You can verify this for yourself by running the CreateListing_12_1 macro on different language source filesif you do, you'll find that Visual C# generates everything but the attribute; Visual C++ generates everything but the delegate and the property; Visual J# generates everything but the delegate, structure, enumeration, attribute, and property; and Visual Basic doesn't generate anything. Again, that's the bad newsthe good news is that the code model is a young branch of the automation object model, so you can expect major improvements in its next version.
Looking AheadThe code model brings us to the end of Part II of this book. Part III takes you into territory mostly uncharted by other programming books: in it you'll learn how to set up Setup, find help on Help, hotwire the V12 command-line engine hidden under the unassuming hood of the Visual Studio .NET IDE, and keep your source safe from everyone, including yourself.
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||