Managed Interception of Local Window Messages Using Subclassing

True to its name, the Windows operating system revolves around the multitude of messages sent to the windows that happen to be active on the desktop. An incredible array of system-level functionality relies on having a window that can participate in the message-passing model Windows employs.

Access to the messages being sent to a window gives us much flexibility and power over our applications. If we happen to be writing managed code, however, we are oftentimes too well insulated from the nitty and gritty, and it can be difficult to get access to the low-level passing of messages to relevant windows.

Using a UI framework such as Windows Forms or Windows Presentation Foundation can simplify accessing relevant window messages; however, knowing how to do so without relying on such behemoths of dependencies is helpful.

In this article, the first in a series, we’ll review how to access messages sent to a local window (i.e., a window belonging to the process hosting our .NET code). Then, in a follow-up article, we’ll expand upon the code we write here to add the ability to access external, out-of-process window messages (a much taller task).

Receiving Window Messages

There are two places in an application where window messages make an appearance: message loops and window procedures.

Message Loops

When a window is created on a thread, a message queue will be created (if one does not already exist). This queue holds the messages for the windows on a particular thread; the message loop retrieves those messages and dispatches them to the intended window procedure.

This is all done via the “famous” trio of methods: GetMessage, TranslateMessage, and DispatchMessage.

We’re not going to bother too much with message loops in this article, rather we’re going to focus on the destination of a window message: the window procedure.

Window Procedure

All messages eventually end up in a window procedure, whether they went through a message queue or were directly sent to one (thereby skipping the queue). The window procedure typically takes on the following, rather (in)famous, signature:

Window Procedure Signature
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

The window procedure is where we’re going to intercept local window messages in our managed program, and we’re going to do it by employing a technique known as subclassing.

Intercepting Messages via Subclassing

Subclassing is a Microsoft term for how one can go about changing or adding additional features to an existing control or window.

Put more succinctly: it allows us to replace the window procedure for a window with our own so that we may intercept and process messages being sent to said window.

There are two ways to subclass the window: the new-school method of using subclassing helper functions and the old-school way of directly changing the pointer to the window procedure.

The New-School Subclassing Helper Functions

The release of ComCtl32.dll version 6 provided us with some new Win32 API functions purposed explicitly for the act of subclassing, something Windows had not offered before.

These helper functions are SetWindowSubclass, GetWindowSubclass, and RemoveWindowSubclass. What each one does is self-explanatory, and they make the process of subclassing windows and controls cleaner and less bug-prone.

Unfortunately, I never use these functions because of one major limitation: you cannot use the subclassing helper functions across threads (i.e., we must call them from the same thread that owns the target window).

The old-school method does not share this limitation, which I consider a major one.

The Old-School Changing of the Pointer (My Preferred Way)

The tried-and-true approach to subclassing involves changing a window’s attribute that points to its window procedure via the SetWindowLongPtr (or SetWindowLong, if we’re in 32-bit land) method.

We need to be a bit more organized when going about it in this manner, as we need to keep track of the window procedure we are replacing. It is imperative that we call the previous window procedure with all unhandled messages, and we will also need to restore it as the actual window procedure should our code need to clean itself up.

The general sequence of events when subclassing/unsubclassing:

  1. Get the window’s current window procedure and store it.
  2. Marshal the delegate for our new window procedure into a function pointer.
  3. Replace the window procedure at nIndex GWLP_WNDPROC using SetWindowLongPtr.
  4. Have sufficient logic in place to restore the old window procedure if our code is being disposed, etc.

That’s more or less the simplified version of what we need to do. Let us take a look, now, at the family of objects that will provide our local (and eventually, external) window message intercept functionality.

A Wrapper Around a Window and Its Messages

At the top of our object hierarchy is the IMessageSource interface. Implementations of this interface provide a means for registering one or more hooks that will be provided with event-related messages.

The purpose and usefulness of this abstraction will become more apparent in the following article when we look at external window message interception via Win32 hooks.

