VSX: How to detect when Property Pages (Project Properties) window is closed

I posted a thread on the MSDN forums to seek for help in detecting when the property pages window (accessible through project->properties) in Visual Studio is closed. You can follow up the thread here: Detect when project properties window is closed. Ziwei Chen was really helpful and could find me a way to achieve this in C# projects but to my surprise, to achieve the same results for C++ projects proved to be harder than I thought.

I decided to write this short tutorial to help those who have the same problem in future.

First of we need to detect when the user click in any project->properties menu selection. Put this code where you initialize your add-in:

m_commandEventsAfterExecuteProperties1 =
    m_applicationObject.DTE.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 396);
m_commandEventsAfterExecuteProperties1.AfterExecute +=
    new _dispCommandEvents_AfterExecuteEventHandler(CommandEvents_AfterExecuteProperties);

m_commandEventsAfterExecuteProperties2 =
    m_applicationObject.DTE.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 397);
m_commandEventsAfterExecuteProperties2.AfterExecute +=
    new _dispCommandEvents_AfterExecuteEventHandler(CommandEvents_AfterExecuteProperties);

These command events ids were found using the technique described in this post: How to find Visual Studio command bars. When registering the event handlers be careful with this: Visual Studio .NET events being disconnected from add-in. So declare these as member attributes and not as method variables:

private CommandEvents m_commandEventsAfterExecuteProperties1;
private CommandEvents m_commandEventsAfterExecuteProperties2;

Now, lets dig into some rather awful code to get our property pages handle. Property pages in .NET projects are document windows but, unfortunately, if we try to call the ActiveDocument property directly through the ActiveEnvDTE library, it will throw an exception. So, with the precious help of Chen, this is the code came up with:

// For C# projects the property pages is a document window, lets try to find it.
// I was unable to get a handle using the EnvDTE library which could have eased things a bit.
bool propertyPagesFound = false;

IVsWindowFrame[] frames = new IVsWindowFrame[1];
uint numFrames;

IEnumWindowFrames ppenum;
m_uiShell.GetDocumentWindowEnum(out ppenum);

while (ppenum.Next(1, frames, out numFrames) == VSConstants.S_OK && numFrames == 1)
{
	m_frame = frames[0] as IVsWindowFrame;
	object title;
	m_frame.GetProperty((int)__VSFPROPID.VSFPROPID_Caption, out title);

	// I really don't like the way I'm using to retrieve the property pages handle.
	//TODO: CHANGE IF BETTER SOLUTION IS FOUND.
	if ((title as String).ToLowerInvariant() == GetStartupProject().Name.ToLowerInvariant())
	{
		propertyPagesFound = true;
		((IVsWindowFrame2)m_frame).Advise(this, out m_cookieWindowFrameNotify);
	}
}

First, all document windows are retrieved to the ppenum variable and next we go one by one and test its caption name to see if it’s equal to the project’s name. After we got our desired window frame we register the IVsWindowNotifyFrame event handler.

In the C++ case, things are a bit different. The property pages window isn’t an instance of document or frame, it’s a dialog window. To catch the dialog window events we use an auxiliary class that listens for the destroy message in the WndProc method.

// If we didn't find any property pages before then we need to go and search for a C++ one since they're
// different from C# property pages.
if (!propertyPagesFound)
{
	//NOTE: This might not work well if we happen to have two Visual Studio instances with the same
	//      project names.
	//TODO: TRY TO FIND A WAY TO FIX IT.
	//HACK: Go through all windows to find the project properties dialog window that matters.
	FindWindowLike.Window[] list =
	    FindWindowLike.Find(IntPtr.Zero, GetStartupProject().Name.ToLowerInvariant() + " property pages", "");
	
	// Release any handle we might have.
	if (m_subclassedMainWindow != null)
	{
		m_subclassedMainWindow.ReleaseHandle();
		m_subclassedMainWindow = null;
	}

	if (m_subclassedMainWindow == null && list != null && list.Length >= 1)
	{
		m_subclassedMainWindow = new SubclassedWindow(list[0].Handle);
	}
}

Apart from that FindWindowLike that we’ll check later, I think the code is pretty simple to understand. I really don’t like to have to search through all windows in the system but apparently the dialog window couldn’t be found under the dte’s MainWindow.HWnd hierarchy.

 

As for the event implementations, here is the C# first:

public int OnShow(int fShow)
{
	switch (fShow)
	{
		case (int)__FRAMESHOW.FRAMESHOW_TabDeactivated:
			// Property pages lost focus, put your code here!
			((IVsWindowFrame2)m_frame).Unadvise(m_cookieWindowFrameNotify);
			m_cookieWindowFrameNotify = 0;
			break;

		default:
			break;

	}

	return VSConstants.S_OK;
}

We need to implement the IVsWindowFrameNotify interface and in the OnShow you can check when the tab is closed. I used the FRAMESHOW_TabDeactivated because it is called when the tab loses focus and also when the tab closes.

