mirror of https://github.com/dahall/Vanara.git
400 lines
16 KiB
C#
400 lines
16 KiB
C#
using System;
|
|
using System.ComponentModel;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using Vanara.Extensions;
|
|
using Vanara.PInvoke;
|
|
using static Vanara.PInvoke.User32;
|
|
using static Vanara.PInvoke.Shell32;
|
|
|
|
namespace Vanara.Windows.Shell
|
|
{
|
|
/// <summary>Changes that might occur to a shell item or folder.</summary>
|
|
[Flags]
|
|
public enum ChangeFilters : uint
|
|
{
|
|
/// <summary>
|
|
/// The name of a nonfolder item has changed. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the previous
|
|
/// PIDL or name of the item. dwItem2 contains the new PIDL or name of the item.
|
|
/// </summary>
|
|
ItemRenamed = SHCNE.SHCNE_RENAMEITEM,
|
|
|
|
/// <summary>
|
|
/// A nonfolder item has been created. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the item that was
|
|
/// created. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
ItemCreated = SHCNE.SHCNE_CREATE,
|
|
|
|
/// <summary>
|
|
/// A nonfolder item has been deleted. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the item that was
|
|
/// deleted. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
ItemDeleted = SHCNE.SHCNE_DELETE,
|
|
|
|
/// <summary>
|
|
/// A folder has been created. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the folder that was created.
|
|
/// dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
FolderCreated = SHCNE.SHCNE_MKDIR,
|
|
|
|
/// <summary>
|
|
/// A folder has been removed. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the folder that was removed.
|
|
/// dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
FolderDeleted = SHCNE.SHCNE_RMDIR,
|
|
|
|
/// <summary>
|
|
/// Storage media has been inserted into a drive. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the root
|
|
/// of the drive that contains the new media. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
MediaInserted = SHCNE.SHCNE_MEDIAINSERTED,
|
|
|
|
/// <summary>
|
|
/// Storage media has been removed from a drive. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the root of
|
|
/// the drive from which the media was removed. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
MediaRemoved = SHCNE.SHCNE_MEDIAREMOVED,
|
|
|
|
/// <summary>
|
|
/// A drive has been removed. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the root of the drive that was
|
|
/// removed. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
DriveRemoved = SHCNE.SHCNE_DRIVEREMOVED,
|
|
|
|
/// <summary>
|
|
/// A drive has been added. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the root of the drive that was
|
|
/// added. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
DriveAdded = SHCNE.SHCNE_DRIVEADD,
|
|
|
|
/// <summary>
|
|
/// A folder on the local computer is being shared via the network. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1
|
|
/// contains the folder that is being shared. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
FolderShared = SHCNE.SHCNE_NETSHARE,
|
|
|
|
/// <summary>
|
|
/// A folder on the local computer is no longer being shared via the network. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags.
|
|
/// dwItem1 contains the folder that is no longer being shared. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
FolderUnshared = SHCNE.SHCNE_NETUNSHARE,
|
|
|
|
/// <summary>
|
|
/// The attributes of an item or folder have changed. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the
|
|
/// item or folder that has changed. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
Attributes = SHCNE.SHCNE_ATTRIBUTES,
|
|
|
|
/// <summary>
|
|
/// The contents of an existing folder have changed, but the folder still exists and has not been renamed. SHCNF_IDLIST or SHCNF_PATH
|
|
/// must be specified in uFlags. dwItem1 contains the folder that has changed. dwItem2 is not used and should be NULL. If a folder
|
|
/// has been created, deleted, or renamed, use SHCNE_MKDIR, SHCNE_RMDIR, or SHCNE_RENAMEFOLDER, respectively.
|
|
/// </summary>
|
|
FolderUpdated = SHCNE.SHCNE_UPDATEDIR,
|
|
|
|
/// <summary>
|
|
/// An existing item (a folder or a nonfolder) has changed, but the item still exists and has not been renamed. SHCNF_IDLIST or
|
|
/// SHCNF_PATH must be specified in uFlags. dwItem1 contains the item that has changed. dwItem2 is not used and should be NULL. If a
|
|
/// nonfolder item has been created, deleted, or renamed, use SHCNE_CREATE, SHCNE_DELETE, or SHCNE_RENAMEITEM, respectively, instead.
|
|
/// </summary>
|
|
ItemUpdated = SHCNE.SHCNE_UPDATEITEM,
|
|
|
|
/// <summary>
|
|
/// The computer has disconnected from a server. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the server
|
|
/// from which the computer was disconnected. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
ServerDisconnected = SHCNE.SHCNE_SERVERDISCONNECT,
|
|
|
|
/// <summary>
|
|
/// An image in the system image list has changed. SHCNF_DWORD must be specified in uFlags. dwItem2 contains the index in the system
|
|
/// image list that has changed. dwItem1 is not used and should be NULL.
|
|
/// </summary>
|
|
SystemImageUpdated = SHCNE.SHCNE_UPDATEIMAGE,
|
|
|
|
/// <summary>
|
|
/// A drive has been added. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the root of the drive that was
|
|
/// added. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
DriveAddedInteractive = SHCNE.SHCNE_DRIVEADDGUI,
|
|
|
|
/// <summary>
|
|
/// The name of a folder has changed. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the previous PIDL or
|
|
/// name of the folder. dwItem2 contains the new PIDL or name of the folder.
|
|
/// </summary>
|
|
FolderRenamed = SHCNE.SHCNE_RENAMEFOLDER,
|
|
|
|
/// <summary>
|
|
/// The amount of free space on a drive has changed. SHCNF_IDLIST or SHCNF_PATH must be specified in uFlags. dwItem1 contains the
|
|
/// root of the drive on which the free space changed. dwItem2 is not used and should be NULL.
|
|
/// </summary>
|
|
DriveFreeSpaceChanged = SHCNE.SHCNE_FREESPACE,
|
|
|
|
/// <summary>
|
|
/// A file type association has changed. SHCNF_IDLIST must be specified in the uFlags parameter. dwItem1 and dwItem2 are not used and
|
|
/// must be NULL. This event should also be sent for registered protocols.
|
|
/// </summary>
|
|
FileAssociationChanged = SHCNE.SHCNE_ASSOCCHANGED,
|
|
|
|
/// <summary>All disk related events.</summary>
|
|
AllDiskEvents = SHCNE.SHCNE_DISKEVENTS,
|
|
|
|
/// <summary>All global events.</summary>
|
|
AllGlobalEvents = SHCNE.SHCNE_GLOBALEVENTS,
|
|
|
|
/// <summary>System event.</summary>
|
|
ExtendedEvent = SHCNE.SHCNE_EXTENDED_EVENT,
|
|
|
|
/// <summary>All events.</summary>
|
|
AllEvents = SHCNE.SHCNE_ALLEVENTS,
|
|
}
|
|
|
|
/// <summary>Listens to the shell item change notifications and raises events when a folder, or item in a folder, changes.</summary>
|
|
[DefaultProperty(nameof(Item)), DefaultEvent(nameof(Changed))]
|
|
public class ShellItemChangeWatcher : Component, ISupportInitialize
|
|
{
|
|
private const SHCNE NoParamEvent = SHCNE.SHCNE_ASSOCCHANGED | SHCNE.SHCNE_DRIVEADDGUI | SHCNE.SHCNE_EXTENDED_EVENT | SHCNE.SHCNE_FREESPACE | SHCNE.SHCNE_UPDATEIMAGE;
|
|
private const SHCNE TwoParamEvent = SHCNE.SHCNE_RENAMEFOLDER | SHCNE.SHCNE_RENAMEITEM;
|
|
private readonly WatcherNativeWindow hPump;
|
|
private bool enabled;
|
|
private bool initializing;
|
|
private ShellItem item;
|
|
private ChangeFilters notifyFilter = ChangeFilters.AllEvents;
|
|
private bool recursive;
|
|
private uint ulRegister;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="ShellItemChangeWatcher"/> class.</summary>
|
|
public ShellItemChangeWatcher() => hPump = new WatcherNativeWindow(this);
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="ShellItemChangeWatcher"/> class, given the shell item.</summary>
|
|
/// <param name="shItem">The shell item.</param>
|
|
/// <param name="inclChildren">if set to <c>true</c> include children.</param>
|
|
public ShellItemChangeWatcher(ShellItem shItem, bool inclChildren = false) : this()
|
|
{
|
|
Item = shItem;
|
|
IncludeChildren = inclChildren;
|
|
}
|
|
|
|
/// <summary>Occurs when a shell folder or item is changed.</summary>
|
|
[Category("Behavior"), Description("Occurs when a shell folder or item is changed.")]
|
|
public event EventHandler<ShellItemChangeEventArgs> Changed;
|
|
|
|
/// <summary>Gets or sets a value indicating whether the component is enabled.</summary>
|
|
/// <value><see langword="true"/> if the component is enabled; otherwise, <see langword="false"/>. The default is <see langword="false"/>.</value>
|
|
[DefaultValue(false), Category("Behavior"), Description("Indicates whether the component is enabled.")]
|
|
public bool EnableRaisingEvents
|
|
{
|
|
get => enabled;
|
|
set
|
|
{
|
|
if (value == enabled) return;
|
|
enabled = value;
|
|
if (IsSuspended) return;
|
|
if (enabled)
|
|
StartWatching();
|
|
else
|
|
StopWatching();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets a value indicating whether the children of the specified shell item should be monitored.</summary>
|
|
/// <value><see langword="true"/> if you want to monitor children; otherwise, <see langword="false"/>. The default is <see langword="false"/>.</value>
|
|
[DefaultValue(false), Category("Behavior"), Description("Indicates whether the children of the specified shell item should be monitored.")]
|
|
public bool IncludeChildren
|
|
{
|
|
get => recursive;
|
|
set
|
|
{
|
|
if (recursive == value) return;
|
|
recursive = value;
|
|
Restart();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the shell item to watch.</summary>
|
|
/// <value>The shell item to monitor. The default is <see langword="null"/>.</value>
|
|
/// <exception cref="ArgumentNullException">Item</exception>
|
|
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null), Category("Data"), Description("The shell item to watch.")]
|
|
public ShellItem Item
|
|
{
|
|
get => item;
|
|
set
|
|
{
|
|
if (value is null) throw new ArgumentNullException(nameof(Item));
|
|
if (item == value) return;
|
|
item = value;
|
|
Restart();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the type of changes to watch for.</summary>
|
|
/// <value>One of the <see cref="ChangeFilters"/> values. The default is <see cref="ChangeFilters.AllEvents"/>.</value>
|
|
[DefaultValue(ChangeFilters.AllEvents), Category("Behavior"), Description("The type of changes to watch for.")]
|
|
public ChangeFilters NotifyFilter
|
|
{
|
|
get => notifyFilter;
|
|
set
|
|
{
|
|
if (notifyFilter == value) return;
|
|
notifyFilter = value;
|
|
Restart();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the path of the shell item to watch.</summary>
|
|
/// <value>The path of the shell item to monitor. The default is <see langword="null"/>.</value>
|
|
[DefaultValue(null), Category("Data"), Description("The shell item to watch."), Editor("System.Windows.Forms.Design.FileNameEditor, System.Design", "System.Drawing.Design.UITypeEditor, System.Drawing")]
|
|
public string Path
|
|
{
|
|
get => item is null ? null : (item.IsFileSystem ? item.FileSystemPath : item.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing));
|
|
set => Item = value is null ? null : new ShellItem(value);
|
|
}
|
|
|
|
private bool IsSuspended => initializing || DesignMode;
|
|
|
|
/// <summary>
|
|
/// Begins the initialization of a <see cref="ShellItemChangeWatcher"/> used on a form or used by another component. The
|
|
/// initialization occurs at run time.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The Visual Studio design environment uses this method to start the initialization of a component used on a form or used by
|
|
/// another component. The <see cref="EndInit"/> method ends the initialization. Using the <see cref="BeginInit"/> and
|
|
/// <see cref="EndInit"/> methods prevents the control from being used before it is fully initialized.
|
|
/// </remarks>
|
|
public void BeginInit()
|
|
{
|
|
var oldEnabled = enabled;
|
|
StopWatching();
|
|
enabled = oldEnabled;
|
|
initializing = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends the initialization of a <see cref="ShellItemChangeWatcher"/> used on a form or used by another component. The initialization
|
|
/// occurs at run time.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The Visual Studio design environment uses this method to start the initialization of a component used on a form or used by
|
|
/// another component. The <see cref="EndInit"/> method ends the initialization. Using the <see cref="BeginInit"/> and
|
|
/// <see cref="EndInit"/> methods prevents the control from being used before it is fully initialized.
|
|
/// </remarks>
|
|
public void EndInit()
|
|
{
|
|
initializing = false;
|
|
if (!(item is null) && enabled)
|
|
StartWatching();
|
|
}
|
|
|
|
/// <summary>Releases unmanaged and - optionally - managed resources.</summary>
|
|
/// <param name="disposing">
|
|
/// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.
|
|
/// </param>
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
try
|
|
{
|
|
StopWatching();
|
|
}
|
|
finally
|
|
{
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|
|
|
|
/// <summary>Raises the <see cref="Changed"/> event.</summary>
|
|
/// <param name="e">The <see cref="ShellItemChangeEventArgs"/> instance containing the event data.</param>
|
|
protected virtual void OnChanged(ShellItemChangeEventArgs e) => Changed?.Invoke(this, e);
|
|
|
|
private void Restart()
|
|
{
|
|
if (IsSuspended || !enabled) return;
|
|
StopWatching();
|
|
StartWatching();
|
|
}
|
|
|
|
private void StartWatching()
|
|
{
|
|
const SHCNRF sources = SHCNRF.SHCNRF_ShellLevel | SHCNRF.SHCNRF_InterruptLevel | SHCNRF.SHCNRF_NewDelivery;
|
|
|
|
enabled = true;
|
|
if (IsSuspended) return;
|
|
|
|
SHGetIDListFromObject(Item.IShellItem, out PIDL pidlWatch).ThrowIfFailed();
|
|
SHChangeNotifyEntry[] entries = { new SHChangeNotifyEntry { pidl = pidlWatch.DangerousGetHandle(), fRecursive = IncludeChildren } };
|
|
ulRegister = SHChangeNotifyRegister(hPump.MessageWindowHandle, sources, (SHCNE)NotifyFilter, hPump.MessageId, entries.Length, entries);
|
|
if (ulRegister == 0) throw new InvalidOperationException("Unable to register shell notifications.");
|
|
}
|
|
|
|
private void StopWatching()
|
|
{
|
|
enabled = false;
|
|
if (IsSuspended) return;
|
|
|
|
if (ulRegister == 0) return;
|
|
SHChangeNotifyDeregister(ulRegister);
|
|
ulRegister = 0;
|
|
}
|
|
|
|
/// <summary>Provides data for <see cref="ShellItemChangeWatcher"/> events.</summary>
|
|
/// <seealso cref="System.EventArgs"/>
|
|
public class ShellItemChangeEventArgs : EventArgs
|
|
{
|
|
internal ShellItemChangeEventArgs(SHCNE levent, IntPtr pidl1 = default, IntPtr pidl2 = default)
|
|
{
|
|
ChangeType = (ChangeFilters)levent;
|
|
ChangedItems = new[] { pidl1, pidl2 }.Where(p => p != IntPtr.Zero).Select(p => new ShellItem(new PIDL(p, false, false))).ToArray();
|
|
}
|
|
|
|
/// <summary>Gets the items affected by the change.</summary>
|
|
/// <value>The changed items.</value>
|
|
public ShellItem[] ChangedItems { get; }
|
|
|
|
/// <summary>Gets the type of change event that occurred.</summary>
|
|
/// <value>One of the <see cref="ChangeFilters"/> values that represents the kind of change detected for the shell item.</value>
|
|
public ChangeFilters ChangeType { get; }
|
|
}
|
|
|
|
private class WatcherNativeWindow : SystemEventHandler
|
|
{
|
|
private readonly ShellItemChangeWatcher p;
|
|
|
|
public WatcherNativeWindow(ShellItemChangeWatcher parent) : base()
|
|
{
|
|
MessageId = User32.RegisterWindowMessage($"{parent.GetType()}{DateTime.Now.Ticks}");
|
|
p = parent;
|
|
}
|
|
|
|
public uint MessageId { get; set; }
|
|
|
|
protected override bool MessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn)
|
|
{
|
|
lReturn = default;
|
|
if (msg == MessageId && p.enabled && !p.IsSuspended)
|
|
{
|
|
HLOCK hNotifyLock = default;
|
|
try
|
|
{
|
|
hNotifyLock = SHChangeNotification_Lock(wParam, (uint)lParam.ToInt32(), out IntPtr rgpidl, out SHCNE lEvent);
|
|
if (hNotifyLock != IntPtr.Zero && rgpidl != IntPtr.Zero && p.NotifyFilter.IsFlagSet((ChangeFilters)lEvent))
|
|
{
|
|
ShellItemChangeEventArgs args;
|
|
if (NoParamEvent.IsFlagSet(lEvent))
|
|
args = new ShellItemChangeEventArgs(lEvent);
|
|
else if (TwoParamEvent.IsFlagSet(lEvent))
|
|
args = new ShellItemChangeEventArgs(lEvent, Marshal.ReadIntPtr(rgpidl, 0), Marshal.ReadIntPtr(rgpidl, IntPtr.Size));
|
|
else
|
|
args = new ShellItemChangeEventArgs(lEvent, Marshal.ReadIntPtr(rgpidl, 0));
|
|
p.OnChanged(args);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (hNotifyLock != default)
|
|
SHChangeNotification_Unlock(hNotifyLock);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} |