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();
}
}
}
}