IMessageSource.cs
/// <summary>
/// Defines a publisher of messages sent through a hook chain.
/// </summary>
/// <typeparam name="THookProc">The type of hook procedure used by the hook type.</typeparam>
public interface IMessageSource<in THookProc> where THookProc : Delegate
{
    /// <summary>
    /// Adds a hook that will receives messages sent to the hook chain.
    /// </summary>
    /// <param name="hook">
    /// The hook to invoke when messages are sent to the associated hook chain.
    /// </param>
    void AddHook(THookProc hook);

    /// <summary>
    /// Removes a hook previously receiving messages sent to the hook chain.
    /// </summary>
    /// <param name="hook">The hook to remove.</param>
    void RemoveHook(THookProc hook);
}

In the follow-up article, we will use this interface to implement event messengers for various Win32 hook types. For now, we will be creating an implementation for window messages.

This implementation will act as a wrapper around an HWND of a provided window and the messages it receives. The WindowWrapper class is a base, abstract type that handles all the boilerplate concerns of registering custom hooks and passing window messages along to them.

WindowWrapper.cs
/// <summary>
/// Provides a wrapper around an <c>HWND</c> of a provided window and the messages it receives.
/// </summary>
public abstract class WindowWrapper : IMessageSource<WindowHookProc>
{
    private readonly CachedWeakList<WindowHookProc> _hooks = new();

    /// <summary>
    /// Initializes a new instance of the <see cref="WindowWrapper"/> class.
    /// </summary>
    /// <param name="handle">A handle to the window being wrapped.</param>
    protected WindowWrapper(WindowHandle handle)
    {
        Require.NotNull(handle, nameof(handle));

        Handle = handle;
    }

    /// <summary>
    /// Gets the handle to the wrapped window.
    /// </summary>
    public WindowHandle Handle 
    { get; init; }

    /// <summary>
    /// Adds a hook that will receive messages prior to any existing hooks receiving them.
    /// </summary>
    /// <param name="hook">The hook to invoke when messages are sent to the wrapped window.</param>
    public void AddStartingHook(WindowHookProc hook)
    {
        _hooks.Insert(0, hook);
    }

    /// <inheritdoc/>
    public void AddHook(WindowHookProc hook)
    {
        Require.NotNull(hook, nameof(hook));

        _hooks.Add(hook);

        OnHookAdded(hook);
    }

    /// <inheritdoc/>
    public void RemoveHook(WindowHookProc hook)
    {
        Require.NotNull(hook, nameof(hook));

        _hooks.Remove(hook);

        OnHookRemoved(hook);
    }
    
    /// <summary>
    /// Called when a hook has been added to the wrapped window.
    /// </summary>
    /// <param name="addedHook">The hook that was added.</param>
    protected virtual void OnHookAdded(WindowHookProc addedHook)
    { }

    /// <summary>
    /// Called when a hook has been removed from the wrapped window.
    /// </summary>
    /// <param name="removedHook">The hook that was removed.</param>
    protected virtual void OnHookRemoved(WindowHookProc removedHook)
    { }

    /// <summary>
    /// Called when the wrapped window is in the process of being destroyed.
    /// </summary>
    /// <remarks>Override this to engage in any last minute cleanup efforts.</remarks>
    protected virtual void OnDestroyingWindow()
    { }
    
    /// <summary>
    /// The callback, or window procedure, that processes messages sent to the wrapped window by
    /// calling any registered hooks.
    /// </summary>
    /// <param name="hWnd">A handle to the window.</param>
    /// <param name="msg">The message.</param>
    /// <param name="wParam">Additional message-specific information.</param>
    /// <param name="lParam">Additional message-specific information.</param>
    /// <returns>
    /// The result of the message processing, which of course depends on the message being
    /// processed.
    /// </returns>
    protected HookResult WindowProcedure(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        bool forceUnhandled = false;
        var result = new HookResult(IntPtr.Zero, false);
        var message = (WindowMessage) msg;

        foreach (WindowHookProc hook in _hooks)
        {
            result = hook(hWnd, msg, wParam, lParam);
                    
            if (result.Handled)
                break;
        }

        if (WindowMessage.CreateNonclientArea == message)
        {
            forceUnhandled = true;
        }
        else if (WindowMessage.DestroyNonclientArea == message)
        {
            OnDestroyingWindow();
            // We want to make sure we always pass on WM_NCDESTROY messages.
            forceUnhandled = true;
        }

        return result with
               {
                   Handled = !forceUnhandled && result.Handled
               };
    }
}

