Roundtrip Shell Programming

It is easier to manipulate a CBFS Shell Namespace Extension from the .NET server, as illustrated in the following image:


The higher parts of the image show the standard CBFS Shell architecture with Explorer (or another application) calling the CBFS Shell .NET server in a pull or fetch mode. The .NET server always reacts to something that was initiated by the Shell itself or by end-user interactions with the Shell.

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 or columns.

Threading

It's very important that these Shell Programming calls do not run in the context of implicit threads (the ones implicitly created by incoming RPC calls from the Shell). Running those calls 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.

The following example code manages the CBFS Shell 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 CBFS Shell 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 an end user may change the UI (close windows, etc.) at any time
                if (view != null)
                {
                    var folderView = view.FolderView;
                    if (folderView != null)
                    {
                        folderView.ViewMode = FOLDERVIEWMODE.FVM_DETAILS;
                    }
                }
                break;
            ....
        }
    }
}

Programming Model

Although it is always possible to write native or interoperability code against the Shell using its native API, CBFS Shell offers a .NET object model that wraps most of the interesting Shell concepts. It does so not necessarily in the context of CBFS Shell but as a general-purpose managed Shell API. CBFS Shell defines this model in the callback.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 also can create an item or open a stream of 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. This is useful to get known folders (like Desktop or My PC) as .NET objects.
  • Propertyy: 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 (e.g., mode, flags, sort columns, group by).
  • Browser: A Shell browser, wrapping a native IShellBrowser reference. Browser is useful to tell the view to navigate 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 CBFS Shell Namespace Extension folder.

Events Support

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, such as SelectionChanged, which is triggered when a ShellItem is selected.
  • Browser events: events related to "browser" applications, such as NavigateComplete2, which is triggered when the view goes from one Shell folder to another. Explorer and a Common Dialog host are the examples of "browsers".

The following example of a C# console application hooks what is happening on 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 do not carry arguments.

Common Dialog Support

Roundtrip Shell Programming does not support Shell views that are hosted in Common Dialog boxes in third-party applications. It only supports views that are opened by Explorer.

Common Dialog Shell views can be programmed like Explorer views only in the case of CBFS Shell Shell Namespace Extensions.

Security Considerations

For roundtrip programming between the CBFS Shell .NET server ("server") and Explorer to succeed, 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 an Explorer process. The server will be able do so only if the Explorer process was started as administrator, which is not the default behavior (and generally is not recommended).

Calling a Namespace Extension Programmatically

If you need to call your Namespace Extension from a custom external program, you may 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 a view, folder, or item access), you may reuse the Shell API and the CBFS Shell 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 may 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, e.g., a string here
             if ("StartupTime".Equals(e.Input))
             {
                    // we can use most standard .NET types, but remember this will cross process boundaries
                    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. The following example shows how this works 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 CBFS Shell 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 (e.g., C/C++). In C/C++, you would program it as follows (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;
}

Copyright (c) 2022 Callback Technologies, Inc. - All rights reserved.
CBFS Shell 2022 .NET Edition - Version 22.0 [Build 8367]