Skip to content

Commit 9c3cc23

Browse files
committed
Improve treading and synchronization approach
Fix PopupListWatcher on first display
1 parent c087c1d commit 9c3cc23

File tree

8 files changed

+530
-372
lines changed

8 files changed

+530
-372
lines changed

Source/ExcelDna.IntelliSense/ExcelDna.IntelliSense.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
<Compile Include="IntelliSenseServer.cs" />
6565
<Compile Include="Logging.cs" />
6666
<Compile Include="Properties\AssemblyInfo.cs" />
67+
<Compile Include="SingleThreadSynchronizationContext.cs" />
6768
<Compile Include="ToolTipForm.cs">
6869
<SubType>Form</SubType>
6970
</Compile>

Source/ExcelDna.IntelliSense/IntelliSenseDisplay.cs

Lines changed: 81 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -27,101 +27,81 @@ public class ArgumentInfo
2727
// And this threading sample using tlbimp version of Windows 7 native UIA: http://code.msdn.microsoft.com/Windows-7-UI-Automation-6390614a/sourcecode?fileId=21469&pathId=715901329
2828
class IntelliSenseDisplay : MarshalByRefObject, IDisposable
2929
{
30-
IntelliSenseDisplay _current;
31-
SynchronizationContext _syncContextMain;
32-
SynchronizationContext _syncContextAuto; // On Automation thread.
33-
34-
// NOTE: Add for separate UI Automation Thread
35-
Thread _threadAuto;
36-
37-
readonly Dictionary<string, IntelliSenseFunctionInfo> _functionInfoMap;
30+
SynchronizationContext _syncContextMain; // Running on the main Excel thread (not a 'macro' context, though)
31+
SingleThreadSynchronizationContext _syncContextAuto; // Running on the Automation thread.
32+
33+
readonly Dictionary<string, IntelliSenseFunctionInfo> _functionInfoMap =
34+
new Dictionary<string, IntelliSenseFunctionInfo>(StringComparer.CurrentCultureIgnoreCase);
3835

3936
WindowWatcher _windowWatcher;
4037
FormulaEditWatcher _formulaEditWatcher;
4138
PopupListWatcher _popupListWatcher;
42-
SelectDataSourceWatcher _selectDataSourceWatcher;
39+
//SelectDataSourceWatcher _selectDataSourceWatcher;
4340

4441
// Need to make these late ...?
4542
ToolTipForm _descriptionToolTip;
4643
ToolTipForm _argumentsToolTip;
4744

48-
private readonly List<string> _addInReferences;
49-
5045
public IntelliSenseDisplay()
5146
{
52-
Debug.Print("### Thread creating IntelliSenseDisplay: " + Thread.CurrentThread.ManagedThreadId);
53-
54-
_current = this;
55-
_functionInfoMap = new Dictionary<string, IntelliSenseFunctionInfo>(StringComparer.CurrentCultureIgnoreCase);
56-
// TODO: Need a separate thread for UI Automation Client - event subscriptions should not be on main UI thread.
47+
// We expect this to be running in a macro context on the main Excel thread (ManagedThreadId = 1).
48+
Debug.Print($"### Thread creating IntelliSenseDisplay: Managed {Thread.CurrentThread.ManagedThreadId}, Native {AppDomain.GetCurrentThreadId()}");
5749

5850
_syncContextMain = new WindowsFormsSynchronizationContext();
5951

60-
_addInReferences = new List<string>();
61-
}
62-
63-
public void SetXllOwner(string xllPath)
64-
{
65-
_threadAuto = new Thread(() => RunUIAutomation(xllPath));
66-
_threadAuto.SetApartmentState(ApartmentState.MTA);
67-
_threadAuto.Start();
68-
}
69-
70-
public void RegisterFunctionInfo(IntelliSenseFunctionInfo functionInfo)
71-
{
72-
// TODO : Dictionary from KeyLookup
73-
_functionInfoMap.Add(functionInfo.FunctionName, functionInfo);
52+
// Make a separate thread and set to MTA, according to: https://msdn.microsoft.com/en-us/library/windows/desktop/ee671692%28v=vs.85%29.aspx
53+
var threadAuto = new Thread(RunUIAutomation);
54+
threadAuto.SetApartmentState(ApartmentState.MTA);
55+
threadAuto.Start();
7456
}
7557

76-
void RunUIAutomation(string xllPath)
58+
// This runs on the new thread we've created to do all the Automation stuff (_threadAuto)
59+
// It returns only after when the SyncContext.Complete() has been called (from the IntelliSenseDisplay.Dispose() below)
60+
void RunUIAutomation()
7761
{
78-
// NOTE: Add for separate UI Automation Thread
79-
_syncContextAuto = new WindowsFormsSynchronizationContext();
80-
//_syncContextAuto = _syncContextMain;
62+
_syncContextAuto = new SingleThreadSynchronizationContext();
8163

82-
_windowWatcher = new WindowWatcher(xllPath);
64+
_windowWatcher = new WindowWatcher(_syncContextAuto);
8365
_formulaEditWatcher = new FormulaEditWatcher(_windowWatcher, _syncContextAuto);
8466
_popupListWatcher = new PopupListWatcher(_windowWatcher, _syncContextAuto);
85-
_selectDataSourceWatcher = new SelectDataSourceWatcher(_windowWatcher, _syncContextAuto);
67+
// _selectDataSourceWatcher = new SelectDataSourceWatcher(_windowWatcher, _syncContextAuto);
8668

87-
_windowWatcher.MainWindowChanged += OnMainWindowChanged;
88-
_popupListWatcher.SelectedItemChanged += OnSelectedItemChanged;
89-
_formulaEditWatcher.StateChanged += OnStateChanged;
69+
_windowWatcher.MainWindowChanged += _windowWatcher_MainWindowChanged;
70+
_popupListWatcher.SelectedItemChanged += _popupListWatcher_SelectedItemChanged;
71+
_formulaEditWatcher.StateChanged += _formulaEditWatcher_StateChanged;
9072

9173
_windowWatcher.TryInitialize();
92-
// NOTE: Add for separate UI Automation Thread
93-
Application.Run();
74+
75+
_syncContextAuto.RunOnCurrentThread();
9476
}
9577

96-
private void OnMainWindowChanged(object sender, EventArgs args)
78+
// Runs on the auto thread
79+
void _windowWatcher_MainWindowChanged(object sender, EventArgs args)
9780
{
98-
Debug.Print("### Thread calling MainWindowChanged event: " + Thread.CurrentThread.ManagedThreadId);
99-
_syncContextMain.Post(delegate { MainWindowChanged(); }, null);
100-
// MainWindowChanged();
81+
_syncContextMain.Post(MainWindowChanged, null);
10182
}
10283

103-
private void OnSelectedItemChanged(object sender, EventArgs args)
84+
// Runs on the auto thread
85+
void _popupListWatcher_SelectedItemChanged(object sender, EventArgs args)
10486
{
105-
_syncContextMain.Post(delegate { PopupListSelectedItemChanged(); }, null);
106-
// PopupListSelectedItemChanged();
87+
_syncContextMain.Post(PopupListSelectedItemChanged, null);
10788
}
10889

109-
private void OnStateChanged(object sender, StateChangeEventArgs args)
90+
// Runs on the auto thread
91+
void _formulaEditWatcher_StateChanged(object sender, FormulaEditWatcher.StateChangeEventArgs args)
11092
{
111-
_syncContextMain.Post(delegate { FormulaEditStateChanged(args.StateChangeType); }, null);
112-
// FormulaEditStateChanged();
93+
_syncContextMain.Post(FormulaEditStateChanged, args.StateChangeType);
11394
}
11495

115-
void MainWindowChanged()
96+
// Runs on the main thread
97+
void MainWindowChanged(object _unused_)
11698
{
11799
// TODO: This is to guard against shutdown, but we should not have a race here
118100
// - shutdown should be on the main thread, as is this event handler.
119101
if (_windowWatcher == null) return;
120102

121-
122103
// TODO: !!! Reset / re-parent ToolTipWindows
123-
Debug.Print("MainWindow Change - " + _windowWatcher.MainWindow.ToString("X"));
124-
Debug.Print("### Thread calling MainWindowChanged method: " + Thread.CurrentThread.ManagedThreadId);
104+
Debug.Print($"IntelliSenseDisplay - MainWindowChanged - New window - {_windowWatcher.MainWindow:X}, Thread {Thread.CurrentThread.ManagedThreadId}");
125105

126106
// _descriptionToolTip.SetOwner(e.Handle); // Not Parent, of course!
127107
if (_descriptionToolTip != null)
@@ -143,8 +123,11 @@ void MainWindowChanged()
143123
// _descriptionToolTip = new ToolTipWindow("", _windowWatcher.MainWindow);
144124
}
145125

146-
void PopupListSelectedItemChanged()
126+
// Runs on the main thread
127+
void PopupListSelectedItemChanged(object _unused_)
147128
{
129+
Debug.Print($"IntelliSenseDisplay - PopupListSelectedItemChanged - New text - {_popupListWatcher?.SelectedItemText}, Thread {Thread.CurrentThread.ManagedThreadId}");
130+
148131
if (_popupListWatcher == null) return;
149132
string functionName = _popupListWatcher.SelectedItemText;
150133

@@ -158,8 +141,9 @@ void PopupListSelectedItemChanged()
158141
}
159142
// It's ours!
160143
_descriptionToolTip.ShowToolTip(
161-
new FormattedText { GetFunctionDescription(functionInfo) },
162-
(int)_popupListWatcher.SelectedItemBounds.Right + 25, (int)_popupListWatcher.SelectedItemBounds.Top);
144+
text: new FormattedText { GetFunctionDescription(functionInfo) },
145+
left: (int)_popupListWatcher.SelectedItemBounds.Right + 25,
146+
top: (int)_popupListWatcher.SelectedItemBounds.Top);
163147
}
164148
else
165149
{
@@ -170,22 +154,24 @@ void PopupListSelectedItemChanged()
170154
}
171155
}
172156

