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 FilterItem event public class FilterShellItemEventArgs : EventArgs { /// Initializes a new instance of the class. /// The shell item. public FilterShellItemEventArgs(ShellItem item) => Item = item; /// Gets or sets a value indicating whether a is included by the filter. /// if included; otherwise, . public bool Include { get; set; } = true; /// The new location of the explorer browser public ShellItem Item { get; private set; } } /// 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; } } /// Encapsulates IShellView for display as a control. /// /// /// /// /// // TODO: Fix problems with IShellView.CreateViewWindow returning E_FAIL internal class ShellView : Control, INotifyPropertyChanged { internal HWND shellViewWindow; private ShellFolder currentFolder; private IShellBrowser iBrowser; private IShellView iShellView; /// Creates a new from a shell folder and assigns it to a window. /// The shell folder. /// A new instance for the supplied shell folder. public ShellView(ShellFolder folder) : this() => currentFolder = folder; /// Initializes a new instance of the class. public ShellView() => History = new ShellNavigationHistory(); /// Initializes a new instance of the class. /// The base interface. internal ShellView(IShellView baseInterface) : this() => IShellView = baseInterface; /// Fires when determining if an item should be shown in the view. [Category("Action"), Description("Filter items in the view.")] public event EventHandler FilterItem; /// 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 . [Category("Data"), DefaultValue(null), Description("The folder currently being browsed.")] public ShellFolder CurrentFolder { get => currentFolder ??= IShellView is null ? ShellFolder.Desktop : new ShellFolder(GetFolderForView(IShellView)); set => Navigate(value); } /// A set of flags that indicate the options for the folder. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public FOLDERFLAGS Flags => IShellView.GetCurrentInfo().fFlags; /// Contains the navigation history of the ExplorerBrowser [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ShellNavigationHistory History { get; private set; } /// Gets the underlying instance. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IShellView IShellView { get => iShellView; private set { iShellView = value; //Items = new ShellItemArray(GetItemArray(iShellView, SVGIO.SVGIO_ALLVIEW)); } } ///// 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 [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ShellItem[] SelectedItems { get => GetItems(IShellView, SVGIO.SVGIO_SELECTION); set { // Deselect all IShellView.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) IShellView.SelectItem((IntPtr)item.PIDL, SVSIF.SVSI_SELECT); } } /// Folder view mode. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public FOLDERVIEWMODE ViewMode => IShellView.GetCurrentInfo().ViewMode; /// /// Retrieves a handle to one of the windows participating in in-place activation (frame, document, parent, or in-place object window). /// /// The window handle. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public HWND WindowHandle => shellViewWindow; /// Gets the default size of the control. protected override Size DefaultSize => new(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); IShellView?.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.IShellView)) || (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 view of existing content, fills it with content from the specified container, and adds a new item to the history. /// /// The shell folder to navigate to. public void Navigate(ShellFolder folder) { if (!OnNavigating(folder)) return; ShellFolder previous = currentFolder; currentFolder = folder; try { RecreateShellView(); History.Add(new ShellItem(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() => IShellView.Refresh(); /// Saves the Shell's view settings so the current state can be restored during a subsequent browsing session. public void SaveState() => IShellView.SaveViewState(); /// Called to determine if an item should be shown in the view. Calls the event if defined. /// The PIDL of the child item. /// if the item should be included (this is the default); otherwise . protected internal virtual bool IncludeItem(PIDL pidl) { if (FilterItem is null) return true; var e = new FilterShellItemEventArgs(ShellItem.Open(CurrentFolder.IShellFolder, pidl)); FilterItem?.Invoke(this, e); return e.Include; } /// Raises the event. /// An that contains the event data. protected internal new virtual void OnDoubleClick(EventArgs e) => base.OnDoubleClick(e); /// 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; } iBrowser = null; History = null; IShellView = null; } base.Dispose(disposing); } /// Raises the method. protected override void OnCreateControl() { base.OnCreateControl(); CreateShellView(); History.Add(new ShellItem(CurrentFolder.PIDL)); OnNavigated(); } /// Raises the event. /// The instance containing the event data. protected override void OnResize(EventArgs eventargs) { base.OnResize(eventargs); if (!shellViewWindow.IsNull) 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 ShellFolder GetFolderForView(IShellView iView) => GetItems(iView, SVGIO.SVGIO_ALLVIEW).FirstOrDefault()?.Parent; private static IShellItemArray GetItemArray(IShellView iView, SVGIO uItem) => ((IFolderView)iView).Items(uItem); private static ShellItem[] GetItems(IShellView iView, SVGIO uItem) { using ComReleaser ido = ComReleaserFactory.Create(iView.GetItemObject(uItem)); var shdo = new ShellDataObject(ido.Item); return shdo.GetShellIdList().ToArray(); } private void CreateShellView() { IShellView prev = IShellView; IShellView = CreateViewObject(CurrentFolder, Handle); try { var fsettings = prev?.GetCurrentInfo() ?? new FOLDERSETTINGS(ViewMode, Flags); shellViewWindow = IShellView.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); } } if (prev != null) { prev.UIActivate(SVUIA.SVUIA_DEACTIVATE); prev.DestroyViewWindow(); prev = null; } IShellView.UIActivate(SVUIA.SVUIA_ACTIVATE_NOFOCUS); if (DesignMode) User32.EnableWindow(shellViewWindow, false); } private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); private void RecreateShellView() { if (IShellView != null) { CreateShellView(); OnNavigated(); } } } }