using System; using System.ComponentModel; using System.Drawing.Design; 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 { /// Changes that might occur to a shell item or folder. [Flags] public enum ChangeFilters : uint { /// /// 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. /// ItemRenamed = SHCNE.SHCNE_RENAMEITEM, /// /// 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. /// ItemCreated = SHCNE.SHCNE_CREATE, /// /// 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. /// ItemDeleted = SHCNE.SHCNE_DELETE, /// /// 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. /// FolderCreated = SHCNE.SHCNE_MKDIR, /// /// 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. /// FolderDeleted = SHCNE.SHCNE_RMDIR, /// /// 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. /// MediaInserted = SHCNE.SHCNE_MEDIAINSERTED, /// /// 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. /// MediaRemoved = SHCNE.SHCNE_MEDIAREMOVED, /// /// 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. /// DriveRemoved = SHCNE.SHCNE_DRIVEREMOVED, /// /// 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. /// DriveAdded = SHCNE.SHCNE_DRIVEADD, /// /// 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. /// FolderShared = SHCNE.SHCNE_NETSHARE, /// /// 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. /// FolderUnshared = SHCNE.SHCNE_NETUNSHARE, /// /// 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. /// Attributes = SHCNE.SHCNE_ATTRIBUTES, /// /// 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. /// FolderUpdated = SHCNE.SHCNE_UPDATEDIR, /// /// 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. /// ItemUpdated = SHCNE.SHCNE_UPDATEITEM, /// /// 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. /// ServerDisconnected = SHCNE.SHCNE_SERVERDISCONNECT, /// /// 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. /// SystemImageUpdated = SHCNE.SHCNE_UPDATEIMAGE, /// /// 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. /// DriveAddedInteractive = SHCNE.SHCNE_DRIVEADDGUI, /// /// 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. /// FolderRenamed = SHCNE.SHCNE_RENAMEFOLDER, /// /// 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. /// DriveFreeSpaceChanged = SHCNE.SHCNE_FREESPACE, /// /// 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. /// FileAssociationChanged = SHCNE.SHCNE_ASSOCCHANGED, /// All disk related events. AllDiskEvents = SHCNE.SHCNE_DISKEVENTS, /// All global events. AllGlobalEvents = SHCNE.SHCNE_GLOBALEVENTS, /// System event. ExtendedEvent = SHCNE.SHCNE_EXTENDED_EVENT, /// All events. AllEvents = SHCNE.SHCNE_ALLEVENTS, } /// Listens to the shell item change notifications and raises events when a folder, or item in a folder, changes. [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; /// Initializes a new instance of the class. public ShellItemChangeWatcher() => hPump = new WatcherNativeWindow(this); /// Initializes a new instance of the class, given the shell item. /// The shell item. /// if set to true include children. public ShellItemChangeWatcher(ShellItem shItem, bool inclChildren = false) : this() { Item = shItem; IncludeChildren = inclChildren; } /// Occurs when a shell folder or item is changed. [Category("Behavior"), Description("Occurs when a shell folder or item is changed.")] public event EventHandler Changed; /// Gets or sets a value indicating whether the component is enabled. /// if the component is enabled; otherwise, . The default is . [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(); } } /// Gets or sets a value indicating whether the children of the specified shell item should be monitored. /// if you want to monitor children; otherwise, . The default is . [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(); } } /// Gets or sets the shell item to watch. /// The shell item to monitor. The default is . /// Item [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(); } } /// Gets or sets the type of changes to watch for. /// One of the values. The default is . [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(); } } /// Gets or sets the path of the shell item to watch. /// The path of the shell item to monitor. The default is . [DefaultValue(null), Category("Data"), Description("The shell item to watch.")]//, Editor(typeof(FileNameEditor), typeof(UITypeEditor))] 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; /// /// Begins the initialization of a used on a form or used by another component. The /// initialization occurs at run time. /// /// /// 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 method ends the initialization. Using the and /// methods prevents the control from being used before it is fully initialized. /// public void BeginInit() { var oldEnabled = enabled; StopWatching(); enabled = oldEnabled; initializing = true; } /// /// Ends the initialization of a used on a form or used by another component. The initialization /// occurs at run time. /// /// /// 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 method ends the initialization. Using the and /// methods prevents the control from being used before it is fully initialized. /// public void EndInit() { initializing = false; if (!(item is null) && enabled) StartWatching(); } /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. /// protected override void Dispose(bool disposing) { try { StopWatching(); } finally { base.Dispose(disposing); } } /// Raises the event. /// The instance containing the event data. 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; } /// Provides data for events. /// 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(); } /// Gets the items affected by the change. /// The changed items. public ShellItem[] ChangedItems { get; } /// Gets the type of change event that occurred. /// One of the values that represents the kind of change detected for the shell item. 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; } } } }