Developing extension packages for Visual Studio 2005/2008/2010/2012 using C# with real-life samples
Upcoming SlideShare
Loading in...5
×
 

Developing extension packages for Visual Studio 2005/2008/2010/2012 using C# with real-life samples

on

  • 2,638 views

Creating extension packages (plug-ins) for Microsoft Visual Studio IDE appears as quite an easy task at the first sight. There exist an excellent MSDN documentation, as well as various articles, ...

Creating extension packages (plug-ins) for Microsoft Visual Studio IDE appears as quite an easy task at the first sight. There exist an excellent MSDN documentation, as well as various articles, examples and a lot of other additional sources on this topic. But, at the same time, it could also appear as a difficult task when an unexpected behavior is encountered along the way. Although it can be said that such issues are quite common to any programming task, the subject of IDE plug-in development is still not thoroughly covered at this moment.

We develop PVS-Studio static code analyzer. Although the tool itself is intended for C++ developers, quite a large fragment of it is written in C#. When we just had been starting the development of our plug-in, Visual Studio 2005 had been considered as modern state-of-the-art IDE. Although, at this moment of Visual Studio 2012 release, some could say that Visual Studio 2005 is not relevant anymore, we still provide support for this version in our tool. During our time supporting various Visual Studio versions and exploring capabilities of the environment, we've accumulated a large practical experience on how to correctly (and even more so incorrectly!) develop IDE plug-ins. As holding all of this knowledge inside us was becoming unbearable, we've decided to publish it here. Some of our solutions that seem quite obvious right now were discovered in the course of several years. And the same issues could still haunt other plug-in developers.

By publishing this collection of small articles we wish to contribute to this area of software development. We hope that developers interested in this topic will discover something new for themselves or at least will use the articles as a collection of how-to's and recipes for Visual Studio extension development.

Statistics

Views

Total Views
2,638
Views on SlideShare
2,638
Embed Views
0

Actions

