using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; using System.Linq; using System.Runtime.InteropServices.ComTypes; using System.Windows.Forms; using Vanara.PInvoke; using static Vanara.PInvoke.Shell32; namespace Vanara.Windows.Shell; /// The direction argument for NavigateFromHistory() public enum NavigationLogDirection { /// Navigates forward through the navigation log Forward, /// Navigates backward through the travel log Backward } /// Undocumented Flags used by Callback Handler. public enum SFVMUD { SFVM_SELECTIONCHANGED = 8, SFVM_DRAWMENUITEM = 9, SFVM_MEASUREMENUITEM = 10, SFVM_EXITMENULOOP = 11, SFVM_VIEWRELEASE = 12, SFVM_GETNAMELENGTH = 13, SFVM_WINDOWCLOSING = 16, SFVM_LISTREFRESHED = 17, SFVM_WINDOWFOCUSED = 18, SFVM_REGISTERCOPYHOOK = 20, SFVM_COPYHOOKCALLBACK = 21, SFVM_ADDINGOBJECT = 29, SFVM_REMOVINGOBJECT = 30, SFVM_GETCOMMANDDIR = 33, SFVM_GETCOLUMNSTREAM = 34, SFVM_CANSELECTALL = 35, SFVM_ISSTRICTREFRESH = 37, SFVM_ISCHILDOBJECT = 38, SFVM_GETEXTVIEWS = 40, SFVM_GET_CUSTOMVIEWINFO = 77, SFVM_ENUMERATEDITEMS = 79, // It seems this msg never gets sent, using Win 10 at least. SFVM_GET_VIEW_DATA = 80, SFVM_GET_WEBVIEW_LAYOUT = 82, SFVM_GET_WEBVIEW_CONTENT = 83, SFVM_GET_WEBVIEW_TASKS = 84, SFVM_GET_WEBVIEW_THEME = 86, SFVM_GETDEFERREDVIEWSETTINGS = 92, } /// Indicates the viewing mode of the ShellBrowser public enum ShellBrowserViewMode { /// Choose the best view mode for the folder Auto = FOLDERVIEWMODE.FVM_AUTO, /// (New for Windows7) Content = FOLDERVIEWMODE.FVM_CONTENT, /// Object names and other selected information, such as the size or date last updated, are shown. Details = FOLDERVIEWMODE.FVM_DETAILS, /// The view should display medium-size icons. Icon = FOLDERVIEWMODE.FVM_ICON, /// Object names are displayed in a list view. List = FOLDERVIEWMODE.FVM_LIST, /// The view should display small icons. SmallIcon = FOLDERVIEWMODE.FVM_SMALLICON, /// The view should display thumbnail icons. Thumbnail = FOLDERVIEWMODE.FVM_THUMBNAIL, /// The view should display icons in a filmstrip format. ThumbStrip = FOLDERVIEWMODE.FVM_THUMBSTRIP, /// The view should display large icons. Tile = FOLDERVIEWMODE.FVM_TILE } /// /// Encapsulates a -Implementation within an .

Implements the following /// Interfaces:
/// -
/// -
/// -

For more Information on used techniques see:
/// -


