- Introduction
- Developer’s Guide
- Samples
- Reference
Roundtrip Shell programming
Starting with ShellBoost version 1.4.0.0, it’s easier to manipulate a ShellBoost namespace extension from the .NET server itself, as explained in the image below:
The higher parts of the image show the standard ShellBoost architecture with the Explorer (or other application) calling the ShellBoost .NET server in a pull or fetch mode. The .NET server always *reacts* to something that was initiated by the Shell itself and/or end-user interactions with it.
With the new 1.4.0.0 version, it is now easier from the .NET server to call into the Shell itself, and especially into Shell Views opened on ShellBoost folders.
The typical scenario that this enables is to allow folder settings modifications from a context menu with a specific context item, for example when you want to add a context menu that allows the user to change the current view settings, columns, etc.
Threading
It’s very important that these Shell Programming calls do not run on the implicit threads (threads implicitly created by RPC incoming calls from the Shell). Running them in implicit threads will cause both the .NET server and the client process to hang immediately as they will be caught in an infinite loop.
Here is an example of a code that programs the ShellBoost view in another thread:
public class MyFolder : ShellFolder
{
public MyFolder(ShellFolder parent, string name)
: base(parent, new StringKeyShellItemId(name))
{
...
}
// hook the context menu so we can add an item to it
protected override void MergeContextMenu(ShellFolder folder, IReadOnlyList<ShellItem> items, ShellMenu existingMenu, ShellMenu appendMenu)
{
...
appendMenu.AddInvokeItemHandler(OnShellMenuItemInvoke);
....
var newItem = new ShellMenuItem(appendMenu, "Switch to Details View") { Tag = MenuCommand.SwitchToDetailsView };
appendMenu.Items.Add(newItem);
...
}
private enum MenuCommand
{
Unknown,
...
SwitchToDetailsView,
...
}
private async void OnShellMenuItemInvoke(object sender, ShellMenuInvokeEventArgs e)
{
// get the menu tag and execute accordingly
var menu = (ShellMenu)sender;
var mc = Conversions.ChangeType(e.MenuItem?.Tag, MenuCommand.Unknown);
switch (mc)
{
....
case MenuCommand.SwitchToDetailsView:
// Use ShellBoost context to get the current view
// You *must not* use Wait() or Result on GetShellBoostViewAsync but continue the task on the thread it was running on
var view = await ShellContext.Current.GetShellBoostViewAsync(this);
// Shell programming must be very defensive and must always check for nulls
// because then end-user may change the UI (close windows, etc.) any time
if (view != null)
{
var folderView = view.FolderView;
if (folderView != null)
{
folderView.ViewMode = FOLDERVIEWMODE.FVM_DETAILS;
}
}
break;
....
}
}
}
Programming model
Although it’s always possible to write native or interop code against the Shell using its native API, ShellBoost proposes a .NET object model that wraps most of the interesting Shell concepts, not necessarily in the context of ShellBoost but as a general-purpose managed Shell API. ShellBoost defines this model in the ShellBoost.Core.WindowsShell namespace, with the following classes:
View: A Shell View, wrapping a native IShellView reference. A view instance has a Folder, a FolderView, a Browser, and a list of Columns.
Item: A Shell Item, wrapping a native IShellItem reference. An item instance has names, and a list of properties. You can also create an item, open a stream on an item, for reading and/or writing operations.
Thumbnail: A Shell Item (or Folder) Thumbnail as displayed by the Shell. From a Thumbnail instance, you can get the corresponding bitmap (GDI+ or WPF) with transparency (alpha channel) support.
Folder: A Shell Folder, also wrapping a native IShellItem reference, capable of enumerating the children as Items.
KnownFolder: A Shell Known Folder, wrapping a native IKnownFolder reference. Useful to get known folders (like “Desktop”, “My PC”, etc.) as .NET objects.
Property: A Windows Property System’s Property. A property instance has a PropertyKey and a value.
FolderView: A Shell Folder View, wrapping a native IFolderView (and IFolderView2) reference. A folder view instance has many properties that can be configured (mode, flags, sort columns, group by, etc.)
Browser: A Shell Browser, wrapping a native IShellBrowser reference. Browser is useful to ask the view browse to other folders.
NamespaceTreeControl: A Shell Tree View, wrapping a native INamespaceTreeControl reference. This can be used to interact with the Explorer’s Tree View.
Menu and MenuItem: A Shell Menu, wrapping a native IContextMenu2 reference. This can be used to interact with Shell Item’s Context menus or “New” Menus.
ShellBoostView: A specialization of the View class that corresponds to a Shell View displaying a ShellBoost namespace extension folder.
Events support
Starting with ShellBoost version 1.7.0.0, the Roundtrip API can subscribe to standard Shell events:
Windows events: raised when windows are opened and closed on the desktop.
Folder View events: events related to a Shell view, for example SelectionChanged triggered when a ShellItem is selected.
Browser events: events related to “browser” applications such as NavigateComplete2 triggered when the view goes from one Shell Folder to another. The Explorer or a Common Dialog host are example of “browsers”.
Here is an example of a C# Console application that hooks what’s happening of the desktop:
static void Main()
{
using (var ev = new ViewEvents())
{
// hook windows events
ev.WindowRegistered += OnWindowRegistered;
ev.WindowRevoked += OnWindowRevoked;
// get the first shell view
var view = View.Windows.FirstOrDefault();
if (view == null)
{
// none was open, just open one on a given folder, here C:\
Item.FromParsingName("c:\\").OpenView(OFASI.OFASI_OPENFOLDER);
view = View.Windows.FirstOrDefault();
}
// hook folder view 'SelectionChanged' events on that view
var viewEvents = new FolderViewEvents(view);
viewEvents.SelectionChanged += OnSelectionChanged;
// get the view's browser and hook 'NavigateComplete2'
using (var browserEvents = new BrowserEvents(view.Browser))
{
browserEvents.NavigateComplete2 += (s, e) =>
{
// the view changes each time the end-user changes the current folder
if (viewEvents != null)
{
viewEvents.SelectionChanged -= OnSelectionChanged;
viewEvents.Dispose();
}
// NavigateComplete2 has arguments https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa768285(v=vs.85)
var newView = View.FromNativeObject(e.Arguments[1]);
Console.WriteLine("A new view was opened on " + newView.Folder.SIGDN_DESKTOPABSOLUTEPARSING);
viewEvents = new FolderViewEvents(newView);
viewEvents.SelectionChanged += OnSelectionChanged;
};
do
{
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Escape)
break;
}
while (true);
ev.WindowRegistered -= OnWindowRegistered;
ev.WindowRevoked -= OnWindowRevoked;
}
}
}
Note: By design, Window and FolderView events don’t carry arguments.
Common Dialog support
Roundtrip Shell Programming doesn’t support Shell views that are hosted in Common Dialog boxes in 3rd party applications. It only supports Views opened by the Windows Explorer.
Starting with ShellBoost version 1.8.2.0, Common Dialog Shell Views can be programmed like Explorer views, only in the case of ShellBoost Shell Namespace Extensions.
Security considerations
For roundtrip programming to succeed between the ShellBoost .NET server (“server”) and the Windows Explorer, they both must be running at the same security (and UAC) level. For example, if the .NET server is started as an administrator, it will not be able to communicate with a Windows Explorer process, unless the explorer process itself was started as administrator, which is not the default behavior (and is generally not recommended).
Calling a Namespace Extension programmatically
If you need to call your namespace extension from a custom external program, you can use the Roundtrip API, just like with any Shell object.
If you need to call it with custom commands not supported by standard namespace extensions (like view, folder, or item access for example), starting with ShellBoost version 1.7.0.4, you can reuse the Shell API and the ShellBoost RPC implementation. For example, you can override the ShellFolder’s OnCommandExec method like this:
private static Guid MyGuid = new Guid("775f7c12-9c5b-45a3-8bcf-9a8dd13b8965"); // TODO: change this guid!
private static int MyGetStatisticsCommand = 1234; // this is an arbitrary value
private static DateTime StartupTime = DateTime.Now;
// You must use your own command group (GUID) and identifiers
// as the Shell or any program can call this method with well-known guids and command id (like Windows’ SDK OLECMDID with Guid.Empty)
protected override void OnCommandExec(object sender, CommandExecEventArgs e)
{
...
if (e.Group == MyGuid && e.Id == MyGetStatisticsCommand)
{
// we can use e.Input as a custom argument, here a string
if ("StartupTime".Equals(e.Input))
{
// we can use most standard .NET types, but remember this will cross processes
e.Output = StartupTime;
}
else if ("IOEngine".Equals(e.Input))
{
// dictionaries are supported too
var dic = new Dictionary<string, object>();
dic["OutboundCalls"] = 12345678;
e.Output = dic;
}
// return OK
e.HResult = ShellUtilities.S_OK;
return;
}
...
base.OnCommandExec(sender, e);
}
From any other program, you can now call your .NET server, for example like this in a .NET Console Application:
class Program
{
private static Guid MyGuid = new Guid("775f7c12-9c5b-45a3-8bcf-9a8dd13b8965");
private static int MyGetStatisticsCommand = 1234;
[STAThread] // if you want to support dictionaries in ExecCommand, you must use STA threads
static void Main()
{
...
// get access to your root folder (the .NET server must be running) using a Shell moniker.
// in this context:
// {20D04FE0-3AEA-1069-A2D8-08002B30309D} is CLSID_MyComputer (because our ShellBoost Root Folder is configured to be a child of "This PC")
// {F3875120-C8BC-4E13-ACF6-C515D400F585} is your Shell Folder's Class Id
var item = Item.FromParsingName(@"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\::{F3875120-C8BC-4E13-ACF6-C515D400F585}");
// get some value
var startupTime = (DateTime)item.ExecCommand(MyGuid, MyGetStatisticsCommand, "StartupTime");
// get dictionary
var dic = item.ExecCommand(MyGuid, MyGetStatisticsCommand, "IOEngine");
...
}
}
Note this programmatic access is not limited to .NET. You can achieve the same result using any COM-capable language (C/C++, etc.). In C/C++, you would program it like this (using Visual Studio’s ATL library):
static CLSID MyGuid = { 0x775f7c12,0x9c5b,0x45a3,{0x8b,0xcf,0x9a,0x8d,0xd1,0x3b,0x89,0x65} };
static int MyGetStatisticsCommand = 1234;
int main()
{
// error checks ommitted
CoInitialize(nullptr);
{
...
CComPtr<IShellItem> item;
SHCreateItemFromParsingName(L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\::{0D0C11AE-0002-0000-0000-0000AE110C0D}", nullptr, IID_PPV_ARGS(&item));
CComPtr<IOleCommandTarget> target;
item->BindToHandler(nullptr, BHID_SFObject, IID_PPV_ARGS(&target));
CComVariant input(L"StartupTime");
CComVariant output;
target->Exec(&MyGuid, MyGetStatisticsCommand, 0, &input, &output);
...
}
CoUninitialize();
return 0;
}