From 1196a4ed4808dab7ac88f1a4717395822a719492 Mon Sep 17 00:00:00 2001 From: dahall Date: Tue, 22 Dec 2020 09:59:32 -0700 Subject: [PATCH] Added abstract `SystemEventHandler` which provides a smart message window that will automatically spin up a thread, if needed, for the message pump. --- PInvoke/User32/SystemEventHandler.cs | 267 +++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 PInvoke/User32/SystemEventHandler.cs diff --git a/PInvoke/User32/SystemEventHandler.cs b/PInvoke/User32/SystemEventHandler.cs new file mode 100644 index 00000000..95195580 --- /dev/null +++ b/PInvoke/User32/SystemEventHandler.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Vanara.InteropServices; +using Vanara.PInvoke; +using static Vanara.PInvoke.Kernel32; +using static Vanara.PInvoke.User32; + +namespace Vanara.PInvoke +{ + /// + /// An event handler that is dependent on window messages. This class works on both windowed and console applications, creating a + /// threaded message pump if needed. + /// + /// To use, derive a class and override the method. When handling the message, use the method to call the event. + /// + /// + /// Delegates can be registered and unregistered using unique GUID values via the and methods. + /// + /// + /// + public abstract class SystemEventHandler : IDisposable + { + private static ManualResetEvent eventWindowReady; + private static Thread windowThread; + private readonly Dictionary eventHandles = new Dictionary(); + private readonly object lockObj = new object(); + private bool disposedValue; + private BasicMessageWindow msgWindow; + + /// Initializes a new instance of the class. + protected SystemEventHandler() + { + if (Thread.GetDomain().GetData(".appDomain") != null) + throw new InvalidOperationException("System events not supported."); + + if (!UserSession.IsInteractive || Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) + { + Init(); + } + else + { + ThreadRunning = new ManualResetEvent(false); + eventWindowReady = new ManualResetEvent(false); + windowThread = new Thread(MTAThreadProc) { IsBackground = true, Name = typeof(SystemEventHandler).FullName }; + windowThread.Start(this); + eventWindowReady.WaitOne(); + } + } + + /// Finalizes an instance of the class. + ~SystemEventHandler() + { + Dispose(false); + } + + /// Gets the message window handle which can be used to register for messaged events. + /// The message window handle. + protected HWND MessageWindowHandle => msgWindow?.Handle ?? HWND.NULL; + + private ManualResetEvent ThreadRunning { get; set; } + + /// Adds a delegate and its associated key to the handler list. + /// The key. + /// The delegate value. + public void AddEvent(Guid key, Delegate value) + { + lock (lockObj) + { + if (!eventHandles.TryGetValue(key, out Delegate h)) + { + eventHandles.Add(key, value); + OnEventAdd(key); + } + else + { + eventHandles[key] = Delegate.Combine(h, value); + } + } + } + + /// Removes a delegate and its associated key to the handler list. + /// The key. + /// The delegate value. + public void RemoveEvent(Guid key, Delegate value) + { + lock (lockObj) + { + if (eventHandles.TryGetValue(key, out Delegate h)) + { + h = Delegate.Remove(h, value); + if (h is null || h.GetInvocationList().Length == 0) + { + eventHandles.Remove(key); + OnEventRemove(key); + } + else + { + eventHandles[key] = h; + } + } + else + { + OnEventRemove(key); + } + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + void IDisposable.Dispose() + { + Dispose(true); + System.GC.SuppressFinalize(this); + } + + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + if (ThreadRunning != null && IsWindow(MessageWindowHandle)) + { + PostMessage(MessageWindowHandle, (uint)WindowMessage.WM_QUIT); + ThreadRunning.WaitOne(5000); + ThreadRunning = null; + } + } + + msgWindow.Dispose(); + + disposedValue = true; + } + } + + /// Determines whether the specified key has a delegate handler in its list. + /// The key. + /// if the specified key exists; otherwise, . + protected bool HasKey(Guid key) + { + lock (lockObj) + return eventHandles.ContainsKey(key); + } + + /// Provides access to the WndProc listening for messages. + /// A handle to the window procedure that received the message. + /// The message. + /// Additional message information. The content of this parameter depends on the value of the Msg parameter. + /// Additional message information. The content of this parameter depends on the value of the Msg parameter. + /// The return value is the result of the message processing and depends on the message. + /// + /// if this message should be considered handled; or to pass the message along to + /// . + /// + protected abstract bool MessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn); + + /// Called when an event has been added. + /// The event key. + protected virtual void OnEventAdd(Guid key) + { + } + + /// Called when an event has been removed. + /// The event key. + protected virtual void OnEventRemove(Guid key) + { + } + + /// Calls the delegate list associated with the key, passing the supplied parameters. + /// The key. + /// The arguments. + /// The value returned by the call to the delegate list. + /// Event for {key} is not registered. + protected object RaiseEvent(Guid key, params object[] args) + { + Delegate h; + lock (lockObj) + { + if (!eventHandles.TryGetValue(key, out h)) + throw new InvalidOperationException($"Event for {key} is not registered."); + } + return h.DynamicInvoke(args); + } + + private static void MTAThreadProc(object param) + { + var handler = (SystemEventHandler)param; + try + { + handler.Init(); + eventWindowReady.Set(); + if (!handler.MessageWindowHandle.IsNull) + { + var keepRunning = true; + while (keepRunning) + { + var ret = MsgWaitForMultipleObjectsEx(0, null, 100, QS.QS_ALLINPUT, MWMO.MWMO_INPUTAVAILABLE); + if (ret == (uint)WAIT_STATUS.WAIT_TIMEOUT) + { + Thread.Sleep(1); + } + else + { + while (PeekMessage(out MSG msg, wRemoveMsg: PM.PM_REMOVE)) + { + if (msg.message == (uint)WindowMessage.WM_QUIT) + { + keepRunning = false; + break; + } + TranslateMessage(msg); + DispatchMessage(msg); + } + } + } + } + } + catch (Exception e) + { + eventWindowReady.Set(); + if (e is not (ThreadInterruptedException or ThreadAbortException)) + System.Diagnostics.Debug.Fail("Unexpected thread exception in SystemEventHandler thread.", e.ToString()); + } + + handler.ThreadRunning.Set(); + } + + private void Init() + { + msgWindow = new BasicMessageWindow(MessageFilter); + } + } + + internal static class UserSession + { + private static bool isUserInteractive; + private static HWINSTA processWinStation; + + public static bool IsInteractive + { + get + { + if (Environment.OSVersion.Platform == System.PlatformID.Win32NT) + { + HWINSTA hwinsta = GetProcessWindowStation(); + if (!hwinsta.IsNull && processWinStation != hwinsta) + { + using var flags = new SafeCoTaskMemStruct(); + isUserInteractive = !GetUserObjectInformation((IntPtr)hwinsta, UserObjectInformationType.UOI_FLAGS, flags, flags.Size, out _) || (flags.Value.dwFlags & 0x0001 /*WSF_VISIBLE*/) != 0; + processWinStation = hwinsta; + } + } + else + { + isUserInteractive = true; + } + return isUserInteractive; + } + } + } +} \ No newline at end of file