KB0201: Come usare il subclassing di Windows durante la programmazione di un componente aggiuntivo .NET

  1. Home
  2. Risorse
  3. Knowledge base
  4. KB0201

Problema

Ho sviluppato un componente aggiuntivo .NET per Microsoft Office che usa il subclassing di Windows. I miei clienti riscontrano arresti anomali di Office quando il mio componente aggiuntivo e un altro componente aggiuntivo (ad esempio thinkcell) sono in esecuzione.

Soluzione

Questo problema è spesso causato dal subclassing con NativeWindow.AssignHandle / NativeWindow.ReleaseHandle.

Seguire invece la raccomandazione di Microsoft di usare P/Invoke per SetWindowSubclass e RemoveWindowSubclass di Comctl32.dll, come descritto in questa pagina Microsoft Docs. Per apportare questa modifica al progetto, è possibile sostituire NativeWindow con thinkcell.SubclassedWindow.cs ovunque NativeWindow venga usato per il subclassing:

using System;
using System.Diagnostics;

namespace thinkcell
{

    internal enum BOOL : int
    {
        FALSE = 0,
        TRUE = 1,
    }

    internal static partial class ComCtl32
    {
        public delegate IntPtr SUBCLASSPROC(
            IntPtr hWnd,
            int msg,
            IntPtr wParam,
            IntPtr lParam,
            UIntPtr uIdSubclass,
            UIntPtr dwRefData
        );

        [System.Runtime.InteropServices.DllImport("comctl32.dll", ExactSpelling = true)]
        public static extern BOOL SetWindowSubclass(
            IntPtr hWnd,
            IntPtr pfnSubclass,
            UIntPtr uIdSubclass,
            UIntPtr dwRefData
        );

        [System.Runtime.InteropServices.DllImport("comctl32.dll", ExactSpelling = true)]
        public static extern BOOL RemoveWindowSubclass(
            IntPtr hWnd,
            IntPtr pfnSubclass,
            UIntPtr uIdSubclass
        );

        [System.Runtime.InteropServices.DllImport("comctl32.dll", ExactSpelling = true)]
        public static extern IntPtr DefSubclassProc(
            IntPtr hWnd,
            int msg,
            IntPtr wParam,
            IntPtr lParam
        );
    }

    class SubclassedWindow : MarshalByRefObject, System.Windows.Forms.IWin32Window
    {
        // impedisce la raccolta di SubclassedWindow che è ancora in uso
        static private System.Collections.Generic.HashSet<SubclassedWindow> _instancesInUse = new System.Collections.Generic.HashSet();

        // Il numero di utilizzi ancora presenti per questa istanza:
        // - una finestra collegata, oppure
        // - all'interno di una procedura di finestra
        private int _uses = 0;

        // Il delegato della procedura di finestra
        private ComCtl32.SUBCLASSPROC _windowProc;

        // Il punto nativo per il delegato
        private IntPtr _windowProcHandle;

        static SubclassedWindow()
        {
            AppDomain.CurrentDomain.ProcessExit += OnShutdown;
        }

        public SubclassedWindow()
        {
            _windowProc = new ComCtl32.SUBCLASSPROC(Callback);
            _windowProcHandle = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(_windowProc);
        }

        /// <summary>
        ///  Ottiene il punto per questa finestra.
        /// </summary>
        public IntPtr Handle { get; private set; }

        /// <summary>
        ///  Assegna un punto a questa istanza <see cref="NativeWindow"/>.
        /// </summary>
        public void AssignHandle(IntPtr handle)
        {
            CheckReleased();
            Debug.Assert(handle != IntPtr.Zero, "handle is 0");

            if (0 == _uses)
            {
                lock(_instancesInUse)
                {
                    _instancesInUse.Add(this);
                }
            } // altrimenti può accadere se il punto viene riassegnato all'interno di WndProc.
            // Questo è consentito dopo qualsiasi chiamata a DefWndProc.

            ++_uses;
            Handle = handle;

            ComCtl32.SetWindowSubclass(handle, _windowProcHandle, UIntPtr.Zero, UIntPtr.Zero);
            OnHandleChange();
        }

        /// <summary>
        ///  Metodo di callback del messaggio della finestra. Il controllo arriva qui quando un
        ///  messaggio di finestra viene inviato a questa finestra. Questo metodo inserisce il messaggio
        ///  di finestra in un oggetto Message e richiama il metodo WndProc(). Un messaggio WM_NCDESTROY
        ///  causa automaticamente la chiamata del metodo ReleaseHandle().
        /// </summary>
        private IntPtr Callback(
            IntPtr hWnd,
            int msg,
            IntPtr wParam,
            IntPtr lParam,
            UIntPtr uIdSubclass,
            UIntPtr dwRefData
        )
        {
            Debug.Assert(0 < _uses);
            ++_uses;

            try
            {
                var m = System.Windows.Forms.Message.Create(hWnd, msg, wParam, lParam);
                WndProc(ref m);
                return m.Result;
            }
            catch (Exception e)
            {
                OnThreadException(e);
                return IntPtr.Zero;
            }
            finally
            {
                if (msg == 0x82/*WM_NCDESTROY*/ && Handle != IntPtr.Zero) {
                    InternalReleaseHandle();
                }
                if (0 == --_uses)
                {
                    lock (_instancesInUse)
                    {
                        _instancesInUse.Remove(this);
                    }
                }
            }
        }

        /// <summary>
        ///  Genera un'eccezione se il punto della finestra non è zero.
        /// </summary>
        private void CheckReleased()
        {
            if (Handle != IntPtr.Zero)
            {
                throw new InvalidOperationException("Window handle already exists.");
            }
        }

        /// <summary>
        ///  Richiama la procedura di finestra predefinita associata a questa finestra. È
        ///  un errore chiamare questo metodo quando la proprietà Handle è zero.
        /// </summary>
        public void DefWndProc(ref System.Windows.Forms.Message m)
        {
            Debug.Assert(m.HWnd==Handle, "SubclassedWindow is not attached to the window m is addressed to.");
            m.Result = ComCtl32.DefSubclassProc(m.HWnd, m.Msg, m.WParam, m.LParam);
        }

        /// <summary>
        ///  Specifica un metodo di notifica che viene chiamato quando il punto per una
        ///  finestra viene modificato.
        /// </summary>
        protected virtual void OnHandleChange()
        {
        }

        /// <summary>
        ///  Al caricamento della classe, colleghiamo un evento all'applicazione per sapere quando
        ///  il processo o il dominio termina. Quando ciò accade, tentiamo di
        ///  cancellare la cache della classe finestra. Non possiamo eliminare finestre (perché non
        ///  abbiamo accesso al loro thread) e non possiamo annullare la registrazione delle classi finestra
        ///  (perché le classi sono in uso dalle finestre che non possiamo eliminare). Invece,
        ///  spostiamo la classe e le procedure di finestra in DefWndProc
        /// </summary>
        [System.Runtime.ConstrainedExecution.PrePrepareMethod]
        private static void OnShutdown(object sender, EventArgs e)
        {
            // Nessun blocco perché l'accesso qui dovrebbe essere privo di race condition, nessuna chiamata simultanea a SubclassedWindow.AttachHandle/ReleaseHandle
            // dovrebbe verificarsi durante l'arresto.
            Debug.Assert(0 == _instancesInUse.Count);
        }

        /// <summary>
        ///  Quando sottoposto a override in una classe derivata, gestisce un'eccezione di thread non gestita.
        /// </summary>
        protected virtual void OnThreadException(Exception e)
        {
        }

        private void InternalReleaseHandle()
        {
            Debug.Assert(Handle != IntPtr.Zero);
            ComCtl32.RemoveWindowSubclass(Handle, _windowProcHandle, UIntPtr.Zero);
            Handle = IntPtr.Zero;
            OnHandleChange();
            --_uses;
        }

        /// <summary>
        ///  Rilascia il punto associato a questa finestra.
        /// </summary>
        public void ReleaseHandle()
        {
            if (Handle != IntPtr.Zero) {
                InternalReleaseHandle();
                if (0 == _uses)
                {
                    lock (_instancesInUse)
                    {
                        _instancesInUse.Remove(this);
                    }
                }
            }
        }

        /// <summary>
        ///  Richiama la procedura di finestra predefinita associata a questa finestra.
        /// </summary>
        protected virtual void WndProc(ref System.Windows.Forms.Message m)
        {
            DefWndProc(ref m);
        }
    }
}

Scarica SubclassedWindow.cs

Limitazioni:

  • Il NativeWindow originale può anche creare finestre, ma questa funzionalità viene usata raramente in combinazione con il subclassing nella stessa istanza di NativeWindow.
  • thinkcell.SubclassedWindow non è thread-safe, ma il subclassing e l'elaborazione dei messaggi avvengono di solito nello stesso thread.

Informarci in caso di problemi nell'uso di thinkcell.SubclassWindow.