The WindowWrapper class is abstract because the numerous types of windows require different procedures to be followed to subclass them.

The critical bit of code is the de facto “window procedure” represented by the WindowProcedure method, which executes all registered hooks and takes care of some of those boilerplate concerns mentioned before, such as initiating a cleanup if the window is closing.

Notice that the WindowWrapper class accepts hooks of the WindowHookProc type. If we look at the definition for this delegate, we can see that it shares an identical signature with the WindowProcedure method.

WindowHookProc.cs
/// <summary>
/// Represents a callback that processes messages sent to a window hook procedure.
/// </summary>
/// <param name="hWnd">A handle to the window.</param>
/// <param name="msg">The message.</param>
/// <param name="wParam">Additional message-specific information.</param>
/// <param name="lParam">Additional message-specific information.</param>
/// <returns>
/// The result of the message processing, which of course depends on the message being processed.
/// </returns>
public delegate HookResult WindowHookProc(IntPtr hWnd, 
                                          uint msg, 
                                          IntPtr wParam, 
                                          IntPtr lParam);

The HookResult type is a simple container type that contains an IntPtr LRESULT and a bool value indicating if the message should be considered to have been handled by a particular hook. A dedicated handled flag is needed because LRESULT values are not at all standardized (their meaning differs based on the message).

A number of concrete derivations of WindowWrapper exist, but for the sake of brevity, let’s examine the most pertinent one: the LocalWindowWrapper type.

LocalWindowWrapper.cs
/// <summary>
/// Provides a wrapper around an <c>HWND</c> of a provided in-process window and the messages it
/// receives.
/// </summary>
/// <remarks>
/// Because this wrapper intercepts window messages by subclassing the provided window, the window
/// being wrapped must be local to the same process hosting the .NET runtime that is executing this
/// code. If the window was created in another process, you'll need to use the
/// <see cref="GlobalWindowWrapper"/> type instead.
/// </remarks>
public sealed class LocalWindowWrapper : WindowWrapper, IDisposable
{
    private readonly WindowSubclass _subclass;
    private bool _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="LocalWindowWrapper"/> class.
    /// </summary>
    /// <param name="handle">A handle to the window being wrapped.</param>
    public LocalWindowWrapper(WindowHandle handle) 
        : base(handle)
    {
        var subclass = new WindowSubclass(WindowProcedure);

        subclass.Attach(handle);
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        if (_disposed)
            return;

        _subclass.Dispose();

        _disposed = true;
    }
}

The above class may appear overly simplistic; however, it is precisely as complex as it needs to be.

The bulk of the work is handled by the WindowSubclass type in the constructor, which takes a reference to this type’s WindowProcedure method. Other WindowWrapper-derived types also use the WindowSubclass type, albeit in different ways.

So, lets take a look at the actual subclassing code which is used by LocalWindowWrapper and other concrete WindowWrapper derivations.

The Subclassing Code

The WindowSubclass type is the main workhorse responsible for managing the window procedure pointers for a target window.

WindowSubclass.cs
/// <summary>
/// Provides a way to subclass a window in a managed environment.
/// </summary>
/// <remarks>
/// <para>
/// This class gives managed objects the ability to subclass an unmanaged window or control. If
/// you are unfamiliar with subclassing, it is a Microsoft term for how one can go about changing
/// or adding additional features to an existing control or window.
/// </para>
/// <para>
/// Put more succinctly, it allows us to replace the window's window procedure with our own so
/// that we may intercept and process messages sent to the window.
/// </para>
/// </remarks>
internal sealed class WindowSubclass : IDisposable
{
    private static readonly WindowMessage _DetachMessage
        = User32.RegisterWindowMessage("WindowSubclass.DetachMessage");

