Bugslayer

Google from Visual Studio .NET

John Robbins

Code download available at:Bugslayer0311.exe(669 KB)

Contents

CommandBars from Hell
F1 Monitoring?
The Real Google Add-in
Implementation Highlights
Wrap Up
The Tips

One key to maximizing your software development efforts is finding answers to your coding challenges as quickly as possible. When it comes to searching for those answers on the Internet, all roads lead to Google, as you undoubtedly know. Not only is Google the most comprehensive search engine, it's also the fastest.

Knowing this, it amazes me how few developers have taken the time to read the docs for Google. Google can return a ton of hits for any query you make. To make the search really efficient, the trick is to tell Google how to scope the search so that instead of getting a billion results, you get the 100 best.

In order to scope things better, Google has set up special search groups for specific topics, including information for Microsoft, Apple Macintosh, BSD, Linux, and the U.S. Government. For instance, when you search from https://www.google.com/microsoft.html, your query will be run only on sites that have predominantly Microsoft content. To use the specific sites, point your browser at https://www.google.com/options/specialsearches.html.

In addition, you can limit your search to a single site. In the search edit control enter "site:" without the quotes followed by the site you want to search, or use the advanced search page. To search MSDN® Online for example, enter "site:msdn.microsoft.com" before your search string.

I'd strongly encourage you to read the complete Google search documentation, which you can find at https://www.google.com/about.html. The answers to all of your development problems are out there on the Internet, but without smart searching, you'll look for them forever.

OK, so my two Google tips alone are worth the price of this column, but there's more. Because I'm fundamentally lazy, I don't like opening a browser and typing "site:msdn. microsoft.com" into Google's search box every time I have a question. I want to click a button to be able to do the Google searches I've mentioned, along with Usenet groups, Google Directory, and whole-Web searches from right inside Visual Studio® .NET. Hey, it's nothing that a nice little add-in couldn't handle. For a preview of what it's going to look like, check out Figure 1.

Figure 1 Google Add-in

Figure 1** Google Add-in **

My original intent was that this column would be short, sweet, and give you a utility to search the Internet but I ran into some very strange limitations in the add-in model and had to do some considerable poking around to circumvent them. My hope is that I'll save you a lot of trouble (and a lot of cursing at the computer) when you set out to develop your own add-ins.

For the rest of this column, I'm going to assume that you're already familiar with the basics of how add-ins work and how to write them. If you want to get up to speed, read the January 2002 Bugslayer column or the Visual Studio .NET documentation itself.

CommandBars from Hell

My original design had the entire Google add-in placed in a single toolbar. The first control would be a combobox that would mimic the existing Find combobox by saving past searches. The following controls would be all buttons that would take the text from the combobox. The order of those buttons would be: Search MSDN Online, Search Microsoft sites, Search Google groups, Search Google Directory, and Search the Web.

CommandBars are the type for toolbars and menus in Visual Studio. The documentation for CommandBars indicates they come from the Microsoft Office DLL, MSO.DLL. The Visual Studio .NET documentation does not directly address CommandBars and simply refers to the Microsoft® Office documentation. The problem with this approach is that the documentation implies that the Visual Studio .NET CommandBars are as capable as the Office CommandBars but, as you'll see in just one moment, they are not. This means you're left to muddle through the Visual Basic® for Applications examples in the Office XP Developer documentation.

As I already had some experience wrestling with Visual Studio .NET CommandBars, I realized my first task would be to prototype the combobox so I could figure out its limitations. I suspect this is only a problem with the add-in model and may be fixed with the Visual Studio Integration Program (VSIP) model, but I haven't checked, mainly because writing VSIP code is more involved and can't be done as managed code. Since most of you are going to be sticking with the add-ins, it's good to finally find out what the limitations are.

In the source code for this month's column you can find my CommandBar testing application, called ToolBarTest, which is written in Visual Basic .NET (see the link at the top of this article). You may want to compile and install the add-in so you can see the limitations for yourself. The installation is accomplished by running the TOOLBARTEST.REG file that's part of the project.