Now for the C++ version:

/// 
/// Auxiliary window to intercept messages sent to the property pages dialog window.
/// 
public class SubclassedWindow : NativeWindow
{
	private const int WM_NCDESTROY = 0x0082;

	public SubclassedWindow(IntPtr hWnd)
	{
		AssignHandle(hWnd);
	}

	protected override void WndProc(ref Message m)
	{
		if (m.Msg == WM_NCDESTROY)
		{
			// Property pages window was closed, put your code here!
		}
		base.WndProc(ref m);
	}
}

As explained before, in this case we have an auxiliary window to catch the destroy event for the dialog window.

I found this script over at FindWindowLike for C# and changed it a bit to fit my needs. I modified the code that compared the window title; created a new class and changed p/invoke methods’ signature just to keep VS quiet about some warnings.

/// <summary>
/// Now we have to declare p/invoke methods in a class called NativeMethods (or similar) to
/// shut up some VS warnings.
/// </summary>
internal static class NativeMethods
{
	[DllImport("user32")]
	internal static extern IntPtr GetWindow(IntPtr hwnd, int wCmd);

	[DllImport("user32")]
	internal static extern IntPtr GetDesktopWindow();

	[DllImport("user32", EntryPoint = "GetWindowLongW")]
	internal static extern int GetWindowLong(IntPtr hwnd, int nIndex);

	[DllImport("user32")]
	internal static extern IntPtr GetParent(IntPtr hwnd);

	[DllImport("user32", EntryPoint = "GetClassNameW", CharSet = CharSet.Unicode)]
	internal static extern int GetClassName(IntPtr hWnd, [Out] StringBuilder lpClassName, int nMaxCount);

	[DllImport("user32", EntryPoint = "GetWindowTextW", CharSet = CharSet.Unicode)]
	internal static extern int GetWindowText(IntPtr hWnd, [Out] StringBuilder lpString, int nMaxCount);
}

// FROM: http://www.experts-exchange.com/Programming/Languages/C_Sharp/Q_21611201.html
// Just changed the way it checked strings.
internal class FindWindowLike
{
	internal class Window
	{
		public string Title;
		public string Class;
		public IntPtr Handle;
	}

	private const int GWL_ID = (-12);
	private const int GW_HWNDNEXT = 2;
	private const int GW_CHILD = 5;

	public static Window[] Find(IntPtr hwndStart, string findText, string findClassName)
	{
		ArrayList windows = DoSearch(hwndStart, findText, findClassName);

		return (Window[])windows.ToArray(typeof(Window));

	} //Find


	private static ArrayList DoSearch(IntPtr hwndStart, string findText, string findClassName)
	{
		ArrayList list = new ArrayList();

		if (hwndStart == IntPtr.Zero)
			hwndStart = NativeMethods.GetDesktopWindow();

		IntPtr hwnd = NativeMethods.GetWindow(hwndStart, GW_CHILD);
		while (hwnd != IntPtr.Zero)
		{
			// Recursively search for child windows.
			list.AddRange(DoSearch(hwnd, findText, findClassName));

			StringBuilder text = new StringBuilder(255);
			int rtn = NativeMethods.GetWindowText(hwnd, text, 255);
			string windowText = text.ToString();
			windowText = windowText.Substring(0, rtn);

			StringBuilder cls = new StringBuilder(255);
			rtn = NativeMethods.GetClassName(hwnd, cls, 255);
			string className = cls.ToString();
			className = className.Substring(0, rtn);

			if (NativeMethods.GetParent(hwnd) != IntPtr.Zero)
				rtn = NativeMethods.GetWindowLong(hwnd, GWL_ID);

			if (windowText.Length > 0 &&
				windowText.ToUpperInvariant().Contains(findText.ToUpperInvariant()) &&
				(className.Length == 0 ||
					className.ToUpperInvariant().Contains(findClassName.ToUpperInvariant())))
			{
				Window currentWindow = new Window();

				currentWindow.Title = windowText;
				currentWindow.Class = className;
				currentWindow.Handle = hwnd;

				list.Add(currentWindow);
			}

			hwnd = NativeMethods.GetWindow(hwnd, GW_HWNDNEXT);
		}

		return list;
	} //DoSearch
} //Class

And that’s it! I’d really appreciate if someone could tell me how to improve some of this code, mostly the parts where I have TODOs and/or HACK.
Thank you and I hope you find this useful!

Thanks to:

I wanted to thank Ziwei Chen for the excellent help at the forums.

References

VSSDK IDE Sample: Combo Box
Detect when project properties window is closed
I need FindWindowLike for C#
How to find Visual Studio command bars
Visual Studio .NET events being disconnected from add-in

Links:

Visual Studio Extensibility Forum
VSX Home on Code Gallery
Visual Studio on MSDN

Tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *