Detect application-specific user activity within Visual Studio Extension (VSIX)

I would like to programmatically detect when a user is interacting with Visual Studio from within a Visual Studio extension (VSIX). I do not want any keypress or mouse event information, just a notification the user is doing something within the running Visual Studio instance the VSIX is installed in. I seek a ‘signal’ to know if the user is moving the mouse across the VS IDE, or clicking any mouse button, or pressing any key or key combination that the running VS instance will ‘see’.

Why do I wish to know this?
I have an inactivity timer running which will trigger some behaviour if inactivity meets or exceeds a defined period. I need to be able to reset the start time of the timer when the user performs any form of keyboard/mouse input before such. This is not limited to code editor windows, but literally any form of user interaction with the running Visual Studio instance. So equally it is not Windows system-wide, but it is beyond the extension (VSIX) itself.

Currently, I am using system-wide hooks and trying to filter the event messages to Visual Studio main window handle as obtained from the EnvDTE80 object. This seems far too intensive and results in excess windows messages needing some kind of evaluation, with a periodic side effect of slowing responsiveness of the entire environment. (It’s not doing a Garbage Collection when the system slows).

Here is a sanitized/simplified gist of what I am using:

using System;
using System.Runtime.InteropServices;

public class UserActivityMonitor
{
    [DllImport("user32.dll")]
    private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelInputProc callback, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    private delegate IntPtr LowLevelInputProc(int nCode, IntPtr wParam, IntPtr lParam);

    private const int WH_KEYBOARD_LL = 13;
    private const int WH_MOUSE_LL = 14;

    private const int WM_KEYDOWN = 0x0100;
    private const int WM_LBUTTONDOWN = 0x0201;
    private const int WM_RBUTTONDOWN = 0x0204;
    private const int WM_MOUSEMOVE = 0x0200;

    private static IntPtr keyboardHookID = IntPtr.Zero;
    private static IntPtr mouseHookID = IntPtr.Zero;

    public void StartMonitoring()
    {
        // Set up the keyboard hook
        keyboardHookID = SetWindowsHookEx(WH_KEYBOARD_LL, HookKeyboardCallback, IntPtr.Zero, 0);

        // Set up the mouse hook
        mouseHookID = SetWindowsHookEx(WH_MOUSE_LL, HookMouseCallback, IntPtr.Zero, 0);
    }

    public void StopMonitoring()
    {
        // Unhook the keyboard hook
        UnhookWindowsHookEx(keyboardHookID);

        // Unhook the mouse hook
        UnhookWindowsHookEx(mouseHookID);
    }

    private IntPtr HookKeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            // Handle keyboard event - reset the start time to now (not shown) 
        }
        return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
    }

    private IntPtr HookMouseCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && (wParam == (IntPtr)WM_LBUTTONDOWN || wParam == (IntPtr)WM_RBUTTONDOWN || wParam == (IntPtr)WM_MOUSEMOVE))
        {
            // Handle mouse move event - reset the start time to now (not shown) 
        }
        return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
    }
}

The difference to my actual proprietary code is that where I have code in the hook callbacks, I also try to determine the window handle from lParam and compare this to Visual Studio’s main window handle obtained by using an EnvDTE80 reference, so that essentially I will only reset my inactivity timer for valid user interactions with the running Visual Studio instance the extension is installed.

I’d like a higher-level way of achieving the same, ideally avoiding P/Invoke calls and using managed code only.

Leave a Comment