Adding a combobox is as simple as calling the Add method of the Controls property in a toolbar. As you can see in the downloadable code, I only create and populate the toolbar when the IDTExtensibility2.OnConnection method is called and the ext_ConnectMode parameter is ext_cm_UISetup, which indicates the add-in is being told to perform its initial user interface setup. As the user interface setup is only supposed to happen once, on subsequent loads of the add-in, I hunt down the already-created toolbar and make it active.

On the second run, my code to find the toolbar worked correctly, but the combobox I created in the first position on the toolbar was no longer there. I was quite perplexed and thought that maybe it was hidden and I needed to "unhide" it. I whipped out a quick macro to enumerate the controls on my toolbar and sure enough the combobox I had added was missing in action. After resetting the CommandPreload option to 1 to force the add-in user interface initialization and rerunning Visual Studio, my combobox came back. Sadly, a restart still lost the combobox.

Those of you who have done some work with CommandBars might be wondering if I mistakenly set the Temporary parameter to true when calling CommandBars.Controls.Add. As you can see, I made sure to add the combobox as a non-temporary control. At this point, I figured that with the add-ins model, it looks like only buttons are saved. My workaround was when the add-in loaded normally in the IDTExtensibility2.OnConnection method, I'd hunt down the CommandBar for my add-in and manually poke the combobox into the first position on the toolbar. You can see that code in Connect.AddComboBox inside ToolBarTest.

While it's odd that the combobox isn't saved for a toolbar, at least you have a partial workaround. The problem occurs when your toolbar is visible but your add-in isn't loaded at startup. In that case, your toolbar won't show up correctly. You can see this demonstrated in the ToolBarTest add-in. While you could tell the user to ensure your add-in is always loaded, that won't always be the case. If you play with ToolBarTest, you'll see that if it's not loaded, clicking on a button will make the combobox magically appear because that's when the add-in has its first chance to add it. This is not ideal from a user interface standpoint.

Since I wasn't coming up with any solution for the disappearing combobox, I turned to handling the CommandBarComboBox.Change event from the combobox, which is the only event supported. I got the event hooked up, stuffed a few items into the combobox, and started testing. Looking at the code, you'll see that whenever the Change notification triggers, I pop up a message box that indicates that the event occurred and notes the text that is in the combobox. Additionally, I hooked up the IDTCommandTarget.Exec method to also pop up a message box with a different title along with the text in the combobox. At first, everything looked great in that I could select an item with the mouse from the combobox dropdown and the CommandBarComboBox.Change event was getting triggered appropriately.

Now I had to see what would happen with items entered into the combobox edit control. I found that entering a new value and pressing ENTER properly pops the Change notification—the correct action. Also, clicking on any of my buttons caused the execution message box to pop up with the appropriate tests. Things quickly went downhill from there.

As I wanted to mimic the standard toolbar's Find combobox, I entered some new text and immediately clicked on one of my buttons. Annoyingly, the entered text was wiped out and the first item in the combobox list replaced the text! That sure looked like a bug and suggested to me that the Visual Studio .NET add-in model doesn't want you using comboboxes in your CommandBars. No matter what I tried, there was no way I was going to get the combobox to stop changing the text. To make matters worse, changing the text in the combobox and clicking anywhere outside the toolbar also caused the entered text to be replaced and a Change notification for the replaced text to be sent.

Doing a little Spy++ action, I then looked at windows procedures for both the standard toolbar Find combobox and my toolbar's combobox edit controls. While mine comes from MSO.DLL, the Office XP controls DLL, the Find combobox edit control comes from VSBROWSE.DLL, which is, oddly enough, the Web browsing package. As the Find combobox has normal combobox behavior and its windows procedure comes from a different DLL, it's obvious that the default combobox is not quite right.

