using System;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using Vanara.PInvoke;
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,
/// 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 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.
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
[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();
}
}
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)
{
base.Dispose(disposing);
if (disposing) StopWatching();
}
/// 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 var pidlWatch).ThrowIfFailed();
SHChangeNotifyEntry[] entries = { new SHChangeNotifyEntry { pidl = pidlWatch.DangerousGetHandle(), fRecursive = IncludeChildren } };
ulRegister = SHChangeNotifyRegister(hPump.Handle, sources, (SHCNE)NotifyFilter, hPump.MessageId, entries.Length, entries);
}
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 rgpidl)
{
ChangeType = (ChangeFilters)levent;
ChangedItems = new PIDL(rgpidl, true).Select(p => new ShellItem(p)).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 : NativeWindow
{
private ShellItemChangeWatcher p;
public WatcherNativeWindow(ShellItemChangeWatcher parent)
{
MessageId = User32.RegisterWindowMessage($"{parent.GetType()}{DateTime.Now.Ticks}");
p = parent;
var cp = new CreateParams { Style = 0, ExStyle = 0, ClassStyle = 0, Parent = IntPtr.Zero, Caption = GetType().Name };
CreateHandle(cp);
}
public uint MessageId { get; set; }
protected override void WndProc(ref Message m)
{
if (m.Msg == MessageId)
{
HLOCK hNotifyLock = default;
try
{
hNotifyLock = SHChangeNotification_Lock(m.WParam, (uint)m.LParam.ToInt32(), out var rgpidl, out var lEvent);
if (hNotifyLock != IntPtr.Zero) // && (lEvent & (int)(SHCNE.SHCNE_UPDATEIMAGE | SHCNE.SHCNE_ASSOCCHANGED | SHCNE.SHCNE_EXTENDED_EVENT | SHCNE.SHCNE_FREESPACE | SHCNE.SHCNE_DRIVEADDGUI | SHCNE.SHCNE_SERVERDISCONNECT)) != 0
p.OnChanged(new ShellItemChangeEventArgs(lEvent, rgpidl));
}
finally
{
if (hNotifyLock != default)
SHChangeNotification_Unlock(hNotifyLock);
}
return;
}
base.WndProc(ref m);
}
}
}
}