A commonly-overlooked implementation detail of shell context menu extensions is the canonical verb, which is returned by the IContextMenu::GetCommandString
method and which can be passed to IContextMenu::InvokeCommand
. Canonical verbs permit your context menu to be consistently invoked programmatically, since they no longer need to rely on the menu text’s display name, which can change based on the user’s preferred language, or which can change due to the whim of the shell extension. (For example, the shell extension might decide that instead of a generic Revert to previous version menu item, it’ll tailor the text to say something like Revert to March 1, 2017 6:34pm, which may look nicer to the user, but it becomes impossible to identify programmatically.)
Another reason to support canonical verbs is to ensure that Explorer itself invokes your verb consistently.
A large category of Explorer hangs were traced back to hangs in shell context menu extensions. To mitigate this problem, Explorer actually juggles two context menus. The first context menu is shown to the user on the UI thread. If the user picks a command from the context menu, then Explorer hands the request to a background thread. The background thread creates a second context menu¹ and uses it to perform the requested operation. That way, if the operation hangs, it’s a background thread that hangs, and that isn’t quite so bad as hanging a UI thread.
Now, how does Explorer hand the request to a background thread? It takes the menu item the user selected and tries to obtain its corresponding canonical verb. If successful, then it uses that same canonical verb to invoke the command from the background thread. If the shell extension responsible for the menu item does not produce a canonical verb, then the shell applies various heuristics to the second context menu to try to find the item that appears to resemble the one selected by the user most closely.
If you cough up a canonical verb, then Explorer can reliably invoke your verb from the background thread. If you don’t, then you end up being subjected to a heuristic, and heuristics sometimes fail.
¹ Explorer cannot simply use the original context menu from a background thread because the original context menu is a COM object that was created on the UI thread, which is a single-threaded apartment. Using it from a background thread requires marshaling, but if you did that and invoked the verb from the background thread, the marshaler would simply switch back to the UI thread and perform the operation there, because it must respect the threading model of the COM object, and the COM object says, “I can be used only on the UI thread.”
Marshaling back to the UI thread to perform the operation means that if the operation hangs, we hang the UI thread, which was exactly the problem we were trying to avoid!
0 comments