using System; using static Vanara.PInvoke.Kernel32; using static Vanara.PInvoke.User32; namespace Vanara.PInvoke { /// /// /// This class encapsulates the management of a message loop for an application. It supports queuing a callback to the application via /// the message loop to enable the app to return from a call and continue processing that call later. This behavior is needed when /// implementing a shell verb as verbs must not block the caller. /// /// The ComObject derived class should call QueueNonBlockingCallback in its invoke function, for example /// IExecuteCommand::Execute() or IDropTarget::Drop() passing a method that will complete the initialization work. /// public class MessageLoop { private readonly uint curThreadId; private Action appCallback; private uint callbackMsg; private object callbackObj; private IntPtr timeoutTimerId; // timer id used to exit the app if the app is not called back within a certain time /// Initializes a new instance of the class. public MessageLoop() => curThreadId = GetCurrentThreadId(); /// Occurs when a new message is available. public event EventHandler ProcessMessage; /// Gets a value indicating whether this is running. /// if running; otherwise, . public virtual bool Running { get; private set; } = false; /// /// Cancel the timeout timer. This should be called when the application knows that it wants to keep running, for example when it /// receives the incoming call to invoke the verb. /// public virtual void CancelTimeout() { if (timeoutTimerId == default) return; KillTimer(default, timeoutTimerId); timeoutTimerId = default; } /// /// Queues a one-time callback function via the message loop. This method is not intended to be used simultaneously by multiple callers. /// /// The callback delegate method. /// An optional argument that will be passed to the callback. /// Another callback is currently queued. /// cannot be . public virtual void QueueCallback(Action callback, object tag = null) { if (appCallback != null) throw new InvalidOperationException("Another callback is currently queued."); appCallback = callback ?? throw new ArgumentNullException(nameof(callback)); callbackObj = tag; if (callbackMsg == 0) callbackMsg = RegisterWindowMessage(GetType().FullName + "+Callback"); PostThreadMessage(curThreadId, callbackMsg); } /// Quits the running message loop by calling . /// An optional exit code. public virtual void Quit(int exitCode = 0) { CancelTimeout(); if (Running) PostQuitMessage(exitCode); } /// Runs the message loop. /// /// The time span after which the message loop will be terminated. If this value equals TimeSpan.Zero or is not specified, the /// message loop will run until the method is called or the message loop receives a quit message. /// public virtual void Run(TimeSpan timeout = default) { const uint WM_TIMER = 0x0113; if (timeout != TimeSpan.Zero) timeoutTimerId = SetTimer(uElapse: (uint)timeout.TotalMilliseconds); Running = true; while (GetMessage(out var msg) != 0) { System.Diagnostics.Debug.WriteLine($"Message loop: message={msg.message}"); try { ProcessMessage?.Invoke(this, new MessageEventArgs(msg)); } catch { } if (msg.message == WM_TIMER) { KillTimer(default, msg.wParam); if (msg.wParam == timeoutTimerId) { timeoutTimerId = IntPtr.Zero; PostQuitMessage(0); } } else if (msg.message == callbackMsg) { try { appCallback?.Invoke(callbackObj); } catch { } appCallback = null; callbackObj = null; } TranslateMessage(msg); DispatchMessage(msg); } Running = false; } /// Holds a copy of the MSG instance retrieved by GetMessage. /// public class MessageEventArgs : EventArgs { internal MessageEventArgs(MSG msg) => MSG = msg; /// Gets or sets the MSG. /// The MSG. public MSG MSG { get; set; } } } }