Although the editable comboboxes weren't working, the straight selection comboboxes worked fine, so I started on a quest to see if I could still salvage my idea of a single toolbar. Because the Office XP Developer documentation indicates that there are actually many different controls you can put on a CommandBar, I tried to use different MsoControlType controls. I learned that other than for buttons or comboboxes, CommandBars.Controls.Add will always throw an exception when you try to add the control. I would have thought that edit controls would have been allowed as well.

In the end, without a huge subclassing effort, add-ins can only safely put buttons and selection comboboxes on the toolbar. I was rather disappointed because I can see lots of uses for editable comboboxes on toolbars. The good news is that at least I suffered through all the CommandBar frustrations so I know exactly what can and can't be placed on them.

F1 Monitoring?

I wanted to work on something else for a while, so I thought it might be interesting if my Google add-in commands could be assigned to keys so you could use the Google search directly from the editor. The ultimate situation would be if I could somehow get the same list that the Dynamic Help window shows when you're editing. That way my add-in would be searching Google for the exact same text that a normal F1 command uses. Additionally, I wouldn't have to do any work to parse out the current keyword from the edit window either.

After poking through the MSDN Help, I ran across the "Adding Custom Links to the Dynamic Help Window" section, which has quite a bit of helpful information. One interesting tidbit mentioned in the section on debugging help topics you add to Dynamic Help was how you can see how Visual Studio .NET was looking at the topics. In the registry key, HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\7.0\Dynamic Help, add a new string value of "Display Debug Output in Detail" and set its value to "YES". After you restart Visual Studio, you'll see some additional details in the Dynamic Help window, as shown in Figure 2.

Figure 2 More Details

Figure 2** More Details **

As you move the focus to different areas, you'll see that the values under Active Context change as appropriate. In fact, it's rather interesting to click around the IDE to see what values show up.

After spending some time poking through the Visual Studio .NET automation documentation, I stumbled into the ContextAttributes collection, which contains all of the attributes associated with a global context or window's context in the Dynamic Help window. As I played around with some macros to access the individual ContextAttribute values, such as the one in Figure 3, I figured out that the DTE object contains the keyword list when focus is in a source window.

Figure 3 Reading the ContextAttributes Collection

Public Sub ListDTEAttrs() Dim Str As New StringBuilder Str.Append("**DTE High Priority ContextAttributes" & vbCrLf) Dim CA As ContextAttribute DTE.ContextAttributes.Refresh() Dim x As ContextAttributes x = DTE.ContextAttributes.HighPriorityAttributes DTE.ContextAttributes.HighPriorityAttributes.Refresh() Str.Append(" HP Count : " & x.Count & vbCrLf) For Each CA In x Try Str.Append("CA Name : " & CA.Name & vbCrLf) Try Dim o As Object For Each o In CA.Values Str.Append(" Value : " & o.ToString() & vbCrLf) Next Catch ex As System.Exception Str.Append(" Unable to get values" & vbCrLf) End Try Catch ex As System.Exception Str.Append("Unable to get value item" & vbCrLf) End Try Next Str.Append("**DTE ContextAttributes" & vbCrLf) ' Only do the keywords. CA = DTE.ContextAttributes.Item(1) Try Str.Append("CA Name : " & CA.Name & vbCrLf) Try Dim o As Object For Each o In CA.Values Str.Append(" Value : " & o.ToString() & vbCrLf) Next Catch ex As System.Exception Str.Append(" Unable to get values" & vbCrLf) End Try Catch ex As System.Exception Str.Append("Unable to get value item" & vbCrLf) End Try Dim ow As New OutputPane("CATest") ow.Clear() ow.WriteLine(Str.ToString()) End Sub

My plan was to write my add-in commands to look at the active window. If it was anything other than a source code window, I'd defer to the normal Help.F1Help command. When I ran the macro similar to the one in Figure 3 on multiple projects in order to get a feeling for the patterns in the ContextAttributes collection, I noticed two patterns. The first was that for various projects the F1 keyword—Active Context as listed in the dynamic help—was not always in the first position in the array, as I would have suspected. In fact, in Visual Basic .NET projects, it seemed to dance all over the place in the array in no particular pattern at all.