    private static readonly List<WindowSubclass> _Subclasses
        = new();

    private static readonly object _SubclassesLock
        = new();

    private static readonly IntPtr _DefaultWindowProc 
        = GetDefaultWindowProc();

    private static readonly AssemblyLoadContext _LoadContext;

    private static int _ShutdownHandled;

    private readonly IThreadExecutor? _executor;
    private readonly WeakReference _hook;

    /// <summary>
    /// <para>
    /// A <see cref="GCHandle"/> is employed by this class so it won't get collected even in
    /// the event that all managed references to it get released. This is very important because
    /// the oh-so-relevant unmanaged component at hand (i.e., the window we're subclassing)
    /// will still have a reference to us, and this is a situation very much outside the purview
    /// of the .NET garbage collector.
    /// </para>
    /// <para>
    /// Premature cleanup could result in situations where the unmanaged component attempts to
    /// call into freshly deallocated managed memory, which would cause us quite a nasty program
    /// crash.
    /// </para>
    /// </summary>
    private GCHandle _gcHandle;
    private WindowHandle? _window;
    private AttachmentState _state;
    private WindowProc? _wndProcCallback;
    private IntPtr _wndProc;
    private IntPtr _oldWndProc;
    private bool _disposed;

    /// <summary>
    /// Initializes the <see cref="WindowSubclass"/> class.
    /// </summary>
    static WindowSubclass()
    {
        // We ensure we're listening to the particular load context responsible for loading Bad Echo
        // framework code; we can't assume we're always a static dependency.
        _LoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly())
            ?? AssemblyLoadContext.Default;

