KB0201: Come usare il subclassing di Windows durante la programmazione di un componente aggiuntivo .NET
- Home
- Risorse
- Knowledge base
- 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
NativeWindoworiginale può anche creare finestre, ma questa funzionalità viene usata raramente in combinazione con il subclassing nella stessa istanza diNativeWindow. thinkcell.SubclassedWindownon è 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.