157+
// Runs on the main thread
173158
// TODO: Need better formula parsing story here
174159
// Here are some ideas: http://fastexcel.wordpress.com/2013/10/27/parsing-functions-from-excel-formulas-using-vba-is-mid-or-a-byte-array-the-best-method/
175-
void FormulaEditStateChanged(StateChangeTypeEnum stateChangeType)
160+
void FormulaEditStateChanged(object stateChangeTypeObj)
176161
{
162+
var stateChangeType = (FormulaEditWatcher.StateChangeType)stateChangeTypeObj;
177163
// Check for watcher already disposed
178164
// CONSIDER: How to manage threading with disposal...?
179165
if (_formulaEditWatcher == null) return;
180166

181-
if (stateChangeType == StateChangeTypeEnum.Move && _argumentsToolTip != null)
167+
if (stateChangeType == FormulaEditWatcher.StateChangeType.Move && _argumentsToolTip != null)
182168
{
183169
_argumentsToolTip.MoveToolTip(
184170
(int)_formulaEditWatcher.EditWindowBounds.Left, (int)_formulaEditWatcher.EditWindowBounds.Bottom + 5);
185171
return;
186172
}
187173

188-
Debug.Print("^^^ FormulaEditStateChanged. CurrentPrefix: " + _formulaEditWatcher.CurrentPrefix);
174+
Debug.Print($"^^^ FormulaEditStateChanged. CurrentPrefix: {_formulaEditWatcher.CurrentPrefix}, Thread {Thread.CurrentThread.ManagedThreadId}");
189175
if (_formulaEditWatcher.IsEditingFormula && _formulaEditWatcher.CurrentPrefix != null)
190176
{
191177
string prefix = _formulaEditWatcher.CurrentPrefix;
@@ -218,6 +204,7 @@ void FormulaEditStateChanged(StateChangeTypeEnum stateChangeType)
218204
_argumentsToolTip.Hide();
219205
}
220206

207+
// TODO: Probably not a good place for LINQ !?
221208
IEnumerable<TextLine> GetFunctionDescription(IntelliSenseFunctionInfo functionInfo)
222209
{
223210
return
@@ -295,66 +282,46 @@ TextLine GetArgumentDescription(IntelliSenseFunctionInfo.ArgumentInfo argumentIn
295282
};
296283
}
297284

298-
public void Shutdown()
299-
{
300-
Debug.Print("Shutdown!");
301-
if (_current != null)
302-
{
303-
try
304-
{
305-
_current.Dispose();
306-
}
307-
catch (Exception ex)
308-
{
309-
Debug.Print("!!! Error during Shutdown: " + ex);
310-
}
285+
//public void Shutdown()
286+
//{
287+
// Debug.Print("Shutdown!");
288+
// if (_current != null)
289+
// {
290+
// try
291+
// {
292+
// _current.Dispose();
293+
// }
294+
// catch (Exception ex)
295+
// {
296+
// Debug.Print("!!! Error during Shutdown: " + ex);
297+
// }
311298

312-
_current = null;
313-
}
314-
}
315-
316-
public void AddReference(string xllName)
317-
{
318-
_addInReferences.Add(xllName);
319-
}
320-
321-
public void RemoveReference(string xllName)
322-
{
323-
List<string> functionsToRemove =
324-
_functionInfoMap.Where(p => p.Value.SourcePath == xllName).Select(p => p.Key).ToList();
325-
326-
foreach (string func in functionsToRemove)
327-
{
328-
_functionInfoMap.Remove(func);
329-
}
330-
331-
_addInReferences.Remove(xllName);
332-
}
333-
334-
public bool IsUsed()
335-
{
336-
return _addInReferences.Count > 0;
337-
}
299+
// _current = null;
300+
// }
301+
//}
338302

339303
public void Dispose()
340304
{
341-
_current._syncContextAuto.Send(delegate
305+
if (_syncContextAuto == null)
306+
return;
307+
308+
_syncContextAuto.Send(delegate
342309
{
343310
if (_windowWatcher != null)
344311
{
345-
_windowWatcher.MainWindowChanged -= OnMainWindowChanged;
312+
_windowWatcher.MainWindowChanged -= _windowWatcher_MainWindowChanged;
346313
_windowWatcher.Dispose();
347314
_windowWatcher = null;
348315
}
349316
if (_formulaEditWatcher != null)
350317
{
351-
_formulaEditWatcher.StateChanged -= OnStateChanged;
318+
_formulaEditWatcher.StateChanged -= _formulaEditWatcher_StateChanged;
352319
_formulaEditWatcher.Dispose();
353320
_formulaEditWatcher = null;
354321
}
355322
if (_popupListWatcher != null)
356323
{
357-
_popupListWatcher.SelectedItemChanged -= OnSelectedItemChanged;
324+
_popupListWatcher.SelectedItemChanged -= _popupListWatcher_SelectedItemChanged;
358325
_popupListWatcher.Dispose();
359326
_popupListWatcher = null;
360327
}
@@ -374,11 +341,18 @@ public void Dispose()
374341
}
375342
}, null);
376343

377-
// NOTE: Add for separate UI Automation Thread
378-
_threadAuto.Abort();
379-
_threadAuto = null;
344+
_syncContextAuto.Complete();
345+
// CONSIDER: Maybe wait for the _syncContextAuto to finish...?
380346
_syncContextAuto = null;
347+
381348
_syncContextMain = null;
382349
}
350+
351+
public void RegisterFunctionInfo(IntelliSenseFunctionInfo functionInfo)
352+
{
353+
// TODO : Dictionary from KeyLookup
354+
_functionInfoMap.Add(functionInfo.FunctionName, functionInfo);
355+
}
356+
// TODO: How to UnregisterFunctionInfo ...?
383357
}
384358
}

