Vanara/Windows.Shell.Common/ShellItemChangeWatcher.cs

395 lines
16 KiB
C#

using System.ComponentModel;
using System.Linq;
using Vanara.PInvoke;
using static Vanara.PInvoke.Shell32;
using static Vanara.PInvoke.User32;
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 ?? throw new InvalidOperationException("Expecting Item to be set before retrieval.");
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.IsFileSystem ? Item.FileSystemPath! : Item.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing)!;
set => Item = 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 not 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="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 = 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;
}
}
}