After another read of the docs, I ran into the HighPriorityAttributes property of the DTE object that seemed to imply that it should contain the F1 keywords. After adding code to check the property HighPriorityAttributes, which is done in the macro for Figure 3, I saw that for source code windows, the HighPriorityAttributes.Count was always zero. This was a setback in my plan since I wanted to execute the Google search on the same F1 keyword.

The second pattern I noticed in the ContextAttributes collection was that for Visual Basic .NET-based applications, most of the keywords started with "vb." Since the keywords are actually keywords specific to the help engine, that's understandable. While I could strip off the "vb.", I figured it wasn't worth the effort since I couldn't get the F1 keyword.

At this point, my two main ideas for the Google add-in were stymied. Since I couldn't get the Visual Studio .NET CommandBar to properly handle comboboxes and could not ensure I was going to search on the correct F1 keyword, I needed to step back and see if I could find another way of getting Google into the IDE.

The Real Google Add-in

If I couldn't get my Google add-in into a CommandBar, I was positive I could make it a hosted tool window. In case you're not familiar with hosting managed controls into tool windows, I'd encourage you to visit the "Automation Samples for Visual Studio .NET" page where you can download the basic ToolWindow add-in. The key part of the sample is a shim C++ COM DLL that takes all the work out of hosting the common language runtime (CLR) and getting your user control loaded into the tool window. As you'll see in a little bit, I took the shim DLL code, added error handling, assertions (so you'll know what's up when something goes wrong), and some additional useful functionality. Since your add-in is getting the shim loaded and telling the shim what to load, you can reuse my improved shim DLL with no code changes whatsoever.

You should take a look back at the screen shot of the Google add-in in action (see Figure 1). I felt that the Google window should be positioned between the Solution/Class dock and the Properties/Dynamic Help dock for easy access. There's nothing stopping you from dragging the Google window into a tab alongside the Dynamic Help window. If you hide the Google window, the View | Other Windows popup has the Google Search Window as the first item. You could also assign a key combination such as Alt+F1, which is available on the default keyboard, to the GoogleAddIn.ViewGoogle command.

Because I can control the combobox in the Google window, I made it behave exactly like the Standard toolbar's Find combobox. Anything you type in the edit control will be added to the combobox and searches are saved between IDE runs. The buttons across the top of the control perform a particular search, as I outlined at the beginning of the column.

When you click one of the search buttons, the add-in will start a browser window according to your Help settings. If you've elected to start the MSDN Help system externally, a full browser window pops up external to the IDE. If you've selected internal help, a browser window opens inside the IDE. You can override the defaults in the options.

As all good add-ins should, the Google add-in has a properly integrated Options dialog property page, as shown in Figure 4. All of the options in the Settings section are self-explanatory, but I just want to mention that the Default Google site dropdown allows you to specify exactly which international Google version you'd like to use. I found all the international sites at https://www.google.com/language_tools.

Figure 4 Google Add-in Options Page

Figure 4** Google Add-in Options Page **

The Group limit string text allows you to specify to which groups you'd like to limit your search. The default is to limit searching to the "comp.*" groups. If you'd like to limit your search to just the Usenet groups hosted by Microsoft, you could substitute "microsoft.public.*" as the text.

Implementation Highlights

With the usage for the Google add-in out of the way, I want to spend a little time on a few interesting implementation highlights. If you're new to add-ins and are wondering how I got the Option property page into the Option dialog, make sure you read Leo Notenboom's MSDN Magazine article, "Custom Add-Ins Help You Maximize the Productivity of Visual Studio .NET".

The first puzzle I wanted to tackle was how to control the built-in browser windows in Visual Studio .NET. Poking through the various window types in the vsWindowKind* constant shows there is a vsWindowKindWebBrowser that is a Window that contains the Web browser. It didn't take a rocket scientist to realize that inside the window was an instance of the SHDocVw.WebBrowser you've come to know and love. Unfortunately, I wasn't at all sure how to get the actual Web browser control so that I could call the SHDocVw.WebBrowser.Navigate method.

