11using System ;
22using System . ComponentModel ;
3+ using System . Diagnostics ;
34using System . Runtime . InteropServices ;
45using System . Threading ;
56
67namespace ExcelDna . IntelliSense
78{
89 // This class sets up a WinEventHook for the main Excel process - watching for a range or event types specified.
9- // Events received (on the main Excel thread) are posted onto the Automation thread (via syncContextAuto)
10+ // Events received (on the main Excel thread) are posted by the handler onto the Automation thread (via syncContextAuto)
11+ // NOTE: Currently we make the SetWinEventHook call on the main Excel thread, which is pumping Windows messages.
12+ // In an alternative implementation we could either create our own thread that pumps Windows messages and use the for WinEvents,
13+ // or we could change our automation thread to also pump windows messages.
1014 class WinEventHook : IDisposable
1115 {
1216 public class WinEventArgs : EventArgs
@@ -106,39 +110,82 @@ public enum WinEventObjectId : int
106110
107111 public event EventHandler < WinEventArgs > WinEventReceived ;
108112
109- readonly IntPtr _hWinEventHook ;
113+ /* readonly */ IntPtr _hWinEventHook ;
110114 readonly SynchronizationContext _syncContextAuto ;
115+ readonly SynchronizationContext _syncContextMain ;
111116 readonly IntPtr _hWndFilterOrZero ; // If non-zero, only these window events are processed
112117 readonly WinEventDelegate _handleWinEventDelegate ; // Ensures delegate that we pass to SetWinEventHook is not GC'd
118+ readonly WinEvent _eventMin ;
119+ readonly WinEvent _eventMax ;
113120
114- public WinEventHook ( WinEvent eventMin , WinEvent eventMax , SynchronizationContext syncContextAuto , IntPtr hWndFilterOrZero )
121+ // Can be called on any thread, but installed by calling in to the main thread, and will only start receiving events then
122+ public WinEventHook ( WinEvent eventMin , WinEvent eventMax , SynchronizationContext syncContextAuto , SynchronizationContext syncContextMain , IntPtr hWndFilterOrZero )
115123 {
116- if ( syncContextAuto == null )
117- throw new ArgumentNullException ( nameof ( syncContextAuto ) ) ;
118- _syncContextAuto = syncContextAuto ;
124+ _syncContextAuto = syncContextAuto ?? throw new ArgumentNullException ( nameof ( syncContextAuto ) ) ;
125+ _syncContextMain = syncContextMain ?? throw new ArgumentNullException ( nameof ( syncContextMain ) ) ;
119126 _hWndFilterOrZero = hWndFilterOrZero ;
120- var xllModuleHandle = Win32Helper . GetXllModuleHandle ( ) ;
121- var excelProcessId = Win32Helper . GetExcelProcessId ( ) ;
122127 _handleWinEventDelegate = HandleWinEvent ;
123- _hWinEventHook = SetWinEventHook ( eventMin , eventMax , xllModuleHandle , _handleWinEventDelegate , excelProcessId , 0 , SetWinEventHookFlags . WINEVENT_INCONTEXT ) ;
128+ _eventMin = eventMin ;
129+ _eventMax = eventMax ;
130+ syncContextMain . Post ( InstallWinEventHook , null ) ;
131+ }
132+
133+ // Must run on the main Excel thread (or another thread where Windows messages are pumped)
134+ void InstallWinEventHook ( object _ )
135+ {
136+ var excelProcessId = Win32Helper . GetExcelProcessId ( ) ;
137+ _hWinEventHook = SetWinEventHook ( _eventMin , _eventMax , IntPtr . Zero , _handleWinEventDelegate , excelProcessId , 0 , SetWinEventHookFlags . WINEVENT_OUTOFCONTEXT ) ;
124138 if ( _hWinEventHook == IntPtr . Zero )
125139 {
140+ Debug . Print ( "++++++++++++++ SetWinEventHook failed +++++++++++++++++++++++++++" ) ;
126141 Logger . WinEvents . Error ( "SetWinEventHook failed" ) ;
127142 // Is SetLastError used? - SetWinEventHook documentation does not indicate so
128143 throw new Win32Exception ( "SetWinEventHook failed" ) ;
129144 }
145+ // Debug.Print($"++++++++++++++ SetWinEventHook Success on thread {Thread.CurrentThread.ManagedThreadId} \r\n {Environment.StackTrace} \r\n +++++++++++++++++++++++++++");
130146 Logger . WinEvents . Info ( $ "SetWinEventHook success on thread { Thread . CurrentThread . ManagedThreadId } ") ;
131147 }
132148
133- // This runs on the Excel main thread - get off quickly
149+ // Must run on the same thread that InstallWinEventHook ran on
150+ void UninstallWinEventHook ( object _ )
151+ {
152+ if ( _hWinEventHook == IntPtr . Zero )
153+ {
154+ Logger . WinEvents . Warn ( $ "UnhookWinEvent unexpectedly called with no hook installed - thread { Thread . CurrentThread . ManagedThreadId } ") ;
155+ return ;
156+ }
157+
158+ try
159+ {
160+ Logger . WinEvents . Info ( $ "UnhookWinEvent called on thread { Thread . CurrentThread . ManagedThreadId } ") ;
161+ bool result = UnhookWinEvent ( _hWinEventHook ) ;
162+ if ( ! result )
163+ {
164+ // GetLastError?
165+ Logger . WinEvents . Info ( $ "UnhookWinEvent failed") ;
166+ }
167+ else
168+ {
169+ Logger . WinEvents . Info ( "UnhookWinEvent success" ) ;
170+ }
171+ }
172+ catch ( Exception ex )
173+ {
174+ Logger . WinEvents . Warn ( $ "UnhookWinEvent Exception { ex } ") ;
175+ }
176+ finally
177+ {
178+ _hWinEventHook = IntPtr . Zero ;
179+ }
180+ }
181+
182+ // This runs on the Excel main thread (usually, not always) - get off quickly
134183 void HandleWinEvent ( IntPtr hWinEventHook , WinEvent eventType , IntPtr hWnd ,
135184 WinEventObjectId idObject , int idChild , uint dwEventThread , uint dwmsEventTime )
136185 {
186+ // Debug.Print($"++++++++++++++ WinEvent Received: {eventType} on thread {Thread.CurrentThread.ManagedThreadId} from thread {dwEventThread} +++++++++++++++++++++++++++");
137187 try
138188 {
139- if ( disposedValue )
140- return ;
141-
142189 if ( _hWndFilterOrZero != IntPtr . Zero && hWnd != _hWndFilterOrZero )
143190 return ;
144191
@@ -154,6 +201,7 @@ void HandleWinEvent(IntPtr hWinEventHook, WinEvent eventType, IntPtr hWnd,
154201 }
155202 }
156203
204+ // A quick filter that runs on the Excel main thread (or other thread handling the WinEvent)
157205 bool IsSupportedWinEvent ( WinEvent winEvent )
158206 {
159207 return winEvent == WinEvent . EVENT_OBJECT_CREATE ||
@@ -179,49 +227,10 @@ void OnWinEventReceived(object winEventArgsObj)
179227 }
180228
181229 #region IDisposable Support
182- bool disposedValue = false ; // To detect redundant calls
183-
184- protected virtual void Dispose ( bool disposing )
185- {
186- if ( ! disposedValue )
187- {
188- Logger . WinEvents . Info ( $ "WinEventHook Dispose on thread { Thread . CurrentThread . ManagedThreadId } ") ;
189- if ( disposing )
190- {
191- // TODO: dispose managed state (managed objects).
192- }
193- _syncContextAuto . Send ( winEventHook =>
194- {
195- try
196- {
197- Logger . WinEvents . Info ( $ "UnhookWinEvent called on thread { Thread . CurrentThread . ManagedThreadId } ") ;
198- bool result = UnhookWinEvent ( ( IntPtr ) winEventHook ) ;
199- Logger . WinEvents . Info ( $ "UnhookWinEvent success? { result } ") ;
200- }
201- catch ( Exception ex )
202- {
203- Logger . WinEvents . Warn ( $ "UnhookWinEvent Exception { ex } ") ;
204- }
205- } , _hWinEventHook ) ;
206-
207- disposedValue = true ;
208- }
209- }
210-
211- // TODO: Does this make any sense?
212- // We _have_to_ Unhook from the automation thread...
213- // ~WinEventHook()
214- //{
215- // // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
216- // Dispose(false);
217- // }
218-
219- // This code added to correctly implement the disposable pattern.
220230 public void Dispose ( )
221231 {
222- // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
223- Dispose ( true ) ;
224- // GC.SuppressFinalize(this);
232+ Logger . WinEvents . Info ( $ "WinEventHook Dispose on thread { Thread . CurrentThread . ManagedThreadId } ") ;
233+ _syncContextMain . Send ( UninstallWinEventHook , null ) ;
225234 }
226235 #endregion
227236 }
0 commit comments