In my previous article, Basics of Extending Your Working Environment in Visual Studio, we learned about how create wizards and some simple objects such as DTE, Solution, Project and Project Item. These objects help us to customize our Visual Studio working environment.
In todays article, we will delve deeper into the object model. We will develop an explorer to browse different classes and interfaces and their methods in a project.
Knowing Automation Object Model
Before we start with development lets get familiar with classes we will be using.
We will be using some classes from System.Reflection namespace and many classes from EnvDte namespace. Lets start with EnvDTE namespace
Object model in EnvDTE namespace is called as Visual Studio .net Automation Object Model. DTE stands for Development Tools Extensibility.
DTE Object
EnvDte namespace has one top-level object DTE. DTE implements interface called as _DTE. To get hold of any other object that represent any entity in Visual Studio.Net runtime environment, we must get hold of DTE object. To get instance of DTE, our class must implement IDTWizard interface and must implement the only method of this interface i.e. Execute method. Of the parameters of the method is DTE object.
Solution
DTE object has a property that represents solution opened in the VS.Net runtime environment.
Projects
Solution object has property named Projects that contains collection of all projects opened in that solution.
ProjectItems collection and ProjectItem
Projects collection contains Project objects; one project object represents one project opened in VS.Net. Each project has property named ProjectItems. This property represents all the project items such as all the files in the project, folder, sub projects, etc. All the project items in each project are grouped together in ProjectItems collection.
Each project item is of one of the following kind
- Misc
- Physical File
- Physical Folder
- Sub Project
So the picture looks like
Object model at this level looks simple and easy to understand. From now onwards it starts getting complicated.
FileCodeModel
Each project item has FileCodeModel object. This is top level object to access programmatic constructs in a source file. Using this object you can add various code elements at source file level such as Namespace, class, Interface, Structures, etc.
FileCodeModel object contains CodeElement collection. This is a collection of various types of code constructs such as CodeClass, CodeInterface, CodeEnum, CodeVariable, CodeFunction, etc. These constructs help you develop code and write it at desired location in the source file.
CodeElement
All these code constructs inherit from CodeElement. Few of the Code constructs contain property called as member such as CodeNamespace and CodeClass constructs. This property exposes members of the parent constructs e.g. CodeNamespace contains CodeClasses, CodeInterface, CodeDelegate, etc.
In overall DTE object model, Window object represents the window opened in the development environment. Whenever we open a project item with particular file path, the item is opened in a window and handle of that window is returned.
So with filling in the details of CodeElement, the model will look like
Develop Sample to read object model in a project
Now that we are aware of various objects, lets start with developing a small sample.
We are building on top of code from the previous article. In previous article we studied Solution object, ProjectItems collection, ProjectItem object.
Lets add a tree view control to UI
For each project item selected we will add Namespaces, Classes and Interfaces in the same. Add following code in lstProjectItems_SelectedIndexChanged event to WizardSampleUI.cs file.
private void lstProjectItems_SelectedIndexChanged(object sender,
System.EventArgs e)
{
trvCodeElements.Nodes.Clear();
foreach(Project prj in this.dte.Solution.Projects)
{
if(prj.Name == lstProjects.SelectedItem.ToString())
{
foreach(ProjectItem prjItem in prj.ProjectItems)
{
if(lstProjectItems.SelectedItem.ToString() == prjItem.Name)
{
LoadProjectItemdataInProjectItem(prjItem);
break;
}
}
}
}
}
The function LoadProjectItemdataInProjectItem will add all the items in the prjItem to treeview.
Add following function to WizardSampleUI.cs file.
/// <summary>
// Displays the project item data for selected project.
/// </summary>
private void LoadProjectItemdataInProjectItem(ProjectItem prjItem)
{
switch(prjItem.Kind)
{
case EnvDTE.Constants.vsProjectItemKindMisc:
lblItemKindText.Text = "Misc";
break;
case EnvDTE.Constants.vsProjectItemKindPhysicalFile:
lblItemKindText.Text = "PhysicalFile";
if(prjItem.Name.EndsWith(".cs"))
loadProjectItemdata(prjItem);
break;
case EnvDTE.Constants.vsProjectItemKindPhysicalFolder:
lblItemKindText.Text = "PhysicalFolder";
break;
case EnvDTE.Constants.vsProjectItemKindSubProject:
lblItemKindText.Text = "SubProject" ;
break;
}
}
Here we called loadProjectItemData function. This function will add Namespaces, Classes and Interfaces within the code file to treeview. Note that we are calling this function only if the file project item is a code file.
Now add following code to WizardSampleUI.cs file.
/// <summary>
/// This function addes Namespaces, Classes and Interfaces of a project item to
treeview.
/// </summary>
/// <param name="prjToLoad"></param>
private void loadProjectItemdata(ProjectItem prjToLoad)
{
prjToLoad.Open("{7651A701-06E5-11D1-8EBD-00A0C90F26EA}"); //Viewkind
pertaining to type of view to use.
TreeNode rootNd = trvCodeElements.Nodes.Add(prjToLoad.Name);
TreeNode nsNodes = trvCodeElements.Nodes.Add("NameSpaces");
foreach(CodeElement cdElement in prjToLoad.FileCodeModel.CodeElements)
{
if(cdElement.Kind == vsCMElement.vsCMElementNamespace)
{
TreeNode ndNameSpace = nsNodes.Nodes.Add(cdElement.FullName);
ndNameSpace.Tag = cdElement; //Tag will later used to navigate to project item.
TreeNode ndClassNodes = ndNameSpace.Nodes.Add("Classes");
TreeNode ndInterfacesNodes = ndNameSpace.Nodes.Add("Interfaces");
foreach(CodeElement cdClassElmnt in ((CodeNamespace)cdElement).Members)
{
if(cdClassElmnt.Kind == vsCMElement.vsCMElementClass)
{
TreeNode classNode = ndClassNodes.Nodes.Add(cdClassElmnt.Name);
classNode.Tag = cdClassElmnt;//Tag will later used to navigate to project item.
loadClassMembers(classNode,(CodeClass)cdClassElmnt);
continue;
}
if(cdClassElmnt.Kind == vsCMElement.vsCMElementInterface)
{
TreeNode interfaceNode = ndInterfacesNodes.Nodes.Add(cdClassElmnt.Name);
interfaceNode.Tag = cdClassElmnt;//Tag will later used to navigate to project item.
loadInterfaceMembers(interfaceNode,(CodeInterface)cdClassElmnt);
continue;
}
//Same way you can add code for Delegates, Enums and Structs.
}
}
}
}
This is an important piece of code. There are couple of important methods and properties used in this function.
Notice the Open function being called on parameter prjToLoad and the parameter passed to this function. The parameter represents a GUID. This GUID represents the kind of view in which the window should be opened. The GUID we mentioned will open the project item as Code Window. Other views possible are
GUID |
View Type |
{7651A701-06E5-11D1-8EBD-00A0C90F26EA} |
Code View |
{7651A700-06E5-11D1-8EBD-00A0C90F26EA} |
Debugger View |
{7651A702-06E5-11D1-8EBD-00A0C90F26EA} |
Designer View |
{00000000-0000-0000-0000-000000000000} |
Default view for the item |
{7651A703-06E5-11D1-8EBD-00A0C90F26EA} |
Text view |
Also check the FileCodeModel property used on prjToLoad. We are using CodeElements collection on this object.
Inside the first for loop we are checking the type of code element. The type of any code element is checked using Kind property. This property is of type Enum vsCMElementClass. The Kind property of the code element is compared with this Enum. We are checking if the code element is a NameSpace declaration.
Unfortunately I could not find any element that will represent Using statements though the Enum - vsCMElementClass supports it.
Lets move ahead.
Once we confirm that the code element is a NameSpace we type cast it to CodeNamespace type and use its Member property to access all the classes and interfaces declared in Namespace. The Member property returns collection of all CodeElements.
While looping thro the namespace members, we check the Kind of each code element and populate tree view with details of the code element using helper functions. These helper functions are similar to loadProjectItemdata function.
Different helper functions are used to populate data about Class, functions in class and properties in the class.
We have Tag property of TreeNode to our benefit. This property can hold any object. We want to add functionality that enables user to select the function/ class/ interface and open it in the code view pane. User will select the node and double click on it.
So while adding any treenode object we are initializing the Tag property of the treenode to the code element it represents
In loadProjectItemdata function we have added code
TreeNode ndNameSpace = nsNodes.Nodes.Add(cdElement.FullName);
ndNameSpace.Tag = cdElement; //Tag will later used to navigate to project item.
navigate to project item.
Now lets add the double click event for tree view as follows
private void trvCodeElements_DoubleClick(object sender, System.EventArgs e)
{
if(trvCodeElements.SelectedNode.Tag != null)
{
//Following code will navigate to selected node.
Window prjItemWindow = ((CodeElement)trvCodeElements.SelectedNode.Tag).ProjectItem.Open
("{7651A701-06E5-11D1-8EBD-00A0C90F26EA}");
//Activate the code pane.
prjItemWindow.Activate();
//Navigate to selected node.
((CodeElement)trvCodeElements.SelectedNode.Tag).StartPoint.TryToShow
vsPaneShowHow.vsPaneShowTop,0);
//"Edit.GoToDefinition",((CodeElement)trvCodeElements.SelectedNode.Tag).FullName);
}
}
In this code notice the use of Window object. In this code snippet, we retrieve the code element selected by user using the Tag property of selected node. We type cast it to CodeElement. Each code element has a property ProjectItem. This property returns you the project item (code file/folder) it belongs to. We open this project item and obtain the Windows object by calling Open method on project item. Once we get the window, we activate the window by calling its Activate method. We want to show the code element user has selected in editor. Till now we have opened the project item that contains the code element user has selected.
Notice the use of StartPoint property of the code element. This property returns a TextPoint. Its similar to placing a cursor at a location in the editor. TextPoint is helps you navigate in the code window in the cursor style. We will see about TextPoint sometime later. The method TryToShow will bring up the code element selected by user in the editor.
When user double clicks on any function, parameter or class in the tree view.
dte.ExecuteCommand
This is very important and powerful method of DTE object. This method expects a command which is a menu operated command. E.g. to build the solution we go to Build menu and select Build Solution command. To execute the same command using dte.ExecuteCommand you will provide string parameter Build.BuildSolution. To get the list of all these commands, you can use command window.
e.g. In command window you can start typing Edit and you will get context sensitive help of all the commands available for Edit menu. In the same way you can find all the commands for main menus such as Build, Debug, Tools etc.
The other parameter for dte.ExecuteCommand is string of argument. This is optional and specific to each command.
What we achieved
- We studied various objects of VS.Net automation object model.
- We studied different properties and methods of these objects.
- Using these objects we browsed through the code modules and projects.
- We created an explorer that is similar to Class View explorer.
- We were introduced to TextPoint object. This is an important object to navigate thro the text window and to manipulate the code.
The next step would be to generate code. Happy Wizarding till then!!!!