Likes
0
Downloads
41
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Developing extension packages for Visual Studio 2005/2008/2010/2012 using C# with real-life samples Developing extension packages for Visual Studio 2005/2008/2010/2012 using C# with real-life samples Document Transcript

  • Developing extension packages for VisualStudio 2005/2008/2010/2012 using C#with real-life samplesAuthor: Paul EremeevDate: 29.10.2012IntroductionCreating extension packages (plug-ins) for Microsoft Visual Studio IDE appears as quite an easy task atthe first sight. There exist an excellent MSDN documentation, as well as various articles, examples and alot of other additional sources on this topic. But, at the same time, it could also appear as a difficult taskwhen an unexpected behavior is encountered along the way. Although it can be said that such issues arequite common to any programming task, the subject of IDE plug-in development is still not thoroughlycovered at this moment.We develop PVS-Studio static code analyzer. Although the tool itself is intended for C++ developers,quite a large fragment of it is written in C#. When we just had been starting the development of ourplug-in, Visual Studio 2005 had been considered as modern state-of-the-art IDE. Although, at thismoment of Visual Studio 2012 release, some could say that Visual Studio 2005 is not relevant anymore,we still provide support for this version in our tool. During our time supporting various Visual Studioversions and exploring capabilities of the environment, weve accumulated a large practical experienceon how to correctly (and even more so incorrectly!) develop IDE plug-ins. As holding all of thisknowledge inside us was becoming unbearable, weve decided to publish it here. Some of our solutionsthat seem quite obvious right now were discovered in the course of several years. And the same issuescould still haunt other plug-in developers.By publishing this collection of small articles we wish to contribute to this area of softwaredevelopment. We hope that developers interested in this topic will discover something new forthemselves or at least will use the articles as a collection of how-tos and recipes for Visual Studioextension development.Creating, debugging and deploying extension packages for MicrosoftVisual Studio 2005/2008/2010/2012AbstractThis article contains the overview of several different methods for extending Visual Studio IDE. Thecreation, debugging, registration and end-user deployment of Visual Studio extension packages will beexplained in detail.
  • IntroductionThe following series of articles is dedicated to the development of the extension package for VisualStudio 2005/2008/2010/2012 IDEs utilizing .NET framework and C# programming language. Thefollowing topics will be covered:  basic information on creating and debugging of MSVS plug-ins and maintaining these extensibility projects for several versions of Visual Studio inside a common source code base;  overview of Automation Object Model and various Managed Package Framework (MPF) classes  extending interface of the IDE though the automation object models API (EnvDTE) and MPF (Managed Package Framework) classes with custom menus, toolbars, windows and options pages;  utilizing Visual C++ project automation model for gathering data needed to operate an external preprocessor/compiler, such as compilation arguments and settings for different platforms and configurations;The content of these articles is based on our experience in developing an MSVS extension package plug-in for PVS-Studio static analyzer. A more detailed in-depth references for the topics covered here areavailable at the end of each article through the links to MSDN library and several other externalresources.The articles will cover the extension development only for Visual Studio 2005 and later versions. Thislimitation reflects that PVS-Studio also supports integration to Visual Studio starting only from version 8(Visual Studio 2005). The main reason behind this is that a new extensibility API model was introducedfor Visual Studio 2005, and this new version is not backward-compatible with previous IDE APIs.Creating and debugging Visual Studio VSPackage extension modulesThere exists a number of ways to extend Microsoft Visual Studio features. On the most basic level itspossible to automate simple routine user actions using macros. An add-In plug-in module can be usedfor obtaining an access to environments UI objects, such as menu commands, windows etc. Extensionof IDEs internal editors is possible through MEF (Managed Extensibility Framework) components(starting with MSVS 2010). Finally, a plug-in of the Extension Package type (known as VSPackage) is bestsuited for integrating large independent components into Visual Studio. VSPackage allows combiningthe environment automation through Automation Object Model with usage of Managed PackageFramework classes (such as Package). It also provides the means of extending the automation modelitself by registering user-defined custom automation objects within it. Such user automation objects, inturn, will become available through the same automation model from other user-created extensibilitypackages, providing these packages with access to your custom components.In its earlier versions, PVS-Studio plug-in (versions 1.xx and 2.xx to be precise, when it was still known asViva64) existed as an Add-In package. Starting with PVS-Studio 3.0 it was redesigned as VSPackagebecause the functionality Add-in was able to provide became insufficient for the tasks at hand and alsothe debugging process was quite inconvenient. After all, we wanted to have our own logo on VisualStudio splash screen!Projects for VSPackage plug-in modules. Creating the extension package.Contrary to Add-In plug-ins, developing VS extension packages requires the installation of MicrosoftVisual Studio SDK for a targeted version of IDE, i.e. a separate SDK should be installed with every versionof Visual Studio for which an extension is being developed. We will be examining the 2005, 2008, 2009and 2012 versions of Visual Studio. Installation of Visual Studio SDK adds a standard project template for
  • Visual Studio Package (on the Other Project Types -> Extensibility page) to VS template manager. Ifselected, this template will generate a basic MSBuild project for an extension package, allowing severalparameters to be specified beforehand, such as a programming language to be used and the automaticgeneration of several stub components for generic UI elements, such as menu item, an editor, usertoolwindow etc.We will be using a C# VSPackage project (csproj), which is a project for managed dynamic-link library(dll). The corresponding csproj MSBuild project for this managed assembly will also contain several XMLnodes specific to a Visual Studio package, such as VSCT compiler and IncludeinVSIX (in later IDEversions).The main class of an extension package should be inherited from theMicrosoft.VisualStudio.Shell.Package. This base class provides managed wrappers for IDE interactionAPIs, implementation of which is required from a fully-functional Visual Studio extension package.public sealed class MyPackage: Package{ public MyPackage () {} ...}The Package class allows overriding of its base Initialize method. This method receives execution controlat the moment of package initialization in the current session of IDE.protected override void Initialize(){ base.Initialize(); ...}The initialization of the module will occur when it is invoked for the first time, but it also could betriggered automatically, for example after IDE is started or when user enters a predefined environmentUI context state.Being aware of the packages initialization and shutdown timings is crucial. Its quite possible that thedeveloper would be requesting some of Visual Studio functionality at the moment when it is stillunavailable to the package. During PVS-Studio development weve encountered several such situationswhen the environment "punished us" for not understanding this, for instance, we are not allowed to"straightforwardly" display message boxes after Visual Studio enters a shutdown process.Debugging extension packages. Experimental Instance.The task of debugging a plug-in module or extension intended for an integrated developmentenvironment is not quite a typical one. Quite often such environment itself is utilized for plug-insdevelopment and debugging. Hooking up an unstable module to this IDE can lead to instability of theenvironment itself. The necessity to uninstall a module under development from the IDE before everydebugging session, which in turn often requires restarting it, is also a major inconvenience (IDE couldblock the dll that needs to be replaced by a newer version for debugging).
  • It should be noted that a VSPackage debugging process in this aspect is substantially easier than that ofan Add-In package. This was one of the main reasons for changing the project type of PVS-Studio plug-in.VSPackage solves the aforementioned development and debugging issues by utilizing Visual StudioExperimental Instance mechanism. Such an experimental instance could be easily started by passing aspecial command line argument:"C:Program Files (x86)Microsoft Visual Studio 10.0 Common7IDEdevenv.exe" /RootSuffix ExpAn experimental instance of the environment utilizes a separate independent Windows registry hive(called experimental hive) for storing all of its settings and component registration data. As such, anymodifications in the IDEs settings or changes in its component registration data, which were madeinside the experimental hive, will not affect the instance which is employed for the development of themodule (that is your main regular instance which is used by default).Visual Studio SDK provides a special tool for creating or resetting such experimental instances —CreateExpInstance. To create a new experimental hive, it should be executed with these arguments:CreateExpInstance.exe /Reset /VSInstance=10.0 /RootSuffix=PVSExpExecuting this command will create a new experimental registry hive with a PVSExp suffix in its name forthe 10th version of IDE (Visual Studio 2010), also resetting all of its settings to their default values inadvance. The registry path for this new instance will look as follows:HKEY_CURRENT_USERSoftwareMicrosoftVisualStudio10.0PVSExpWhile the Exp suffix is utilized by default for package debugging inside VSPackage template project,other experimental hives with unique names could also be created by the developer at will. To start aninstance of the environment for the hive weve created earlier (containing PVSExp in its name), thesearguments should be used:"C:Program Files (x86)Microsoft Visual Studio 10.0 Common7IDEdevenv.exe" /RootSuffix PVSExpA capacity for creating several different experimental hives on a single local workstation could be quiteuseful, as, for example, to provide a simultaneous and isolated development of several extensionpackages.After installing the SDK package, a link is created in the Visual Studio programs menu group forresetting the default Experimental Instance for this version of the IDE (for instance, "Reset the MicrosoftVisual Studio 2010 Experimental Instance").In the end, the faster youll figure out how the debugging environment works, the fewer issues youllencounter in understanding how plug-in initialization works during development.Registering and deploying Visual Studio extension packagesRegistering a VS extension package requires registering a package itself, as well as registering all of thecomponents it integrates into the IDE (for example, menu items, option pages, user windows etc.). The
  • registration is accomplished by creating records corresponding to these components inside the mainsystem registry hive of Visual Studio.All the information required for registration is placed, after building your VSPackage, inside a specialpkgdef file, according to several special attributes of the main class of your package (which itself shouldbe a subclass of the MPF Package class). The pkgdef can also be created manually using theCreatePkgDef utility. This tool collects all of the required module registration information from thesespecial attributes by the means of .NET reflection. Lets study these registration attributes in detail.The PackageRegistration attribute tells the registration tool that this class is indeed a Visual Studioextension package. Only if this attribute is discovered will the tool perform its search for additional ones. [PackageRegistration(UseManagedResourcesOnly = true)]The Guid attribute specifies a unique package module identifier, which will be used for creating aregistry sub-key for this module in Visual Studio hive. [Guid("a0fcf0f3-577e-4c47-9847-5f152c16c02c")]The InstalledProductRegistration attribute adds information to Visual Studio Help -> About dialog andthe loading splash screen. [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]The ProvideAutoLoad attribute links automatic module initialization with the activation of a specifiedenvironment UI context. When a user enters this context, the package will be automatically loaded andinitialized. This is an example of setting module initialization to the opening of a solution file: [ProvideAutoLoad("D2567162-F94F-4091-8798-A096E61B8B50")]The GUID values for different IDE UI contexts can be found in theMicrosoft.VisualStudio.VSConstants.UICONTEXT class.The ProvideMenuResource attribute specifies an ID of resource that contains user created menus andcommands for their registration inside IDE. [ProvideMenuResource("Menus.ctmenu", 1)]The DefaultRegistryRoot attribute specifies a path to be used for writing registration data to the systemregistry. Starting with Visual Studio 2010 this attribute can be dropped as the corresponding data will bepresent in manifest file of a VSIX container. An example of registering a package for Visual Studio 2008:[DefaultRegistryRoot("SoftwareMicrosoftVisualStudio9.0")]Registration of user-created components, such as toolwidows, editors, option pages etc. also requiresthe inclusion of their corresponding attributes for the users Package subclass. We will examine theseattributes separately when we will be examining corresponding components individually.Its also possible to write any user-defined registry keys (and values to already existing keys) duringpackage registration through custom user registration attributes. Such attributes can be created byinheriting the RegistrationAttribute abstract class.
  • [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] public class CustomRegistrationAttribute : RegistrationAttribute { }The RegistrationAttribute-derived attribute must override its Register and Unregister methods, whichare used to modify registration information in the system registry.The RegPkg tool can be used for writing registration data to Windows registry. It will add all of the keysfrom pkgdef file passed to it into the registry hive specified by the /root argument. For instance, theRegPkg is utilized by default in Visual Studio VSPackage project template for registering the module inthe Visual Studio experimental hive, providing convenient seamless debugging of the package beingdeveloped. After all of the registration information have been added to the registry, Visual Studio(devenv.exe) should be started with /setup switch to complete registration for new components insidethe IDE.Deploying plug-ins for developers and end-users. Package Load Key.Before proceeding to describe the deployment process itself, one particular rule should be stressed:Each time after a new version of the distribution containing your plug-in is created, this newdistribution should be tested on a system without Visual Studio SDK installed, as to make sure that itwill be registered correctly on the end-user system.Today, as the releases of early versions of PVS-Studio are past us, we do not experience these kinds ofissues, but several of these early first versions were prone to them.Deploying a package for Visual Studio 2005/2008 will require launching of regpkg tool for a pkgdef fileand passing the path to Visual Studio main registry hive into it. Alternately, all keys from a pkgdef can bewritten to Windows registry manually. Here is the example of automatically writing all the registrationdata from a pkgdef file by regpkg tool (in a single line):RegPkg.exe /root:SoftwareMicrosoftVisualStudio9.0Exp "/pkgdeffile:objDebugPVS-Studio-vs2008.pkgdef" "C:MyPackageMyPackage.dll"After adding the registration information to the system registry, it is necessary to start Visual Studiowith a /setup switch to complete the component registration. It is usually the last step in the installationprocedure of a new plug-in.Devenv.exe /setupStarting the environment with this switch instructs Visual Studio to absorb resource metadata for user-created components from all available extension packages, so that these components will be correctlydisplayed by IDEs interface. Starting devenv with this key will not open its main GUI window.We do not employ RepPkg utility as part of PVS-Studio deployment, instead manually writing requireddata to the registry by using our stand-alone installer. We chose this method because we have no desireof being dependent on some external third-party tools and we want full control over the installationprocess. Still, we do use RegPkg during plug-in development for convenient debugging.
  • VSIX packagesBeginning from Visual Studio 2010, VSPackage deployment process can be significantly simplifiedthrough the usage of VSIX packages. VSIX package itself is a common (Open Packaging Conventions)archive containing plug-ins binary files and all of the other auxiliary files which are necessary for plug-ins deployment. By passing such archive to the standard VSIXInstaller.exe utility, its contents will beautomatically registered in the IDE:VSIXInstaller.exe MyPackage.vsixVSIX installer could also be used with /uninstall switch to remove the previously installed package froma system. A unique GUID of the extension package should be used to identify such package:VSIXInstaller.exe /uninstall: 009084B1-6271-4621-A893-6D72F2B67A4DContents of a VSIX container are defined through the special vsixmanifest file, which should be added toplug-ins project. Vsixmanifest file permits the following properties to be defined for an extension:  targeted Visual Studio versions and editions, which will be supported by the plug-in;  a unique GUID identifier;  a list of components to be registered (VSPackage, MEF components, toolbox control etc.);  general information about the plug-in to be installed (description, license, version, etc.);To include additional files into a VSIX container, the IncludeInVSIX node should be added to theirdeclarations inside your MSBuild project (alternately, they could also be marked as included into VSIXfrom their respective property windows, by opening it from Visual Studio Solution Explorer).<Content Include="MyPackage.pdb"> <IncludeInVSIX>true</IncludeInVSIX></Content>In fact, the VSIX file could be viewed as an almost full-fledged installer for extension packages on thelatest versions of Visual Studio (2010 and 2012), allowing the extensions to be deployed by a "one-click"method. Publishing your VSIX container in the official Visual Studio Gallery for extensions allows end-users to install such package through the Tools -> Extension Manager IDE dialog.This new VSIX installation procedure in Visual Studio 2010 does substantially alleviate packagedeployment for end-users (as well as for developers themselves). Some developers even had decided tosupport only VS2010 IDE and versions above it, if only not to get involved with the development of apackage and installer for earlier IDE versions.Unfortunately, several issues can be encountered when using VSIX installer together with Visual Studio2010 extension manager interface. For instance, sometimes the extensions binary files are not removedcorrectly after uninstall, which in turn blocks the VSIX installer from installing/reinstalling the sameextension. As such, we advise you not to depend upon the VSIX installer entirely and to provide somebackup, for example by directly removing your files from a previous plug-in installation beforeproceeding with a new one.Package Load KeyEach VSPackage module loaded into Visual Studio must possess a unique Package Load Key (PLK). PLKkey is specified through the ProvideLoadKey attribute for the Package subclass in 2005/2008 versions ofthe IDE.
  • [ProvideLoadKey("Standard", "9.99", "MyPackage", "My Company", 100)]Starting with Visual Studio 2010, the presence of a PLK, as well as of the ProvideLoadKey attributerespectively, in a package is not required, but it can still be specified in case the module underdevelopment is targeting several versions of MSVS. The PLK can be obtained by registering at the VisualStudio Industry Partner portal, meaning it guarantees that the development environment can load onlypackages certified by Microsoft.However, systems containing Visual Studio SDK installed are exceptions to this, as Developer License Keyis installed together with the SDK. It allows the corresponding IDE to load any extension package,regardless of validity of its PLK.Considering the aforementioned, one more time it is necessary to stress the importance of testing thedistribution on a system without Visual Studio SDK present, because the extension package will operateproperly on developers workstation regardless of its PLK correctness.Extension registration specifics in the context of supporting several different versions ofVisual Studio IDEBy default, VSPackage project template will generate an extensibility project for the version of VisualStudio that is used for the development. This is not a mandatory requirement though, so it is possible todevelop an extension for a particular version of IDE using a different one. It also should be noted thatafter automatically upgrading a project file to a newer version through devenv /Upgrade switch, thetargeted version of the IDE and its corresponding managed API libraries will remain unchanged, i.e.from a previous version of Visual Studio.To change the target of the extension to another version of Visual Studio (or to register an extensioninto this version to be more precise), you should alter values passed to the DefaultRegistryRoot attribute(only for 2005/2008 IDE versions, as starting from Visual Studio 2010 this attribute is no longer required)or change the target version in the VSIX manifest file (for versions above 2008).VSIX support appears only starting from Visual Studio 2010, so building and debugging the plug-intargeted for the earlier IDE version from within Visual Studio 2010 (and later) requires setting up all theaforementioned registration steps manually, without VSIX manifest. While changing target IDE versionone should also not forget to switch referenced managed assemblies, which contain COM interfacewrappers utilized by the plug-in, to the corresponding versions as well.Altering the IDE target version of the plug-in affects the following Package subclass attributes:  the InstalledProductRegistration attribute does not support overloading of its constructor with a (Boolean, String, String, String) signature, starting from Visual Studio 2010;  the presence of DefaultRegistryRoot and ProvideLoadKey attributes is not mandatory starting from Visual Studio 2010, as similar values are now specified inside VSIX manifest;References 1. MSDN. Experimental Build. 2. MSDN. How to: Register a VSPackage. 3. MSDN. VSIX Deployment. 4. MSDN. How to: Obtain a PLK for a VSPackage. 5. MZ-Tools. Resources about Visual Studio .NET extensibility. 6. MSDN. Creating Add-ins and Wizards.
  • 7. MSDN. Using a Custom Registration Attribute to Register an Extension.Visual Studio Automation Object Model. EnvDTE interfaces.AbstractThis article contains an overview of Visual Studio Automation Object Model. Models overall structureand the means of obtaining access to its interfaces through DTE/DTE2 top level objects are examined.Several examples of utilizing elements of the model are provided. Also discussed are the issues of usingmodels interfaces within multithreaded applications; an example of implementing such mechanism formultithreaded interaction with COM interfaces in managed code is provided as well.IntroductionVisual Studio development environment is built upon the principles of automation and extensibility,providing the developers using it with the ability of integrating almost any custom element into the IDEand allowing for an easy interaction with its default and user-created components. As the means ofimplementing these tasks, Visual Studio users are provided with several cross-complementing toolsets,the most basic and versatile among these is the Visual Studio Automation Object Model.Automation Object Model is represented by a series of libraries containing a vast and well-structuredAPI set which covers all aspects of IDE automation and the majority of its extensibility capabilities.Although, in comparison to other IDE extensibility tools, this model does not provide access to someportions of Visual Studio (this applies mostly to the extension of some IDEs features), it is nonethelessthe most flexible and versatile among them.The majority of the models interfaces are accessible from within every type of IDE extension module,which allows interacting with the environment even from an external independent process. Moreover,the model itself could be extended along with the extension of Visual Studio IDE, providing other third-party developers with an access to user-created custom components.Automation Object Model structureVisual Studio automation model is composed of several interconnected functional object groupscovering all aspects of the development environment; it also provides capabilities for controlling andextending these groups. Accessing any of them is possible through the top-level global DTE interface(Development Tools Environment). Figure 1 shows the overall structure of the automation model andhow it is divided among functionality groups.
  • Figure 1 — Visual Studio Automation Object Model (click the picture to zoom in)
  • The model itself could be extended by user in one of the following groups:  project models (implementing new project types, support for new languages);  document models (implementing new document types and document editors)  code editor level models (support for specific language constructs)  project build-level modelsAutomation model could be extended from plug-ins of VSPackage type only.Despite the models versatility, not every group belonging to the model could be equally utilized from allthe types of IDE extensions. For instance, some of the models capabilities are inaccessible to externalprocesses; these capabilities are tied to specific extension types, such as Add-In or VSPackage.Therefore, when selecting the type for the extension to be developed, it is important to consider thefunctionality that this extension will require.Obtaining references to DTE/DTE2 objects.In order to create a Visual Studio automation application it is necessary to obtain access to theautomation objects themselves in the first place. To accomplish this, first of all it is necessary to hook upthe correct versions of libraries containing the required managed API wrappers in the EnvDTEnamespace. Secondly, the reference to the automation model top-level object, that is the DTE2interface, should be obtained.In the course of Visual Studio evolution, several of its automation objects had been modified or receivedsome additional functionality. So, to maintain a backward compatibility with existing extensionpackages, new EnvDTE80, EnvDTE90, EnvDTE100 etc. namespaces were created instead of updating theinterfaces from the original EnvDTE namespace. The majority of such updated interfaces from thesenew namespaces do maintain the same names as in the original ones, but with addition of an ordinalnumber at the end of the name, for example Solution and Solution2. It is advised that these updatedinterfaces should be utilized when creating a new project, as they do contain the most recentfunctionality. Its worth noting that properties and methods of DTE2 interface usually return objectreferences with types corresponding to the original DTE, i.e. accessing dte2.Solution will return Solutionand not the Solution2 as it would seem.Although these new EnvDTE80, EnvDTE90, EnvDTE100 namespaces do contain some of the updatedfunctionality as mentioned above, still it is the EnvDTE interface that contains the majority ofautomation objects. Therefore, in order to possess access to all of the existing interfaces, it is necessaryto link all versions of the managed COM wrapper libraries to the project, as well as to obtain thereferences to DTE and also to DTE2.The way of obtaining top-level EnvDTE object reference is dependent upon the type of IDE extensionbeing developed. Lets examine 3 of such extension types: Add-In, VSPackage and an MSVS-independentexternal process.Add-In extensionIn the case of an Add-In extension, access to the DTE interface can be obtained inside the OnConnectionmethod which should be implemented for the IDTExtensibility interface that provides access to theextension-environment interaction events. The OnConnection method is called at the moment when themodule is loaded by the IDE; it can happen either when the environment is being loaded itself or after
  • the extension was called for the first time in the IDE session. The example of obtaining the referencefollows:public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom) { _dte2 = (DTE2)application; ... }An Add-In module can be initialized either at the moment of IDE start-up, or when it is called for the firsttime in current IDE session. So, the connectMode can be used to correctly determine the moment ofinitialization inside the OnConnection method.switch(connectMode){ case ext_ConnectMode.ext_cm_UISetup: ... break; case ext_ConnectMode.ext_cm_Startup: ... break; case ext_ConnectMode.ext_cm_AfterStartup: ... break; case ext_ConnectMode.ext_cm_CommandLine: ... break;}As in the example above, add-In could be loaded either simultaneously with the IDE itself (if the startupoption in the Add-In manager is checked), when it is called the first time or when it is called through thecommand line. The ext_ConnectMode.ext_cm_UISetup option is invoked only for a single time in theplug-ins overall lifetime, which is during its first initialization. This case should be used for initializinguser UI elements which are to be integrated into the environment (more on this later on).If an Add-In is being loaded during Visual Studio start-up (ext_ConnectMode.ext_cm_Startup), then atthe moment OnConnect method receives control for the first time, it is possible that the IDE still is notfully initialized itself. In such a case, it is advised to postpone the acquisition of the DTE reference untilthe environment is fully loaded. The OnStartupComplete handler provided by the IDTExtensibility can beused for this.public void OnStartupComplete(ref Array custom){ ...}
  • VSPackage extensionFor VSPackage type of extension, the DTE could be obtained through the global Visual Studio servicewith the help of GetService method of a Package subclass:DTE dte = MyPackage.GetService(typeof(DTE)) as DTE;Please note that the GetService method could potentially return null in case Visual Studio is not fullyloaded or initialized at the moment of such access, i.e. it is in the so called "zombie" state. To correctlyhandle this situation, it is advised that the acquisition of DTE reference should be postponed until thisinterface is inquired. But in case the DTE reference is required inside the Initialize method itself, theIVsShellPropertyEvents interface can be utilized (also by deriving our Package subclass from it) and thenthe reference could be safely obtained inside the OnShellPropertyChange handler.DTE dte;uint cookie;protected override void Initialize(){ base.Initialize(); IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell; if (shellService != null) ErrorHandler.ThrowOnFailure( shellService.AdviseShellPropertyChanges(this,out cookie));...}public int OnShellPropertyChange(int propid, object var){ // when zombie state changes to false, finish package initialization if ((int)__VSSPROPID.VSSPROPID_Zombie == propid) { if ((bool)var == false) { this.dte = GetService(typeof(SDTE)) as DTE; IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell; if (shellService != null) ErrorHandler.ThrowOnFailure( shellService.UnadviseShellPropertyChanges(this.cookie) ); this.cookie = 0; } } return VSConstants.S_OK;}It should be noted that the process of VSPackage module initialization at IDE startup could vary fordifferent Visual Studio versions. For instance, in case of VS2005 and VS2008, an attempt at accessingDTE during IDE startup will almost always result in null being returned, owning to the relative fastloading times of these versions. But, one does not simply obtain access into DTE. In Visual Studio 2010case, it mistakenly appears that one could simply obtain an access to the DTE from inside the Initialize()
  • method. In fact, this impression is a false one, as such method of DTE acquisition could potentially causethe occasional appearance of "floating" errors which are hard to identify and debug, and even the DTEitself may be still uninitialized when the reference is acquired. Because of these disparities, theaforementioned acquisition method for handling IDE loading states should not be ignored on anyversion of Visual Studio.Independent external processThe DTE interface is a top-level abstraction for Visual Studio environment in the automation model. Inorder to acquire a reference to this interface from an external application, its ProgID COM identifiercould be utilized; for instance, it will be "VisualStudio.DTE.10.0" for Visual Studio 2010. Consider thisexample of initializing a new IDE instance and when obtaining a reference to the DTE interface.// Get the ProgID for DTE 8.0.System.Type t = System.Type.GetTypeFromProgID( "VisualStudio.DTE.10.0", true);// Create a new instance of the IDE.object obj = System.Activator.CreateInstance(t, true);// Cast the instance to DTE2 and assign to variable dte.EnvDTE80.DTE2 dte = (EnvDTE80.DTE2)obj;// Show IDE Main Windowdte.MainWindow.Activate();In the example above weve actually created a new DTE object, starting deven.exe process by theCreateInstance method. But at the same time, the GUI window of the environment will be displayedonly after the Activate method is called.Next, lets review a simple example of obtaining the DTE reference from an already running VisualStudio Instance:EnvDTE80.DTE2 dte2;dte2 = (EnvDTE80.DTE2) System.Runtime.InteropServices.Marshal.GetActiveObject( "VisualStudio.DTE.10.0");However, in case several instances of the Visual Studio are executing at the moment of our inquiry, theGetActiveObject method will return a reference to the IDE instance that was started the earliest. Letsexamine a possible way of obtaining the reference to DTE from a running Visual Studio instance by thePID of its process.using EnvDTE80;using System.Diagnostics;using System.Runtime.InteropServices;using System.Runtime.InteropServices.ComTypes;[DllImport("ole32.dll")]private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);[DllImport("ole32.dll")]private static extern void GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
  • public static DTE2 GetByID(int ID){ //rot entry for visual studio running under current process. string rotEntry = String.Format("!VisualStudio.DTE.10.0:{0}", ID); IRunningObjectTable rot; GetRunningObjectTable(0, out rot); IEnumMoniker enumMoniker; rot.EnumRunning(out enumMoniker); enumMoniker.Reset(); IntPtr fetched = IntPtr.Zero; IMoniker[] moniker = new IMoniker[1]; while (enumMoniker.Next(1, moniker, fetched) == 0) { IBindCtx bindCtx; CreateBindCtx(0, out bindCtx); string displayName; moniker[0].GetDisplayName(bindCtx, null, out displayName); if (displayName == rotEntry) { object comObject; rot.GetObject(moniker[0], out comObject); return (EnvDTE80.DTE2)comObject; } } return null;}Here weve acquired the DTE interface by identifying the required instance of the IDE in the table ofrunning COM objects (ROT, Running Object Table) by its process identifier. Now we can access the DTEfor every of the executing instances of Visual Studio, for example:Process Devenv;...//Get DTE by Process IDEnvDTE80.DTE2 dte2 = GetByID(Devenv.Id);Additionally, to acquire any project-specific interface (including custom model extensions), for examplethe CSharpProjects model, through a valid DTE interface, the GetObject method should be utilized:Projects projects = (Projects)dte.GetObject("CSharpProjects");The GetObject method will return a Projects collection of regular Project objects, and each one of themwill contain a reference to our project-specific properties, among other regular ones.Visual Studio text editor documentsAutomation model represents Visual Studio text documents through the TextDocument interface. Forexample, C/C++ source code files are opened by the environment as text documents. TextDocument isbased upon the common automation model document interface (the Document interface), whichrepresents file of any type opened in Visual Studio editor or designer. A reference to the text documentobject can be obtained through the Object field of the Document object. Lets acquire a text documentfor the currently active (i.e. the one possessing focus) document from IDEs text editor.
  • EnvDTE.TextDocument objTextDoc =(TextDocument)PVSStudio.DTE.ActiveDocument.Object("TextDocument");Modifying documentsThe TextSelection document allows controlling text selection or to modify it. The methods of thisinterface represent the functionality of Visual Studio text editor, i.e. they allow the interaction with thetext as it presented directly by the UI.EnvDTE.TextDocument Doc = (TextDocument)PVSStudio.DTE.ActiveDocument.Object(string.Empty);Doc.Selection.SelectLine();TextSelection Sel = Doc.Selection;int CurLine = Sel.TopPoint.Line;String Text = Sel.Text;Sel.Insert("testrn");In this example we selected a text line under the cursor, read the selected text and replaced it with atest string.TextDocument interface also allows text modification through the EditPoint interface. This interface issomewhat similar to the TextSelection, but instead of operating with the text through the editor UI, itdirectly manipulates text buffer data. The difference between them is that the text buffer is notinfluenced by such editor-specific notions as WordWrap and Virtual Spaces. It should be noted that bothof these editing methods are not able to modify read-only text blocks.Lets examine the example of modifying text with EditPoint by placing additional lines at the end ofcurrent line with a cursor.objEditPt = objTextDoc.StartPoint.CreateEditPoint();int lineNumber = objTextDoc.Selection.CurrentLine;objEditPt.LineDown(lineNumber - 1);EditPoint objEditPt2 = objTextDoc.StartPoint.CreateEditPoint();objEditPt2.LineDown(lineNumber - 1);objEditPt2.CharRight(objEditPt2.LineLength);String line = objEditPt.GetText(objEditPt.LineLength);String newLine = line + "test";objEditPt.ReplaceText(objEditPt2, newLine, (int)vsEPReplaceTextOptions.vsEPReplaceTextKeepMarkers);Navigating the documentsVSPackage modules are able to obtain access to a series of global services which could be used foropening and handling environment documents. These services could be acquired by thePackage.GetGlobalService() method. It should be noted that the services described here are not part ofthe DTE model and are accessible only from a Package-type extension, and therefore they could not beutilized in other types of Visual Studio extensions. Nonetheless, they can be quite useful for handling IDEdocuments when they are utilized in addition to the Documents interface described earlier. Next, wellexamine these services in more detail.
  • The IVsUIShellOpenDocument interface controls the state of documents opened in the environment.Following is the example that uses this interface to open a document through path to a file which thisdocument will represent.String path = "C:Testtest.cpp";IVsUIShellOpenDocument openDoc = Package.GetGlobalService(typeof(IVsUIShellOpenDocument)) as IVsUIShellOpenDocument;IVsWindowFrame frame;Microsoft.VisualStudio.OLE.Interop.IServiceProvider sp;IVsUIHierarchy hier;uint itemid;Guid logicalView = VSConstants.LOGVIEWID_Code;if (ErrorHandler.Failed( openDoc.OpenDocumentViaProject(path, ref logicalView, out sp, out hier, out itemid, out frame)) || frame == null){ return;}object docData;frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocData, out docData);The file will be opened in a new editor or will receive focus in case it already has been opened earlier.Next, lets read a VsTextBuffer text buffer from this document we opened:// Get the VsTextBufferVsTextBuffer buffer = docData as VsTextBuffer;if (buffer == null){ IVsTextBufferProvider bufferProvider = docData as IVsTextBufferProvider; if (bufferProvider != null) { IVsTextLines lines; ErrorHandler.ThrowOnFailure(bufferProvider.GetTextBuffer( out lines)); buffer = lines as VsTextBuffer; Debug.Assert(buffer != null, "IVsTextLines does not implement IVsTextBuffer"); if (buffer == null) { return; } }}
  • The IVsTextManager interface controls all of the active text buffers in the environment. For example wecan navigate a text document using the NavigateToLineAndColumn method of this manager on a bufferweve acquired earlier:IVsTextManager mgr =Package.GetGlobalService(typeof(VsTextManagerClass)) as IVsTextManager;mgr.NavigateToLineAndColumn(buffer, ref logicalView, line, column, line, column);Subscribing and handling eventsAutomation objects events are represented by the DTE.Events property. This element references all ofthe common IDE events (such as CommandEvents, SolutionEvents), as well as the events of separateenvironment components (project types, editors, tools etc.), also including the ones designed by third-party developers. To acquire a reference for this automation object, the GetObject method could beutilized.When subscribing to the DTE events one should remember that this interface could be still unavailableat the moment of extension being initialized. So it is always important to consider the sequence of yourextension initialization process if the access to DTE.Events is required in the Initialize() method of yourextension package. The correct handling of initialization sequence will vary for different extension types,as it was described earlier.Lets acquire a reference for an events object of Visual C++ project model defined by theVCProjectEngineEvents interface and assign a handler for the removal of an element from the SolutionExplorer tree:VCProjectEngineEvents m_ProjectItemsEvents = PVSStudio.DTE.Events.GetObject("VCProjectEngineEventsObject") as VCProjectEngineEvents;m_ProjectItemsEvents.ItemRemoved += new _dispVCProjectEngineEvents_ItemRemovedEventHandler( m_ProjectItemsEvents_ItemRemoved);MDI windows eventsThe Events.WindowEvents property could be utilized to handle regular events of an environment MDIwindow. This interface permits the assignment of a separate handler for a single window (definedthrough the EnvDTE.Window interface) or the assignment of a common handler for all of theenvironments windows. Following example contains the assignment of a handler for the event ofswitching between IDE windows:WindowEvents WE = PVSStudio.DTE.Events.WindowEvents;WE.WindowActivated += new _dispWindowEvents_WindowActivatedEventHandler( Package.WE_WindowActivated);Next example is the assignment of a handler for window switching to the currently active MDI windowthrough WindowEvents indexer:
  • WindowEvents WE =m_dte.Events.WindowEvents[MyPackage.DTE.ActiveWindow];WE.WindowActivated += new _dispWindowEvents_WindowActivatedEventHandler( MyPackage.WE_WindowActivated);IDE commands eventsThe actual handling of environments commands and their extension through the automation model iscovered in a separate article of this series. In this section we will examine the handling of the eventsrelated to these commands (and not of the execution of the commands themselves). Assigning thehandlers to these events is possible through the Events.CommandEvents interface. The CommandEventsproperty, as in the case of MDI windows events, also permits the assignment of a handler either for allof the commands or for a single one through the indexer.Lets examine the assignment of a handler for the event of a command execution being complete (i.e.when the command finishes its execution):CommandEvents CEvents = DTE.Events.CommandEvents;CEvents.AfterExecute += new _dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);But in order to assign such a handler for an individual command, it is necessary to identify this commandin the first place. Each command of the environment is identified by a pair of GUID:ID, and in case of auser-created commands these values are specified directly by the developer during their integration, forexample through the VSCT table. Visual Studio possesses a special debug mode which allows identifyingany of the environments comamnds. To activate this mode, it is required that the following key is to beadded to the system registry (an example for Visual Studio 2010):[HKEY_CURRENT_USERSoftwareMicrosoftVisualStudio10.0General]"EnableVSIPLogging"=dword:00000001Now, after restarting the IDE, hovering your mouse over menu or toolbar elements with CTRL+SHIFTbeing simultaneously pressed (though sometime it will not work until you left-click it) will display adialog window containing all of the commands internal identifiers. We are interested in the values ofGuid and CmdID. Lets examine the handling of events for the File.NewFile command:CommandEvents CEvents = DTE.Events.CommandEvents[ "{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 221];CEvents.AfterExecute += new _dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);The handler obtained in this way will receive control only after the command execution is finished.void C_AfterExecute(string Guid, int ID, object CustomIn, object CustomOut){ ...}
  • This handler should not be confused with an immediate handler for the execution of the command itselfwhich could be assigned during this commands initialization (from an extension package and in case thecommand is user-created). Handling the IDE commands is described in a separate article that is entirelydevoted to IDE commands.In conclusion to this section it should be mentioned that in the process of developing our ownVSPackage extension, weve encountered the necessity to store the references to interface objectscontaining our handler delegates (such as CommandEvents, WindowEvents etc.) on the top-level fieldsof our main Package subclass. The reason for this is that in case of the handler being assigned through afunction-level local variable, it is lost immediately after leaving the method. Such behavior couldprobably be attributed to the .NET garbage collector, although weve obtained these references fromthe DTE interface which definitely exists during the entire lifetime of our extension package.Interacting with DTE2 COM interfaces from within a multithreadedapplicationInitially PVS-Studio extension package had not contained any specific thread-safety mechanisms for itsinteraction with Visual Studio APIs. At the same time, we had been attempting to confine theinteractions with this APIs within a single background thread which was created and owned by our plug-in. And such approach functioned flawlessly for quite a long period. However, several bug reports fromour users, each one containing a similar ComExeption error, prompted us to examine this issue in moredetail and to implement a threading safety mechanism for our COM Interop.Although Visual Studio automation model is not a thread-safe one, it still provides a way for interactingwith multi-threaded applications. Visual Studio application is a COM (Component Object Mode) server.For the task of handling calls from COM clients (in our case, this will be our extension package) tothread-unsafe servers, COM provides a mechanism known as STA (single-threaded apartment) model. Inthe terms of COM, an Apartment represents a logical container inside a process in which objects andthreads share the same thread access rules. STA can hold only a single thread, but an unlimited numberof objects, inside such container. Calls from other threads to such thread-unsafe objects inside STA areconverted into messages and posted to a message queue. Messages are retrieved from the messagequeue and converted back into method calls one at a time by the thread running in the STA, so itbecomes possible for only a single thread to access these unsafe objects on the server.Utilizing Apartment mechanism inside managed codeThe .NET Framework does not utilize COM Apartment mechanics directly. Therefore, when a managedapplication calls a COM object in the COM interoperation scenarios, CLR (Common Language Runtime)creates and initializes apartment container. A managed thread is able to create and enter either an MTA(multi-threaded apartment, a container that, contrary to STA, can host several threads at the sametime), or an STA, though a thread will be started as an MTA by default. The type of the apartment couldbe specified before thread is launched:Thread t = new Thread(ThreadProc);t.SetApartmentState(ApartmentState.STA);...t.Start();As an apartment type could not be changed once thread had been started, the STAThread attributeshould be used to specify the main thread of a managed application as an STA:
  • [STAThread]static void Main(string[] args){...}Implementing message filter for COM interoperation errors in a managed environmentAs STA serializes all of calls to the COM server, one of the calling clients could potentially be blocked oreven rejected when the server is busy, processing different calls or another thread is already inside theapartment container. In case COM server rejects its client, .NET COM interop will generate aSystem.Runtime.InteropServices.COMException ("The message filter indicated that the application isbusy").When working on a Visual Studio module (add-in, vspackage) or a macro, the execution control usuallypasses into the module from the environments main STA UI thread (such as in case of handling eventsor environment state changes, etc.). Calling automation COM interfaces from this main IDE thread issafe. But if other background threads are planned to be utilized and EnvDTE COM interfaces are to becalled from these background threads (as in case of long calculations that could potentially hang theIDEs interface, if these are performed on the main UI thread), then it is advised to implement amechanism for handling calls rejected by a server.While working on PVS-Studio plug-in weve often encountered these kinds of COM exceptions insituations when other third-party extensions were active inside the IDE simultaneously with PVS-Studioplug-in. Heavy user interaction with the UI also was the usual cause for such issues. It is quite logical thatthese situations often resulted in simultaneous parallel calls to COM objects inside STA andconsequently to the rejection of some of them.To selectively handle incoming and outgoing calls, COM provides the IMessageFilter interface. If it isimplemented by the server, all of the calls are passed to the HandleIncomingCall method, and the clientis informed on the rejected calls through the RetryRejectedCall method. This in turn allows the rejectedcalls to be repeated, or at least to correctly present this rejection to a user (for example, by displaying adialog with a server is busy message). Following is the example of implementing the rejected callhandling for a managed application.[ComImport()][Guid("00000016-0000-0000-C000-000000000046")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]public interface IMessageFilter{ [PreserveSig] int HandleInComingCall( int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo); [PreserveSig] int RetryRejectedCall( IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
  • [PreserveSig] int MessagePending( IntPtr hTaskCallee, int dwTickCount, int dwPendingType);}class MessageFilter : MarshalByRefObject, IDisposable, IMessageFilter{ [DllImport("ole32.dll")] [PreserveSig] private static extern int CoRegisterMessageFilter( IMessageFilter lpMessageFilter, out IMessageFilter lplpMessageFilter); private IMessageFilter oldFilter; private const int SERVERCALL_ISHANDLED = 0; private const int PENDINGMSG_WAITNOPROCESS = 2; private const int SERVERCALL_RETRYLATER = 2; public MessageFilter() { //Starting IMessageFilter for COM objects int hr = MessageFilter.CoRegisterMessageFilter( (IMessageFilter)this, out this.oldFilter); System.Diagnostics.Debug.Assert(hr >= 0, "Registering COM IMessageFilter failed!"); } public void Dispose() { //disabling IMessageFilter IMessageFilter dummy; int hr = MessageFilter.CoRegisterMessageFilter(this.oldFilter, out dummy); System.Diagnostics.Debug.Assert(hr >= 0, "De-Registering COM IMessageFilter failed!") System.GC.SuppressFinalize(this); } int IMessageFilter.HandleInComingCall(int dwCallType, IntPtr threadIdCaller, int dwTickCount, IntPtr lpInterfaceInfo) { // Return the ole default (dont let the call through). return MessageFilter.SERVERCALL_ISHANDLED; }
  • int IMessageFilter.RetryRejectedCall(IntPtr threadIDCallee, int dwTickCount, int dwRejectType) { if (dwRejectType == MessageFilter.SERVERCALL_RETRYLATER) { // Retry the thread call immediately if return >=0 & // <100. return 150; //waiting 150 mseconds until retry } // Too busy; cancel call. SERVERCALL_REJECTED return -1; //Call was rejected by callee. //(Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED)) } int IMessageFilter.MessagePending( IntPtr threadIDCallee, int dwTickCount, int dwPendingType) { // Perform default processing. return MessageFilter.PENDINGMSG_WAITNOPROCESS; }}Now we can utilize our MessageFilter while calling COM interfaces from a background thread:using (new MessageFilter()){ //COM-interface dependent code ...}References 1. MSDN. Referencing Automation Assemblies and the DTE2 Object. 2. MSDN. Functional Automation Groups. 3. MZ-Tools. HOWTO: Use correctly the OnConnection method of a Visual Studio add-in. 4. The Code Project. Understanding The COM Single-Threaded Apartment. 5. MZ-Tools. HOWTO: Add an event handler from a Visual Studio add-in. 6. Dr. eXs Blog. Using EnableVSIPLogging to identify menus and commands with VS 2005 + SP1.Visual Studio commandsAbstractThis article deals with creation, utilization and handling of Visual Studio commands in its extensionmodules through automation object model APIs and IDE services. The relations between IDE commandsand environment UI elements, such as user menus and toolbars, will also be examined.
  • IntroductionVisual Studio commands provide a way for direct interaction with development environment throughthe keyboard input. Almost all capabilities of different dialog and tool windows, toolbars and usermenus are represented by the environments commands. In fact, main menu items and toolbar buttonsare practically commands themselves. Although it is possible for a command not to possess a directrepresentation in the development environments UI, as commands are not the UI elements per se, theycan be represented by such UI elements as menu items and toolbar buttons.PVS-Studio IDE extension package integrates several subgroups of its commands into Visual Studio mainmenu, and these commands serve as one of the plug-ins main UI components (with another one beingits MDI toolwindow), allowing a user to control all of the aspects of static code analysis either from theenvironments UI or by invoking the commands directly through command line.Using IDE commandsAny IDE command, regardless of its UI representation in the IDE (or of the lack of it), could be executeddirectly through the Command or Immediate windows, as well as by starting devenv.exe with the/command argument.The full name of a command is formed according to its affiliation with a functional group, as for examplethe commands of the File main menu item. Commands full name could be examined in the Keyboard,Environment Options page. Also, the Tools -> Customize -> Commands dialog allows inspecting all ofthe commands which are currently registered within the environment. This dialog sorts the commandsby their respective functional groups and UI presentation types (menus, toolbars), also allowing tomodify, add or delete them.Commands can receive additional arguments which should be separated from the commands name bya space. Lets examine a call to a standard system command of the main menu, File -> New -> File forexample, with a passing of additional parameters to it through the Command Window:>File.NewFile Mytext /t:"GeneralText File" /e:"Source Code (text) Editor"A commands syntax generally complies with the following rules:  commands name and arguments are separated by a space  arguments containing spaces are wrapped by double quotes  The caret (^) is used as an escape character  One-character abridgments for command names can be combined, as for example, /case(/c) and /word(/w) could be presented as /cwWhen using the command command-line switch, name of a command with all of its arguments shouldbe wrapped by double quotes:devenv.exe /command "MyGroup.MyCommandName arg1 arg2"For the sake of convenience, a command could be associated with an alias:>alias MyAlias File.NewFile MyFileCommands integrated into IDE by PVS-Studio extension can be utilized through the /command switch aswell. For example, this mode could be used for the integration of our static analysis into the automated
  • build process. Our analyzer itself (PVS-Studio.exe) is a native command-line application, which operatesquite similar to the compiler, i.e. it takes a path to the file containing source code and its compilationarguments and then it outputs analysis results to stdout/stderr streams. Its quite obvious that theanalyzer could easily be integrated directly into the build system (for instance, into a system which isbased on MSBuild, NMake or even GNU Make) at the same level where C/C++ compiler is being called.Of course, such integration already provides us, by its own definition, with complete enumeration of allof the source files being built, with all of their compilation parameters. In turn, this allows for asubstitution (or supplementation) of a compiler call by call to the analyzer. Although the describedscenario is fully supported by PVS-Studio.exe analyzer, it still requires a complete understanding of buildsystems internals as well as an opportunity to modify a system in the first place, which could beproblematic or even impossible at times.Therefore, the integration of the analyzer into the build process can be performed in a more convenientway, on a higher level (i.e. at the level of Continuous Integration Server), by utilizing Visual Studioextension commands through the /command switch, for example, by using the PVS-Studio.CheckSolution command to perform analysis on MSVS solution. Of course, such use case is onlypossible when building Visual C++ native project types (vcproj/vcxproj).In case Visual Studio is started form a command line, the /command switch will be executedimmediately after the environment is fully loaded. In this case, the IDE will be started as a regular GUIapplication, without redirecting its standard I/O streams to the console that was used to launch theenvironment. It should be noted that, in general, Visual Studio is a UI based development environmentand so it is not intended for command line operations. It is recommended to employ Microsoft MSBuildutility for building inside build automation systems, as this tool supports all of native Visual Studioproject types.Caution should be applied when using Visual Studio /command switch together with non-interactivedesktop mode (for example when calling IDE from a Windows service). Weve encountered severalinteresting issues ourselves when we were evaluating the possibility of integrating PVS-Studio staticanalysis into Microsoft Team Foundation build process, as Team Foundation operates as a Windowsservice by default. At that moment, our plug-in had not been tested for non-interactive desktop sessionsand was incorrectly handling its child windows and dialogs, which in turn lead to exceptions and crashes.But Visual Studio itself experienced none of such issues, almost none to be more precise. The case is,Visual Studio displays a particular dialog for every user when it is started for a first time after aninstallation, and this dialog offers the user to select a default UI configuration. And it was this dialog thatVisual Studio displayed for a LocalSystem account, the account which actually owns the TeamFoundation service. It turns out that the same dialog is displayed even in the non-interactive desktopmode, and it subsequently blocks the execution of the /command switch. As this user doesnt have aninteractive desktop, he is also unable to close this dialog normally by manually starting the IDE himself.But, in the end, we were able to close the dialog manually by launching Visual Studio for LocalSystemaccount in the interactive mode through psexec tool from PSTools utilities.Creating and handling commands in VSPackage. Vsct files.VSPackage extension utilizes Visual Studio command table (*.vsct) file for creating and managingcommands that it integrates into the IDE. Command tables are text files in XML format which can becompiled by VSCT compiler into binary CTO files (command table output). CTO files are then included asa resources into final builds of IDE extension packages. With the help of VSCT, commands can beassociated with menus or toolbar buttons. Support for VSCT is available starting from Visual Studio
  • 2005. Earlier IDE versions utilized CTC (command table compiler) files handling their commands, butthey will not be covered in this article.In a VSCT file each command is assigned a unique ID — CommandID, a name, a group and a quick accesshotkey combination, while its representation in the interface (if any) is specified by special flags.Lets examine a basic structure of VSCT file. The root element of file is CommandTable node thatcontains the Commands sub-node, which defines all of the users commands, groups, menu items,toolbars etc. Value of the "Package" attribute of the "Commands" node must correspond with the ID ofyour extension. The "Symbols" sub-node should contain definitions for all identifiers used throughoutthis VSCT file. The KeyBindings sub-node contains default quick access hotkey combinations for thecommands.<CommandTable"http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"> <Extern href="stdidcmd.h"/> <Extern href="vsshlids.h"/> <Commands> <Groups> ... </Groups> <Bitmaps> ... </Bitmaps> </Commands> <Commands package="guidMyPackage"> <Menus> ... </Menus> <Buttons> ... </Buttons> </Commands> <KeyBindings> <KeyBinding guid="guidMyPackage" id="cmdidMyCommand1" editor="guidVSStd97" key1="221" mod1="Alt" /> </KeyBindings> <Symbols> <GuidSymbol name="guidMyPackage" value="{B837A59E-5BF0-4190-B8FC-FDC35BE5C342}" /> <GuidSymbol name="guidMyPackageCmdSet" value="{CC8B1E36-FE6B-48C1-B9A9-2CC0EAB4E71F}"> <IDSymbol name="cmdidMyCommand1" value="0x0101" /> </GuidSymbol> </Symbols></CommandTable>
  • The Buttons node defines the commands themselves by specifying their UI representation style andbinding them to various command groups.<Button guid="guidMyPackageCmdSet" id="cmdidMyCommand1"priority="0x0102" type="Button"> <Parent guid="guidMyPackageCmdSet" id="MyTopLevelMenuGroup" /> <Icon guid="guidMyPackageCmdSet" id="bmpMyCommand1" /> <CommandFlag>Pict</CommandFlag> <CommandFlag>TextOnly</CommandFlag> <CommandFlag>IconAndText</CommandFlag> <CommandFlag>DefaultDisabled</CommandFlag> <Strings> <ButtonText>My &amp;Command 1</ButtonText> </Strings></Button>The Menus node defines the structure of UI elements (such as menus and toolbars), also binding themto command groups in the Groups node. A group of commands bound with a Menu element will bedisplayed by the UI as a menu or a toolbar.<Menu guid=" guidMyPackageCmdSet" id="SubMenu1" priority="0x0000"type="Menu"> <Parent guid="guidMyPackageCmdSet" id="MyTopLevelMenuGroup"/> <Strings> <ButtonText>Sub Menu 1</ButtonText> </Strings></Menu><Menu guid=" guidMyPackageCmdSet" id="MyToolBar1" priority="0x0010"type="Toolbar"></Menu>And finally, the Groups element organizes users IDE command groups.<Group guid="guidMyPackageCmdSet" id="MySubGroup1" priority="0x0020"> <Parent guid="guidMyPackageCmdSet" id="MyGroup1" /></Group>To include vsct file into MSBuild-based VSPackage project, it is necessary to insert the following nodeused for calling VSCT compiler into your csproj project file (note, that in the auto-generated projectcreated from an SDK template, a vsct file will be already included in a project):<ItemGroup> <VSCTCompile Include="TopLevelMenu.vsct"> <ResourceName>Menus.ctmenu</ResourceName> </VSCTCompile></ItemGroup>Next, the ProvideMenuResource attribute of your Package-derived class should point to this node thatyouve inserted into your project earlier:
  • [ProvideMenuResource("Menus.ctmenu", 1)]...public sealed class MyPackage : PackageAssigning handlers to the commands defined in a VSCT file is possible through a service that is availablethrough the IMenuCommandService. A reference for it can be obtained by the GetService method ofyour Package subclass:OleMenuCommandService MCS = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;Lets examine an example in which we assign a handler to a menu command (this command should bedeclared in a vsct file beforehand):EventHandler eh = new EventHandler(CMDHandler);CommandID menuCommandID = new CommandID(guidCommand1CmdSet, id);//ID and GUID should be the same as in the VCST fileOleMenuCommand menuItem = new OleMenuCommand(eh, menuCommandID);menuItem.ParametersDescription = "$";MCS.AddCommand(menuItem);To obtain commands arguments while handling its invocation, the EventArgs object should be castedinto OleMenuCmdEventArgs:void CMDHandler(object sender, EventArgs e){ OleMenuCmdEventArgs eventArgs = (OleMenuCmdEventArgs)e; if (eventArgs.InValue != null) param = eventArgs.InValue.ToString(); ...}Handling commands through EnvDTE.DTE interfaces.The EnvDTE.DTE automation object allows for a direct manipulation (creation, modification andexecution) of commands through the dte.Commands interface and dte.ExecuteCommand method.Utilizing the Automation Object Model for invoking, modifying and creating IDE commands, as opposedto using VSCT mechanism available only for VSPackage, allows the interaction with IDE commands fromwithin Add-In extension packages as well.The DTE automation object allows a direct creation, modification and invocation of commands throughthe DTE.Commands interface. A command can be directly added to the IDE byCommands.AddNamedCommand method (but only for an Add-In extension):dte.Commands.AddNamedCommand(add_in, "MyCommand", "My Command", "My Tooltip", true);The command added in this way will be preserved by the IDE — it will reappear in the menu after IDErestart, even if the extension which created the command is not loaded itself. Thats why this methodshould only be utilized during the first initialization of an Add-In module, after its installation (this isdescribed in the article dedicated to Visual Studio Automation Object Model). The OnConnection
  • method of an Add-In contains a special initialization mode which is invoked only for a single time in themodules entire lifetime. This method can be used to integrate UI elements into the IDE:public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom){ switch(connectMode) { case ext_ConnectMode.ext_cm_UISetup: ... break; ... }}The EnvDTE.Command interface represents a single IDE command. This interface can be used to modifya command which it references. It permits managing IDE commands from either a VSPackage, or anAdd-In module. Lets obtain a reference to the EnvDTE.Command object for our custom commandMyCommand1 and utilize this interface to assign a hot-key to it for a quick access:EnvDTE.Command MyCommand1 =MyPackage.DTE.Commands.Item("MyGroup.MyCommand1", -1);MyCommand1.Bindings = new object[1] { "Global::Alt+1" };The quick-access combination assigned to MyGroup.MyCommand1 will now be available throughKeyboard, Environment environment settings dialog.As was mentioned before, Visual Studio command is not a UI element by itself. TheCommands.AddCommandBar method allows the creation of such UI elements, as main menu items,toolbars, context menus and the association of these elements with user-created commands.CommandBar MyToolbar = dte.Commands.AddCommandBar("MyToolbar1", vsCommandBarType.vsCommandBarTypeToolbar) as CommandBar;CommandBar MyMenu = dte.Commands.AddCommandBar("MyMenu1", vsCommandBarType.vsCommandBarTypeMenu) as CommandBar;CommandBarButton MyButton1 = MyCommand1.AddControl(MyToolbar) as CommandBarButton;MyButton1.Caption = "My Command 1";The Delete method of Command/ CommandBar objects could be utilized to remove a command ortoolbar from IDE.MyCommand1.Delete();In general, it is not recommended creating commands each time an Add-In plug-in is loaded andremoving them each time it is un-loaded, as such behavior could slow-down the initialization of IDEitself. Even more, in case the OnDisconnect method is somehow interrupted in the process, it is possiblethat the user commands will not be completely deleted from the IDE. That is why it is advised that theintegration, and subsequent removal, of IDE commands should be handled at the times of modules
  • installation/uninstallation, as for example, by obtaining DTE interface reference from a stand-aloneinstaller application. The initialization of Add-In modules and acquisition of DTE references is thoroughlydescribed in the article devoted to EnvDTE Automation Object Model.Any IDE command (either custom or default one) could be called by the ExecuteComand method. Hereis the example of invoking our custom MyCommand1 command:MyPackage.DTE.ExecuteCommand("MyGroup.MyCommand1", args);To handle command execution, an Add-In extension should be derived from the IDTCommandTargetinterface and it should also implement the Exec method:public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled){ handled = false; if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault) { if(commandName == "MyAddin1.Connect.MyCommand1") { ... handled = true; return; } }}References 1. MSDN. Visual Studio Commands and Switches. 2. MSDN. Visual Studio Command Table (.Vsct) Files. 3. MSDN. Designing XML Command Table (.Vsct) Files. 4. MSDN. Walkthrough: Adding a Toolbar to the IDE. 5. MSDN. How VSPackages Add User Interface Elements to the IDE. 6. MZ-Tools. HOWTO: Adding buttons, commandbars and toolbars to Visual Studio .NET from an add-in. 7. MSDN. How to: Create Toolbars for Tool Windows.Visual Studio tool windowsAbstractThis article covers the extension of Visual Studio IDE through integration of a custom user toolwindowinto the environment. Discussed are the issues of window registration and initialization in VSPackageand Add-In plug-in modules, hosting of user components and handling of windows events and states.IntroductionTool windows are child windows of Visual Studio MDI (Multiple Document Interface) interface and theyare responsible for presenting various pieces of information to the user. Solution Explorer and Error List
  • are the examples of tool windows. Usually tool windows contents are not associated with any files anddo not contain any editors, as separate document windows are reserved for such tasks.For instance, PVS-Studio extension package integrates several tool windows into the IDE, with OutputWindow being the primary one. All other of its tool windows can be opened from this main window, as,for example, a search window for the grid. PVS-Studio Output Window itself can be opened from VisualStudio main menu (PVS-Studio -> Show PVS-Studio Output Window), but it also will be invokedautomatically each time the analysis starts.In most cases IDE creates and utilizes just a single instance for each one of its toolwindows, and thisinstance will be preserved until IDE itself needs to shut down. Therefore, pressing the close button on atool window does actually hide it, and when this window is invoked for the second time, it becomesvisible again, thus preserving any data that it contained before being closed. But still, is it possible tocrate Multi-Instance toolwindows in the IDE, which are the windows that can exist in several instancesat once. A toolwindow can also be associated with a certain UI context (as the so called dynamicwindow), and such window will be automatically displayed when the user enters this context.Integration of a tool window into the IDE is supported by VSPackage and Add-In extensions (althoughthe methods for it are different); it requires the specification of the windows initial settings and itsregistration in the system registry.Registering and initializing user tool windowsA VSPackage project template that is installed together with Visual Studio SDK allows you to create asample tool window in the extension project which this template generates. Such a project shouldalready contain all of the basic components which will be described below, so it could be convenientlyused as a sample for experimenting with Visual Studio toolwindow integration process for VSPackageplug-ins.Registering, initializing and invoking a tool window in VSPackageRegistering a custom user window in the environment requires writing of the data that defines thiswindow into a special section of Visual Studio registry hive. This process can be automated bygenerating a pkgdef file that can contain all of the required window registration information. Thecontents of this pkgdef files can be specified through special registration attributes of your Packagesubclass.The immediate registration of a user-created tool window into VSPackage extension is handled byProvideToolWindow attribute of Package subclass: [ProvideToolWindow(typeof(MyWindowPane), Orientation =ToolWindowOrientation.Right, Style = VsDockStyle.Tabbed, Window =Microsoft.VisualStudio.Shell.Interop.ToolWindowGuids.Outputwindow,MultiInstances = false, Transient = true, Width = 500, Height = 250,PositionX = 300, PositionY = 300)]Lets examine several parameters of this attribute. The Typeof parameter points to userimplementation of the windows client area (a subclass of ToolWindowPane). The MultiInstancesparameter enables the Multi-Instance mode for a window, in which multiple instances of the windowcan be opened simultaneously. The Orientation, Size and Style parameters specify the initial position ofa window when it is opened for the first time by the user. It should be noted that the position specifiedby these parameters will only be used once, when a tool window is displayed for the first time; at all of
  • the subsequent iterations of opening this window, the IDE will be restoring its screen position from theprevious one, that is the position before a window was closed. The Transient parameter indicateswhether the window will be automatically opened after Visual Studio environment is loaded in case italready have been opened during the previous session of the IDE.It should also be remembered that the initialization of a user window by VSPackage (the initializationitself will be covered later) does not necessarily occur at the same moment as the initialization of aPackage subclass for which we provided this registration attribute. For example, after implementing atool window for PVS-Studio plug-in, weve encountered an issue in which our custom window wasautomatically opened (but not focused/displayed) and placed among other window tabs at the bottomof the main window, and it was done immediately after Visual Studio started up, even though wevepassed the Transient=true parameter to the ProvideToolWindow attribute. Although the plug-in itself isalways initialized at IDE start-up, the window had not been fully initialized until after a first call to it,which was evident by the corrupted icon on aforementioned tab.A dynamic visibility context can be specified for a window by the ProvideToolWindowVisibility attribute: [ProvideToolWindowVisibility(typeof(MyWindowPane),/*UICONTEXT_SolutionExists*/"f1536ef8-92ec-443c-9ed7-fdadf150da82")]In this example, the window is set to be automatically displayed when the user enters the "SolutionExists" UI context. Take a note that each one of users toolwindow requires a separate attribute and awindows type should be passed as a first argument to it.The FindToolWindow method of a Package subclass can be utilized to create and display a toolwindowfrom a VSPackage extension. This method returns a reference to the specified toolwindow object,creating it if necessary (for instance, in case a single-instance window is called for a first time). Followingis the example of invoking a single-instance toolwindow:private void ShowMyWindow(object sender, EventArgs e){ ToolWindowPane MyWindow = this.FindToolWindow(typeof(MyToolWindow), 0, true); if ((null == MyWindow) || (null == MyWindow.Frame)) { throw new NotSupportedException(Resources.CanNotCreateWindow); } IVsWindowFrame windowFrame = (IVsWindowFrame) MyWindow.Frame; ErrorHandler.ThrowOnFailure(windowFrame.Show());}In this example, the window will be created in case it is called for the first time, or the window will bemade visible in case it had been created before and then hidden. The FindToolWindow s third argumentof the bool type specifies whether a new instance of a window should be created if the method wasunable to find an already existing one.To create a Multi-Instance tool window, the CreateToolWindow method can be used. It allows thecreation of a window with a pre-defined identifier. An example of invoking such window:private void CreateMyWindow(object sender, EventArgs e){ for (int i = 0; ; i++)
  • { // Find existing windows. var currentWindow = this.FindToolWindow(typeof(MyToolWindow), i, false); if (currentWindow == null) { // Create the window with the first free ID. var window = (ToolWindowPane)this.CreateToolWindow(typeof(MyToolWindow), i); if ((null == window) || (null == window.Frame)) { throw new NotSupportedException(Resources.CanNotCreateWindow); } IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame; ErrorHandler.ThrowOnFailure(windowFrame.Show()); break; } }}Note that in this example the FindToolWindow method receives false value as its third argument, i.e.we are searching for an unoccupied index before initializing a new window instance.As was mentioned above, the environment will preserve position of a window after it is closed. But if,for whatever reason, it is necessary to specify the size and position of a window, it could be achievedthrough the SetFramePos method of the IVsWindowFrame interface:Guid gd = Guid.Empty;windowFrame.SetFramePos(VSSETFRAMEPOS.SFP_fDockBottom, ref gd, 20, 20, 200, 200);A call to the SetFramePos() should always be made only after the Show() method is executed.Creating and invoking a window from Add-In extensionA user tool window can be initialized from an Add-In extension with the help of the EnvDTE Window2interface:public void OnConnection(object application,ext_ConnectMode connectMode, object addInInst, ref Array custom){ _applicationObject = (DTE2)application; _addInInstance = (AddIn)addInInst; EnvDTE80.Windows2 window; AddIn add_in; object ctlobj = null; Window myWindow; // Get the window object
  • add_in = _applicationObject.AddIns.Item(1); window = (Windows2)_applicationObject.Windows; // This section specifies the path and class name of the windows // control that you want to host in the new tool window, as well as // its caption and a unique GUID. string assemblypath = "C:MyToolwindowMyToolWindowControl.dll"; string classname = " MyToolWindowControl.MyUserControl"; string guidpos = "{E87F0FC8-5330-442C-AF56-4F42B5F1AD11}"; string caption = "My Window"; // Creates the new tool window and inserts the user control into it. myWindow = window.CreateToolWindow2(add_in, assemblypath, classname, caption, guidpos, ref ctlobj); myWindow.Visible = true;}In the example above, a user toolwindow was created using the MyToolWindowControl.MyUserControlas a client area control. The MyToolWindowControl.MyUserControl class could either be located in thesame assembly as the add-in that initializes it, or it could be provided by a stand-alone assembly with afull COM visibility (though the Register for COM Interop option in project settings). The regularcomposite UserControl subclass could be utilized as MyUserControl.Implementing a user toolwindow in a VSPackage moduleTool window consists of a frame border and a client area. A frame is provided by the environment and isresponsible for performing docking with other interface objects of the environment, as well as for sizeand position of the window itself. A client area is a pane, controlled by a user, which houses thecontents of a window. Tool windows can host user-created WinForms and WPF components and arecapable of handling regular events, such as OnShow , OnMove, etc.A user toolwindow, or its client area to be more precise, can be implemented by inheriting the classrepresenting a standard empty IDE window — ToolWindowPane. [Guid("870ab1d8-b434-4e86-a479-e49b3c6797f0")]public class MyToolWindow : ToolWindowPane{ public MyToolWindow():base(null) { this.Caption = Resources.ToolWindowTitle; this.BitmapResourceID = 301; this.BitmapIndex = 1; ... }}The Guid attribute is used to uniquely identify each custom user window. In case a plug-in modulecreates several windows of different types, each one of them should be identified by its own uniqueGuid. A ToolWIndowPane subclass can be subsequently modified and host user-controlled components.
  • Hosting user componentsA base ToolWindowPane class implements an empty tool window of the environment. Inheriting formthis class allows hosting user-created WinForms or WPF components.Up until Visual Studio 2008 version, toolwindows only provided a native supported for WinForms usercomponents, although it still was possible to host WPF components through the WPF InteroperabilityElementHost object. Starting from Visual Studio 2010, toolwindows themselves are based on WPFtechnology, although they still provide a backward compatibility for hosting of WinForms components.To host a user-created WinForms component inside a user toolwindow, the Window property of theToolWindowPane base class should be overridden:public MyUserControl control;public MyToolWindow():base(null){ this.Caption = Resources.ToolWindowTitle; this.BitmapResourceID = 301; this.BitmapIndex = 1; this.control = new MyUserControl();}public override IWin32Window Window{ get { return (IWin32Window)control; }}In the example above, the MyUserControl object is a regular composite component of theSystem.Windows.Forms.UserControl type and it can host any other user component inside itself.UserControl can also host WPF components by using WPF ElementHost object.Starting from Visual Studio 2010, WPF components can be hosted by tool windows natively. To do this, areference to the WPF component should be passed to the Content property of a base class:public MyToolWindow():base(null){ this.Caption = Resources.ToolWindowTitle; this.BitmapResourceID = 301; this.BitmapIndex = 1; base.Content = new MyWPFUserControl();}Please note that using the two methods described above simultaneously is not possible. When areference to WPF component is assigned to the base.Content property, an overridden Window propertyis ignored.The main PVS-Studio Output window of our extension plug-in hosts a virtual grid based on SourceGridopen-source project. This window provides an interface for handling the results of static analysis. Thegrid itself is bound to a regular ADO.NET table of the System.Data.Datatable type, which is utilized forstoring analysis results. Until 4.00 version of PVS-Studio extension, it utilized a regular IDE Error List
  • window, but as the analyzer evolved, the capabilities of this default window became insufficient. Apartfrom being un-extendable with such specific static analysis UI elements as, for example, false positivesuppression and filtering mechanisms, the Error List is itself basically a real grid, as it stores all of thedisplayed elements inside itself. Therefore, this grid only permits an adequate handling of 1-2kmessages at a time, performance wise, as a greater number of messages already can cause quite anoticeable lag to the environments UI. On the other hand, our own practice of using static analysis onrelatively large projects, such as Chromium or LLVM, demonstrated that a total number of diagnosticmessages (taking into account all of the marked false alarms and low-lever user diagnostics as well)could easily reach tens of thousands or even more.Therefore, by implementing a custom output window, based on virtual grid that is connected to a DBtable, PVS-Studio is able to display and provide convenient handling for hundreds of thousands ofdiagnostic messages at once. Also, the ability for a convenient and flexible filtering of the analysis resultsis quite an important aspect of handling a static analyzer, as the manual examination even of only such a"tiny" amount of messages as 1-2k is nearly impossible for a single user. The storage of analysis resultsin a Datatable object by itself provides quite a convenient filtering mechanism based on a simple SQLqueries, even more so because the results of such queries become visible immediately inside the boundvirtual grid.Handling tool windows events A client area of a tool window (represented by our ToolWindowPane subclass) can process the regularevents of user-interface interactions. The IVsWindowFrameNotify3 interface can be used for subscribingto window events. Lets provide an example of implementing this interface:public sealed class WindowStatus: IVsWindowFrameNotify3{ // Private fields to keep track of the last known state private int x = 0; private int y = 0; private int width = 0; private int height = 0; private bool dockable = false; #region Public properties // Return the current horizontal position of the window public int X { get { return x; } } // Return the current vertical position of the window public int Y { get { return y; } } // Return the current width of the window public int Width {
  • get { return width; }}// Return the current height of the windowpublic int Height{ get { return height; }}// Is the window dockablepublic bool IsDockable{ get { return dockable; }}#endregionpublic WindowStatus(){}#region IVsWindowFrameNotify3 Members// This is called when the window is being closedpublic int OnClose(ref uint pgrfSaveOptions){ return Microsoft.VisualStudio.VSConstants.S_OK;}// This is called when a window "dock state" changes.public int OnDockableChange(int fDockable, int x, int y, int w,int h){ this.x = x; this.y = y; this.width = w; this.height = h; this.dockable = (fDockable != 0); return Microsoft.VisualStudio.VSConstants.S_OK;}// This is called when the window is movedpublic int OnMove(int x, int y, int w, int h){ this.x = x; this.y = y; this.width = w; this.height = h; return Microsoft.VisualStudio.VSConstants.S_OK;}
  • // This is called when the window is shown or hidden public int OnShow(int fShow) { return Microsoft.VisualStudio.VSConstants.S_OK; } /// This is called when the window is resized public int OnSize(int x, int y, int w, int h) { this.x = x; this.y = y; this.width = w; this.height = h; return Microsoft.VisualStudio.VSConstants.S_OK; } #endregion}As evident by this sample code above, the WindowsStatus class implementing the interface is able toprocess such window state changes, as the alterations in windows size, position, visibility propertiesand so on. Now, lets subscribe our window for handling these events. It requires theOnToolWindowCreated method to be overridden in our ToolWindowPane subclass:public class MyToolWindow: ToolWindowPane{ public override void OnToolWindowCreated() { base.OnToolWindowCreated(); // Register to the window events WindowStatus windowFrameEventsHandler = new WindowStatus();ErrorHandler.ThrowOnFailure(((IVsWindowFrame)this.Frame).SetProperty( (int)__VSFPROPID.VSFPROPID_ViewHelper, (IVsWindowFrameNotify3)windowFrameEventsHandler)); } ...}Controlling window stateA window state can be controlled through event handlers of our IVsWindowFrameNotify3implementation.The OnShow method notifies the extension package about changes in tool windows visibility state,allowing to track the appearance of the window to a user, when, for example, user switches windows by
  • clicking on window tabs. Current visibility state could be obtained by the fShow parameter, whichcorresponds to the __FRAMESHOW list.The OnClose method notifies about the closure of a window frame, allowing to define IDE behavior incase ofthis event with the pgrfSaveOptions parameter, which controls the default document savingdialog (__FRAMECLOSE).The OnDockableChange method informs the package on windows docking status changes. ThefDockable parameter indicates whether a window is docked to another one; other parameters controlwindows size and position before and after the docking event.The parameters of OnMove and OnSize methods provide windows coordinates and size while it isbeing dragged of resized.References 1. MSDN. Kinds of Windows. 2. MSDN. Tool Windows. 3. MSDN. Tool Window Essentials. 4. MSDN. Tool Window Walkthroughs. 5. MSDN. Arranging and Using Windows in Visual Studio. 6. MZ-Tools. HOWTO: Understanding toolwindow states in Visual Studio.Integrating into Visual Studio settingsAbstractThis article covers the extension of Visual Studio by integrating into its Settings dialog pages. Optionpage registration and integration into the IDE for different kinds of extension packages will be examined,as well as the means to display various standard and user-created components inside a custom settingspage. Also covered are the ways of accessing environment settings through Visual Studio Automationmodel and preservation mechanism for option pages.IntroductionVisual Studio employs a single unified dialog window to provide an access to the settings of its variouscomponents. This window is available through the IDE Tools -> Options main menu item. A basicelement of Visual Studio settings is an Options Page. The Options dialog window arranges its pages in atree-like structure according to the membership of the pages in their respective functional groups. Eachone of these pages could be uniquely identified by the name of its group and its own name. Forexample, Visual Basic source code editor settings page is "Text Editor, Basic".Extension packages are able to access and modify the values of various settings from option pagesregistered in the IDE. They can also create and register their own custom options pages in theenvironment through the automation object model and MPF classes (Managed Package Framework,available only to VSPackage extensions). Visual Studio contains an embedded mechanism for preservingthe state of its settings objects; it is enabled by default, but can be overridden or disabled.Creating and registering user options pagesIt can be useful for a Visual Studio extension plug-in to be associated with one or several custom optionspages from the Tools->Options dialog window. Such tool for configuring an extension will conform to
  • the environments UI paradigm and is actually quite convenient for handling your extensions settingsfrom within the IDE itself. The methods of implementing and integrating custom user options page intothe IDE can vary, as they depend upon the type of the extension being developed and the technologybeing used (either an automation model or MPF).Integrating settings through an MPF classManaged Package Framework allows creating custom options pages by inheriting from the DialogPageclass. As the environment loads each of its options pages independently when accessing thecorresponding section of the Tools->Options dialog, each page must be implemented with anindependent object as a result.The object which implements a custom page should be associated with your VSPackage through theProvideOptionPage attribute of the corresponding Package subclass. [ProvideOptionPageAttribute(typeof(OptionsPageRegistration),"MyPackage", "MyOptionsPage", 113, 114, true)]This attribute designates the names for the options page itself and for group that it belongs to, as itshould be displayed in the IDE options dialog. A separate attribute should be used for every custom pagethat is to be integrated by the extension. In fact, this attribute is used to provide a registration for thepage through pkgdef file and it does not directly affect the execution in any other way. For the useroptions page to be correctly rendered by the environment, the page should be registered in thefollowing node of the system registry:HKEY_LOCAL_MACHINESOFTWAREMicrosoftVisualStudio<VsVersion>ToolsOptionsPagesHere <VsVersion> is the version number of Visual Studio IDE, 10.0 for example. This record will beautomatically created when ProvideOptionPage attribute is utilized. It should be noted that a correctuninstallation of an extension package also requires purging all of the records that this extension hadwritten to the system registry before, including the ones belonging to its options pages. As the versionsof Visual Studio IDE starting from 2010 can utilize VSIX packages to deploy/uninstall VSPackage plug-ins,the VSIX installer will automatically perform such registry operations according to its pkgdef file. Butearlier versions of IDE may require manual registry cleaning, for instance by a stand-alone installerapplication.The 6th bool-type argument of the attributes constructor allows the users custom options page to beregistered as an automation object. This exposes the page to the Automation Object Model, providingan access to the its settings through the EnvDTE interfaces for other third-party plug-ins. Registering anautomation object requires the creation of several records in the system registry (it is performedautomatically when using the aforementioned attributes) in the following nodes:HKEY_LOCAL_MACHINESOFTWAREMicrosoftVisualStudio<VersionPackages<PackageGUID>AutomationHKEY_LOCAL_MACHINESOFTWAREMicrosoftVisualStudio<Version>AutomationPropertiesThe ProvideProfile attribute allows registering an options page or any other independent object with thebuild-in mechanism for settings state preservation, provided that such user object implements theIProfileManager interface.
  • Implementing an MPF DialogPage subclassAs a minimal requirement for DialogPage subclass to implement an IDE options page, this derived classshould contain at least one public property. Here is an example of such a basic implementation:namespace MyPackage{ class MyOptionsPage : DialogPage { bool myOption = true; public bool MyOption { get { return this. myOption; } set { this. myOption = value; } } }}To display such a generic implementation of DialogPage subclass, IDE will utilize a standard PropertyGridcontrol as a client area of the page window, which in turn will contain all public properties of thissubclass. This could be convenient in case your extensions configuration properties are rather simple,so handling them through embedded PropertyGrid editors does not present any troubles. Using acontrol native to IDE will also exempt you from common issues with incorrect UI scaling for componentson different DPI resolutions in the Visual Studio Options dialog.However, if you want to host a user-created control inside an options page, it could be achieved byoverriding the Window property of your DialogPage subclass:[BrowsableAttribute(false)]protected override IWin32Window Window{ get { return MyUserControl; }}A reference to the IWin32Window object implementing the windows client area should be returned bythis property. Visual Studio required its options pages to be constant, i.e. they should not be recreatedfor any of their subsequent calls. As Windows Forms objects can delete and recreate their windowhandles at will, it is recommend passing a reference to the object derived from a UserControl type.The AutomationObject property of a custom options page derived from the DialogPage class determinesthe public properties which are shown by the default display mechanism and persisted by the IDE.AutomationObject returns a reference to the DialogPage subclass itself by default, but if it returns areference to some other object, than the properties of that returned object will be preserved anddisplayed instead. By default, system registry serves as a local storage for state preservation mechanism.Overriding the DialogPage.SaveSettingsToStorage method makes it possible to change the way ofobjects state preservation method (similar could be done to the state restoration throughLoadSettingsFromStorage override).
  • public override void SaveSettingsToStorage() { ... }Custom pages registered as automation objects can store their settings, along with settings from otheroptions pages, in an external XML file through the standard IDE command Tools -> Import/ExportSettings, by the default implementation of the SaveSettingsToXml method which also can be overriddenif necessary.Of course, integrating a page into Visual Studio settings dialog is not the exclusive or mandatory way ofcreating configuration interface for an IDE plug-in. If the capabilities of a regular PropertyGrid areinsufficient and there are no future plans to utilize the embedded mechanism for settings preservation,then it could be quite reasonable to implement an IDE independent settings dialog. The advantages ofthis approach are high portability (for instance, a plug-in that could be used with multiple IDEs) andcomplete control over the dialog window itself, which in turn substantially alleviates the support ofvarious end-user configurations. On the downside, such solution makes your settings inaccessible tothird-party developers through the automation object model.For configuring its settings, PVS-Studio extension package utilizes a custom state preservationmechanism that operates through an external XML file, so that the options pages which the plug-inintegrates into the IDE are provided only as means for displaying and modifying these internal settings.Initially, the embedded settings preservation functionality of Visual Studio created conflicts with PVS-Studio own settings mechanism in the earlier versions of the plug-in, leading to setting de-synchronization issues. This demonstrated us that even in the presence of an independent settingsmanagement inside the extension, it still may be necessary to override some of Visual Studio regularmechanisms (maybe even by an empty method).Integrating settings through an Add-In xml definitionA user options page can be integrated into the IDE through an independent XML definition of an Add-Inextension. The contents of such user page should be implemented as a user component, for example asan System.Windows.Forms.UserControl. This component is not associated with an Add-In itself, thus itcan be implemented either inside the extensions assembly or as an independent library altogether. Anadd-in XML file could even be created for such user component alone, without any definitions for anAdd-In extension. Lets examine an xml definition for an Add-In module which also contains a definitionfor a users custom options page.<?xml version="1.0" encoding="UTF-16" standalone="no"?><Extensibilityxmlns="http://schemas.microsoft.com/AutomationExtensibility"> <HostApplication> <Name>Microsoft Visual Studio Macros</Name> <Version>10.0</Version> </HostApplication> <HostApplication> <Name>Microsoft Visual Studio</Name> <Version>10.0</Version> </HostApplication> <Addin> <FriendlyName>My Add in</FriendlyName> <Description>My Addin 1</Description> <Assembly>c:MyAddIn1MyAddin1.dll</Assembly> <FullClassName>MyAddin1.Connect</FullClassName>
  • <LoadBehavior>0</LoadBehavior> <CommandPreload>1</CommandPreload> <CommandLineSafe>0</CommandLineSafe> </Addin> <ToolsOptionsPage> <Category Name="MyAddIn1"> <SubCategory Name="My Tools Options Page"> <Assembly> c:MyAddIn1MyAddin1.dll</Assembly> <FullClassName>MyAddin1.UserControl1</FullClassName> </SubCategory> </Category> </ToolsOptionsPage></Extensibility>A description for custom options page is located inside the <ToolsOptionsPage> node. The <Assembly>sub-node points to the library which contains a user component for the client area of the page. The<FullClassName> contains the full name of a user component in a Namespace.ClassName format. The<Category> and <SubCategory> nodes define position of a user page inside the Tools->Options tree-likestructure by specifying pages group and personal names. Any existing group names, as well as a newone, can be used as a <Category> value. As evident by the example, a user MyAddin1.UserControl1component is located inside the same assembly as the add-in itself, though this is not a mandatoryrequirement.Visual Studio loads a page after it is opened by a user for the first time through the Options dialogwindow. As opposed to the integration of a page through the Managed Package Framework, thedescription of a page is stored within an xml description addin file, so the page will be initialized onlyafter the environment discovers such a file. Visual Studio reads addin files which are available to itimmediately after start-up. The Environment -> Add-In/Macross Security options page specifies thepaths which are used for addin discovery. Contrary to custom option pages implemented throughinheriting the MPF classes, such high level approach to the integration does not register such a page asan automation object, and so it does not provide ways to access the pages contents throughautomation object model or to utilize the embedded sate preservation mechanism of the environment.Accessing option pages through the automationVisual Studio Automation Object Model provides the means of accessing various system settings of theTools->Options dialog , excluding some of the pages, such as Dynamic Help and Fonts and Colors pages (they are available through separate APIs). User-created custom option pages are also availablethrough the automation model in case they are registered as automation objects themselves (asdescribed in the previous section).The get_Properties method can be utilized to obtain the necessary settings:Properties propertiesList = PVSStudio.DTE.get_Properties("MyPackage","MyOptionsPage");The option page can be identified by its own name and the name of group it belongs to. Here is theexample of obtaining value for a specific property:Property MyProp1 = propertiesList.Item("MyOption1");
  • The value of the property can be accessed and modified through the MyProp1.Value.The ShowOptionPage method of the Package MPF subclass can be used to open and display the customoptions page inside the Options window.MyPackage.ShowOptionPage(typeof(MyOptionsPage));As evident by the example, this method takes the type of a user-created DialogPage subclass. However,if it is required to open any other page which is not part or your extension project, a standard IDE pagefor example, then it could be located by its GUID identifier available at this registry branch:HKEY_LOCAL_MACHINESOFTWAREMicrosoftVisualStudio9.0ToolsOptionsPages<OptionsPageNme>Here <OptionsPageName> is the name of a page inside the Tools -> Options dialog. Following is theexample of opening a standard TextEditor -> General IDE settings page through theIMenuCommandService global service:string targetGUID = "734A5DE2-DEBA-11d0-A6D0-00C04FB67F6A";var command = new CommandID(VSConstants.GUID_VSStandardCommandSet97, VSConstants.cmdidToolsOptions);var mcs = GetService(typeof(IMenuCommandService)) as MenuCommandService;mcs.GlobalInvoke(command, targetGUID);In fact, this code is equivalent to the execution of the Tools.Options IDE command. It acn be invokedthrough the ExecuteCommand method of the EnvDTE.DTE:dte.ExecuteCommand("Tools.Options","734A5DE2-DEBA-11d0-A6D0-00C04FB67F6A").References 1. MSDN. Options Pages. 2. MSDN. State Persistence and the Visual Studio IDE. 3. MSDN. User Settings and Options. 4. MSDN. Registering Custom Options Pages. 5. MSDN. Providing Automation for VSPackages.Visual C++ project modelAbstractThis article covers the structure of Visual C++ project model (VCProject). Also included are the cases ofusing the project model for enumeration of project elements and obtaining their compilation propertiesthrough the corresponding configurations.IntroductionVisual C++ project model is a collection of interfaces describing the properties of a compiler, linker andother build tools, as well as the structure of MSVS-compatible projects themselves, and it is connectedwith the Visual Studio Automation Object Model through the VCProjects late-bound properties. Visual
  • C++ project model extends the standard Visual Studio project model, providing access to the specificfunctionality of Visual C++ (vcproj/vcxproj) project types. Visual C++ project model is a stand-alone COMcomponent available through the VCProjectEngine.dll assembly, which could also be used independentlyoutside of Visual Studio development environment.VCProject model structureVisual Studio provides an extendable project-neutral object model that represents solutions, projects,code objects, documents, etc. Every MSVS project type has a corresponding project automationinterface. Every tool in the environment that has a project also has an object of the Project typeassociated with it. Visual C++ project model also complies with this general automation project modelscheme:Projects |- Project -- Object(unique for the project type) |- ProjectItems (a collection of ProjectItem) |- ProjectItem (single object) -- ProjectItems (another collection) |- Object(unique for the project type)The Projects interface provides an ensemble of abstract objects of the Project type. The Projectinterface defines an abstract project, i.e. it can reference an object from any project model thatcomplies with the standard scheme. Any peculiar properties of a specific model should be definedthrough a special interface which is unique only to this model alone. A reference for such an objectcould be acquired through the Project.Object property. For instance, specific properties of Visual C++project could be obtained through the VCProject interface:VCProject vcproj = proj.Object as VCProject;It is possible to obtain a list of all projects loaded in IDE and belonging to any project model typethrough the dte.Solution.Projects field; projects belonging to a particular model can be acquired throughthe DTE.GetObject method (see the example below for Visual C++ model):Projects vcprojs = m_dte.GetObject("VCProjects") as Projects;The ProjectItems interface represents an ensemble of abstract solution tree elements of ProjectItemtype. Similar to the Project interface, the ProjectItem can define any kind of element; it can evencontain the same ProjectItems collection inside itself (accessible through the ProjectItem.ProjectItems)or it can be a Project altogether. An object unique for a specific project model can be obtained throughthe ProjectItem.Object field. For instance, a Visual C++ source code file is represented by a VCFile type:VCFile file = projectItem.Object as VCFile;An embedded project can be obtained in a similar manner:Project proj = projectItem.Object as Project;Recursively walking all elements of a Solution trees branchThe interface for controlling hierarchies IVsHierarchy can be used to perform a passing of Solution treesbranch. This interface provides an access to abstract nodes of a tree, each one of which in turn could bea leaf, a container of elements or a link to another hierarchy. Each tree node is uniquely identified
  • through the DWORD identifier VSITEMID. Such identifiers are unique within the scope of a singlehierarchy and possess a limited lifetime within it.A hierarchy object can be obtained for a tree branch of a single project through theVsShellUtilities.GetHierarchy method:public static IVsHierarchy ToHierarchy(EnvDTE.Project project){ System.IServiceProvider serviceProvider = new ServiceProvider(project.DTE as Microsoft.VisualStudio.OLE.Interop.IServiceProvider); Guid guid = GetProjectGuid(serviceProvider, project); if (guid == Guid.Empty) return null; return VsShellUtilities.GetHierarchy(serviceProvider, guid);}In the example above, the hierarchy was obtained for a project through its GUID identifier. Consider theexample of obtaining this GUID identifier for a project:private static Guid GetProjectGuid(System.IServiceProvider serviceProvider, Project project){ if (ProjectUnloaded(project)) return Guid.Empty; IVsSolution solution = (IVsSolution)serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution; IVsHierarchy hierarchy; solution.GetProjectOfUniqueName(project.FullName, out hierarchy); if (hierarchy != null) { Guid projectGuid; ErrorHandler.ThrowOnFailure( hierarchy.GetGuidProperty( VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_ProjectIDGuid, out projectGuid)); if (projectGuid != null) { return projectGuid; } } return Guid.Empty;}
  • The IEnumHierarchies interface permits obtaining all of the hierarchies for projects of a particular typethrough the solution. GetProjectEnum method. Here is an example of obtaining the hierarchies for everyVisual C++ project in a solution tree:IVsSolution solution = PVSStudio._IVsSolution;if (null != solution){ IEnumHierarchies penum; Guid nullGuid = Guid.Empty; Guid vsppProjectGuid = new Guid("8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942"); //You can ask the solution to enumerate projects based on the //__VSENUMPROJFLAGS flags passed in. For //example if you want to only enumerate C# projects use //EPF_MATCHTYPE and pass C# project guid. See //CommonIDLvsshell.idl for more details. int hr = solution.GetProjectEnum( (uint)(__VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION | __VSENUMPROJFLAGS.EPF_MATCHTYPE), ref vsppProjectGuid, out penum); ErrorHandler.ThrowOnFailure(hr); if ((VSConstants.S_OK == hr) && (penum != null)) { uint fetched; IVsHierarchy[] rgelt = new IVsHierarchy[1]; PatternsForActiveConfigurations.Clear(); while (penum.Next(1, rgelt, out fetched) == 0 && fetched == 1) { ... } }}As evident by the example above, the GetProjectEnum method provides hierarchies for projects basedon a project kind specified by the GUID identifier. GUID identifiers for regular Visual Studio/MSBuildproject types can be obtained here. The penum.Next() method allows us to enumerate all projecthierarchies weve acquired (the rgelt array). It should be remembered that user-created project modelscould possess their own unique identifiers in case they define a new project type for themselves.But our own experience in developing PVS-Studio IDE plug-in demonstrates that an opposite situation isquite possible as well, that is, when a user-created project type uses a GUID from one of the stockproject types, usually the one from which it was derived. In particular, weve encountered a VCProjecttype that was extended to provide development for Android platform. As a result, this project modelextension had caused crashes in our plug-in because it did not provide several properties which areotherwise present in VCProject model (OpenMP for example) through the automation API. An intricacyof this situation is that such an extended project model type cannot be differentiated from a regularone, and thus, it is quite hard to correctly process it as well. Therefore, when you are extending aproject model through your custom types, to avoid such conflicts with various IDE components
  • (including other third-party extensions as well), it is always important to remember the necessity ofproviding means to uniquely identify your types.Possessing an IVsHierarchy for the project, we are able to recursively enumerate all the elements ofsuch solution tree branch through the hierarchy.GetProperty method, which in turn provides us with thespecified properties for each one of the hierarchy nodes:EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, MyProjectHierarchy,0, true); ...public void EnumHierarchyItemsFlat(uint itemid, IVsHierarchy hierarchy, int recursionLevel, bool visibleNodesOnly){ if (hierarchy == null) return; int hr; object pVar; hr = hierarchy.GetProperty(itemid, (int)__VSHPROPID.VSHPROPID_ExtObject, out pVar); ProjectItem projectItem = pVar as ProjectItem; if (projectItem != null) { ... } recursionLevel++; //Get the first child node of the current hierarchy being walked hr = hierarchy.GetProperty(itemid, (visibleNodesOnly ? (int)__VSHPROPID.VSHPROPID_FirstVisibleChild :(int)__VSHPROPID.VSHPROPID_FirstChild), out pVar); Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr); if (VSConstants.S_OK == hr) { //We are using Depth first search so at each level we recurse //to check if the node has any children // and then look for siblings. uint childId = GetItemId(pVar); while (childId != VSConstants.VSITEMID_NIL) { EnumHierarchyItemsFlat(childId, hierarchy, recursionLevel, visibleNodesOnly); hr = hierarchy.GetProperty(childId, (visibleNodesOnly ? (int)__VSHPROPID.VSHPROPID_NextVisibleSibling : (int)__VSHPROPID.VSHPROPID_NextSibling), out pVar); if (VSConstants.S_OK == hr) {
  • childId = GetItemId(pVar); } else { Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr); break; } } } } private uint GetItemId(object pvar) { if (pvar == null) return VSConstants.VSITEMID_NIL; if (pvar is int) return (uint)(int)pvar; if (pvar is uint) return (uint)pvar; if (pvar is short) return (uint)(short)pvar; if (pvar is ushort) return (uint)(ushort)pvar; if (pvar is long) return (uint)(long)pvar; return VSConstants.VSITEMID_NIL; }A ProjectItem object that weve acquired for each one of the trees nodes will allow us to obtain itscorresponding Visual C++ object through the Object filed, as was described earlier.Enumerating all projects in solution treeDTE.Solution.Projects interface can be used to enumerate all projects in the solution:if (m_DTE.Solution.Projects != null) { try { foreach (object prj in m_DTE.Solution.Projects) { EnvDTE.Project proj = prj as EnvDTE.Project; if (proj != null) WalkSolutionFolders(proj); } } }Besides projects, Solution tree can also contain folder nodes (Solution Folders). They should also betaken into account while processing each Project element:public void WalkSolutionFolders(Project prj){ VCProject vcprj = prj.Object as VCProject; if (vcprj != null && prj.Kind.Equals(VCCProjectTypeGUID)) { if (!ProjectExcludedFromBuild(prj)) {
  • IVsHierarchy projectHierarchy = ToHierarchy(prj); EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, projectHierarchy, 0, false); } } else if (prj.ProjectItems != null) { foreach (ProjectItem item in prj.ProjectItems) { Project nextlevelprj = item.Object as Project; if (nextlevelprj != null && !ProjectUnloaded(nextlevelprj)) WalkSolutionFolders(nextlevelprj); } }}Projects that are excluded from the build should be inspected separately, as they are not accessiblethrough the automation model after being unloaded from the IDE:public bool ProjectExcludedFromBuild(Project project){ if (project.UniqueName.Equals("<MiscFiles>", StringComparison.InvariantCultureIgnoreCase)) return true; Solution2 solution = m_DTE.Solution as Solution2; SolutionBuild2 solutionBuild = (SolutionBuild2)solution.SolutionBuild; SolutionContexts projectContexts = solutionBuild.ActiveConfiguration.SolutionContexts; //Skip this project if it is excluded from build. bool shouldbuild = projectContexts.Item(project.UniqueName).ShouldBuild; return !shouldbuild;}Enumerating selected elementsThe DTE.SelectedItems interface can be used to enumerate solution elements which are selected in theSolution Explorer window.foreach (SelectedItem item in items){ VCProject vcproj = null; if (item.Project != null) { vcproj = item.Project.Object as VCProject; if (vcproj != null && item.Project.Kind.Equals("{" + VSProjectTypes.VCpp + "}")) {
  • IVsHierarchy projectHierarchy = ToHierarchy(item.Project); PatternsForActiveConfigurations.Clear(); EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, projectHierarchy, 0, false, files, showProgressDialog); } else if (item.Project.ProjectItems != null) { //solution folder if (!ProjectUnloaded(item.Project)) WalkSolutionFolders(item.Project); } } else if (item.ProjectItem != null) { //walking files ... else if (item.ProjectItem.ProjectItems != null) if (item.ProjectItem.ProjectItems.Count > 0) WalkProjectItemTree(item.ProjectItem); } }private void WalkProjectItemTree(object CurrentItem){ Project CurProject = null; CurProject = CurrentItem as Project; if (CurProject != null) { IVsHierarchy projectHierarchy = ToHierarchy(CurProject); PatternsForActiveConfigurations.Clear(); EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, projectHierarchy, 0, false); return; } ProjectItem item = null; item = CurrentItem as ProjectItem; if (item != null) { ... if (item.ProjectItems != null) if (item.ProjectItems.Count > 0) { foreach (object NextItem in item.ProjectItems) WalkProjectItemTree(NextItem); } }}
  • Configurations and properties of projects and filesVisual C++ stores build configurations (compilation and linking parameters, pre-build and post-buildsteps, external tool command lines etc.) for C/C++ source files inside its XML-based project files(vcproj/vcxproj). These settings are available to Visual Studio users through property page dialogs.Each combination of projects build configuration (Debug, Release, etc.) and a platform (Win32, IA64,x64, etc.) is associated with a separate collection of settings. Although majority of the settings aredefined at the project level, it is possible to redefine separate properties for each individual file (fileproperties are inherited from its project by default). The list of properties which can be redefined at thefile level is dependent upon the type of a file in question. For example, only the ExcludedFromBuildproperty can be redefined for header files, but cpp source files permit the redefinition for any of itscompilation properties.Obtaining configurationsVisual C++ project model presents property pages through the VCConfiguration (for a project) andVCFileConfiguration (for a file) interfaces. To obtain these objects we will start from a ProjectItem objectwhich represents an abstract Solution tree element.ProjectItem item;VCFile vcfile = item.Object as VCFile;Project project = item.ContainingProject;String pattern = "Release|x64";if (String.IsNullOrEmpty(pattern)) return null;VCFileConfiguration fileconfig = null;IVCCollection fileCfgs = (IVCCollection)vcfile.FileConfigurations;fileconfig = fileCfgs.Item(pattern) as VCFileConfiguration;if (fileconfig == null) if (fileCfgs.Count == 1) fileconfig = (VCFileConfiguration)fileCfgs.Item(0);In the example above weve acquired a file configuration for VCFile object (which represents a C/C++header or a source file) by passing a configuration pattern (configurations name and platform) to theItem() method. Build configuration pattern is defined on the project level. The following exampledemonstrates the acquisition of active configuration (the one that is selected in IDE) of a project.ConfigurationManager cm = project.ConfigurationManager;Configuration conf = cm.ActiveConfiguration;String platformName = conf.PlatformName;String configName = conf.ConfigurationName;String pattern = configName + "|" + platformName;return pattern;The ActiveConfiguration interface should be handled with care. Quite often weve encounteredexceptions when calling it from our PVS-Studio IDE extension package. In particular, this field sometimesbecomes inaccessible through the automation object model when a user is building a project, or in thepresence of any other heavy user interaction with Visual Studio UI. As there is no assured way ofpredicting such user actions, it is advised to provide additional error handlers for such bottlenecks
  • when accessing settings with automation model. It should be noted that this particular situation is notrelated to COM exception handling that was described in the previous article dedicated to EnvDTEinterfaces, and it is probably related to some internal issues within the automation model itself.Next, lets acquire the configuration for a project that contains the file in question:VCConfiguration cfg=(VCConfiguration)fileconfig.ProjectConfiguration;While the interfaces representing configurations themselves contain settings only from the General tabof the property pages, references for individual build tools can be acquired through theVCConfiguration.Tools and VCFileConfiguration.Tool interfaces (note that a single file contains settingsrespectively for only one build tool). Lets examine the VCCLCompilerTool interface representing the C++compiler:ct = ((IVCCollection)cfg.Tools).Item("VCCLCompilerTool") as VCCLCompilerTool;ctf = fileconfig.Tool as VCCLCompilerTool;Now lets acquire the contents of, for example, the AdditionalOptions field belonging to the compilertool, using the Evaluate method to process any macros that we can encounter within its value:String ct_add = fileconfig.Evaluate(ct.AdditionalOptions);String ctf_add = fileconfig.Evaluate(ctf.AdditionalOptions);Property SheetsProperty sheets are XML files with a props extension. They allow an independent definition of projectsbuild properties, i.e. the command line parameters for various building tools, such as a compiler or alinker. Property sheets also support inheritance and can be used for specifying build configurations forseveral projects at once, i.e. the configuration defined inside the project file itself (vcproj/vcxproj) couldinherit some of its properties from single or multiple props files.To handle property sheets, Visual C++ project model provides the VCPropertySheet interface. Acollection of VCPropertySheet objects can be obtained through the VCConfiguration. PropertySheetsfield:IVCCollection PSheets_all = fileconfig.PropertySheets;Similarly, the PropertySheets filed of the VCPropertySheet interface provides a reference to a collectionof child property sheet files for this object. Lets examine the recursive enumeration of all of theprojects property sheets:private void ProcessAllPropertySheets(VCConfiguration cfg,IVCCollection PSheets){ foreach (VCPropertySheet propertySheet in PSheets) { VCCLCompilerTool ctPS = (VCCLCompilerTool)((IVCCollection)propertySheet.Tools).Item( "VCCLCompilerTool"); if (ctPS != null) {
  • ... IVCCollection InherPSS = propertySheet.PropertySheets; if (InherPSS != null) if (InherPSS.Count != 0) ProcessAllPropertySheets(cfg, InherPSS); } }}In the example above weve obtained an object of VCCLCompilerTool type (that is compilation settings)for PropertySheet on every level. In this way we could gather all compilation parameters defined inevery property sheet, including the embedded ones.The VCPropertySheet interface does not contain means to evaluate macros within its fields, so as awork-around, the Evaluate method from the projects configuration can be used instead. But, suchapproach could also lead to the incorrect behavior in case the value of the macro being evaluated isrelated to the props file itself. For instance, several MSBuild macros which were introduced in theMSBuild version 4 could also be utilized inside vcxproj projects from Visual Studio 2010. Lets take theMSBuildThisFileDirectory macro that evaluates as a path to the directory containing file in which it isused. Now, evaluating this macro through the cfg.Evaluate will result in a path to the vcxproj file, andnot to props file, which actually does contains this macro.All of the property sheets in Visual C++ project can be divided between user and system files. By userfiles we understand the props files which were created and added to the project by a user himself. Buteven an empty template-generated MSVC project often includes several property sheets by default.These system props files are utilized by the environment to specify various compilation parameterswhich were set inside the projects property page interface by the user. For example, setting up theCharacterSet property to use Unicode manually in the Property Page interface will result in theappearance of a special property sheet in the Property Sheets window which will define severalpreprocessor symbols (Unicode, _Unicode), and this properties subsequently will be inherited by theproject. Therefore when processing properties from inside a Property sheet, one should alwaysremember that compilation symbols defined in system props files are also returned by theircorresponding property in the projects configuration through the automation API. Evidently, processingthese two simultaneously while gathering compilation arguments can result in a duplication of sucharguments.References 1. MSDN. Visual C++ Project Model. 2. MSDN. Project Modeling. 3. MSDN. Automation Model Overview.