From 0900c08159d3bfa474e1133b689181ade9404842 Mon Sep 17 00:00:00 2001 From: Corey Kosak Date: Mon, 2 Sep 2024 15:59:53 -0400 Subject: [PATCH] feat(csharp/ExcelAddIn): Make connections in the background (#6011) The "Test Connection" and "Set Connection" button could delay your progress because they were serialized on a single thread. This can get annoying if you type in the wrong credentials, as computer might take several seconds to figure that out. Even if you correct your credentials in the meantime, you would have to wait for the original connection request to resolve. This PR changes the code so that it does that work on a background thread. Doing so also exposed a bug in my organization of Observables, so they have been reorganized a little bit. In particular I was trying to be clever by having one object observe two different things and the logic wasn't quite right. --- csharp/ExcelAddIn/StateManager.cs | 29 ++-- .../factories/CredentialsDialogFactory.cs | 53 +++++-- csharp/ExcelAddIn/models/Session.cs | 65 +++------ csharp/ExcelAddIn/providers/ClientProvider.cs | 129 ++++++++++++++++++ .../providers/CorePlusClientProvider.cs | 73 ---------- .../ExcelAddIn/providers/SessionProvider.cs | 38 +++++- .../providers/TableHandleProvider.cs | 58 +------- .../ExcelAddIn/util/SimpleAtomicReference.cs | 19 +++ csharp/ExcelAddIn/util/StatusOr.cs | 21 +-- csharp/ExcelAddIn/util/Utility.cs | 12 ++ csharp/ExcelAddIn/views/CredentialsDialog.cs | 4 +- 11 files changed, 296 insertions(+), 205 deletions(-) create mode 100644 csharp/ExcelAddIn/providers/ClientProvider.cs delete mode 100644 csharp/ExcelAddIn/providers/CorePlusClientProvider.cs create mode 100644 csharp/ExcelAddIn/util/SimpleAtomicReference.cs diff --git a/csharp/ExcelAddIn/StateManager.cs b/csharp/ExcelAddIn/StateManager.cs index c1b849b7b34..a4bb4e44e4e 100644 --- a/csharp/ExcelAddIn/StateManager.cs +++ b/csharp/ExcelAddIn/StateManager.cs @@ -36,28 +36,33 @@ public IDisposable SubscribeToDefaultCredentials(IObserver> observer) { - // There is a chain with three elements. - // The final observer (i.e. the argument to this method) will be a subscriber to a TableHandleProvider that we create here. - // That TableHandleProvider will in turn be a subscriber to a session. - - // So: + // There is a chain with multiple elements: + // // 1. Make a TableHandleProvider - // 2. Subscribe it to either the session provider named by the endpoint id - // or to the default session provider - // 3. Subscribe our observer to it - // 4. Return a dispose action that disposes both Subscribes + // 2. Make a ClientProvider + // 3. Subscribe the ClientProvider to either the session provider named by the endpoint id + // or to the default session provider + // 4. Subscribe the TableHandleProvider to the ClientProvider + // 4. Subscribe our observer to the TableHandleProvider + // 5. Return a dispose action that disposes all the needfuls. var thp = new TableHandleProvider(WorkerThread, descriptor, filter); + var cp = new ClientProvider(WorkerThread, descriptor); + var disposer1 = descriptor.EndpointId == null ? - SubscribeToDefaultSession(thp) : - SubscribeToSession(descriptor.EndpointId, thp); - var disposer2 = thp.Subscribe(observer); + SubscribeToDefaultSession(cp) : + SubscribeToSession(descriptor.EndpointId, cp); + var disposer2 = cp.Subscribe(thp); + var disposer3 = thp.Subscribe(observer); // The disposer for this needs to dispose both "inner" disposers. return ActionAsDisposable.Create(() => { + // TODO(kosak): probably don't need to be on the worker thread here WorkerThread.Invoke(() => { var temp1 = Utility.Exchange(ref disposer1, null); var temp2 = Utility.Exchange(ref disposer2, null); + var temp3 = Utility.Exchange(ref disposer3, null); + temp3?.Dispose(); temp2?.Dispose(); temp1?.Dispose(); }); diff --git a/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs b/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs index adada638e9c..c748847e852 100644 --- a/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs +++ b/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs @@ -1,5 +1,5 @@ using Deephaven.ExcelAddIn.Models; -using Deephaven.ExcelAddIn.Providers; +using Deephaven.ExcelAddIn.Util; using Deephaven.ExcelAddIn.ViewModels; using ExcelAddIn.views; @@ -23,6 +23,43 @@ void OnSetCredentialsButtonClicked() { credentialsDialog!.Close(); } + // This is used to ignore the results from stale "Test Credentials" invocations + // and to only use the results from the latest. It is read and written from different + // threads so we protect it with a synchronization object. + var sharedTestCredentialsCookie = new SimpleAtomicReference(new object()); + + void TestCredentials(CredentialsBase creds) { + // Make a unique sentinel object to indicate that this thread should be + // the one privileged to provide the system with the answer to the "Test + // Credentials" question. If the user doesn't press the button again, + // we will go ahead and provide our answer to the system. However, if the + // user presses the button again, triggering a new thread, then that + // new thread will usurp our privilege and it will be the one to provide + // the answer. + var localLatestTcc = new object(); + sharedTestCredentialsCookie.Value = localLatestTcc; + + var state = "OK"; + try { + // This operation might take some time. + var temp = SessionBaseFactory.Create(creds, sm.WorkerThread); + temp.Dispose(); + } catch (Exception ex) { + state = ex.Message; + } + + // If sharedTestCredentialsCookie is still the same, then our privilege + // has not been usurped and we can provide our answer to the system. + // On the other hand, if it changes, then we will just throw away our work. + if (!ReferenceEquals(localLatestTcc, sharedTestCredentialsCookie.Value)) { + // Our results are moot. Dispose of them. + return; + } + + // Our results are valid. Keep them and tell everyone about it. + credentialsDialog!.SetTestResultsBox(state); + } + void OnTestCredentialsButtonClicked() { if (!cvm.TryMakeCredentials(out var newCreds, out var error)) { ShowMessageBox(error); @@ -30,18 +67,8 @@ void OnTestCredentialsButtonClicked() { } credentialsDialog!.SetTestResultsBox("Checking credentials"); - - sm.WorkerThread.Invoke(() => { - var state = "OK"; - try { - var temp = SessionBaseFactory.Create(newCreds, sm.WorkerThread); - temp.Dispose(); - } catch (Exception ex) { - state = ex.Message; - } - - credentialsDialog!.SetTestResultsBox(state); - }); + // Check credentials on its own thread + Utility.RunInBackground(() => TestCredentials(newCreds)); } // Save in captured variable so that the lambdas can access it. diff --git a/csharp/ExcelAddIn/models/Session.cs b/csharp/ExcelAddIn/models/Session.cs index a3cbfa1998f..b3851ef7a77 100644 --- a/csharp/ExcelAddIn/models/Session.cs +++ b/csharp/ExcelAddIn/models/Session.cs @@ -1,7 +1,5 @@ -using Deephaven.DeephavenClient.ExcelAddIn.Util; -using Deephaven.DeephavenClient; +using Deephaven.DeephavenClient; using Deephaven.DheClient.Session; -using Deephaven.ExcelAddIn.Providers; using Deephaven.ExcelAddIn.Util; namespace Deephaven.ExcelAddIn.Models; @@ -21,70 +19,49 @@ public abstract class SessionBase : IDisposable { } public sealed class CoreSession(Client client) : SessionBase { - public Client? Client = client; + private Client? _client = client; public override T Visit(Func onCore, Func onCorePlus) { return onCore(this); } public override void Dispose() { - Utility.Exchange(ref Client, null)?.Dispose(); + Utility.Exchange(ref _client, null)?.Dispose(); + } + + public Client Client { + get { + if (_client == null) { + throw new Exception("Object is disposed"); + } + + return _client; + } } } public sealed class CorePlusSession(SessionManager sessionManager, WorkerThread workerThread) : SessionBase { private SessionManager? _sessionManager = sessionManager; - private readonly Dictionary _clientProviders = new(); public override T Visit(Func onCore, Func onCorePlus) { return onCorePlus(this); } - public IDisposable SubscribeToPq(PersistentQueryId persistentQueryId, - IObserver> observer) { - if (_sessionManager == null) { - throw new Exception("Object has been disposed"); - } - - CorePlusClientProvider? cp = null; - IDisposable? disposer = null; - - workerThread.Invoke(() => { - if (!_clientProviders.TryGetValue(persistentQueryId, out cp)) { - cp = CorePlusClientProvider.Create(workerThread, _sessionManager, persistentQueryId); - _clientProviders.Add(persistentQueryId, cp); - } - - disposer = cp.Subscribe(observer); - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - var old = Utility.Exchange(ref disposer, null); - // Do nothing if caller Disposes me multiple times. - if (old == null) { - return; - } - old.Dispose(); - - // Slightly weird. If "old.Dispose()" has removed the last subscriber, - // then dispose it and remove it from our dictionary. - cp!.DisposeIfEmpty(() => _clientProviders.Remove(persistentQueryId)); - }); - }); - } - public override void Dispose() { if (workerThread.InvokeIfRequired(Dispose)) { return; } - var localCps = _clientProviders.Values.ToArray(); - _clientProviders.Clear(); Utility.Exchange(ref _sessionManager, null)?.Dispose(); + } + + public SessionManager SessionManager { + get { + if (_sessionManager == null) { + throw new Exception("Object is disposed"); + } - foreach (var cp in localCps) { - cp.Dispose(); + return _sessionManager; } } } diff --git a/csharp/ExcelAddIn/providers/ClientProvider.cs b/csharp/ExcelAddIn/providers/ClientProvider.cs new file mode 100644 index 00000000000..5d296e35221 --- /dev/null +++ b/csharp/ExcelAddIn/providers/ClientProvider.cs @@ -0,0 +1,129 @@ +using Deephaven.DeephavenClient; +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Util; +using Deephaven.DeephavenClient.ExcelAddIn.Util; +using Deephaven.DheClient.Session; + +namespace Deephaven.ExcelAddIn.Providers; + +internal class ClientProvider( + WorkerThread workerThread, + TableTriple descriptor) : IObserver>, IObservable>, IDisposable { + + private readonly ObserverContainer> _observers = new(); + private StatusOr _client = StatusOr.OfStatus("[No Client]"); + private DndClient? _ownedDndClient = null; + + public IDisposable Subscribe(IObserver> observer) { + // We need to run this on our worker thread because we want to protect + // access to our dictionary. + workerThread.Invoke(() => { + _observers.Add(observer, out _); + observer.OnNext(_client); + }); + + return ActionAsDisposable.Create(() => { + workerThread.Invoke(() => { + _observers.Remove(observer, out _); + }); + }); + } + + public void Dispose() { + DisposeClientState(); + } + + public void OnNext(StatusOr session) { + // Get onto the worker thread if we're not already on it. + if (workerThread.InvokeIfRequired(() => OnNext(session))) { + return; + } + + try { + // Dispose whatever state we had before. + DisposeClientState(); + + // If the new state is just a status message, make that our status and transmit to our observers + if (!session.GetValueOrStatus(out var sb, out var status)) { + _observers.SetAndSendStatus(ref _client, status); + return; + } + + var pqId = descriptor.PersistentQueryId; + + // New state is a Core or CorePlus Session. + _ = sb.Visit(coreSession => { + if (pqId != null) { + _observers.SetAndSendStatus(ref _client, "[PQ Id Not Valid for Community Core]"); + return Unit.Instance; + } + + // It's a Core session so we have our Client. + _observers.SetAndSendValue(ref _client, coreSession.Client); + return Unit.Instance; // Essentially a "void" value that is ignored. + }, corePlusSession => { + // It's a CorePlus session so subscribe us to its PQ observer for the appropriate PQ ID + // If no PQ id was provided, that's a problem + if (pqId == null) { + _observers.SetAndSendStatus(ref _client, "[PQ Id is Required]"); + return Unit.Instance; + } + + // Connect to the PQ on a separate thread + Utility.RunInBackground(() => ConnectToPq(corePlusSession.SessionManager, pqId)); + return Unit.Instance; + }); + } catch (Exception ex) { + _observers.SetAndSendStatus(ref _client, ex.Message); + } + } + + /// + /// This is executed on a separate thread because it might take a while. + /// + /// + /// + private void ConnectToPq(SessionManager sessionManager, PersistentQueryId pqId) { + StatusOr result; + DndClient? dndClient = null; + try { + dndClient = sessionManager.ConnectToPqByName(pqId.Id, false); + result = StatusOr.OfValue(dndClient); + } catch (Exception ex) { + result = StatusOr.OfStatus(ex.Message); + } + + // commit the results, but on the worker thread + workerThread.Invoke(() => { + // This should normally be null, but maybe there's a race. + var oldDndClient = Utility.Exchange(ref _ownedDndClient, dndClient); + _observers.SetAndSend(ref _client, result); + + // Yet another thread + if (oldDndClient != null) { + Utility.RunInBackground(() => Utility.IgnoreExceptions(() => oldDndClient.Dispose())); + } + }); + } + + private void DisposeClientState() { + // Get onto the worker thread if we're not already on it. + if (workerThread.InvokeIfRequired(DisposeClientState)) { + return; + } + + var oldClient = Utility.Exchange(ref _ownedDndClient, null); + if (oldClient != null) { + _observers.SetAndSendStatus(ref _client, "Disposing client"); + oldClient.Dispose(); + } + } + + public void OnCompleted() { + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + throw new NotImplementedException(); + } +} diff --git a/csharp/ExcelAddIn/providers/CorePlusClientProvider.cs b/csharp/ExcelAddIn/providers/CorePlusClientProvider.cs deleted file mode 100644 index f7f9f90c7f5..00000000000 --- a/csharp/ExcelAddIn/providers/CorePlusClientProvider.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Deephaven.DeephavenClient.ExcelAddIn.Util; -using Deephaven.DeephavenClient; -using Deephaven.DheClient.Session; -using Deephaven.ExcelAddIn.Models; -using Deephaven.ExcelAddIn.Util; - -namespace Deephaven.ExcelAddIn.Providers; - -/// -/// This Observable provides StatusOr<Client> objects for Core+. -/// If it can successfully connect to a PQ on Core+, it will send a Client. -/// In the future it will be an Observer of PQ up/down messages. -/// -internal class CorePlusClientProvider : IObservable>, IDisposable { - public static CorePlusClientProvider Create(WorkerThread workerThread, SessionManager sessionManager, - PersistentQueryId persistentQueryId) { - var self = new CorePlusClientProvider(workerThread); - workerThread.Invoke(() => { - try { - var dndClient = sessionManager.ConnectToPqByName(persistentQueryId.Id, false); - self._client = StatusOr.OfValue(dndClient); - } catch (Exception ex) { - self._client = StatusOr.OfStatus(ex.Message); - } - }); - return self; - } - - private readonly WorkerThread _workerThread; - private readonly ObserverContainer> _observers = new(); - private StatusOr _client = StatusOr.OfStatus("Not connected"); - - private CorePlusClientProvider(WorkerThread workerThread) { - _workerThread = workerThread; - } - - public IDisposable Subscribe(IObserver> observer) { - _workerThread.Invoke(() => { - // New observer gets added to the collection and then notified of the current status. - _observers.Add(observer, out _); - observer.OnNext(_client); - }); - - return ActionAsDisposable.Create(() => { - _workerThread.Invoke(() => { - _observers.Remove(observer, out _); - }); - }); - } - - public void Dispose() { - if (_workerThread.InvokeIfRequired(Dispose)) { - return; - } - - _ = _client.GetValueOrStatus(out var c, out _); - _client = StatusOr.OfStatus("Disposed"); - c?.Dispose(); - } - - public void DisposeIfEmpty(Action onEmpty) { - if (_workerThread.InvokeIfRequired(() => DisposeIfEmpty(onEmpty))) { - return; - } - - if (_observers.Count != 0) { - return; - } - - Dispose(); - onEmpty(); - } -} diff --git a/csharp/ExcelAddIn/providers/SessionProvider.cs b/csharp/ExcelAddIn/providers/SessionProvider.cs index 35e98926360..11882df8144 100644 --- a/csharp/ExcelAddIn/providers/SessionProvider.cs +++ b/csharp/ExcelAddIn/providers/SessionProvider.cs @@ -11,6 +11,10 @@ internal class SessionProvider(WorkerThread workerThread) : IObservable _session = StatusOr.OfStatus("[Not connected]"); private readonly ObserverContainer> _credentialsObservers = new(); private readonly ObserverContainer> _sessionObservers = new(); + /// + /// This is used to ignore the results from multiple invocations of "SetCredentials". + /// + private readonly SimpleAtomicReference _sharedSetCredentialsCookie = new(new object()); public void Dispose() { // Get on the worker thread if not there already. @@ -83,12 +87,42 @@ public void SetCredentials(CredentialsBase credentials) { _sessionObservers.SetAndSendStatus(ref _session, "Trying to connect"); + Utility.RunInBackground(() => CreateSessionBaseInSeparateThread(credentials)); + } + + void CreateSessionBaseInSeparateThread(CredentialsBase credentials) { + // Make a unique sentinel object to indicate that this thread should be + // the one privileged to provide the system with the Session corresponding + // to the credentials. If SetCredentials isn't called in the meantime, + // we will go ahead and provide our answer to the system. However, if + // SetCredentials is called again, triggering a new thread, then that + // new thread will usurp our privilege and it will be the one to provide + // the answer. + var localLatestCookie = new object(); + _sharedSetCredentialsCookie.Value = localLatestCookie; + + StatusOr result; try { + // This operation might take some time. var sb = SessionBaseFactory.Create(credentials, workerThread); - _sessionObservers.SetAndSendValue(ref _session, sb); + result = StatusOr.OfValue(sb); } catch (Exception ex) { - _sessionObservers.SetAndSendStatus(ref _session, ex.Message); + result = StatusOr.OfStatus(ex.Message); } + + // If sharedTestCredentialsCookie is still the same, then our privilege + // has not been usurped and we can provide our answer to the system. + // On the other hand, if it has changed, then we will just throw away our work. + if (!ReferenceEquals(localLatestCookie, _sharedSetCredentialsCookie.Value)) { + // Our results are moot. Dispose of them. + if (result.GetValueOrStatus(out var sb, out _)) { + sb.Dispose(); + } + return; + } + + // Our results are valid. Keep them and tell everyone about it (on the worker thread). + workerThread.Invoke(() => _sessionObservers.SetAndSend(ref _session, result)); } public void Reconnect() { diff --git a/csharp/ExcelAddIn/providers/TableHandleProvider.cs b/csharp/ExcelAddIn/providers/TableHandleProvider.cs index e45ee7ce5df..db7ddccd4e9 100644 --- a/csharp/ExcelAddIn/providers/TableHandleProvider.cs +++ b/csharp/ExcelAddIn/providers/TableHandleProvider.cs @@ -8,11 +8,9 @@ namespace Deephaven.ExcelAddIn.Providers; internal class TableHandleProvider( WorkerThread workerThread, TableTriple descriptor, - string filter) : IObserver>, IObserver>, - IObservable>, IDisposable { + string filter) : IObserver>, IObservable>, IDisposable { private readonly ObserverContainer> _observers = new(); - private IDisposable? _pqDisposable = null; private StatusOr _tableHandle = StatusOr.OfStatus("[no TableHandle]"); public IDisposable Subscribe(IObserver> observer) { @@ -39,47 +37,6 @@ public void Dispose() { DisposePqAndThState(); } - public void OnNext(StatusOr session) { - // Get onto the worker thread if we're not already on it. - if (workerThread.InvokeIfRequired(() => OnNext(session))) { - return; - } - - try { - // Dispose whatever state we had before. - DisposePqAndThState(); - - // If the new state is just a status message, make that our status and transmit to our observers - if (!session.GetValueOrStatus(out var sb, out var status)) { - _observers.SetAndSendStatus(ref _tableHandle, status); - return; - } - - // New state is a Core or CorePlus Session. - _ = sb.Visit(coreSession => { - // It's a Core session so just forward its client field to our own OnNext(Client) method. - // We test against null in the unlikely/impossible case that the session is Disposed - if (coreSession.Client != null) { - OnNext(StatusOr.OfValue(coreSession.Client)); - } - - return Unit.Instance; // Essentially a "void" value that is ignored. - }, corePlusSession => { - // It's a CorePlus session so subscribe us to its PQ observer for the appropriate PQ ID - // If no PQ id was provided, that's a problem - var pqid = descriptor.PersistentQueryId; - if (pqid == null) { - throw new Exception("PQ id is required"); - } - _observers.SetAndSendStatus(ref _tableHandle, $"Subscribing to PQ \"{pqid}\""); - _pqDisposable = corePlusSession.SubscribeToPq(pqid, this); - return Unit.Instance; - }); - } catch (Exception ex) { - _observers.SetAndSendStatus(ref _tableHandle, ex.Message); - } - } - public void OnNext(StatusOr client) { // Get onto the worker thread if we're not already on it. if (workerThread.InvokeIfRequired(() => OnNext(client))) { @@ -118,18 +75,17 @@ public void OnNext(StatusOr client) { } private void DisposePqAndThState() { - _ = _tableHandle.GetValueOrStatus(out var oldTh, out var _); - var oldPq = Utility.Exchange(ref _pqDisposable, null); + // Get onto the worker thread if we're not already on it. + if (workerThread.InvokeIfRequired(DisposePqAndThState)) { + return; + } + + _ = _tableHandle.GetValueOrStatus(out var oldTh, out _); if (oldTh != null) { _observers.SetAndSendStatus(ref _tableHandle, "Disposing TableHandle"); oldTh.Dispose(); } - - if (oldPq != null) { - _observers.SetAndSendStatus(ref _tableHandle, "Disposing PQ"); - oldPq.Dispose(); - } } public void OnCompleted() { diff --git a/csharp/ExcelAddIn/util/SimpleAtomicReference.cs b/csharp/ExcelAddIn/util/SimpleAtomicReference.cs new file mode 100644 index 00000000000..d5a2f76cd5b --- /dev/null +++ b/csharp/ExcelAddIn/util/SimpleAtomicReference.cs @@ -0,0 +1,19 @@ +namespace Deephaven.ExcelAddIn.Util; + +internal class SimpleAtomicReference(T value) { + private readonly object _sync = new(); + private T _value = value; + + public T Value { + get { + lock (_sync) { + return _value; + } + } + set { + lock (_sync) { + _value = value; + } + } + } +} diff --git a/csharp/ExcelAddIn/util/StatusOr.cs b/csharp/ExcelAddIn/util/StatusOr.cs index 4aeae778e98..1de6f583af6 100644 --- a/csharp/ExcelAddIn/util/StatusOr.cs +++ b/csharp/ExcelAddIn/util/StatusOr.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Windows.Forms; namespace Deephaven.ExcelAddIn.Util; @@ -38,20 +39,24 @@ public static void SendStatus(this IObserver> observer, string me observer.OnNext(so); } - public static void SetAndSendStatus(this IObserver> observer, ref StatusOr sor, - string message) { - sor = StatusOr.OfStatus(message); - observer.OnNext(sor); - } - public static void SendValue(this IObserver> observer, T value) { var so = StatusOr.OfValue(value); observer.OnNext(so); } + public static void SetAndSendStatus(this IObserver> observer, ref StatusOr sor, + string message) { + SetAndSend(observer, ref sor, StatusOr.OfStatus(message)); + } + public static void SetAndSendValue(this IObserver> observer, ref StatusOr sor, T value) { - sor = StatusOr.OfValue(value); - observer.OnNext(sor); + SetAndSend(observer, ref sor, StatusOr.OfValue(value)); + } + + public static void SetAndSend(this IObserver> observer, ref StatusOr sor, + StatusOr newSor) { + sor = newSor; + observer.OnNext(newSor); } } diff --git a/csharp/ExcelAddIn/util/Utility.cs b/csharp/ExcelAddIn/util/Utility.cs index 0fce1f7a6f9..96e910b43c9 100644 --- a/csharp/ExcelAddIn/util/Utility.cs +++ b/csharp/ExcelAddIn/util/Utility.cs @@ -7,6 +7,18 @@ public static T Exchange(ref T item, T newValue) { item = newValue; return result; } + + public static void RunInBackground(Action a) { + new Thread(() => a()) { IsBackground = true }.Start(); + } + + public static void IgnoreExceptions(Action action) { + try { + action(); + } catch { + // Ignore errors + } + } } public class Unit { diff --git a/csharp/ExcelAddIn/views/CredentialsDialog.cs b/csharp/ExcelAddIn/views/CredentialsDialog.cs index 4ca2260aec1..4224b7c43ed 100644 --- a/csharp/ExcelAddIn/views/CredentialsDialog.cs +++ b/csharp/ExcelAddIn/views/CredentialsDialog.cs @@ -51,8 +51,8 @@ public CredentialsDialog(CredentialsDialogViewModel vm, Action onSetCredentialsB vm, nameof(vm.IsDefault)); } - public void SetTestResultsBox(string painState) { - Invoke(() => testResultsTextBox.Text = painState); + public void SetTestResultsBox(string testResultsState) { + Invoke(() => testResultsTextBox.Text = testResultsState); } private void setCredentialsButton_Click(object sender, EventArgs e) {