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; }
}
}
}