Source/ExcelDna.IntelliSense/IntellisenseHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ public class IntelliSenseHelper : IDisposable
1212
private readonly IIntelliSenseProvider _workbookProvider = new WorkbookIntelliSenseProvider();
1313
// TODO: Others
1414

15+
1516
public IntelliSenseHelper()
1617
{
1718
_id = new IntelliSenseDisplay();
18-
_id.SetXllOwner(ExcelDnaUtil.XllPath);
1919
RegisterIntellisense();
2020
}
2121

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
5+
using System.Threading;
6+
7+
namespace ExcelDna.IntelliSense
8+
{
9+
// This code is from Stephen Toub's blog post on AsyncPump: http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx?PageIndex=2#comments
10+
11+
/// <summary>Provides a SynchronizationContext that's single-threaded.</summary>
12+
sealed class SingleThreadSynchronizationContext : SynchronizationContext
13+
{
14+
/// <summary>The queue of work items.</summary>
15+
private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> m_queue =
16+
new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
17+
18+
// /// <summary>The processing thread.</summary>
19+
// private readonly Thread m_thread = Thread.CurrentThread;
20+
21+
/// <summary>Dispatches an asynchronous message to the synchronization context.</summary>
22+
/// <param name="d">The System.Threading.SendOrPostCallback delegate to call.</param>
23+
/// <param name="state">The object passed to the delegate.</param>
24+
public override void Post(SendOrPostCallback d, object state)
25+
{
26+
if (d == null) throw new ArgumentNullException("d");
27+
m_queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
28+
}
29+
30+
/// <summary>Not supported.</summary>
31+
public override void Send(SendOrPostCallback d, object state)
32+
{
33+
throw new NotSupportedException("Synchronously sending is not supported.");
34+
}
35+
36+
/// <summary>Runs a loop to process all queued work items.</summary>
37+
public void RunOnCurrentThread()
38+
{
39+
foreach (var workItem in m_queue.GetConsumingEnumerable())
40+
workItem.Key(workItem.Value);
41+
42+
Debug.Print("SingleThreadSynchronizationContext Complete!");
43+
}
44+
45+
/// <summary>Notifies the context that no more work will arrive.</summary>
46+
public void Complete() { m_queue.CompleteAdding(); }
47+
}
48+
}

Source/ExcelDna.IntelliSense/ToolTipForm.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,12 @@ protected override bool ShowWithoutActivation
8787
private const int WS_EX_NOACTIVATE = 0x08000000;
8888
protected override CreateParams CreateParams
8989
{
90-
9190
get
9291
{
93-
CreateParams baseParams = base.CreateParams;
94-
baseParams.ClassStyle |= CS_DROPSHADOW;
95-
baseParams.ExStyle |= ( WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW );
96-
return baseParams;
92+
CreateParams baseParams = base.CreateParams;
93+
baseParams.ClassStyle |= CS_DROPSHADOW;
94+
baseParams.ExStyle |= ( WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW );
95+
return baseParams;
9796
}
9897
}
9998

0 commit comments

Comments
 (0)