        _LoadContext.Unloading += HandleContextUnloading;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="WindowSubclass"/> class.
    /// </summary>
    /// <param name="hook">
    /// The delegate that will be executed to process messages that are sent or posted to the
    /// window.
    /// </param>
    /// <param name="executor">The executor that will power the subclass.</param>
    public WindowSubclass(WindowHookProc hook, IThreadExecutor executor)
        : this(hook)
    {
        Require.NotNull(executor, nameof(executor));

        _executor = executor;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="WindowSubclass"/> class.
    /// </summary>
    /// <param name="hook">
    /// The delegate that will be executed to process messages that are sent or posted to the
    /// window.
    /// </param>
    /// <remarks>
    /// Subclassing without the use of a <see cref="IThreadExecutor"/> will result in direct
    /// invocation of the message processing hook. Use of an executor is recommended if
    /// support for more complex functionality such as async operations is desired.
    /// </remarks>
    public WindowSubclass(WindowHookProc hook)
    {
        Require.NotNull(hook, nameof(hook));
        
        _hook = new WeakReference(hook);

        _gcHandle = GCHandle.Alloc(this);
    }
    
    /// <summary>
    /// Attaches to and effectively subclasses the provided window by changing the address of
    /// its <see cref="WindowAttribute.WindowProcedure"/>.
    /// </summary>
    /// <param name="window">The window to subclass.</param>
    public void Attach(WindowHandle window)
    {
        Require.NotNull(window, nameof(window));
        
        IntPtr oldWndProc = User32.GetWindowLongPtr(window, WindowAttribute.WindowProcedure);

        Attach(window, WndProc, oldWndProc);
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        if (_disposed)
            return;

        Unhook(false);

        // If somehow we're still pinned, unpin ourselves so we can get garbage collected.
        // This should only occur if attachment never occurred due to an error occurring
        // with the creation of the window which we intended to subclass.
        if (_gcHandle.IsAllocated)
            _gcHandle.Free();

        _disposed = true;
    }

    /// <summary>
    /// Processes messages sent to the subclassed window.
    /// </summary>
    /// <param name="hWnd">A handle to the window.</param>
    /// <param name="msg">The message.</param>
    /// <param name="wParam">Additional message-specific information.</param>
    /// <param name="lParam">Additional message-specific information.</param>
    /// <returns>Value indicating the success of the operation.</returns>
    /// <remarks>
    /// <para>
    /// Attachment and thus the subclassing of the window is performed here
    /// if <see cref="Attach(WindowHandle)"/> has not already been called. This sort
    /// of scenario is possible if a window class was manually registered, with its
    /// <see cref="WindowClass.WindowProc"/> set to this method.
    /// </para>
    /// <para>
    /// Otherwise, this method will only begin to process messages once
    /// <see cref="Attach(WindowHandle)"/> has been executed.
    /// </para>
    /// </remarks>
    internal IntPtr WndProc(IntPtr hWnd, uint msg, nint wParam, nint lParam)
    {
        var lResult = IntPtr.Zero;
        var message = (WindowMessage)msg;
        bool handled = false;

        switch (_state)
        {
            case AttachmentState.Unattached:
                var window = new WindowHandle(hWnd, false);
                Attach(window, WndProc, _DefaultWindowProc);
                break;

            case AttachmentState.Detached:
                throw new InvalidOperationException(Strings.SubclassDetachedWndProc);
        }

        IntPtr oldWndProc = _oldWndProc;

        if (_DetachMessage == message)
        {
            if (IntPtr.Zero == wParam || wParam == (IntPtr) _gcHandle)
            {
                bool forcibly = lParam > 0;

                lResult = Detach(forcibly) ? new IntPtr(1) : IntPtr.Zero;
                handled = !forcibly;
            }
        }
        else
        {
            if (_executor is not { IsShutdownComplete: true })
            {
                HookResult? result = SendOperation(hWnd, msg, wParam, lParam);

                if (result != null)
                {
                    lResult = result.LResult;
                    handled = result.Handled;
                }
            }

            if (WindowMessage.DestroyNonclientArea == message)
            {
                Detach(true);
                // WM_NCDESTROY should always be passed down the chain.
                handled = false;
            }
        }

        // If the message wasn't handled, pass it down the WndProc chain.
        if (!handled)
            lResult = User32.CallWindowProc(oldWndProc, hWnd, message, wParam, lParam);

        return lResult;
    }

    private static IntPtr GetDefaultWindowProc()
    {
        IntPtr hModule = User32.GetModuleHandle();

        return Kernel32.GetProcAddress(hModule, User32.ExportDefWindowProcW);
    }

    private static void RestoreWindowProc(WindowHandle window, 
                                          IntPtr procToRestore, 
                                          bool forceClose)
    {
        if (window.IsInvalid)
            return;

        if (procToRestore == IntPtr.Zero)
            return;

        IntPtr result = User32.SetWindowLongPtr(window, 
                                                WindowAttribute.WindowProcedure, 
                                                procToRestore);
        if (result == IntPtr.Zero)
        {   // The only acceptable outcome here is a window handle that is now invalid due to the
            // window being destroyed already.
            var error = (ErrorCode)Marshal.GetLastWin32Error();

            if (error is not ErrorCode.InvalidWindowHandle and not ErrorCode.Success)
                throw new Win32Exception((int)error);
        }

        if (result != IntPtr.Zero)
            User32.PostMessage(window, WindowMessage.Close, IntPtr.Zero, IntPtr.Zero);
    }

    private static void HandleContextUnloading(AssemblyLoadContext loadContext)
    {
        if (Interlocked.Exchange(ref _ShutdownHandled, 1) != 0)
            return;

        _LoadContext.Unloading -= HandleContextUnloading;

        lock (_SubclassesLock)
        {
            foreach (var subclass in _Subclasses)
            {
                if (subclass._window == null)
                    continue;

                // Back when multiple AppDomains existed, there was a chance the AppDomain hosting
                // this code could be unloading in a multiple AppDomain environment where a
                // host window belonging to a separate AppDomain had one or more of its child
                // windows subclassed, which meant we needed to notify the parent window in some
                // fashion by sending a blocking detach message. Don't need to worry about that
                // now! I think...

                // If the default context is being unloaded, we will simply restore the
                // DefaultWindowProc in case the previous window procedure is also managed.
                IntPtr procToRestore = loadContext == AssemblyLoadContext.Default
                    ? _DefaultWindowProc
                    : subclass._oldWndProc;

                RestoreWindowProc(subclass._window, procToRestore, true);
            }
        }
    }

    private void Attach(WindowHandle window, WindowProc newWndProcCallback, IntPtr oldWndProc)
    {
        _window = window;
        _state = AttachmentState.Attached;

        _wndProcCallback = newWndProcCallback;
        _wndProc = Marshal.GetFunctionPointerForDelegate(_wndProcCallback);
        _oldWndProc = oldWndProc;

        User32.SetWindowLongPtr(_window, WindowAttribute.WindowProcedure, _wndProc);
        
        lock (_SubclassesLock)
        {
            _Subclasses.Add(this);
        }
    }

    /// <remarks>
    /// <para>
    /// The <c>forcibly</c> parameter exists because, due to how subclassing works, it is not always
    /// possible to safely remove a particular window procedure from a <see cref="WindowProc"/>
    /// chain. "Safely", used in this context, means in a way that limits the amount of disruption
    /// caused to other subclasses that may have contributed to the WndProc chain.
    /// </para>
    /// <para>
    /// If we are not in a position to unhook from the window's message chain with <c>forcibly</c>
    /// set to false, then we essentially leave everything untouched. If instead we force the
    /// detachment, then it is guaranteed that <see cref="WndProc"/> and the hook supplied at
    /// initialization will no longer be executed; unfortunately, this guarantee extends to
    /// all subclasses appearing before this one on the <see cref="WindowProc"/> chain as
    /// well (bad).
    /// </para>
    /// </remarks>
    private bool Detach(bool forcibly)
    {
        bool detached;

        if (_state is AttachmentState.Detached or AttachmentState.Unattached)
            detached = true;
        else
        {
            _state = AttachmentState.Detaching;

            detached = Unhook(forcibly);
        }

        if (!detached)
        {
            Logger.Warning(forcibly
                               ? Strings.SubclassForcibleDetachmentFailed
                               : Strings.SubclassDetachmentFailed);
        }

        return detached;
    }

    /// <remarks>
    /// <para>
    /// If the current <see cref="WindowProc"/> assigned to the window subclassed by this type is
    /// something other than our own, then that means the window has been subclassed by some
    /// other code and our <see cref="WindowProc"/> is no longer at the head. This also means
    /// that we are unable to unhook our own <see cref="WindowProc"/> from the chain without
    /// causing disruption to the other subclasses.
    /// </para>
    /// <para>
    /// Setting <c>forcibly</c> to false will result in our <see cref="WindowProc"/> only being
    /// removed if the current <see cref="WindowProc"/> points to <see cref="WndProc"/>. If this
    /// is the case, or if <c>forcibly</c> is true (and we're going to go ahead with a detachment
    /// whether it's disruptive or not), then we restore the original <see cref="WindowProc"/>
    /// function that was stored previously during this subclass's attachment phase.
    /// </para>
    /// <para>
    /// If the detachment occurs, then we also end up freeing the <see cref="GCHandle"/> that was
    /// allocated previously, making this class eligible for garbage collection.
    /// </para>
    /// </remarks>
    private bool Unhook(bool forcibly)
    {
        if (_state is AttachmentState.Unattached or AttachmentState.Detached || _window == null)
            return true;

        if (!forcibly)
        {
            IntPtr currentWndProc = User32.GetWindowLongPtr(_window, 
                                                            WindowAttribute.WindowProcedure);
            forcibly = currentWndProc == _wndProc;
        }

        if (!forcibly)
            return false;

        _state = AttachmentState.Detaching;
            
        lock (_Subclasses)
        {
            _Subclasses.Remove(this);
        }

        RestoreWindowProc(_window, _oldWndProc, false);

        _state = AttachmentState.Detached;
        _oldWndProc = IntPtr.Zero;
        _wndProcCallback = null;
        _wndProc = IntPtr.Zero;

        if (_gcHandle.IsAllocated)
            _gcHandle.Free();

        return true;
    }

    private HookResult? SendOperation(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        SubclassOperationParameters parameters = new (hWnd, msg, wParam, lParam);
        
        object? operationResult = _executor != null
            ? _executor.Invoke(ExecuteHook, null)
            : ExecuteHook();

        return operationResult as HookResult;

        HookResult? ExecuteHook()
        {
            HookResult? result = null;

            if (_state == AttachmentState.Attached)
            {   // The message received by our subclass shall now be passed to the entry point for
                // our registered window message hooks.
                if (_hook is { Target: WindowHookProc hook })
                {
                    result = hook(parameters.HWnd,
                                  parameters.Msg,
                                  parameters.WParam,
                                  parameters.LParam);
                }
            }

            return result;
        }
    }

    private enum AttachmentState
    {
        Unattached,
        Attached,
        Detaching,
        Detached
    }

    private sealed record SubclassOperationParameters(IntPtr HWnd = default,
                                                      uint Msg = default,
                                                      IntPtr WParam = default,
                                                      IntPtr LParam = default);
}

This provides a full-featured subclassing of a provided WindowHandle instance. The above code is direct from the Bad Echo core frameworks, so it contains more code than what is required for a barebones subclassing.

The most important bits for a cursory understanding of managed subclassing are in the Attach and WndProc methods — the latter of which, you will note, actually mirrors a proper window procedure’s signature.

It is this very WndProc method that ends up being marshalled into the function pointer that we register with SetWindowLongPtr, effectively realizing the subclass.

What Else the Subclass Code Does

I regret not providing a more simplified iteration of the WindowSubclass type; however, the hour draws late, and too much information should suffice where too little would not.

A great deal of logic is dedicated to ensure that the subclass is effectively cleaned up when it is time for us to go. This is achieved by the detachment and unsubclassing of our subclass when the relevant “window messages of doom” (like WM_NCDESTROY) are received.

Manual disposal will, of course, be required if we don’t wish to tie the lifetime of the subclass object with the lifetime of the actual window.

In addition to direct invocations of hook operations in response to message intercepts, the WindowSubclass type also supports the use of an optional IThreadExecutor instance.

Thread executors are defined in the Bad Echo core frameworks, and allow for the execution of code on specific threads — the main implementation achieves this through the use of a wrapped message-only window (something which is incredibly useful for when we wish to make use of Windows APIs while not actually having a window handle handy) and was inspired by WPF’s Dispatcher class.

Because we actually are responsible for creating the message-only window, the WindowSubclass has been designed such that its window procedure can also be provided as the lpfnWndProc parameter when registering the window’s class.

So, the WindowSubclass type supports “subclassing” either at the time of window creation (and class registration) or after it. An example of the former can be observed by looking at the inner workings of the MessageOnlyWindowWrapper type.

Omni Dab!

While all of that is fascinating, these topics are outside the scope of this article.

The basic technique for the managed interception of local window messages using subclassing has been provided; therefore, the article shall come to an end.

Out-Of-Process Window Message Interception?

The following article in this series will be the truly interesting one, as it will cover how we can expand upon the code defined in this article so that we can intercept messages being sent to windows belonging to other (probably unmanaged) processes.

External window message interception will require the use of Win32 hooks — a programming technique that is difficult to take advantage of in managed development environments.

After covering how we might intercept out-of-process window messages, we will expand our coverage beyond mere window procedure messages and look at how we might intercept other types of hook messages.

Until then, feel free to comment if you have any questions/comments/concerns. Thanks for reading!