Tuesday, 22 March 2016

Refreshing "Programs and Features" Programmatically

I've just spent some time trying to solve the following problem: When the uninstaller for a piece of software is run outside of the Programs and Features view, how do you tell the Programs and Features view that its underlying data has changed and it should refresh itself?
It was pretty easy to find out how to enumerate the windows. It turns out all Windows Explorer and Internet Explorer windows implement a COM interface called IWebBrowserApp in an object that MSDN documents as the "InternetExplorer" object. A COM server called Shell.Application exposes a list of all currently-open windows in a collection called Windows().
The next step, though, is to figure out what kind of window each of these is. That is not as straightforward. Some of the InternetExplorer objects might actually be web browsers. Some might be folder views. Looking through the various properties at my disposal, identifying the ones that are showing the Programs and Features view has no obvious solution. The closest seems to be the LocationName property, but the problem with that property is that it is localized – that is to say, if you run the same code on e.g. a Japanese Windows installation, you’ll get back the string “プログラムと機能” instead of “Programs and Features”. Having this feature depend on the way the string gets translated into foreign languages is not going to be robust.
I made a post on an MSDN community forum asking if anyone had any advice. I got a fairly prompt, but terse, reply from a helpful commenter, suggesting that I compare the PIDL of the window with the canonical PIDL for “Programs and Features”. He listed a few COM interfaces – IShellFolder, IFolderView, IPersistFolder2 – that expose this type of functionality. The missing piece, though, is how to get from a reference to an InternetExplorer object into the Shell world and its PIDL-based interfaces.
The way to do it is to take advantage of the fact that the InternetExplorer object also implements IServiceProvider. IServiceProvider can then be queried for the service SID_STopLevelBrowser, which will return the IShellBrowser controlling the InternetExplorer. (I'm not certain, but it might be possible for an InternetExplorer to be hosted by something other than an IShellBrowser, in which case the call would fail. That doesn't apply to my scenario, though -- I just need to make my code resilient to the possibility.)
Once you have an IShellBrowser, you call QueryActiveShellView to get an IShellView reference. The object underlying that reference may also implement IFolderView, and if it does, that in turn gives you access to an IPersistFolder2 object via the GetFolder method. IPersistFolder2, finally, returns the view's PIDL via GetCurFolder. It’s a bit of a chain, but none of the steps along the way are hard.
As for what to do with the PIDL... The helpful forum commenter suggested parsing “FOLDERID_ControlPanelFolder\8\FOLDERID_ChangeRemovePrograms”, but didn’t give any indication of how to parse it. I tried all sorts of options, but I couldn't find any combination of input string and function that would do it. Finally, I researched whether the raw bytes of a PIDL would be stable, and it seems for things like Control Panel entries, yes, they will be, across sessions, reboots, machines, and so on. So, I used the above technique to get the PIDL for a Programs and Features window on my PC, and then broke it into its components and stashed those in my source code. Then, I reworked the code to compare the PIDL with what I'd stashed. On my development PC, it worked, but that wasn’t a surprise – I’d simply copied the PIDL from an existing window, and then moments later compared my copy with the original. They were the same – what did that prove? So, I copied my test app to a Windows 7 VM and ran it, and ... it didn't work?
Okay, so the Control Panel has two different views. The default view on a fresh installation is a fancy new categorized view, compared to the way the Control Panel worked up to XP. However, you can change the view to make it explicitly a flat, old-fashioned view. Turns out the PIDL for the "Programs and Features" view encodes either that intermediary category view ("Programs") or a placeholder for the flat view ("All Control Panel Items"). I contemplated encoding both forms of PIDL and doing exact matches, but in the end I implemented it by checking that the PIDL contains the component for "Control Panel" at some point, and the PIDL for "Programs and Features" at some point, and that the two appear in that relative order, ignoring all other ITEMIDs in the PIDL.
Hope this helps someone else trying to figure out some shell stuff, and thanks to Sheng Jiang on the forum for the pointers! :-)

No comments:

Post a Comment