The Window class has an Object property, which the documentation says returns an interface or object that can be accessed at run time by name. While that description is as about as vague as can be, I thought since I was doing the Google add-in in C#, I'd try casting the returned object as a SHDocVw.WebBrowser and see what happened. After adding a reference to SHDOCVW.DLL to my project, I set up the code in Figure 5.

Figure 5 Get Browser

// Get the first window that's a browser. Window Win = m_ApplicationObject. Windows.Item(Constants.vsWindowKindWebBrowser); // Force the window to be visible. Win.Visible = true ; // Get the Internet Explorer control inside the browser window. SHDocVw.WebBrowser x = (SHDocVw.WebBrowser)Win.Object ; Object Zero = 0 ; Object EmptyString = "" ; // Navigate to the URL. x.Navigate ( Args.UrlString, ref Zero, ref EmptyString, ref EmptyString ,ref EmptyString ) ;

While undocumented, the trick of casting the Window.Object value to a SHDocVw.WebBrowser works great. Even nicer is that calling the Navigate method automatically brings up the Web toolbar and puts the URL into the browsing address combobox.

After verifying that I could control the internal Web browser windows inside the IDE, I turned to creating the actual control itself, which is nothing fancy. However, I wanted to get the control completely debugged and tested before ever using it inside the IDE. While it's possible to develop and debug .NET user controls that reside in add-ins, I didn't want that hassle. The appropriately named GoogleControl.DLL contains the UI for the control.

Because I wanted the control to be self-contained, all the logic for building up the Google search URLs is in GoogleControl.BtnBar_ButtonClick. Figuring out the various parameters was simply a matter of performing a few of each kind of search and looking for the common elements. The only moderately interesting design decision I made was that I didn't want the control itself accessing the IDE to open a window and such, so when a particular search button is pressed, the control fires its GoogleSearch event whose argument parameter contains the fully formed URL. That way, the controlling application can do the dirty work of spawning the browser as appropriate.

Little did I know that choosing to use an event was going to cause some serious head scratching when it came to hosting the Google control. As I mentioned earlier, Microsoft supplied a shim control with one of the add-in samples. The whole reason for this shim is that the Visual Studio .NET IDE is an ActiveX® host and the .NET Framework does not create ActiveX controls. Your add-in will create a ToolWindow telling it to host the shim. Once the ToolWindow is created, you access the shim inside the ToolWindow and tell it to host the CLR and create a specific control. It's quite instructional to walk through the hosting code in the CVSNetToolWinShim::HostUserControl2 and CVSNetToolWinShim::FinalConstruct methods in Figure 6. The full implementation is in VSNETTOOLHOSTSHIM.CPP from the VSNetToolHostShim2project.

Figure 6 CVSNetToolWinShim::HostUserControl2 and CVSNetToolWinShim::FinalConstruct

HRESULT CVSNetToolWinShim:: HostUserControl2 ( IUnknown * pToolWindow , BSTR Assembly , BSTR Class , BSTR SatelliteDLL , int ResourceID , IUnknown** ppControlObject ) { CComQIPtr<EnvDTE::Window> pWindow ( pToolWindow ) ; RECT rc ; CComPtr<IWin32Window> pIWin32Window ; // Try to create the assembly out of the GAC. HRESULT hr = m_pDefaultDomain->CreateInstance ( Assembly, Class, &m_pObjHandle) ; if ( FAILED ( hr ) || ( !m_pObjHandle ) ) { // Assume the complete path is passed in and try and // to create that one. hr = m_pDefaultDomain->CreateInstanceFrom ( Assembly, Class, &m_pObjHandle ) ; if ( FAILED ( hr ) || ( !m_pObjHandle ) ) { // At least let the caller know something is bad in // a debug build. ATLASSERT ( FALSE ) ; hr = E_FAIL ; Reset ( ) ; return ( hr ) ; } } hr = m_pObjHandle->Unwrap ( &m_varUnwrappedObject ) ; if ( ( m_varUnwrappedObject.vt != VT_DISPATCH ) && ( m_varUnwrappedObject.vt != VT_UNKNOWN ) || ( !m_varUnwrappedObject.punkVal ) ) { ATLASSERT ( FALSE ) ; hr = E_FAIL ; Reset ( ) ; return ( hr ) ; } hr = m_varUnwrappedObject.pdispVal->QueryInterface ( IID_IUnknown , (LPVOID*)ppControlObject ) ; ATLASSERT ( SUCCEEDED ( hr ) ) ; hr = m_varUnwrappedObject.pdispVal->QueryInterface ( IID_IWin32Window, (LPVOID*)&pIWin32Window ) ; ATLASSERT ( SUCCEEDED ( hr ) ) ; if ( FAILED ( hr ) ) { Reset ( ) ; return ( hr ) ; } long hWndTemp ; hr = pIWin32Window->get_Handle ( &hWndTemp ) ; // 'type cast' : conversion from 'long' to 'HWND' of greater size #pragma warning ( disable : 4312 ) m_hWndForm = (HWND)hWndTemp ; #pragma warning ( default : 4312 ) if ( FAILED ( hr ) || ( !m_hWndForm ) ) { ATLASSERT ( FALSE ) ; Reset ( ) ; return ( hr ) ; } if ( pWindow.p ) { USES_CONVERSION ; CComVariant varPic ; CComPtr<IPictureDisp> pPictureDisp ; PICTDESC pd ; pd.cbSizeofstruct = sizeof ( PICTDESC ) ; pd.picType = PICTYPE_BITMAP ; HMODULE hModule = LoadLibrary ( W2T ( SatelliteDLL ) ); pd.bmp.hbitmap = LoadResourceBitmap ( hModule , MAKEINTRESOURCE ( ResourceID ) , &pd.bmp.hpal ) ; OleCreatePictureIndirect ( &pd, IID_IPictureDisp, FALSE, (LPVOID*)&pPictureDisp ) ; pPictureDisp->QueryInterface ( IID_IUnknown , (LPVOID*)&varPic.punkVal ) ; varPic.vt = VT_UNKNOWN ; pWindow->SetTabPicture ( varPic ) ; FreeLibrary ( hModule ) ; } ::SetParent((HWND)m_hWndForm, m_hWnd); ::GetWindowRect(m_hWnd, &rc); ::MoveWindow((HWND)m_hWndForm, 0, 0, rc.right-rc.left, rc.bottom-rc.top, TRUE); ::ShowWindow((HWND)m_hWndForm, SW_SHOW); return ( hr ) ; } HRESULT CVSNetToolWinShim::FinalConstruct() { CComPtr<IUnknown> pAppDomainPunk; HRESULT hr = CorBindToRuntimeEx(NULL, NULL, STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC, __uuidof(CorRuntimeHost), __uuidof(ICorRuntimeHost), (LPVOID*)&m_pHost); if(FAILED(hr)) { ATLASSERT ( FALSE ) ; return hr; } hr = m_pHost->Start(); if(FAILED(hr)) { ATLASSERT ( FALSE ) ; return hr; } hr = m_pHost->GetDefaultDomain(&pAppDomainPunk); if(FAILED(hr) || !pAppDomainPunk) { ATLASSERT ( FALSE ) ; return hr; } hr = pAppDomainPunk->QueryInterface(__uuidof(mscorlib::_AppDomain), (LPVOID*)&m_pDefaultDomain); if(FAILED(hr) || !m_pDefaultDomain) { ATLASSERT ( FALSE ) ; return hr; } return hr; }

The Microsoft control, though it was in need of some serious assertions, did the job pretty well. However, with GoogleControl triggering an event when a button was pressed, I realized the Microsoft code wasn't going to help. While it was great at creating the control and letting it run, there was not a way to access the actual hosted CLR control inside the VSNetHostShim ActiveX control. Without access to the control, I couldn't receive the event notification nor call methods on the control itself.

After a few moments of panic, thinking that I was going to have to rethink the Google control implementation, I remembered the System.Runtime.Remoting.ObjectHandle class that allows you to pass an object between application domains while controlling the instance. If I could somehow get the IObjectHandle interface from the VSNetToolHostShim2 returned to my C# code, I'd be all set. The good news is that calling AppDomain.CreateInstance, which is the method that has to be called in CVSNetToolWinShim::HostControl2 to create a .NET object, returns the IObjectHandle interface. The result is that the Microsoft hosting shim already had the IObjectHandle interface inside its implementation, but the necessary methods weren't there to expose it.

All I needed to do was add a new method to the VSNetToolHostShim2 ActiveX control that just did a QueryInterface on the ObjectHandle class member data for the IID_IObjectHandle. You can see the one line of code in the CVSNetToolWinShim::GetObjectHandle method.

Once your .NET written add-in has the ObjectHandle, all it needs to do is call the Unwrap method and cast the return value to the appropriate type and you've got a reference to the hosted control. While I really wanted my CVSNetToolWinShim::GetObjectHandle method to return the ObjectHandle directly, which I would have done by importing MSCOREE.IDL in VSNetToolHostShim2.IDL, the MSCOREE.IDL file contains references to files that don't exist so it does not compile if imported or if MSCOREE.H is included in the IDL file. Consequently, I return IUnknown and you are required to cast the returned value to an ObjectHandle in order to call the Unwrap method.

If the last few paragraphs were a little confusing to you, all you need to do is read Figure 7, which shows the method in the Google add-in that creates the ToolWindow and gets the Google Control hosted properly. For your own tool windows, you can use the code to ensure your hosted controls are properly set up.

Figure 7 Hosting Controls in a ToolWindow

/// <summary> /// Takes care of ensuring the window is created. /// </summary> private void ConstructWindow ( ) { try { // This'll contain the VSNetToolHostShim2 on output. Object RefObj = null ; // Create the main tool window by loading host shim. m_TheToolWindow = m_ApplicationObject.Windows. CreateToolWindow ( m_AddInInstance , "VSNetToolHostShim2.VSNetToolWinShim2", ResConst.ToolWinCaption, ResConst.ToolWinGUID, ref RefObj); // Make the window visible. You must do this before // calling the HostUserControl method or things won't // get hooked up right. m_TheToolWindow.Visible = true ; // Get the shim. VSNetToolHostShimLib2.IVSNetToolWinShim2 ShimObj = (VSNetToolHostShimLib2.VSNetToolWinShim2Class)RefObj ; // Get this assembly so I can pass the location to the shim. System.Reflection.Assembly CurrAsm = System.Reflection.Assembly.GetExecutingAssembly ( ) ; // Figure out the path to the the GoogleControl DLL. StringBuilder StrCtlDll = new StringBuilder ( ) ; String StrTemp = CurrAsm.Location.ToLower ( ) ; int iPos =StrTemp.IndexOf(ResConst.AddInName.ToLower ( ) ) ; StrCtlDll.Append ( CurrAsm.Location.Substring ( 0 , iPos )); StrCtlDll.Append ( ResConst.GoogleCtlDLL ) ; // Load the managed control into the ActiveX control // and have it load the tab bitmap. ShimObj.HostUserControl2 ( m_TheToolWindow, StrCtlDll.ToString ( ), ResConst.GoogleCtlName, m_AddInInstance.SatelliteDllPath, ResConst.TabBitmapID); // Ask the VSNetShim control for the IUnknown of the // hosted control. From the object handle, I can use // casting to get to the actual hosted GoogleControl. ObjectHandle oHnd = (ObjectHandle) ShimObj.GetObjectHandle ( ) ; GoogleCtl = (GoogleControl)oHnd.Unwrap ( ) ; // Set up to handle the events from the control. GoogleCtl.GoogleSearch += new GoogleControl. GoogleSearchEventHandler(GoogleCtl_GoogleSearch); // Initialize the user options. InitializeUserOptions ( m_ApplicationObject.RegistryRoot, GoogleCtl ); } catch ( System.Exception Ex ) { MessageBox.Show ( Ex.Message + "\r\n" + Ex.StackTrace ) ; throw Ex ; } }

The good news about all this messing around with the VSNetToolHostShim2 control is that you can use the control exactly as-is for any control you want to host inside Visual Studio .NET. Armed with VSNetToolHostShim2, you can now handle controls that fire events. As I did with the Google control, you can isolate functionality better and just maybe achieve the dream of truly reusable controls that can be plugged into multiple apps.

The final task for the Google add-in was to have it integrate seamlessly into the environment, I wanted to add a Google Search Window option to the View | Other Windows popup. According to my understanding of the documentation, all I had to do was get the "&View" CommandBar, enumerate through to the Other Windows popup menu, and poke my command on the end of the popup menu. I didn't think this would be much of a problem, but I went at it with a bad assumption.

My first step was to write the following macro and enumerate through the items on the "&View" menu and spit out their types:

Sub PrintMenu() Dim cw As New CmdWindow cw.Clear() Dim Cmds As _CommandBars Cmds = CType(DTE.CommandBars, _CommandBars) Dim FileMenu As CommandBar = Cmds("&View") For Each Ctrl As CommandBarControl In FileMenu.Controls cw.WriteLine(Ctrl.Caption + vbTab + Ctrl.Type.ToString()) Next End Sub

By the way, the CmdWindow is a simple class to encapsulate writing to the Command window and Output panes. The idea for my macro was to enumerate the controls, and output their types so I could see exactly what values I needed.

I was surprised to see that the Other Windows menu was not listed in the output and there were no types other than msoControlButton with each menu item. With a little poking around, I found I was able to enumerate the Other Windows menu on its own so it was easy enough to add my menu option in the correct place. My assumption was that I would have been able to get msoControlPopup types directly from the CommandBar.

Wrap Up

While it was a wild journey, I eventually ended up with an add-in that I use every day to slay development problems that creep up. Google is an amazing search engine and now that I have it in my IDE, I wonder how I lived without it.

The Tips

In my last column, there was only enough room to publish one of the two tips. To make up for the missing tip, this month you have a whopping three! If you have any tips to add to the collection, don't hesitate to send them to me at john@wintellect.com.

Tip 56 The Microsoft Visual Studio team has put together a collection of power toy add-ins for Visual Studio that you might find useful. You can download them at PowerToys for Visual Studio .NET 2003. The Visual Basic Commenter, which lets you put XML comments in Visual Basic .NET, and the Custom Help Builder, which makes it easy to add your own class documentation to the Help System, are both outstanding additions to the IDE.

Tip 57 To hasten the startup of the Visual Studio .NET IDE, get rid of the Start Page. Because the Start Page requires all Web-browsing components to be loaded, you can chop off a considerable amount of startup time by skipping it. To turn off the Start Page, select Options from the Tools menu to bring up the Options dialog. In the Environment/General property page, select "Show empty environment" in the "at Startup" combobox.

Tip 58 Another performance tip is to turn off tracking the active item in the Solution Explorer window. This will keep the Solution Explorer selection from bouncing all over the place when working on different files in a project. From the Tools menu, select Options. In the Options dialog, select the Environment/Projects and Solutions property page and uncheck Track Active Item in Solution Explorer.

Send your questions and comments for John to  slayer@microsoft.com.

John Robbins is a cofounder of Wintellect, a software consulting, education, and development firm that specializes in programming for the .NET Framework and Windows. His latest book is Debugging Applications for Microsoft .NET and Microsoft Windows (Microsoft Press, 2003). You can contact John at https://www.wintellect.com.