Known Issues:
/// - Using windows 10, the virtual Quick-Access folder doesn't get displayed properly. It has to be grouped by "Group" (as shown in /// Windows Explorer UI), but I couldn't find the OLE-Property for this. Also, if using Groups, the Frequent Files List doesn't have its /// Icons. Maybe we have to bind to another version of ComCtrls to get this rendered properly - That's just an idea though, cause the /// Collapse-/Expand-Icons of the Groups have the Windows Vista / Windows 7-Theme, not the Windows 10 Theme as I can see.
/// - DONE: Keyboard input doesn't work so far.
/// - DONE: Only Details-Mode should have column headers: (Using Shell32.FOLDERFLAGS.FWF_NOHEADERINALLVIEWS)
https://stackoverflow.com/questions/11776266/ishellview-columnheaders-not-hidden-if-autoview-does-not-choose-details /// - TODO: CustomDraw, when currently no shellView available
/// - DONE: Network folder: E_FAIL => DONE: Returning HRESULT.E_NOTIMPL from MessageSFVCB fixes this
/// - DONE: Disk Drive (empty): E_CANCELLED_BY_USER
/// - DONE: Disable header in Details view when grouping is enabled /// - DONE: Creating ViewWindow using '.CreateViewWindow()' fails on Zip-Folders; => Fixed again by returning HRESULT.E_NOTIMPL from MessageSFVCB /// - TODO: internal static readonly bool IsMinVista = Environment.OSVersion.Version.Major >= 6; // TODO: We use one interface, /// afaik, that only works in vista and above: IFolderView2 /// - TODO: Windows 10' Quick Access folder has a special type of grouping, can't find out how this works yet. As soon as we would be /// able to get all the available properties for an particular item, we would be able found out how this grouping works. However, it /// seems to be a special group, since folders are Tiles, whereas files are shown in Details mode. /// - NOTE: The grouping is done by 'Group'. Activate it using "Group by->More->Group", and then do the grouping. However, the /// Icons for 'Recent Files'-Group get lost. /// - TODO: ViewMode-Property, Thumbnailsize => Set ThumbnailSize for Large, ExtraLarge, etc. /// - DONE: Keyboard-Handling /// - DONE: BrowseObject ->Parent -> Relative /// - TODO: Properties in design editor!!! /// - TODO: Write History correctly! /// - TODO: Check getting / losing Focus! again /// - TODO: Context-Menu -> "Open File Location" doesn't work on folder "Quick Access" /// - TODO: When columns get reordered in details mode, then navigate to another folder, then back => column content gets messed /// /// NOTE: https://stackoverflow.com/questions/7698602/how-to-get-embedded-explorer-ishellview-to-be-browsable-i-e-trigger-browseobje /// NOTE: https://stackoverflow.com/questions/54390268/getting-the-current-ishellview-user-is-interacting-with /// NOTE: https://www.codeproject.com/Articles/35197/Undocumented-List-View-Features // IMPORTANT! /// NOTE: https://answers.microsoft.com/en-us/windows/forum/windows_10-files-winpc/windows-10-quick-access-folders-grouped-separately/ecd4be4a-1847-4327-8c44-5aa96e0120b8 ///
[ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] [Description("A Shell object that displays a list of Shell Items.")] [Guid("B8B0F852-9527-4CA8-AB1D-648AE95B618E")] public class ShellBrowser : UserControl, IWin32Window, IShellBrowser, Shell32.IServiceProvider { private const string defEmptyFolderText = "This folder is empty."; internal const int defaultThumbnailSize = 32; private const string processCmdKeyClassNameEdit = "Edit"; private const int processCmdKeyClassNameMaxLength = 31; private readonly StringBuilder processCmdKeyClassName = new(processCmdKeyClassNameMaxLength + 1); /// Required designer variable. private IContainer components; private string emptyFolderText = defEmptyFolderText; private FOLDERSETTINGS folderSettings = new(FOLDERVIEWMODE.FVM_AUTO, FOLDERFLAGS.FWF_NOHEADERINALLVIEWS | FOLDERFLAGS.FWF_NOWEBVIEW | FOLDERFLAGS.FWF_USESEARCHFOLDER); private IStream? viewStateStream; private string? viewStateStreamIdentifier; /// Initializes a new instance of the class. public ShellBrowser() : base() { InitializeComponent(); History = new ShellNavigationHistory(); Items = new ShellItemCollection(this, SVGIO.SVGIO_ALLVIEW); SelectedItems = new ShellItemCollection(this, SVGIO.SVGIO_SELECTION); } /// Fires when the Items collection changes. [Category("Action"), Description("Items changed.")] public event EventHandler? ItemsChanged; /// Fires when ShellBrowser has navigated to a new folder. [Category("Action"), Description("ShellBowser has navigated to a new folder.")] public event EventHandler? Navigated; /// Fires when the SelectedItems collection changes. [Category("Behavior"), Description("Selection changed.")] public event EventHandler? SelectionChanged; /// The default text that is displayed when an empty folder is shown [Category("Appearance"), DefaultValue(defEmptyFolderText), Description("The default text that is displayed when an empty folder is shown.")] public string EmptyFolderText { get => emptyFolderText; set { emptyFolderText = value; if (IsHandlerValid) ViewHandler!.Text = value; } } /// Contains the navigation history of the ShellBrowser [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ShellNavigationHistory History { get; private set; } /// The set of ShellItems in the ShellBrowser [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IReadOnlyList Items { get; } /// The set of selected ShellItems in the ShellBrowser [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IReadOnlyList SelectedItems { get; } /// The size of the thumbnails in pixels. [Category("Appearance"), DefaultValue(defaultThumbnailSize), Description("The size of the thumbnails in pixels.")] public int ThumbnailSize { get => IsHandlerValid ? ViewHandler!.ThumbnailSize : defaultThumbnailSize; set { if (IsHandlerValid) ViewHandler!.ThumbnailSize = value; } } /// The viewing mode of the ShellBrowser /// Internally, this uses LVM_SETVIEW and LVM_GETVIEW messages on the ListView control [Category("Appearance"), DefaultValue(typeof(ShellBrowserViewMode), "Auto"), Description("The viewing mode of the ShellBrowser.")] public ShellBrowserViewMode ViewMode { get => (ShellBrowserViewMode)folderSettings.ViewMode; set { // TODO: Set ThumbnailSize accordingly? folderSettings.ViewMode = (FOLDERVIEWMODE)value; if (IsHandlerValid) ViewHandler!.ViewMode = folderSettings.ViewMode; } } /// The Registry Key where Browser ViewStates get serialized /// Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Streams\\ [Category("Behavior"), Description("The Registry Key where Browser ViewStates get serialized.")] public string ViewStateRegistryKey { get; set; } = $"Software\\{Application.CompanyName}\\{Application.ProductName}\\ShellBrowser\\ViewStateStreams"; /// ///

/// Note: I've tried using ComCtl32.ListViewMessage.LVM_SETBKIMAGE, but this doesn't work properly. That's why this property has /// been hidden. ///
[Bindable(false), Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override Image BackgroundImage => base.BackgroundImage; /// protected override Size DefaultSize => new(200, 150); /// The that is currently in use. protected ShellBrowserViewHandler? ViewHandler { get; private set; } /// /// /// /// HRESULT.STG_E_PATHNOTFOUND if path n found public HRESULT BrowseObject(IntPtr pidl, SBSP wFlags) { ShellItem? shellObject = null; // The given PIDL equals Desktop, so ignore the other flags if (ShellFolder.Desktop.PIDL.Equals(pidl)) { shellObject = new ShellItem(ShellFolder.Desktop.PIDL); } // SBSP_NAVIGATEBACK stands for the last item in the navigation history list (and ignores the pidl) else if (wFlags.HasFlag(SBSP.SBSP_NAVIGATEBACK)) { if (History.CanSeekBackward) shellObject = History.SeekBackward(); else return HRESULT.STG_E_PATHNOTFOUND; } // SBSP_NAVIGATEFORWARD stands for the next item in the navigation history list (and ignores the pidl) else if (wFlags.HasFlag(SBSP.SBSP_NAVIGATEFORWARD)) { if (History.CanSeekForward) shellObject = History.SeekForward(); else return HRESULT.STG_E_PATHNOTFOUND; } // SBSP_RELATIVE stands for a pidl relative to the current folder else if (wFlags.HasFlag(SBSP.SBSP_RELATIVE)) { ShellItem? currentObject = History.Current; if (currentObject is null) return HRESULT.STG_E_PATHNOTFOUND; shellObject = new ShellItem(ILCombine((IntPtr)currentObject.PIDL, pidl)); } // SBSP_PARENT stands for the parent folder (and ignores the pidl) else if (wFlags.HasFlag(SBSP.SBSP_PARENT)) { ShellItem? currentObject = History.Current; ShellFolder? parentObject = currentObject?.Parent; if ((parentObject is not null) && parentObject.PIDL.IsParentOf(currentObject!.PIDL)) shellObject = parentObject; else return HRESULT.STG_E_PATHNOTFOUND; } // SBSP_ABSOLUTE as the remaining option stands for an absolute pidl that is given else { // Remember we are not the owner of this pidl, so clone it to have our own copy on the heap. shellObject = new ShellItem(new PIDL(pidl, true)); } if (InvokeRequired) BeginInvoke(() => BrowseShellItemInternal(shellObject!)); else BrowseShellItemInternal(shellObject!); return HRESULT.S_OK; void BrowseShellItemInternal(ShellItem shellItem) { // Save ViewState of current folder GetValidHandler()?.ShellView?.SaveViewState(); if (viewStateStream is not null) Marshal.ReleaseComObject(viewStateStream); viewStateStreamIdentifier = shellItem.ParsingName; var viewHandler = new ShellBrowserViewHandler(this, new ShellFolder(shellItem), folderSettings, emptyFolderText); // Clone the PIDL, to have our own object copy on the heap! if (!wFlags.HasFlag(SBSP.SBSP_WRITENOHISTORY)) History.Add(new ShellItem(new PIDL(viewHandler.ShellFolder.PIDL))); ShellBrowserViewHandler? oldViewHandler = ViewHandler; ViewHandler = viewHandler; oldViewHandler?.UIDeactivate(); viewHandler.UIActivate(); oldViewHandler?.DestroyView(); OnNavigated(viewHandler.ShellFolder); OnSelectionChanged(); } } /// public HRESULT ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL; /// public HRESULT EnableModelessSB(bool fEnable) => HRESULT.E_NOTIMPL; /// public HRESULT GetControlWindow(FCW id, out HWND hwnd) { hwnd = HWND.NULL; return HRESULT.E_NOTIMPL; } /// public HRESULT GetViewStateStream(STGM grfMode, [MaybeNull] out IStream stream) { if (viewStateStream is not null) Marshal.ReleaseComObject(viewStateStream); stream = viewStateStream = ShlwApi.SHOpenRegStream2(HKEY.HKEY_CURRENT_USER, ViewStateRegistryKey, viewStateStreamIdentifier, grfMode); return stream is null ? HRESULT.E_FAIL : HRESULT.S_OK; } /// public HRESULT GetWindow(out HWND phwnd) { phwnd = Handle; return HRESULT.S_OK; } /// public HRESULT InsertMenusSB(HMENU hmenuShared, ref Ole32.OLEMENUGROUPWIDTHS lpMenuWidths) => HRESULT.E_NOTIMPL; /// /// Navigates to the last item in the navigation history list. This does not change the set of locations in the navigation log. /// /// True if the navigation succeeded, false if it failed for any reason. public bool NavigateBack() => BrowseObject(IntPtr.Zero, SBSP.SBSP_NAVIGATEBACK).Succeeded; /// /// Navigates to the next item in the navigation history list. This does not change the set of locations in the navigation log. /// /// True if the navigation succeeded, false if it failed for any reason. public bool NavigateForward() => BrowseObject(IntPtr.Zero, SBSP.SBSP_NAVIGATEFORWARD).Succeeded; /// /// Navigate within the navigation log in a specific direciton. This does not change the set of locations in the navigation log. /// /// The direction to navigate within the navigation logs collection. /// True if the navigation succeeded, false if it failed for any reason. public bool NavigateFromHistory(NavigationLogDirection direction) => direction switch { NavigationLogDirection.Backward => NavigateBack(), NavigationLogDirection.Forward => NavigateForward(), _ => false, }; /// Navigates to the parent folder. /// True if the navigation succeeded, false if it failed for any reason. public bool NavigateParent() => BrowseObject(IntPtr.Zero, SBSP.SBSP_PARENT).Succeeded; /// Navigate within the navigation log. This does not change the set of locations in the navigation log. /// An index into the navigation logs Locations collection. /// True if the navigation succeeded, false if it failed for any reason. public bool NavigateToHistoryIndex(int historyIndex) { using ShellItem? shellFolder = History.Seek(historyIndex, SeekOrigin.Current); return shellFolder is not null && BrowseObject((IntPtr)shellFolder.PIDL, SBSP.SBSP_ABSOLUTE | SBSP.SBSP_WRITENOHISTORY).Succeeded; } /// public HRESULT OnViewWindowActive(IShellView ppshv) => HRESULT.E_NOTIMPL; /// public HRESULT QueryActiveShellView([MaybeNull] out IShellView shellView) { if (IsHandlerValid) { Marshal.AddRef(Marshal.GetIUnknownForObject(ViewHandler!.ShellView!)); shellView = ViewHandler.ShellView; return HRESULT.S_OK; } shellView = null; return HRESULT.E_PENDING; } /// public HRESULT RemoveMenusSB(HMENU hmenuShared) => HRESULT.E_NOTIMPL; /// Selects all items in the current view. public void SelectAll() { ShellBrowserViewHandler? viewHandler = GetValidHandler(); if (viewHandler is not null) { // NOTE: The for-loop is rather slow, so send (Ctrl+A)-KeyDown-Message instead and let the ShellView do the work for (var i // = 0; i < viewHandler.FolderView2.ItemCount(SVGIO.SVGIO_ALLVIEW); i++) viewHandler.FolderView2.SelectItem(i, SVSIF.SVSI_SELECT); // // TODO: Another way would be to use this Windows Message-Pattern (Workaround #2): https://stackoverflow.com/questions/9039989/how-to-selectall-in-a-winforms-virtual-listview var msg = new Message() { HWnd = (IntPtr)viewHandler.ViewWindow, Msg = (int)User32.WindowMessage.WM_KEYDOWN, }; ProcessCmdKey(ref msg, Keys.Control | Keys.A); } } /// public HRESULT SendControlMsg(FCW id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret) { pret = IntPtr.Zero; return HRESULT.E_NOTIMPL; } /// public HRESULT SetMenuSB(HMENU hmenuShared, IntPtr holemenuRes, HWND hwndActiveObject) => HRESULT.E_NOTIMPL; /// public HRESULT SetStatusTextSB(string pszStatusText) => HRESULT.E_NOTIMPL; /// public HRESULT SetToolbarItems(ComCtl32.TBBUTTON[]? lpButtons, uint nButtons, FCT uFlags) => HRESULT.E_NOTIMPL; /// public HRESULT TranslateAcceleratorSB(ref MSG pmsg, ushort wID) => HRESULT.E_NOTIMPL; /// Unselects all items in the current view. public void UnselectAll() => GetValidHandler()?.FolderView2?.SelectItem(-1, SVSIF.SVSI_DESELECTOTHERS); /// /// -Interface Implementation for .

Responds to the /// following Interfaces:
/// -
/// -
///
/// The service's unique identifier (SID). /// The IID of the desired service interface. /// /// When this method returns, contains the interface pointer requested riid. If successful, the calling application is responsible /// for calling IUnknown::Release using this value when the service is no longer needed. In the case of failure, this value is NULL. /// /// or
HRESULT Shell32.IServiceProvider.QueryService(in Guid guidService, in Guid riid, out IntPtr ppvObject) { // IShellBrowser: Guid("000214E2-0000-0000-C000-000000000046") if (riid.Equals(typeof(IShellBrowser).GUID)) { ppvObject = Marshal.GetComInterfaceForObject(this, typeof(IShellBrowser)); return HRESULT.S_OK; } // IShellFolderViewCB: Guid("2047E320-F2A9-11CE-AE65-08002B2E1262") if (riid.Equals(typeof(IShellFolderViewCB).GUID)) { ShellBrowserViewHandler? shvwHandler = GetValidHandler(); if (shvwHandler is not null) { ppvObject = Marshal.GetComInterfaceForObject(shvwHandler, typeof(IShellFolderViewCB)); return HRESULT.S_OK; } } ppvObject = IntPtr.Zero; return HRESULT.E_NOINTERFACE; } /// Gets the items in the ShellBrowser as an IShellItemArray /// An instance or if not available. internal IShellItemArray? GetItemsArray(SVGIO opt) { try { return GetValidHandler()?.FolderView2?.Items(opt) ?? null; } catch { return null; } } /// Raises the event. protected internal virtual void OnItemsChanged() => ItemsChanged?.Invoke(this, EventArgs.Empty); /// Raises the event. protected internal virtual void OnNavigated(ShellFolder shellFolder) { if (Navigated is not null) { ShellBrowserNavigatedEventArgs eventArgs = new(shellFolder); Navigated.Invoke(this, eventArgs); } } /// Raises the event. protected internal virtual void OnSelectionChanged() => SelectionChanged?.Invoke(this, EventArgs.Empty); /// Clean up any resources being used. /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components is not null)) { components.Dispose(); } base.Dispose(disposing); } /// Raises the event. Saves ViewState when ShellBrowser gets closed. /// The instance containing the event data. protected override void OnHandleDestroyed(EventArgs e) { GetValidHandler()?.ShellView?.SaveViewState(); base.OnHandleDestroyed(e); } /// Raises the event. Resize ViewWindow when ShellBrowser gets resized. /// The instance containing the event data. protected override void OnResize(EventArgs e) { ViewHandler?.MoveWindow(0, 0, ClientRectangle.Width, ClientRectangle.Height, false); base.OnResize(e); } /// Process known command keys of the ShellBrowser. /// Windows Message /// Key codes and modifiers /// true if character was processed by the control; otherwise, false protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { // We have to take special care when the ShellView is currently renaming an item: If message's sender equals class 'Edit', and // its parent window is our ViewWindow, the ShellView is currently showing an Edit-field to let the User edit an item's name. // Thus, we have to pass all Key Strokes directly to the ShellView. if (IsHandlerValid) { // Note: I tried using the LVM_GETEDITCONTROL message for finding the edit control without luck if (User32.GetClassName(msg.HWnd, processCmdKeyClassName, processCmdKeyClassNameMaxLength) > 0) { if (processCmdKeyClassName.ToString().Equals(processCmdKeyClassNameEdit)) { // Try to get Edit field's parent 'SysListView32' handle HWND hSysListView32 = User32.GetParent(msg.HWnd); if (!hSysListView32.IsNull) { // Try to get SysListView32's parent 'SHELLDLL_DefView' handle HWND hShellDllDefViewWindow = User32.GetParent(hSysListView32); if (!hShellDllDefViewWindow.IsNull && (hShellDllDefViewWindow == ViewHandler!.ViewWindow)) { ViewHandler.ShellView!.TranslateAccelerator(new MSG(msg.HWnd, (uint)msg.Msg, msg.WParam, msg.LParam)); return true; } } } } } // Process tab key for control focus cycle: Tab => Focus next control Tab + Shift => Focus previous control if ((keyData & Keys.KeyCode) == Keys.Tab) { var forward = (keyData & Keys.Shift) != Keys.Shift; Parent.SelectNextControl(ActiveControl, forward: forward, tabStopOnly: true, nested: true, wrap: true); return true; } // Process folder navigation shortcuts: Alt + Left OR BrowserBack => Navigate back in history Alt + Right OR BrowserForward => // Navigate forward in history Backspace => Navigate to parent folder switch (keyData) { case Keys.BrowserBack: case Keys.Alt | Keys.Left: NavigateBack(); return true; case Keys.BrowserForward: case Keys.Alt | Keys.Right: NavigateForward(); return true; case Keys.Back: NavigateParent(); return true; } // Let the ShellView process all other keystrokes if (IsHandlerValid) { ViewHandler!.ShellView!.TranslateAccelerator(new MSG(msg.HWnd, (uint)msg.Msg, msg.WParam, msg.LParam)); return true; } return base.ProcessCmdKey(ref msg, keyData); } /// Required method for Designer support - do not modify the contents of this method with the code editor. [MemberNotNull(nameof(components))] private void InitializeComponent() { components = new Container(); AutoScaleMode = AutoScaleMode.Font; } private ShellBrowserViewHandler? GetValidHandler() => IsHandlerValid ? ViewHandler : null; private bool IsHandlerValid => ViewHandler is not null && IsHandlerValid; /// Represents a collection of attached to an . private class ShellItemCollection : IReadOnlyList { private readonly SVGIO option; private readonly ShellBrowser shellBrowser; internal ShellItemCollection(ShellBrowser shellBrowser, SVGIO opt) { this.shellBrowser = shellBrowser; option = opt; } /// Gets the number of elements in the collection. /// Returns a value. public int Count => shellBrowser.GetValidHandler()?.FolderView2!.ItemCount(option) ?? 0; private IShellItemArray? Array => shellBrowser.GetItemsArray(option); private IEnumerable Items { get { IShellItemArray? array = Array; if (array is null) yield break; try { for (uint i = 0; i < array.GetCount(); i++) yield return array.GetItemAt(i); } finally { Marshal.ReleaseComObject(array); } } } /// Gets the at the specified index. /// The . /// The zero-based index of the element to get. public ShellItem this[int index] { get { IShellItemArray? array = Array; try { if (array is not null) return ShellItem.Open(array.GetItemAt((uint)index)); } catch { } finally { if (array is not null) Marshal.ReleaseComObject(array); } throw new ArgumentOutOfRangeException(nameof(index)); } } /// Returns an enumerator that iterates through the collection. /// An enumerator that can be used to iterate through the collection. public IEnumerator GetEnumerator() => Items.Select(ShellItem.Open).GetEnumerator(); /// Returns an enumerator that iterates through the collection. /// An enumerator that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } /// Event argument for The Navigated event. public class ShellBrowserNavigatedEventArgs : EventArgs { /// Initializes a new instance of the class. public ShellBrowserNavigatedEventArgs(ShellFolder currentFolder) => CurrentFolder = currentFolder ?? throw new ArgumentNullException(nameof(currentFolder)); /// The new location of the ShellBrowser public ShellFolder CurrentFolder { get; } } /// /// Encapsulates an IShellFolderViewCB-Implementation within an -Object. /// Beside that it's implemented as a Wrapper-Object that is responsible for creating and disposing the following objects aka /// Interface-Instances:
/// -
/// -
/// -

While doing that, it also handles some common error cases:
/// - When there's no disk in a disk drive

Implements the following Interfaces:
/// -

This class make use of some undocumented Messages in its /// Callback Handler.

For more Information on these see:
/// - Google Drive Shell Extension: ShellFolderViewCBHandler.cpp
/// - ReactOS: IShellFolderViewCB.cpp File Reference /// , IShellFolderViewCB.cpp ///
public class ShellBrowserViewHandler : IShellFolderViewCB { /// /// {"The operation was canceled by the user. (Exception from HRESULT: 0x800704C7)"} /// is the result of a call to on a Shell Item that targets a removable Disk Drive when /// currently no Media is present. Let's catch these to use our own error handling for this. /// internal static readonly HRESULT HRESULT_CANCELLED = new(0x800704C7); private string? text; private int thumbnailSize = ShellBrowser.defaultThumbnailSize; /// Create an instance of to handle Callback messages for the given ShellFolder. /// The that is owner of this instance. /// The ShellFolder for the view. /// The folder settings for the view. /// Text to display if the folder is empty. public ShellBrowserViewHandler(ShellBrowser owner, ShellFolder shellFolder, FOLDERSETTINGS folderSettings, string emptyFolderText) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); ShellFolder = shellFolder ?? throw new ArgumentNullException(nameof(shellFolder)); // Create ShellView and FolderView2 objects, then its ViewWindow try { SFV_CREATE sfvCreate = new() { cbSize = (uint)Marshal.SizeOf(typeof(SFV_CREATE)), pshf = shellFolder.IShellFolder, psvOuter = null, psfvcb = this, }; SHCreateShellFolderView(sfvCreate, out IShellView? shellView).ThrowIfFailed(); ShellView = shellView ?? throw new InvalidComObjectException(nameof(ShellView)); FolderView2 = ShellView as IFolderView2 ?? throw new InvalidComObjectException(nameof(FolderView2)); // Try to create ViewWindow and take special care of Exception {"The operation was canceled by the user. (Exception from // HRESULT: 0x800704C7)"} cause this happens when there's no disk in a drive. try { ViewWindow = ShellView.CreateViewWindow(null, folderSettings, owner, owner.ClientRectangle); IsValid = true; Text = emptyFolderText; } catch (COMException ex) { // TODO: Check if the target folder IS actually a drive with removable disks in it! if (HRESULT_CANCELLED.Equals(ex.ErrorCode)) NoDiskInDriveError = true; throw; } } catch (COMException ex) { // TODO: e.g. C:\Windows\CSC => Permission denied! 0x8007 0005 E_ACCESSDENIED ValidationError = ex; } } /// The . public IFolderView2? FolderView2 { get; private set; } /// Indicates that no error occured while creating this instance, i.e. the View is fully functional. public bool IsValid { get; private set; } /// Indicates that an "No Disk In Drive"-error occured while creating this instance. public bool NoDiskInDriveError { get; } /// The owner of this instance of , i.e. the . public ShellBrowser Owner { get; } /// The . public ShellFolder ShellFolder { get; private set; } /// The . public IShellView? ShellView { get; private set; } /// The default text to be used when there are no items in the view. public string? Text { get => text; set { text = value; if (IsValid) FolderView2!.SetText(FVTEXTTYPE.FVST_EMPTYTEXT, value); } } /// The size of the thumbnails in pixels. public int ThumbnailSize { get { if (IsValid) FolderView2!.GetViewModeAndIconSize(out _, out thumbnailSize); return thumbnailSize; } set { if (IsValid) { FolderView2!.GetViewModeAndIconSize(out FOLDERVIEWMODE fvm, out _); FolderView2!.SetViewModeAndIconSize(fvm, thumbnailSize = value); } } } /// The that occured, if creation of the instance failed. public COMException? ValidationError { get; private set; } /// The viewing mode of the ShellBrowser. public FOLDERVIEWMODE ViewMode { get { if (IsValid) { return FolderView2!.GetCurrentViewMode(); // TODO: Check ThumbNailSize for new ViewModes with larger sized icons } return FOLDERVIEWMODE.FVM_AUTO; // TODO! } set { if (IsValid) FolderView2!.SetCurrentViewMode(value); } } /// The ViewWindow. public HWND ViewWindow { get; private set; } /// Destroy the view. public void DestroyView() { IsValid = false; // TODO: Remove MessageSFVCB here! // Destroy ShellView's ViewWindow ViewWindow = HWND.NULL; ShellView?.DestroyViewWindow(); FolderView2 = null; ShellView = null; //this.ShellFolder = null; // NOTE: I >>think<< this one causes RPC-Errors } /// Changes the position and dimensions of this . /// Left /// Top /// Width /// Height /// Force redraw /// If the function succeeds, the return value is nonzero. public bool MoveWindow(int X, int Y, int nWidth, int nHeight, bool bRepaint) => ViewWindow != HWND.NULL && User32.MoveWindow(ViewWindow, X, Y, nWidth, nHeight, bRepaint); /// Activate the ShellView of this ShellBrowser. /// The to be set public void UIActivate(SVUIA uState = SVUIA.SVUIA_ACTIVATE_NOFOCUS) => ShellView?.UIActivate(uState); /// Deactivate the ShellView of this ShellBrowser. public void UIDeactivate() => UIActivate(SVUIA.SVUIA_DEACTIVATE); /// Allows communication between the system folder view object and a system folder view callback object. /// One of the SFVM_* notifications. /// Additional information. See the individual notification pages for specific requirements. /// Additional information. See the individual notification pages for specific requirements. /// TODO: @dahall: Where does this come from? /// S_OK if the notification has been handled. E_NOTIMPL otherwise. HRESULT IShellFolderViewCB.MessageSFVCB(SFVM uMsg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult) { switch ((SFVMUD)uMsg) { case SFVMUD.SFVM_SELECTIONCHANGED: Owner.OnSelectionChanged(); return HRESULT.S_OK; case SFVMUD.SFVM_LISTREFRESHED: Owner.OnItemsChanged(); return HRESULT.S_OK; default: // TODO: What happens when the ViewMode gets changed via Context-Menu? => Msg #33, #18 return HRESULT.E_NOTIMPL; } } }