diff --git a/Windows.Shell/ShellObjects/ComObjWrapper.cs b/Windows.Shell/ShellObjects/ComObjWrapper.cs
new file mode 100644
index 00000000..2b2cef11
--- /dev/null
+++ b/Windows.Shell/ShellObjects/ComObjWrapper.cs
@@ -0,0 +1,59 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace Vanara.Windows.Shell
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public abstract class ComObjWrapper : IDisposable, IEquatable, INotifyPropertyChanged where TObj : ComObjWrapper where TComType : class
+ {
+ /// The internal reference to the COM object.
+ protected TComType iObj;
+
+ /// Initializes a new instance of the class.
+ /// The base interface.
+ protected ComObjWrapper(TComType baseInterface) => iObj = baseInterface ?? throw new ArgumentNullException(nameof(baseInterface));
+
+ /// Occurs when a property value changes.
+ public virtual event PropertyChangedEventHandler PropertyChanged;
+
+ /// Gets the COM interface supporting this type.
+ /// The COM interface.
+ public virtual TComType ComInterface => iObj;
+
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ public virtual void Dispose()
+ {
+ Marshal.ReleaseComObject(iObj);
+ iObj = null;
+ }
+
+ /// Determines whether the specified , is equal to this instance.
+ /// The to compare with this instance.
+ /// if the specified is equal to this instance; otherwise, .
+ public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is TObj view ? Equals(view) : obj is TComType com && Equals(com));
+
+ /// Indicates whether the current object is equal to another object of the same type.
+ /// An object to compare with this object.
+ /// true if the current object is equal to the parameter; otherwise, false.
+ public abstract bool Equals(TComType other);
+
+ /// Indicates whether the current object is equal to another object of the same type.
+ /// An object to compare with this object.
+ /// true if the current object is equal to the parameter; otherwise, false.
+ public virtual bool Equals(TObj other) => Equals(other.iObj);
+
+ /// Returns a hash code for this instance.
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ public abstract override int GetHashCode();
+
+ /// Called when a property's value has changed.
+ /// Name of the property.
+ protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
+ }
+}
\ No newline at end of file
diff --git a/Windows.Shell/ShellObjects/ShellBrowser.cs b/Windows.Shell/ShellObjects/ShellBrowser.cs
new file mode 100644
index 00000000..1a60c92a
--- /dev/null
+++ b/Windows.Shell/ShellObjects/ShellBrowser.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Vanara.Extensions;
+using Vanara.PInvoke;
+using static Vanara.PInvoke.Shell32;
+
+namespace Vanara.Windows.Shell
+{
+ internal class ShellBrowser : IShellBrowser, IOleCommandTarget, Shell32.IServiceProvider
+ {
+ private readonly ShellView shellView;
+
+ internal ShellBrowser(ShellView view) => shellView = view ?? throw new ArgumentNullException(nameof(view));
+
+ HRESULT IShellBrowser.BrowseObject(IntPtr pidl, SBSP wFlags)
+ {
+ if (wFlags.IsFlagSet(SBSP.SBSP_PARENT))
+ {
+ shellView.NavigateParent();
+ }
+ else if (wFlags.IsFlagSet(SBSP.SBSP_NAVIGATEBACK))
+ {
+ shellView.NavigateBack();
+ }
+ else if (wFlags.IsFlagSet(SBSP.SBSP_NAVIGATEFORWARD))
+ {
+ shellView.NavigateForward();
+ }
+ else
+ {
+ shellView.Navigate(new ShellFolder(pidl));
+ }
+ return HRESULT.S_OK;
+ }
+
+ HRESULT IShellBrowser.ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL;
+
+ HRESULT Ole32.IOleWindow.ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.EnableModelessSB(bool fEnable) => HRESULT.E_NOTIMPL;
+
+ HRESULT IOleCommandTarget.Exec(in Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, in object pvaIn, ref object pvaOut) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.GetControlWindow(FCW id, out HWND phwnd)
+ {
+ phwnd = HWND.NULL;
+ return HRESULT.E_NOTIMPL;
+ }
+
+ HRESULT IShellBrowser.GetViewStateStream(STGM grfMode, out IStream ppStrm)
+ {
+ ppStrm = null;
+ return HRESULT.E_NOTIMPL;
+ }
+
+ HRESULT IShellBrowser.GetWindow(out HWND phwnd)
+ {
+ phwnd = shellView.shellViewWindow;
+ return HRESULT.E_NOTIMPL;
+ }
+
+ HRESULT Ole32.IOleWindow.GetWindow(out HWND phwnd) => shellView.iObj.GetWindow(out phwnd);
+
+ HRESULT IShellBrowser.InsertMenusSB(HMENU hmenuShared, ref Ole32.OLEMENUGROUPWIDTHS lpMenuWidths) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.OnViewWindowActive(IShellView ppshv) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.QueryActiveShellView(out IShellView ppshv)
+ {
+ ppshv = null;
+ return HRESULT.E_NOTIMPL;
+ }
+
+ HRESULT Shell32.IServiceProvider.QueryService(in Guid guidService, in Guid riid, out IntPtr ppvObject)
+ {
+ if (riid == typeof(IOleCommandTarget).GUID)
+ {
+ ppvObject = Marshal.GetComInterfaceForObject(this, typeof(IOleCommandTarget));
+ return HRESULT.S_OK;
+ }
+ else if (riid == typeof(IShellBrowser).GUID)
+ {
+ ppvObject = Marshal.GetComInterfaceForObject(this, typeof(IShellBrowser));
+ return HRESULT.S_OK;
+ }
+ else
+ {
+ ppvObject = IntPtr.Zero;
+ return HRESULT.E_NOINTERFACE;
+ }
+ }
+
+ HRESULT IOleCommandTarget.QueryStatus(in Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, OLECMDTEXT pCmdText) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.RemoveMenusSB(HMENU hmenuShared) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.SendControlMsg(FCW id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret)
+ {
+ pret = default;
+ return HRESULT.E_NOTIMPL;
+ }
+
+ HRESULT IShellBrowser.SetMenuSB(HMENU hmenuShared, IntPtr holemenuRes, HWND hwndActiveObject) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.SetStatusTextSB(string pszStatusText) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.SetToolbarItems(ComCtl32.TBBUTTON[] lpButtons, uint nButtons, FCT uFlags) => HRESULT.E_NOTIMPL;
+
+ HRESULT IShellBrowser.TranslateAcceleratorSB(ref MSG pmsg, ushort wID) => HRESULT.E_NOTIMPL;
+ }
+}
\ No newline at end of file
diff --git a/Windows.Shell/ShellObjects/ShellContextMenu.cs b/Windows.Shell/ShellObjects/ShellContextMenu.cs
new file mode 100644
index 00000000..9e6654da
--- /dev/null
+++ b/Windows.Shell/ShellObjects/ShellContextMenu.cs
@@ -0,0 +1,278 @@
+// Credit due to Gong-Shell from which this was largely taken.
+#if !NETCOREAPP3_1
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+using Vanara.PInvoke;
+using static Vanara.PInvoke.Shell32;
+using static Vanara.PInvoke.User32;
+
+namespace Vanara.Windows.Shell
+{
+ /// Provides support for displaying the context menu of a shell item.
+ ///
+ /// Use this class to display a context menu for a shell item, either as a popup menu, or as a main menu.
+ ///
+ /// To display a popup menu, simply call with the parent control and the position at which the menu should
+ /// be shown.
+ ///
+ ///
+ /// To display a shell context menu in a Form's main menu, call the method to populate the menu. In addition, you
+ /// must intercept a number of special messages that will be sent to the menu's parent form. To do this, you must override like so:
+ ///
+ ///
+ ///protected override void WndProc(ref Message m) {
+ ///if ((m_ContextMenu == null) || (!m_ContextMenu.HandleMenuMessage(ref m))) {
+ ///base.WndProc(ref m);
+ ///}
+ ///}
+ ///
+ /// Where m_ContextMenu is the being shown.
+ /// Standard menu commands can also be invoked from this class, for example and .
+ ///
+ public class ShellContextMenu
+ {
+ private const int m_CmdFirst = 0x8000;
+ private readonly IContextMenu2 m_ComInterface2;
+ private readonly IContextMenu3 m_ComInterface3;
+ private readonly MessageWindow m_MessageWindow;
+
+ /// Initialises a new instance of the class.
+ /// The item to which the context menu should refer.
+ public ShellContextMenu(ShellItem item) : this(new ShellItem[] { item }) { }
+
+ /// Initialises a new instance of the class.
+ /// The items to which the context menu should refer.
+ public ShellContextMenu(ShellItem[] items)
+ {
+ var pidls = new IntPtr[items.Length];
+ ShellFolder parent = null;
+
+ for (var n = 0; n < items.Length; ++n)
+ {
+ pidls[n] = ILFindLastID((IntPtr)items[n].PIDL);
+
+ if (parent is null)
+ {
+ if (items[n] == ShellFolder.Desktop)
+ {
+ parent = ShellFolder.Desktop;
+ }
+ else
+ {
+ parent = items[n].Parent;
+ }
+ }
+ else
+ {
+ if (items[n].Parent != parent)
+ {
+ throw new Exception("All shell items must have the same parent");
+ }
+ }
+ }
+
+ ComInterface = parent.IShellFolder.GetUIObjectOf(HWND.NULL, pidls);
+ m_ComInterface2 = ComInterface as IContextMenu2;
+ m_ComInterface3 = ComInterface as IContextMenu3;
+ m_MessageWindow = new MessageWindow(this);
+ }
+
+ /// Gets the underlying COM interface.
+ public IContextMenu ComInterface { get; set; }
+
+ /// Handles context menu messages when the is displayed on a Form's main menu bar.
+ ///
+ ///
+ /// To display a shell context menu in a Form's main menu, call the method to populate the menu with the
+ /// shell item's menu items. In addition, you must intercept a number of special messages that will be sent to the menu's parent
+ /// form. To do this, you must override like so:
+ ///
+ ///
+ ///protected override void WndProc(ref Message m) {
+ ///if ((m_ContextMenu == null) || (!m_ContextMenu.HandleMenuMessage(ref m))) {
+ ///base.WndProc(ref m);
+ ///}
+ ///}
+ ///
+ /// Where m_ContextMenu is the being shown.
+ ///
+ /// The message to handle.
+ ///
+ /// if the message was a Shell Context Menu message, if not. If the method returns
+ /// false, then the message should be passed down to the base class's method.
+ ///
+ public bool HandleMenuMessage(ref Message m)
+ {
+ try
+ {
+ if ((m.Msg == (int)WindowMessage.WM_COMMAND) && ((int)m.WParam >= m_CmdFirst))
+ {
+ InvokeCommand((int)m.WParam - m_CmdFirst);
+ return true;
+ }
+ else
+ {
+ if (m_ComInterface3 != null)
+ {
+ m_ComInterface3.HandleMenuMsg2((uint)m.Msg, m.WParam, m.LParam, out IntPtr result);
+ m.Result = result;
+ return true;
+ }
+ else if (m_ComInterface2 != null)
+ {
+ m_ComInterface2.HandleMenuMsg((uint)m.Msg, m.WParam, m.LParam);
+ m.Result = IntPtr.Zero;
+ return true;
+ }
+ }
+ }
+ catch { }
+ return false;
+ }
+
+ /// Invokes the Copy command on the shell item(s).
+ public void InvokeCopy() => InvokeVerb("copy");
+
+ /// Invokes the Copy command on the shell item(s).
+ public void InvokeCut() => InvokeVerb("cut");
+
+ /// Invokes the Delete command on the shell item(s).
+ public void InvokeDelete()
+ {
+ try
+ {
+ InvokeVerb("delete");
+ }
+ catch (COMException e)
+ {
+ // Ignore the exception raised when the user cancels a delete operation.
+ if (e.ErrorCode != (HRESULT)(Win32Error)Win32Error.ERROR_CANCELLED &&
+ e.ErrorCode != HRESULT.COPYENGINE_E_USER_CANCELLED)
+ {
+ throw;
+ }
+ }
+ }
+
+ /// Invokes the Paste command on the shell item(s).
+ public void InvokePaste() => InvokeVerb("paste");
+
+ /// Invokes the Rename command on the shell item.
+ public void InvokeRename() => InvokeVerb("rename");
+
+ /// Invokes the specified verb on the shell item(s).
+ public void InvokeVerb(string verb)
+ {
+ var invoke = new CMINVOKECOMMANDINFOEX();
+ invoke.cbSize = (uint)Marshal.SizeOf(invoke);
+ invoke.lpVerb = new SafeResourceId(verb);
+ ComInterface.InvokeCommand(invoke);
+ }
+
+ /// Populates a with the context menu items for a shell item.
+ ///
+ /// If this method is being used to populate a Form's main menu then you need to call in the Form's
+ /// message handler.
+ ///
+ /// The menu to populate.
+ public void Populate(Menu menu)
+ {
+ RemoveShellMenuItems(menu);
+ ComInterface.QueryContextMenu(menu.Handle, 0, m_CmdFirst, int.MaxValue, CMF.CMF_EXPLORE);
+ }
+
+ /// Shows a context menu for a shell item.
+ /// The parent control.
+ /// The position on that the menu should be displayed at.
+ public void ShowContextMenu(Control control, Point pos)
+ {
+ using var menu = new ContextMenu();
+ Populate(menu);
+ var command = TrackPopupMenuEx(menu.Handle, TrackPopupMenuFlags.TPM_RETURNCMD, pos.X, pos.Y, m_MessageWindow.Handle);
+ if (command > 0)
+ {
+ InvokeCommand((int)command - m_CmdFirst);
+ }
+ }
+
+ private void InvokeCommand(int index)
+ {
+ var invoke = new CMINVOKECOMMANDINFOEX(index) { nShow = ShowWindowCommand.SW_SHOWNORMAL };
+ m_ComInterface2.InvokeCommand(invoke);
+ }
+
+ private void RemoveShellMenuItems(Menu menu)
+ {
+ const int tag = 0xAB;
+
+ var menuInfo = new MENUINFO();
+ menuInfo.cbSize = (uint)Marshal.SizeOf(menuInfo);
+ menuInfo.fMask = MenuInfoMember.MIM_MENUDATA;
+
+ var itemInfo = new MENUITEMINFO();
+ itemInfo.cbSize = (uint)Marshal.SizeOf(itemInfo);
+ itemInfo.fMask = MenuItemInfoMask.MIIM_ID | MenuItemInfoMask.MIIM_SUBMENU;
+
+ // First, tag the managed menu items with an arbitary value (0xAB).
+ TagManagedMenuItems(menu, tag);
+
+ var remove = new List();
+ var count = GetMenuItemCount(menu.Handle);
+ for (uint n = 0; n < count; ++n)
+ {
+ GetMenuItemInfo(menu.Handle, n, true, ref itemInfo);
+
+ if (itemInfo.hSubMenu.IsNull)
+ {
+ // If the item has no submenu we can't get the tag, so check its ID to determine if it was added by the shell.
+ if (itemInfo.wID >= m_CmdFirst) remove.Add(n);
+ }
+ else
+ {
+ GetMenuInfo(itemInfo.hSubMenu, ref menuInfo);
+ if ((int)menuInfo.dwMenuData != tag) remove.Add(n);
+ }
+ }
+
+ // Remove the unmanaged menu items.
+ remove.Reverse();
+ foreach (var position in remove)
+ {
+ DeleteMenu(menu.Handle, (uint)position, MenuFlags.MF_BYPOSITION);
+ }
+ }
+
+ private void TagManagedMenuItems(Menu menu, int tag)
+ {
+ var info = new MENUINFO();
+ info.cbSize = (uint)Marshal.SizeOf(info);
+ info.fMask = MenuInfoMember.MIM_MENUDATA;
+ info.dwMenuData = (UIntPtr)tag;
+
+ foreach (MenuItem item in menu.MenuItems)
+ {
+ SetMenuInfo(item.Handle, info);
+ }
+ }
+
+ private class MessageWindow : Control
+ {
+ private readonly ShellContextMenu m_Parent;
+
+ public MessageWindow(ShellContextMenu parent) => m_Parent = parent;
+
+ protected override void WndProc(ref Message m)
+ {
+ if (!m_Parent.HandleMenuMessage(ref m))
+ {
+ base.WndProc(ref m);
+ }
+ }
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/Windows.Shell/ShellObjects/ShellNavigationHistory.cs b/Windows.Shell/ShellObjects/ShellNavigationHistory.cs
new file mode 100644
index 00000000..699c0376
--- /dev/null
+++ b/Windows.Shell/ShellObjects/ShellNavigationHistory.cs
@@ -0,0 +1,96 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using Vanara.Collections;
+using Vanara.Extensions;
+using static Vanara.PInvoke.Shell32;
+
+namespace Vanara.Windows.Shell
+{
+ /// The navigation log is a history of the locations visited by a shell view object.
+ public class ShellNavigationHistory : Vanara.Collections.IHistory
+ {
+ private readonly Vanara.Collections.History pidls = new Collections.History();
+
+ internal ShellNavigationHistory()
+ {
+ }
+
+ /// Occurs when [collection changed].
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ /// Occurs when a property value changes.
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ /// Indicates the presence of items in the history that can be reached by calling .
+ /// if this instance can seek backward; otherwise, .
+ public bool CanSeekBackward => pidls.CanSeekBackward;
+
+ /// Indicates the presence of items in the history that can be reached by calling .
+ /// if this instance can seek forward; otherwise, .
+ public bool CanSeekForward => pidls.CanSeekForward;
+
+ /// Gets the items in the history.
+ /// The number of items.
+ public int Count => pidls.Count;
+
+ /// Gets the shell object in the Locations collection pointed to by CurrentLocationIndex.
+ public ShellItem Current => new ShellItem(pidls.Current);
+
+ ///
+ /// Adds the specified item as the last history entry and sets the property to
+ /// it's value.
+ ///
+ /// The item to add to the history.
+ public void Add(ShellItem item) => pidls.Add(item.PIDL, true);
+
+ /// Clears the history of all items.
+ public void Clear() => pidls.Clear();
+
+ /// Returns an enumerator that iterates through the collection.
+ /// A that can be used to iterate through the collection.
+ public IEnumerator GetEnumerator() => pidls.Select(p => new ShellItem(p)).GetEnumerator();
+
+ /// Gets a specified number of items starting at a location within the history.
+ /// The maximum number of items to retrieve. The actual number of items returned may be less if not avaialable.
+ /// The reference point within the history at which to start fetching items.
+ /// A read-only list of items.
+ public IReadOnlyList GetItems(int count, SeekOrigin origin) => (IReadOnlyList)new List(pidls.GetItems(count, origin).Select(ShIFromPIDL));
+
+ ///
+ /// Seeks through the history a given number of items starting at a known location within the history. This updates the property.
+ ///
+ /// The number of items to move. This value can be negative to search backwards or positive to search forwards.
+ /// The reference point within the history at which to start seeking.
+ /// The value at the new current pointer position.
+ public ShellItem Seek(int count, SeekOrigin origin) => ShIFromPIDL(pidls.Seek(count, origin));
+
+ /// Seeks one position backwards.
+ /// The value at the new current pointer position.
+ public ShellItem SeekBackward() => ShIFromPIDL(pidls.SeekBackward());
+
+ /// Seeks one position forwards.
+ /// The value at the new current pointer position.
+ public ShellItem SeekForward() => ShIFromPIDL(pidls.SeekForward());
+
+ ///
+ /// Adds the specified item as the last history entry and sets the property to
+ /// it's value.
+ ///
+ /// The item to add to the history.
+ ///
+ /// indicates to remove all items forward of the current pointer; leaves the history intact.
+ ///
+ void IHistory.Add(ShellItem item, bool removeForwardItems) => pidls.Add(item?.PIDL, removeForwardItems);
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ internal void Add(PIDL pidl) => pidls.Add(pidl, true);
+
+ private static ShellItem ShIFromPIDL(PIDL pidl) => pidl is null ? null : new ShellItem(pidl);
+ }
+}
\ No newline at end of file
diff --git a/Windows.Shell/ShellObjects/ShellView.cs b/Windows.Shell/ShellObjects/ShellView.cs
new file mode 100644
index 00000000..52671208
--- /dev/null
+++ b/Windows.Shell/ShellObjects/ShellView.cs
@@ -0,0 +1,402 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+using Vanara.InteropServices;
+using Vanara.PInvoke;
+using static Vanara.PInvoke.Shell32;
+
+namespace Vanara.Windows.Shell
+{
+ /// Event argument for The Navigated event
+ public class NavigatedEventArgs : EventArgs
+ {
+ /// Initializes a new instance of the class.
+ /// The folder.
+ public NavigatedEventArgs(ShellFolder folder) => NewLocation = folder;
+
+ /// The new location of the explorer browser
+ public ShellItem NewLocation { get; private set; }
+ }
+
+ /// Event argument for The Navigating event
+ public class NavigatingEventArgs : CancelEventArgs
+ {
+ /// Initializes a new instance of the class.
+ /// The pending location.
+ public NavigatingEventArgs(ShellItem pendingLocation) => PendingLocation = pendingLocation;
+
+ /// The location being navigated to.
+ public ShellItem PendingLocation { get; private set; }
+ }
+
+ /// Event argument for the NavigatinoFailed event
+ public class NavigationFailedEventArgs : EventArgs
+ {
+ /// The location the browser would have navigated to.
+ public ShellItem FailedLocation { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class ShellView : Control, INotifyPropertyChanged
+ {
+ internal IShellView iObj;
+ internal HWND shellViewWindow;
+ private ShellFolder currentFolder;
+ private IShellBrowser iBrowser;
+
+ /// Creates a new from a shell folder and assigns it to a window.
+ /// The shell folder.
+ /// The owner window.
+ /// A new instance for the supplied shell folder.
+ public ShellView(ShellFolder folder, IWin32Window owner) : this(CreateViewObject(folder, owner?.Handle ?? IntPtr.Zero)) => Navigate(folder);
+
+ /// Initializes a new instance of the class.
+ public ShellView() : this(ShellFolder.Desktop, null) { }
+
+ /// Initializes a new instance of the class.
+ /// The base interface.
+ internal ShellView(IShellView baseInterface)
+ {
+ iObj = baseInterface;
+ History = new ShellNavigationHistory();
+ Items = new ShellItemArray(GetItemArray(iObj, SVGIO.SVGIO_ALLVIEW));
+ }
+
+ /// Fires when the Items collection changes.
+ [Category("Action"), Description("Items changed.")]
+ public event EventHandler ItemsChanged;
+
+ /// Fires when the ExplorerBorwser view has finished enumerating files.
+ [Category("Behavior"), Description("View is done enumerating files.")]
+ public event EventHandler ItemsEnumerated;
+
+ ///
+ /// Fires when a navigation has been 'completed': no Navigating listener has canceled, and the ExplorerBorwser has created a new
+ /// view. The view will be populated with new items asynchronously, and ItemsChanged will be fired to reflect this some time later.
+ ///
+ [Category("Action"), Description("Navigation complete.")]
+ public event EventHandler Navigated;
+
+ /// Fires when a navigation has been initiated, but is not yet complete.
+ [Category("Action"), Description("Navigation initiated, but not complete.")]
+ public event EventHandler Navigating;
+
+ ///
+ /// Fires when either a Navigating listener cancels the navigation, or if the operating system determines that navigation is not possible.
+ ///
+ [Category("Action"), Description("Navigation failed.")]
+ public event EventHandler NavigationFailed;
+
+ /// Occurs when a property value changes.
+ [Category("Behavior"), Description("Property changed.")]
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ /// Fires when the item selected in the view has changed (i.e., a rename ). This is not the same as SelectionChanged.
+ [Category("Action"), Description("Selected item has changed.")]
+ public event EventHandler SelectedItemModified;
+
+ /// Fires when the SelectedItems collection changes.
+ [Category("Behavior"), Description("Selection changed.")]
+ public event EventHandler SelectionChanged;
+
+ /// Gets or sets the currently being browsed by the .
+ [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ShellFolder CurrentFolder { get => currentFolder ??= new ShellFolder(GetFolderForView(iObj)); set => Navigate(value); }
+
+ /// A set of flags that indicate the options for the folder.
+ public FOLDERFLAGS Flags => iObj.GetCurrentInfo().fFlags;
+
+ /// Contains the navigation history of the ExplorerBrowser
+ [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ShellNavigationHistory History { get; private set; }
+
+ /// Gets all the items in the view.
+ /// An array with all the items.
+ public ShellItemArray Items { get; private set; }
+
+ /// Gets or sets the currently selected items in the view.
+ /// An array with the selected items.
+ /// All items must belong to the folder hosted by this view. - SelectedItems
+ public ShellItem[] SelectedItems
+ {
+ get => Array.ConvertAll(GetItems(iObj, SVGIO.SVGIO_SELECTION), i => new ShellItem(i));
+ set
+ {
+ // Deselect all
+ iObj.SelectItem(default, SVSIF.SVSI_DESELECTOTHERS);
+ if (value is null || value.Length == 0) return;
+ // Get parent folder of view items
+ PIDL pidl = CurrentFolder.PIDL;
+ // Ensure all have this parent
+ if (!value.All(shi => shi.PIDL.Parent.Equals(pidl)))
+ throw new ArgumentException("All items must belong to the folder hosted by this view.", nameof(SelectedItems));
+ // Select all provided
+ foreach (ShellItem item in value)
+ iObj.SelectItem((IntPtr)item.PIDL, SVSIF.SVSI_SELECT);
+ }
+ }
+
+ ///
+ /// Retrieves a handle to one of the windows participating in in-place activation (frame, document, parent, or in-place object window).
+ ///
+ /// The window handle.
+ public HWND WindowHandle => shellViewWindow;
+
+ /// Folder view mode.
+ public FOLDERVIEWMODE ViewMode => iObj.GetCurrentInfo().ViewMode;
+
+ /// Gets the default size of the control.
+ protected override Size DefaultSize => new Size(250, 200);
+
+ private IShellBrowser Browser => iBrowser ??= new ShellBrowser(this);
+
+ /// Implements the operator !=.
+ /// The left.
+ /// The right.
+ /// The result of the operator.
+ public static bool operator !=(ShellView left, ShellView right) => !(left == right);
+
+ /// Implements the operator ==.
+ /// The left.
+ /// The right.
+ /// The result of the operator.
+ public static bool operator ==(ShellView left, ShellView right) => EqualityComparer.Default.Equals(left, right);
+
+ /// Indicates whether the current object is equal to another object of the same type.
+ /// An object to compare with this object.
+ /// true if the current object is equal to the parameter; otherwise, false.
+ ///
+ public bool Equals(IShellView other)
+ {
+ HWND w1 = HWND.NULL, w2 = HWND.NULL;
+ other?.GetWindow(out w1);
+ iObj?.GetWindow(out w2);
+ return w1 == w2;
+ }
+
+ /// Determines whether the specified , is equal to this instance.
+ /// The to compare with this instance.
+ /// if the specified is equal to this instance; otherwise, .
+ public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is ShellView sv && Equals(sv.iObj)) || (obj is IShellView isv && Equals(isv));
+
+ /// Returns a hash code for this instance.
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ public override int GetHashCode() => (int)(IntPtr)WindowHandle;
+
+ ///
+ /// Clears the Explorer Browser of existing content, fills it with content from the specified container, and adds a new point to the
+ /// Travel Log.
+ ///
+ /// The shell container to navigate to.
+ public void Navigate(ShellFolder folder)
+ {
+ if (!OnNavigating(folder)) return;
+
+ ShellFolder previous = currentFolder;
+ currentFolder = folder;
+
+ try
+ {
+ RecreateShellView();
+ History.Add(folder.PIDL);
+ OnNavigated();
+ }
+ catch (Exception)
+ {
+ currentFolder = previous;
+ RecreateShellView();
+ throw;
+ }
+ OnPropertyChanged(nameof(CurrentFolder));
+ }
+
+ ///
+ /// Navigates to the last item in the navigation history list. This does not change the set of locations in the navigation log.
+ ///
+ public bool NavigateBack() { if (History.CanSeekBackward) { Navigate(new ShellFolder(History.SeekBackward())); return true; } return false; }
+
+ ///
+ /// 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() { if (History.CanSeekForward) { Navigate(new ShellFolder(History.SeekForward())); return true; } return false; }
+
+ /// Navigates to the parent of the currently displayed folder.
+ public void NavigateParent() { if (CurrentFolder != ShellFolder.Desktop) Navigate(CurrentFolder.Parent); }
+
+ /// Refreshes the view's contents in response to user input.
+ public override void Refresh() => iObj.Refresh();
+
+ /// Saves the Shell's view settings so the current state can be restored during a subsequent browsing session.
+ public void SaveState() => iObj.SaveViewState();
+
+ /// Raises the event.
+ protected internal virtual void OnItemsChanged() => ItemsChanged?.Invoke(this, EventArgs.Empty);
+
+ /// Raises the event.
+ protected internal virtual void OnItemsEnumerated() => ItemsEnumerated?.Invoke(this, EventArgs.Empty);
+
+ /// Raises the event.
+ protected internal virtual void OnNavigated() => Navigated?.Invoke(this, new NavigatedEventArgs(CurrentFolder));
+
+ /// Raises the event.
+ protected internal virtual bool OnNavigating(ShellFolder pendingLocation)
+ {
+ var e = new NavigatingEventArgs(pendingLocation);
+ Navigating?.Invoke(this, e);
+ return !e.Cancel;
+ }
+
+ /// Raises the event.
+ protected internal virtual void OnNavigationFailed(NavigationFailedEventArgs nfevent)
+ {
+ if (nfevent?.FailedLocation is null) return;
+ NavigationFailed?.Invoke(this, nfevent);
+ }
+
+ /// Raises the event.
+ protected internal virtual void OnSelectedItemModified() => SelectedItemModified?.Invoke(this, EventArgs.Empty);
+
+ /// Raises the event.
+ protected internal virtual void OnSelectionChanged() => SelectionChanged?.Invoke(this, EventArgs.Empty);
+
+ ///
+ /// Releases the unmanaged resources used by the and its child controls and optionally
+ /// releases the managed resources.
+ ///
+ ///
+ /// to release both managed and unmanaged resources; to release only unmanaged resources.
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (!shellViewWindow.IsNull)
+ {
+ User32.DestroyWindow(shellViewWindow);
+ shellViewWindow = HWND.NULL;
+ }
+ if (iBrowser != null)
+ {
+ Marshal.ReleaseComObject(iBrowser);
+ iBrowser = null;
+ }
+ if (Items != null)
+ {
+ Items.Dispose();
+ Items = null;
+ }
+ if (History != null)
+ {
+ History = null;
+ }
+ if (iObj != null)
+ {
+ Marshal.ReleaseComObject(iObj);
+ iObj = null;
+ }
+ }
+ base.Dispose(disposing);
+ }
+
+ /// Raises the method.
+ protected override void OnCreateControl()
+ {
+ base.OnCreateControl();
+ CreateShellView();
+ OnNavigated();
+ }
+
+ /// Raises the event.
+ /// The instance containing the event data.
+ protected override void OnResize(EventArgs eventargs)
+ {
+ base.OnResize(eventargs);
+ User32.SetWindowPos(shellViewWindow, HWND.HWND_TOP, 0, 0, ClientRectangle.Width, ClientRectangle.Height, 0);
+ }
+
+ /// Processes Windows messages.
+ /// The Windows to process.
+ protected override void WndProc(ref Message m)
+ {
+ const int CWM_GETISHELLBROWSER = 0x407;
+
+ // Windows 9x sends the CWM_GETISHELLBROWSER message and expects the IShellBrowser for the window to be returned or an Access
+ // Violation occurs. This is pseudo-documented in knowledge base article Q157247.
+ if (m.Msg == CWM_GETISHELLBROWSER)
+ {
+ m.Result = Marshal.GetComInterfaceForObject(iBrowser, typeof(IShellBrowser));
+ }
+ else
+ {
+ base.WndProc(ref m);
+ }
+ }
+
+ private static IShellView CreateViewObject(ShellFolder folder, HWND owner) =>
+ folder?.iShellFolder.CreateViewObject(owner);
+
+ private static PIDL GetFolderForView(IShellView iView)
+ {
+ PIDL pidl = GetItems(iView, SVGIO.SVGIO_ALLVIEW).FirstOrDefault();
+ pidl?.RemoveLastId();
+ return pidl;
+ }
+
+ private static IShellItemArray GetItemArray(IShellView iView, SVGIO uItem) => ((IFolderView)iView).Items(uItem);
+
+ private static PIDL[] GetItems(IShellView iView, SVGIO uItem)
+ {
+ using ComReleaser ido = ComReleaserFactory.Create(iView.GetItemObject(uItem));
+ var shdo = new ShellDataObject(ido.Item);
+ return shdo.GetShellIdList();
+ }
+
+ private void CreateShellView()
+ {
+ IShellView prev = iObj;
+ iObj = CreateViewObject(CurrentFolder, Handle);
+ Items = new ShellItemArray(GetItemArray(iObj, SVGIO.SVGIO_ALLVIEW));
+ try
+ {
+ var fsettings = new FOLDERSETTINGS(ViewMode, Flags);
+ shellViewWindow = iObj.CreateViewWindow(prev, fsettings, Browser, ClientRectangle);
+ }
+ catch (COMException ex)
+ {
+ // If the operation was cancelled by the user (for example because an empty removable media drive was selected, then
+ // "Cancel" pressed in the resulting dialog) convert the exception into something more meaningfil.
+ if (ex.ErrorCode == unchecked((int)0x800704C7U))
+ {
+ throw new OperationCanceledException("User cancelled.", ex);
+ }
+ }
+
+ iObj.UIActivate(SVUIA.SVUIA_ACTIVATE_NOFOCUS);
+
+ if (DesignMode) User32.EnableWindow(shellViewWindow, false);
+
+ if (prev != null) prev.DestroyViewWindow();
+ }
+
+ private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ private void RecreateShellView()
+ {
+ if (iObj != null)
+ {
+ CreateShellView();
+ OnNavigated();
+ }
+ }
+ }
+}
\ No newline at end of file