From a0b70a6912e7a66448539898606314999bb586f1 Mon Sep 17 00:00:00 2001 From: Corey Kosak Date: Sat, 7 Sep 2024 18:21:12 -0400 Subject: [PATCH] feat(csharp/ExcelAddIn): ExcelAddIn demo v7: Better GUI, better threading, better sharing (#6031) More responses to demo user feedback. This version seems to be working pretty well. --- csharp/ExcelAddIn/DeephavenExcelFunctions.cs | 24 +-- csharp/ExcelAddIn/ExcelAddIn.csproj.user | 3 + csharp/ExcelAddIn/StateManager.cs | 203 +++++++++++++----- .../ConnectionManagerDialogFactory.cs | 77 +------ .../factories/CredentialsDialogFactory.cs | 169 ++++++++++----- .../ConnectionManagerDialogManager.cs | 173 +++++++++++++-- .../ConnectionManagerDialogRowManager.cs | 68 +++--- csharp/ExcelAddIn/models/Session.cs | 2 +- csharp/ExcelAddIn/models/TableTriple.cs | 12 ++ .../operations/SnapshotOperation.cs | 41 ++-- .../operations/SubscribeOperation.cs | 35 ++- csharp/ExcelAddIn/providers/ClientProvider.cs | 129 ----------- .../providers/CredentialsProvider.cs | 37 ++++ .../providers/DefaultEndpointTableProvider.cs | 83 +++++++ .../providers/DefaultSessionProvider.cs | 88 -------- .../providers/FilteredTableProvider.cs | 106 +++++++++ csharp/ExcelAddIn/providers/ITableProvider.cs | 11 + .../providers/PersistentQueryProvider.cs | 110 ++++++++++ .../ExcelAddIn/providers/SessionProvider.cs | 155 +++++-------- .../ExcelAddIn/providers/SessionProviders.cs | 137 ------------ .../providers/TableHandleProvider.cs | 98 --------- csharp/ExcelAddIn/providers/TableProvider.cs | 100 +++++++++ csharp/ExcelAddIn/util/ObservableConverter.cs | 4 +- .../ExcelAddIn/util/SimpleAtomicReference.cs | 19 -- csharp/ExcelAddIn/util/TableDescriptor.cs | 2 - csharp/ExcelAddIn/util/Utility.cs | 34 ++- csharp/ExcelAddIn/util/VersionTracker.cs | 27 +++ csharp/ExcelAddIn/util/WorkerThread.cs | 13 +- .../viewmodels/ConnectionManagerDialogRow.cs | 21 +- .../viewmodels/CredentialsDialogViewModel.cs | 3 +- .../views/ConnectionManagerDialog.cs | 35 ++- csharp/ExcelAddIn/views/CredentialsDialog.cs | 17 +- .../views/DeephavenMessageBox.Designer.cs | 111 ++++++++++ .../ExcelAddIn/views/DeephavenMessageBox.cs | 34 +++ .../ExcelAddIn/views/DeephavenMessageBox.resx | 120 +++++++++++ 35 files changed, 1369 insertions(+), 932 deletions(-) delete mode 100644 csharp/ExcelAddIn/providers/ClientProvider.cs create mode 100644 csharp/ExcelAddIn/providers/CredentialsProvider.cs create mode 100644 csharp/ExcelAddIn/providers/DefaultEndpointTableProvider.cs delete mode 100644 csharp/ExcelAddIn/providers/DefaultSessionProvider.cs create mode 100644 csharp/ExcelAddIn/providers/FilteredTableProvider.cs create mode 100644 csharp/ExcelAddIn/providers/ITableProvider.cs create mode 100644 csharp/ExcelAddIn/providers/PersistentQueryProvider.cs delete mode 100644 csharp/ExcelAddIn/providers/SessionProviders.cs delete mode 100644 csharp/ExcelAddIn/providers/TableHandleProvider.cs create mode 100644 csharp/ExcelAddIn/providers/TableProvider.cs delete mode 100644 csharp/ExcelAddIn/util/SimpleAtomicReference.cs delete mode 100644 csharp/ExcelAddIn/util/TableDescriptor.cs create mode 100644 csharp/ExcelAddIn/util/VersionTracker.cs create mode 100644 csharp/ExcelAddIn/views/DeephavenMessageBox.Designer.cs create mode 100644 csharp/ExcelAddIn/views/DeephavenMessageBox.cs create mode 100644 csharp/ExcelAddIn/views/DeephavenMessageBox.resx diff --git a/csharp/ExcelAddIn/DeephavenExcelFunctions.cs b/csharp/ExcelAddIn/DeephavenExcelFunctions.cs index d80d6627c85..9aeab0ad654 100644 --- a/csharp/ExcelAddIn/DeephavenExcelFunctions.cs +++ b/csharp/ExcelAddIn/DeephavenExcelFunctions.cs @@ -1,12 +1,8 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Deephaven.ExcelAddIn.ExcelDna; using Deephaven.ExcelAddIn.Factories; using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Operations; -using Deephaven.ExcelAddIn.Providers; -using Deephaven.ExcelAddIn.Viewmodels; -using Deephaven.ExcelAddIn.Views; using ExcelDna.Integration; namespace Deephaven.ExcelAddIn; @@ -21,38 +17,38 @@ public static void ShowConnectionsDialog() { [ExcelFunction(Description = "Snapshots a table", IsThreadSafe = true)] public static object DEEPHAVEN_SNAPSHOT(string tableDescriptor, object filter, object wantHeaders) { - if (!TryInterpretCommonArgs(tableDescriptor, filter, wantHeaders, out var td, out var filt, out var wh, out var errorText)) { + if (!TryInterpretCommonArgs(tableDescriptor, filter, wantHeaders, out var tq, out var wh, out var errorText)) { return errorText; } // These two are used by ExcelDNA to share results for identical invocations. The functionName is arbitary but unique. const string functionName = "Deephaven.ExcelAddIn.DeephavenExcelFunctions.DEEPHAVEN_SNAPSHOT"; var parms = new[] { tableDescriptor, filter, wantHeaders }; - ExcelObservableSource eos = () => new SnapshotOperation(td!, filt, wh, StateManager); + ExcelObservableSource eos = () => new SnapshotOperation(tq, wh, StateManager); return ExcelAsyncUtil.Observe(functionName, parms, eos); } [ExcelFunction(Description = "Subscribes to a table", IsThreadSafe = true)] public static object DEEPHAVEN_SUBSCRIBE(string tableDescriptor, object filter, object wantHeaders) { - if (!TryInterpretCommonArgs(tableDescriptor, filter, wantHeaders, out var td, out var filt, out var wh, out string errorText)) { + if (!TryInterpretCommonArgs(tableDescriptor, filter, wantHeaders, out var tq, out var wh, out string errorText)) { return errorText; } // These two are used by ExcelDNA to share results for identical invocations. The functionName is arbitary but unique. const string functionName = "Deephaven.ExcelAddIn.DeephavenExcelFunctions.DEEPHAVEN_SUBSCRIBE"; var parms = new[] { tableDescriptor, filter, wantHeaders }; - ExcelObservableSource eos = () => new SubscribeOperation(td, filt, wh, StateManager); + ExcelObservableSource eos = () => new SubscribeOperation(tq, wh, StateManager); return ExcelAsyncUtil.Observe(functionName, parms, eos); } private static bool TryInterpretCommonArgs(string tableDescriptor, object filter, object wantHeaders, - [NotNullWhen(true)]out TableTriple? tableDescriptorResult, out string filterResult, out bool wantHeadersResult, out string errorText) { - filterResult = ""; + [NotNullWhen(true)]out TableQuad? tableQuadResult, out bool wantHeadersResult, out string errorText) { + tableQuadResult = null; wantHeadersResult = false; - if (!TableTriple.TryParse(tableDescriptor, out tableDescriptorResult, out errorText)) { + if (!TableTriple.TryParse(tableDescriptor, out var tt, out errorText)) { return false; } - if (!ExcelDnaHelpers.TryInterpretAs(filter, "", out filterResult)) { + if (!ExcelDnaHelpers.TryInterpretAs(filter, "", out var condition)) { errorText = "Can't interpret FILTER argument"; return false; } @@ -62,6 +58,8 @@ private static bool TryInterpretCommonArgs(string tableDescriptor, object filter errorText = "Can't interpret WANT_HEADERS argument"; return false; } + + tableQuadResult = new TableQuad(tt.EndpointId, tt.PersistentQueryId, tt.TableName, condition); return true; } } diff --git a/csharp/ExcelAddIn/ExcelAddIn.csproj.user b/csharp/ExcelAddIn/ExcelAddIn.csproj.user index f39927bb679..476df3e2650 100644 --- a/csharp/ExcelAddIn/ExcelAddIn.csproj.user +++ b/csharp/ExcelAddIn/ExcelAddIn.csproj.user @@ -8,5 +8,8 @@ Form + + Form + \ No newline at end of file diff --git a/csharp/ExcelAddIn/StateManager.cs b/csharp/ExcelAddIn/StateManager.cs index fafcd158648..9faa64a24ae 100644 --- a/csharp/ExcelAddIn/StateManager.cs +++ b/csharp/ExcelAddIn/StateManager.cs @@ -1,4 +1,5 @@ -using Deephaven.DeephavenClient.ExcelAddIn.Util; +using System.Diagnostics; +using System.Net; using Deephaven.DeephavenClient; using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Providers; @@ -8,80 +9,170 @@ namespace Deephaven.ExcelAddIn; public class StateManager { public readonly WorkerThread WorkerThread = WorkerThread.Create(); - private readonly SessionProviders _sessionProviders; + private readonly Dictionary _credentialsProviders = new(); + private readonly Dictionary _sessionProviders = new(); + private readonly Dictionary _persistentQueryProviders = new(); + private readonly Dictionary _tableProviders = new(); + private readonly ObserverContainer> _credentialsPopulationObservers = new(); + private readonly ObserverContainer _defaultEndpointSelectionObservers = new(); + + private EndpointId? _defaultEndpointId = null; + + public IDisposable SubscribeToCredentialsPopulation(IObserver> observer) { + WorkerThread.EnqueueOrRun(() => { + _credentialsPopulationObservers.Add(observer, out _); + + // Give this observer the current set of endpoint ids. + var keys = _credentialsProviders.Keys.ToArray(); + foreach (var endpointId in keys) { + observer.OnNext(AddOrRemove.OfAdd(endpointId)); + } + }); - public StateManager() { - _sessionProviders = new SessionProviders(WorkerThread); + return WorkerThread.EnqueueOrRunWhenDisposed( + () => _credentialsPopulationObservers.Remove(observer, out _)); } - public IDisposable SubscribeToSessions(IObserver> observer) { - return _sessionProviders.Subscribe(observer); - } + public IDisposable SubscribeToDefaultEndpointSelection(IObserver observer) { + WorkerThread.EnqueueOrRun(() => { + _defaultEndpointSelectionObservers.Add(observer, out _); + observer.OnNext(_defaultEndpointId); + }); - public IDisposable SubscribeToSession(EndpointId endpointId, IObserver> observer) { - return _sessionProviders.SubscribeToSession(endpointId, observer); + return WorkerThread.EnqueueOrRunWhenDisposed( + () => _defaultEndpointSelectionObservers.Remove(observer, out _)); } - public IDisposable SubscribeToCredentials(EndpointId endpointId, IObserver> observer) { - return _sessionProviders.SubscribeToCredentials(endpointId, observer); + /// + /// The major difference between the credentials providers and the other providers + /// is that the credential providers don't remove themselves from the map + /// upon the last dispose of the subscriber. That is, they hang around until we + /// manually remove them. + /// + public IDisposable SubscribeToCredentials(EndpointId endpointId, + IObserver> observer) { + IDisposable? disposer = null; + LookupOrCreateCredentialsProvider(endpointId, + cp => disposer = cp.Subscribe(observer)); + + return WorkerThread.EnqueueOrRunWhenDisposed(() => + Utility.Exchange(ref disposer, null)?.Dispose()); } - public IDisposable SubscribeToDefaultSession(IObserver> observer) { - return _sessionProviders.SubscribeToDefaultSession(observer); + public void SetCredentials(CredentialsBase credentials) { + LookupOrCreateCredentialsProvider(credentials.Id, + cp => cp.SetCredentials(credentials)); } - public IDisposable SubscribeToDefaultCredentials(IObserver> observer) { - return _sessionProviders.SubscribeToDefaultCredentials(observer); + public void Reconnect(EndpointId id) { + // Quick-and-dirty trick for reconnect is to re-send the credentials to the observers. + LookupOrCreateCredentialsProvider(id, cp => cp.Resend()); } - public IDisposable SubscribeToTableTriple(TableTriple descriptor, string filter, - IObserver> observer) { - // There is a chain with multiple elements: - // - // 1. Make a TableHandleProvider - // 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(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(); - }); - }); + public void TryDeleteCredentials(EndpointId id, Action onSuccess, Action onFailure) { + if (WorkerThread.EnqueueOrNop(() => TryDeleteCredentials(id, onSuccess, onFailure))) { + return; + } + + if (!_credentialsProviders.TryGetValue(id, out var cp)) { + onFailure($"{id} unknown"); + return; + } + + if (cp.ObserverCountUnsafe != 0) { + onFailure($"{id} is still active"); + return; + } + + if (id.Equals(_defaultEndpointId)) { + SetDefaultEndpointId(null); + } + + _credentialsProviders.Remove(id); + _credentialsPopulationObservers.OnNext(AddOrRemove.OfRemove(id)); + onSuccess(); } - public void SetCredentials(CredentialsBase credentials) { - _sessionProviders.SetCredentials(credentials); + private void LookupOrCreateCredentialsProvider(EndpointId endpointId, + Action action) { + if (WorkerThread.EnqueueOrNop(() => LookupOrCreateCredentialsProvider(endpointId, action))) { + return; + } + if (!_credentialsProviders.TryGetValue(endpointId, out var cp)) { + cp = new CredentialsProvider(this); + _credentialsProviders.Add(endpointId, cp); + cp.Init(); + _credentialsPopulationObservers.OnNext(AddOrRemove.OfAdd(endpointId)); + } + + action(cp); } - public void SetDefaultCredentials(CredentialsBase credentials) { - _sessionProviders.SetDefaultCredentials(credentials); + public IDisposable SubscribeToSession(EndpointId endpointId, + IObserver> observer) { + IDisposable? disposer = null; + WorkerThread.EnqueueOrRun(() => { + if (!_sessionProviders.TryGetValue(endpointId, out var sp)) { + sp = new SessionProvider(this, endpointId, () => _sessionProviders.Remove(endpointId)); + _sessionProviders.Add(endpointId, sp); + sp.Init(); + } + disposer = sp.Subscribe(observer); + }); + + return WorkerThread.EnqueueOrRunWhenDisposed(() => + Utility.Exchange(ref disposer, null)?.Dispose()); } - public void Reconnect(EndpointId id) { - _sessionProviders.Reconnect(id); + public IDisposable SubscribeToPersistentQuery(EndpointId endpointId, PersistentQueryId? pqId, + IObserver> observer) { + + IDisposable? disposer = null; + WorkerThread.EnqueueOrRun(() => { + var key = new PersistentQueryKey(endpointId, pqId); + if (!_persistentQueryProviders.TryGetValue(key, out var pqp)) { + pqp = new PersistentQueryProvider(this, endpointId, pqId, + () => _persistentQueryProviders.Remove(key)); + _persistentQueryProviders.Add(key, pqp); + pqp.Init(); + } + disposer = pqp.Subscribe(observer); + }); + + return WorkerThread.EnqueueOrRunWhenDisposed( + () => Utility.Exchange(ref disposer, null)?.Dispose()); } - public void SwitchOnEmpty(EndpointId id, Action onEmpty, Action onNotEmpty) { - _sessionProviders.SwitchOnEmpty(id, onEmpty, onNotEmpty); + public IDisposable SubscribeToTable(TableQuad key, IObserver> observer) { + IDisposable? disposer = null; + WorkerThread.EnqueueOrRun(() => { + if (!_tableProviders.TryGetValue(key, out var tp)) { + Action onDispose = () => _tableProviders.Remove(key); + if (key.EndpointId == null) { + tp = new DefaultEndpointTableProvider(this, key.PersistentQueryId, key.TableName, key.Condition, + onDispose); + } else if (key.Condition.Length != 0) { + tp = new FilteredTableProvider(this, key.EndpointId, key.PersistentQueryId, key.TableName, + key.Condition, onDispose); + } else { + tp = new TableProvider(this, key.EndpointId, key.PersistentQueryId, key.TableName, onDispose); + } + _tableProviders.Add(key, tp); + tp.Init(); + } + disposer = tp.Subscribe(observer); + }); + + return WorkerThread.EnqueueOrRunWhenDisposed( + () => Utility.Exchange(ref disposer, null)?.Dispose()); + } + + public void SetDefaultEndpointId(EndpointId? defaultEndpointId) { + if (WorkerThread.EnqueueOrNop(() => SetDefaultEndpointId(defaultEndpointId))) { + return; + } + + _defaultEndpointId = defaultEndpointId; + _defaultEndpointSelectionObservers.OnNext(_defaultEndpointId); } } diff --git a/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs b/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs index 3df7ee89504..72441c93323 100644 --- a/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs +++ b/csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs @@ -1,74 +1,17 @@ -using System.Collections.Concurrent; -using Deephaven.ExcelAddIn.Managers; -using Deephaven.ExcelAddIn.Viewmodels; -using Deephaven.ExcelAddIn.ViewModels; +using Deephaven.ExcelAddIn.Managers; +using Deephaven.ExcelAddIn.Util; using Deephaven.ExcelAddIn.Views; namespace Deephaven.ExcelAddIn.Factories; internal static class ConnectionManagerDialogFactory { - public static void CreateAndShow(StateManager sm) { - var rowToManager = new ConcurrentDictionary(); - - // The "new" button creates a "New/Edit Credentials" dialog - void OnNewButtonClicked() { - var cvm = CredentialsDialogViewModel.OfEmpty(); - var dialog = CredentialsDialogFactory.Create(sm, cvm); - dialog.Show(); - } - - void OnDeleteButtonClicked(ConnectionManagerDialogRow[] rows) { - foreach (var row in rows) { - if (!rowToManager.TryGetValue(row, out var manager)) { - continue; - } - manager.DoDelete(); - } - } - - void OnReconnectButtonClicked(ConnectionManagerDialogRow[] rows) { - foreach (var row in rows) { - if (!rowToManager.TryGetValue(row, out var manager)) { - continue; - } - manager.DoReconnect(); - } - } - - void OnMakeDefaultButtonClicked(ConnectionManagerDialogRow[] rows) { - // Make the last selected row the default - if (rows.Length == 0) { - return; - } - - var row = rows[^1]; - if (!rowToManager.TryGetValue(row, out var manager)) { - return; - } - - manager.DoSetAsDefault(); - } - - void OnEditButtonClicked(ConnectionManagerDialogRow[] rows) { - foreach (var row in rows) { - if (!rowToManager.TryGetValue(row, out var manager)) { - continue; - } - manager.DoEdit(); - } - } - - var cmDialog = new ConnectionManagerDialog(OnNewButtonClicked, OnDeleteButtonClicked, - OnReconnectButtonClicked, OnMakeDefaultButtonClicked, OnEditButtonClicked); - cmDialog.Show(); - var dm = new ConnectionManagerDialogManager(cmDialog, rowToManager, sm); - var disposer = sm.SubscribeToSessions(dm); - - cmDialog.Closed += (_, _) => { - disposer.Dispose(); - dm.Dispose(); - }; + public static void CreateAndShow(StateManager stateManager) { + Utility.RunInBackground(() => { + var cmDialog = new ConnectionManagerDialog(); + var dm = ConnectionManagerDialogManager.Create(stateManager, cmDialog); + cmDialog.Closed += (_, _) => dm.Dispose(); + // Blocks forever (in this private thread) + cmDialog.ShowDialog(); + }); } } - - diff --git a/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs b/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs index 4d179569bff..4c1e2694be7 100644 --- a/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs +++ b/csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs @@ -1,83 +1,138 @@ -using System.Diagnostics; -using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Util; using Deephaven.ExcelAddIn.ViewModels; using ExcelAddIn.views; +using static System.Windows.Forms.AxHost; namespace Deephaven.ExcelAddIn.Factories; internal static class CredentialsDialogFactory { - public static CredentialsDialog Create(StateManager sm, CredentialsDialogViewModel cvm) { - CredentialsDialog? credentialsDialog = null; + public static void CreateAndShow(StateManager stateManager, CredentialsDialogViewModel cvm, + EndpointId? whitelistId) { + Utility.RunInBackground(() => { + var cd = new CredentialsDialog(cvm); + var state = new CredentialsDialogState(stateManager, cd, cvm, whitelistId); - void OnSetCredentialsButtonClicked() { - if (!cvm.TryMakeCredentials(out var newCreds, out var error)) { - ShowMessageBox(error); - return; - } - sm.SetCredentials(newCreds); - if (cvm.IsDefault) { - sm.SetDefaultCredentials(newCreds); + cd.OnSetCredentialsButtonClicked += state.OnSetCredentials; + cd.OnTestCredentialsButtonClicked += state.OnTestCredentials; + + cd.Closed += (_, _) => state.Dispose(); + // Blocks forever (in this private thread) + cd.ShowDialog(); + }); + } +} + +internal class CredentialsDialogState : IObserver>, IDisposable { + private readonly StateManager _stateManager; + private readonly CredentialsDialog _credentialsDialog; + private readonly CredentialsDialogViewModel _cvm; + private readonly EndpointId? _whitelistId; + private IDisposable? _disposer; + private readonly object _sync = new(); + private readonly HashSet _knownIds = new(); + private readonly VersionTracker _versionTracker = new(); + + public CredentialsDialogState( + StateManager stateManager, + CredentialsDialog credentialsDialog, + CredentialsDialogViewModel cvm, + EndpointId? whitelistId) { + _stateManager = stateManager; + _credentialsDialog = credentialsDialog; + _cvm = cvm; + _whitelistId = whitelistId; + _disposer = stateManager.SubscribeToCredentialsPopulation(this); + } + + public void Dispose() { + Utility.Exchange(ref _disposer, null)?.Dispose(); + } + + public void OnCompleted() { + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + throw new NotImplementedException(); + } + + public void OnNext(AddOrRemove value) { + lock (_sync) { + if (value.IsAdd) { + _knownIds.Add(value.Value); + } else { + _knownIds.Remove(value.Value); } + } + } - credentialsDialog!.Close(); + public void OnSetCredentials() { + if (!_cvm.TryMakeCredentials(out var newCreds, out var error)) { + ShowMessageBox(error); + return; } - // 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; - } + bool isKnown; + lock (_sync) { + isKnown = _knownIds.Contains(newCreds.Id); + } - // 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. + if (isKnown && !newCreds.Id.Equals(_whitelistId)) { + const string caption = "Modify existing connection?"; + var text = $"Are you sure you want to modify connection \"{newCreds.Id}\""; + var dhm = new DeephavenMessageBox(caption, text, true); + var dialogResult = dhm.ShowDialog(_credentialsDialog); + if (dialogResult != DialogResult.OK) { return; } + } - // Our results are valid. Keep them and tell everyone about it. - credentialsDialog!.SetTestResultsBox(state); + _stateManager.SetCredentials(newCreds); + if (_cvm.IsDefault) { + _stateManager.SetDefaultEndpointId(newCreds.Id); } - void OnTestCredentialsButtonClicked() { - if (!cvm.TryMakeCredentials(out var newCreds, out var error)) { - ShowMessageBox(error); - return; - } + _credentialsDialog!.Close(); + } + + public void OnTestCredentials() { + if (!_cvm.TryMakeCredentials(out var newCreds, out var error)) { + ShowMessageBox(error); + return; + } + + _credentialsDialog!.SetTestResultsBox("Checking credentials"); + // Check credentials on its own thread + Utility.RunInBackground(() => TestCredentialsThreadFunc(newCreds)); + } + + private void TestCredentialsThreadFunc(CredentialsBase creds) { + var latestCookie = _versionTracker.SetNewVersion(); + + var state = "OK"; + try { + // This operation might take some time. + var temp = SessionBaseFactory.Create(creds, _stateManager.WorkerThread); + temp.Dispose(); + } catch (Exception ex) { + state = ex.Message; + } - credentialsDialog!.SetTestResultsBox("Checking credentials"); - // Check credentials on its own thread - Utility.RunInBackground(() => TestCredentials(newCreds)); + if (!latestCookie.IsCurrent) { + // Our results are moot. Dispose of them. + return; } - // Save in captured variable so that the lambdas can access it. - credentialsDialog = new CredentialsDialog(cvm, OnSetCredentialsButtonClicked, OnTestCredentialsButtonClicked); - return credentialsDialog; + // Our results are valid. Keep them and tell everyone about it. + _credentialsDialog!.SetTestResultsBox(state); } - private static void ShowMessageBox(string error) { - MessageBox.Show(error, "Please provide missing fields", MessageBoxButtons.OK); + private void ShowMessageBox(string error) { + _credentialsDialog.Invoke(() => { + var dhm = new DeephavenMessageBox("Please provide missing fields", error, false); + dhm.ShowDialog(_credentialsDialog); + }); } } diff --git a/csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs b/csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs index 8cf9ec5435d..5ebcbc958f6 100644 --- a/csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs +++ b/csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs @@ -2,56 +2,82 @@ using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Viewmodels; using Deephaven.ExcelAddIn.Views; -using System.Diagnostics; using Deephaven.ExcelAddIn.Util; +using Deephaven.ExcelAddIn.Factories; +using Deephaven.ExcelAddIn.ViewModels; +using ExcelAddIn.views; namespace Deephaven.ExcelAddIn.Managers; -internal class ConnectionManagerDialogManager( - ConnectionManagerDialog cmDialog, - ConcurrentDictionary rowToManager, - StateManager stateManager) : IObserver>, IDisposable { - private readonly WorkerThread _workerThread = stateManager.WorkerThread; +internal class ConnectionManagerDialogManager : IObserver>, IDisposable { + // + // ConnectionManagerDialog cmDialog, + // ConcurrentDictionary rowToManager, + // StateManager stateManager) + public static ConnectionManagerDialogManager Create(StateManager stateManager, + ConnectionManagerDialog cmDialog) { + var result = new ConnectionManagerDialogManager(stateManager, cmDialog); + cmDialog.OnNewButtonClicked += result.OnNewButtonClicked; + cmDialog.OnDeleteButtonClicked += result.OnDeleteButtonClicked; + cmDialog.OnReconnectButtonClicked += result.OnReconnectButtonClicked; + cmDialog.OnMakeDefaultButtonClicked += result.OnMakeDefaultButtonClicked; + cmDialog.OnEditButtonClicked += result.OnEditButtonClicked; + + var disp = stateManager.SubscribeToCredentialsPopulation(result); + result._disposables.Add(disp); + return result; + } + + private readonly StateManager _stateManager; + private readonly WorkerThread _workerThread; + private readonly ConnectionManagerDialog _cmDialog; private readonly Dictionary _idToRow = new(); + private readonly Dictionary _rowToManager = new(); private readonly List _disposables = new(); + public ConnectionManagerDialogManager(StateManager stateManager, ConnectionManagerDialog cmDialog) { + _stateManager = stateManager; + _workerThread = stateManager.WorkerThread; + _cmDialog = cmDialog; + } + public void OnNext(AddOrRemove aor) { - if (_workerThread.InvokeIfRequired(() => OnNext(aor))) { + if (_workerThread.EnqueueOrNop(() => OnNext(aor))) { return; } if (aor.IsAdd) { var endpointId = aor.Value; var row = new ConnectionManagerDialogRow(endpointId.Id); - var statusRowManager = ConnectionManagerDialogRowManager.Create(row, endpointId, stateManager); - _ = rowToManager.TryAdd(row, statusRowManager); + var statusRowManager = ConnectionManagerDialogRowManager.Create(row, endpointId, _stateManager); + _rowToManager.Add(row, statusRowManager); _idToRow.Add(endpointId, row); _disposables.Add(statusRowManager); - cmDialog.AddRow(row); + _cmDialog.AddRow(row); return; } // Remove! if (!_idToRow.Remove(aor.Value, out var rowToDelete) || - !rowToManager.TryRemove(rowToDelete, out var rowManager)) { + !_rowToManager.Remove(rowToDelete, out var rowManager)) { return; } - cmDialog.RemoveRow(rowToDelete); + _cmDialog.RemoveRow(rowToDelete); rowManager.Dispose(); } public void Dispose() { - // Since the GUI thread is where we added these disposables, the GUI thread is where we will - // access and dispose them. - cmDialog.Invoke(() => { - var temp = _disposables.ToArray(); - _disposables.Clear(); - foreach (var disposable in temp) { - disposable.Dispose(); - } - }); + if (_workerThread.EnqueueOrNop(Dispose)) { + return; + } + + var temp = _disposables.ToArray(); + _disposables.Clear(); + foreach (var disposable in temp) { + disposable.Dispose(); + } } public void OnCompleted() { @@ -63,4 +89,109 @@ public void OnError(Exception error) { // TODO(kosak) throw new NotImplementedException(); } + + void OnNewButtonClicked() { + var cvm = CredentialsDialogViewModel.OfEmpty(); + CredentialsDialogFactory.CreateAndShow(_stateManager, cvm, null); + } + + private class FailureCollector { + private readonly ConnectionManagerDialog _cmDialog; + private readonly object _sync = new(); + private int _rowsLeft = 0; + private readonly List _failures = new(); + + public FailureCollector(ConnectionManagerDialog cmDialog, int rowsLeft) { + _cmDialog = cmDialog; + _rowsLeft = rowsLeft; + } + + public void OnFailure(EndpointId id, string reason) { + lock (_sync) { + _failures.Add(reason); + } + + FinalSteps(); + } + + public void OnSuccess(EndpointId id) { + FinalSteps(); + } + + private void FinalSteps() { + string text; + lock (_sync) { + --_rowsLeft; + if (_rowsLeft > 0 || _failures.Count == 0) { + return; + } + + text = string.Join(Environment.NewLine, _failures); + } + + const string caption = "Couldn't delete some selections"; + _cmDialog.Invoke(() => { + var mbox = new DeephavenMessageBox(caption, text, false); + mbox.ShowDialog(_cmDialog); + }); + } + } + + void OnDeleteButtonClicked(ConnectionManagerDialogRow[] rows) { + if (_workerThread.EnqueueOrNop(() => OnDeleteButtonClicked(rows))) { + return; + } + + var fc = new FailureCollector(_cmDialog, rows.Length); + foreach (var row in rows) { + if (!_rowToManager.TryGetValue(row, out var manager)) { + continue; + } + manager.DoDelete(fc.OnSuccess, fc.OnFailure); + } + } + + void OnReconnectButtonClicked(ConnectionManagerDialogRow[] rows) { + if (_workerThread.EnqueueOrNop(() => OnReconnectButtonClicked(rows))) { + return; + } + + foreach (var row in rows) { + if (!_rowToManager.TryGetValue(row, out var manager)) { + continue; + } + manager.DoReconnect(); + } + } + + void OnMakeDefaultButtonClicked(ConnectionManagerDialogRow[] rows) { + if (_workerThread.EnqueueOrNop(() => OnMakeDefaultButtonClicked(rows))) { + return; + } + + // Make the last selected row the default + if (rows.Length == 0) { + return; + } + + var row = rows[^1]; + if (!_rowToManager.TryGetValue(row, out var manager)) { + return; + } + + manager.DoSetAsDefault(); + } + + void OnEditButtonClicked(ConnectionManagerDialogRow[] rows) { + if (_workerThread.EnqueueOrNop(() => OnEditButtonClicked(rows))) { + return; + } + + foreach (var row in rows) { + if (!_rowToManager.TryGetValue(row, out var manager)) { + continue; + } + manager.DoEdit(); + } + } } diff --git a/csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs b/csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs index 366c0c596ab..1b98da60e65 100644 --- a/csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs +++ b/csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs @@ -6,8 +6,11 @@ namespace Deephaven.ExcelAddIn.Managers; -public sealed class ConnectionManagerDialogRowManager : IObserver>, - IObserver>, IObserver, IDisposable { +public sealed class ConnectionManagerDialogRowManager : + IObserver>, + IObserver>, + IObserver, + IDisposable { public static ConnectionManagerDialogRowManager Create(ConnectionManagerDialogRow row, EndpointId endpointId, StateManager stateManager) { @@ -31,11 +34,11 @@ private ConnectionManagerDialogRowManager(ConnectionManagerDialogRow row, Endpoi } public void Dispose() { - Unsubcribe(); + Unsubscribe(); } private void Resubscribe() { - if (_workerThread.InvokeIfRequired(Resubscribe)) { + if (_workerThread.EnqueueOrNop(Resubscribe)) { return; } @@ -45,22 +48,12 @@ private void Resubscribe() { // We watch for session and credential state changes in our ID var d1 = _stateManager.SubscribeToSession(_endpointId, this); var d2 = _stateManager.SubscribeToCredentials(_endpointId, this); - // Now we have a problem. We would also like to watch for credential - // state changes in the default session. But the default session - // has the same observable type (IObservable>) - // as the specific session we are watching. To work around this, - // we create an Observer that translates StatusOr to - // MyWrappedSOSB and then we subscribe to that. - var converter = ObservableConverter.Create( - (StatusOr socb) => new MyWrappedSocb(socb), _workerThread); - var d3 = _stateManager.SubscribeToDefaultCredentials(converter); - var d4 = converter.Subscribe(this); - - _disposables.AddRange(new[] { d1, d2, d3, d4 }); + var d3 = _stateManager.SubscribeToDefaultEndpointSelection(this); + _disposables.AddRange(new[] { d1, d2, d3 }); } - private void Unsubcribe() { - if (_workerThread.InvokeIfRequired(Unsubcribe)) { + private void Unsubscribe() { + if (_workerThread.EnqueueOrNop(Unsubscribe)) { return; } var temp = _disposables.ToArray(); @@ -79,8 +72,8 @@ public void OnNext(StatusOr value) { _row.SetSessionSynced(value); } - public void OnNext(MyWrappedSocb value) { - _row.SetDefaultCredentialsSynced(value.Value); + public void OnNext(EndpointId? value) { + _row.SetDefaultEndpointIdSynced(value); } public void DoEdit() { @@ -90,20 +83,28 @@ public void DoEdit() { var cvm = creds.AcceptVisitor( crs => CredentialsDialogViewModel.OfIdAndCredentials(_endpointId.Id, crs), _ => CredentialsDialogViewModel.OfIdButOtherwiseEmpty(_endpointId.Id)); - var cd = CredentialsDialogFactory.Create(_stateManager, cvm); - cd.Show(); + CredentialsDialogFactory.CreateAndShow(_stateManager, cvm, _endpointId); } - public void DoDelete() { + public void DoDelete(Action onSuccess, Action onFailure) { + if (_workerThread.EnqueueOrNop(() => DoDelete(onSuccess, onFailure))) { + return; + } + // Strategy: // 1. Unsubscribe to everything - // 2. If it turns out that we were the last subscriber to the session, then great, the + // 2. If it turns out that we were the last subscriber to the credentials, then great, the // delete can proceed. - // 3. Otherwise (there is some other subscriber to the session), then the delete operation + // 3. If the credentials we are deleting are the default credentials, then unset default credentials + // 4. Otherwise (there is some other subscriber to the credentials), then the delete operation // should be denied. In that case we restore our state by resubscribing to everything. - Unsubcribe(); - - _stateManager.SwitchOnEmpty(_endpointId, () => { }, Resubscribe); + Unsubscribe(); + _stateManager.TryDeleteCredentials(_endpointId, + () => onSuccess(_endpointId), + reason => { + Resubscribe(); + onFailure(_endpointId, reason); + }); } public void DoReconnect() { @@ -116,13 +117,7 @@ public void DoSetAsDefault() { return; } - // If we don't have credentials, then we can't make them the default. - var credentials = _row.GetCredentialsSynced(); - if (!credentials.GetValueOrStatus(out var creds, out _)) { - return; - } - - _stateManager.SetDefaultCredentials(creds); + _stateManager.SetDefaultEndpointId(_endpointId); } public void OnCompleted() { @@ -134,7 +129,4 @@ public void OnError(Exception error) { // TODO(kosak) throw new NotImplementedException(); } - - public record MyWrappedSocb(StatusOr Value) { - } } diff --git a/csharp/ExcelAddIn/models/Session.cs b/csharp/ExcelAddIn/models/Session.cs index 3934ff74226..5f3d34eb02a 100644 --- a/csharp/ExcelAddIn/models/Session.cs +++ b/csharp/ExcelAddIn/models/Session.cs @@ -54,7 +54,7 @@ public override T Visit(Func onCore, Func } public override void Dispose() { - if (workerThread.InvokeIfRequired(Dispose)) { + if (workerThread.EnqueueOrNop(Dispose)) { return; } diff --git a/csharp/ExcelAddIn/models/TableTriple.cs b/csharp/ExcelAddIn/models/TableTriple.cs index 95d7e7847b5..89a298dd350 100644 --- a/csharp/ExcelAddIn/models/TableTriple.cs +++ b/csharp/ExcelAddIn/models/TableTriple.cs @@ -1,5 +1,10 @@ namespace Deephaven.ExcelAddIn.Models; +public record PersistentQueryKey( + EndpointId EndpointId, + PersistentQueryId? PersistentQueryId) { +} + public record TableTriple( EndpointId? EndpointId, PersistentQueryId? PersistentQueryId, @@ -35,3 +40,10 @@ public static bool TryParse(string text, out TableTriple result, out string erro return true; } } + +public record TableQuad( + EndpointId? EndpointId, + PersistentQueryId? PersistentQueryId, + string TableName, + string Condition) { +} diff --git a/csharp/ExcelAddIn/operations/SnapshotOperation.cs b/csharp/ExcelAddIn/operations/SnapshotOperation.cs index 70b577de0e3..b9a299b8309 100644 --- a/csharp/ExcelAddIn/operations/SnapshotOperation.cs +++ b/csharp/ExcelAddIn/operations/SnapshotOperation.cs @@ -8,18 +8,15 @@ namespace Deephaven.ExcelAddIn.Operations; internal class SnapshotOperation : IExcelObservable, IObserver> { - private readonly TableTriple _tableDescriptor; - private readonly string _filter; + private readonly TableQuad _tableQuad; private readonly bool _wantHeaders; private readonly StateManager _stateManager; private readonly ObserverContainer> _observers = new(); private readonly WorkerThread _workerThread; private IDisposable? _filteredTableDisposer = null; - public SnapshotOperation(TableTriple tableDescriptor, string filter, bool wantHeaders, - StateManager stateManager) { - _tableDescriptor = tableDescriptor; - _filter = filter; + public SnapshotOperation(TableQuad tableQuad, bool wantHeaders, StateManager stateManager) { + _tableQuad = tableQuad; _wantHeaders = wantHeaders; _stateManager = stateManager; // Convenience @@ -28,42 +25,40 @@ public SnapshotOperation(TableTriple tableDescriptor, string filter, bool wantHe public IDisposable Subscribe(IExcelObserver observer) { var wrappedObserver = ExcelDnaHelpers.WrapExcelObserver(observer); - _workerThread.Invoke(() => { + _workerThread.EnqueueOrRun(() => { _observers.Add(wrappedObserver, out var isFirst); if (isFirst) { - _filteredTableDisposer = _stateManager.SubscribeToTableTriple(_tableDescriptor, _filter, this); + _filteredTableDisposer = _stateManager.SubscribeToTable(_tableQuad, this); } }); - return ActionAsDisposable.Create(() => { - _workerThread.Invoke(() => { - _observers.Remove(wrappedObserver, out var wasLast); - if (!wasLast) { - return; - } + return _workerThread.EnqueueOrRunWhenDisposed(() => { + _observers.Remove(wrappedObserver, out var wasLast); + if (!wasLast) { + return; + } - Utility.Exchange(ref _filteredTableDisposer, null)?.Dispose(); - }); + Utility.Exchange(ref _filteredTableDisposer, null)?.Dispose(); }); } - public void OnNext(StatusOr soth) { - if (_workerThread.InvokeIfRequired(() => OnNext(soth))) { + public void OnNext(StatusOr tableHandle) { + if (_workerThread.EnqueueOrNop(() => OnNext(tableHandle))) { return; } - if (!soth.GetValueOrStatus(out var tableHandle, out var status)) { + if (!tableHandle.GetValueOrStatus(out var th, out var status)) { _observers.SendStatus(status); return; } - _observers.SendStatus($"Snapshotting \"{_tableDescriptor.TableName}\""); + _observers.SendStatus($"Snapshotting \"{_tableQuad.TableName}\""); try { - using var ct = tableHandle.ToClientTable(); - var result = Renderer.Render(ct, _wantHeaders); - _observers.SendValue(result); + using var ct = th.ToClientTable(); + var rendered = Renderer.Render(ct, _wantHeaders); + _observers.SendValue(rendered); } catch (Exception ex) { _observers.SendStatus(ex.Message); } diff --git a/csharp/ExcelAddIn/operations/SubscribeOperation.cs b/csharp/ExcelAddIn/operations/SubscribeOperation.cs index e451861546a..f2478d67a53 100644 --- a/csharp/ExcelAddIn/operations/SubscribeOperation.cs +++ b/csharp/ExcelAddIn/operations/SubscribeOperation.cs @@ -2,27 +2,23 @@ using Deephaven.DeephavenClient.ExcelAddIn.Util; using Deephaven.ExcelAddIn.ExcelDna; using Deephaven.ExcelAddIn.Models; -using Deephaven.ExcelAddIn.Providers; using Deephaven.ExcelAddIn.Util; using ExcelDna.Integration; namespace Deephaven.ExcelAddIn.Operations; internal class SubscribeOperation : IExcelObservable, IObserver> { - private readonly TableTriple _tableDescriptor; - private readonly string _filter; + private readonly TableQuad _tableQuad; private readonly bool _wantHeaders; private readonly StateManager _stateManager; private readonly ObserverContainer> _observers = new(); private readonly WorkerThread _workerThread; - private IDisposable? _filteredTableDisposer = null; + private IDisposable? _tableDisposer = null; private TableHandle? _currentTableHandle = null; private SubscriptionHandle? _currentSubHandle = null; - public SubscribeOperation(TableTriple tableDescriptor, string filter, bool wantHeaders, - StateManager stateManager) { - _tableDescriptor = tableDescriptor; - _filter = filter; + public SubscribeOperation(TableQuad tableQuad, bool wantHeaders, StateManager stateManager) { + _tableQuad = tableQuad; _wantHeaders = wantHeaders; _stateManager = stateManager; // Convenience @@ -31,39 +27,40 @@ public SubscribeOperation(TableTriple tableDescriptor, string filter, bool wantH public IDisposable Subscribe(IExcelObserver observer) { var wrappedObserver = ExcelDnaHelpers.WrapExcelObserver(observer); - _workerThread.Invoke(() => { + _workerThread.EnqueueOrRun(() => { _observers.Add(wrappedObserver, out var isFirst); if (isFirst) { - _filteredTableDisposer = _stateManager.SubscribeToTableTriple(_tableDescriptor, _filter, this); + _tableDisposer = _stateManager.SubscribeToTable(_tableQuad, this); } }); return ActionAsDisposable.Create(() => { - _workerThread.Invoke(() => { + _workerThread.EnqueueOrRun(() => { _observers.Remove(wrappedObserver, out var wasLast); if (!wasLast) { return; } - var temp = _filteredTableDisposer; - _filteredTableDisposer = null; - temp?.Dispose(); + Utility.Exchange(ref _tableDisposer, null)?.Dispose(); }); }); } public void OnNext(StatusOr soth) { - if (_workerThread.InvokeIfRequired(() => OnNext(soth))) { + if (_workerThread.EnqueueOrNop(() => OnNext(soth))) { return; } // First tear down old state if (_currentTableHandle != null) { - _currentTableHandle.Unsubscribe(_currentSubHandle!); - _currentSubHandle!.Dispose(); + if (_currentSubHandle != null) { + _currentTableHandle.Unsubscribe(_currentSubHandle!); + _currentSubHandle!.Dispose(); + _currentSubHandle = null; + } + _currentTableHandle = null; - _currentSubHandle = null; } if (!soth.GetValueOrStatus(out var tableHandle, out var status)) { @@ -71,7 +68,7 @@ public void OnNext(StatusOr soth) { return; } - _observers.SendStatus($"Subscribing to \"{_tableDescriptor.TableName}\""); + _observers.SendStatus($"Subscribing to \"{_tableQuad.TableName}\""); _currentTableHandle = tableHandle; _currentSubHandle = _currentTableHandle.Subscribe(new MyTickingCallback(_observers, _wantHeaders)); diff --git a/csharp/ExcelAddIn/providers/ClientProvider.cs b/csharp/ExcelAddIn/providers/ClientProvider.cs deleted file mode 100644 index 5d296e35221..00000000000 --- a/csharp/ExcelAddIn/providers/ClientProvider.cs +++ /dev/null @@ -1,129 +0,0 @@ -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/CredentialsProvider.cs b/csharp/ExcelAddIn/providers/CredentialsProvider.cs new file mode 100644 index 00000000000..9a2d8c2deee --- /dev/null +++ b/csharp/ExcelAddIn/providers/CredentialsProvider.cs @@ -0,0 +1,37 @@ +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Util; + +namespace Deephaven.ExcelAddIn.Providers; + +internal class CredentialsProvider : IObservable> { + private readonly WorkerThread _workerThread; + private readonly ObserverContainer> _observers = new(); + private StatusOr _credentials = StatusOr.OfStatus("[No Credentials]"); + + public CredentialsProvider(StateManager stateManager) { + _workerThread = stateManager.WorkerThread; + } + + public void Init() { + // Do nothing + } + + public IDisposable Subscribe(IObserver> observer) { + _workerThread.EnqueueOrRun(() => { + _observers.Add(observer, out _); + observer.OnNext(_credentials); + }); + + return _workerThread.EnqueueOrRunWhenDisposed(() => _observers.Remove(observer, out _)); + } + + public void SetCredentials(CredentialsBase newCredentials) { + _observers.SetAndSendValue(ref _credentials, newCredentials); + } + + public void Resend() { + _observers.OnNext(_credentials); + } + + public int ObserverCountUnsafe => _observers.Count; +} diff --git a/csharp/ExcelAddIn/providers/DefaultEndpointTableProvider.cs b/csharp/ExcelAddIn/providers/DefaultEndpointTableProvider.cs new file mode 100644 index 00000000000..26d1dab5f35 --- /dev/null +++ b/csharp/ExcelAddIn/providers/DefaultEndpointTableProvider.cs @@ -0,0 +1,83 @@ +using Deephaven.DeephavenClient; +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Util; +using System.Diagnostics; + +namespace Deephaven.ExcelAddIn.Providers; + +internal class DefaultEndpointTableProvider : + IObserver>, + IObserver, + // IObservable>, // redundant, part of ITableProvider + ITableProvider { + private const string UnsetTableHandleText = "[No Default Connection]"; + + private readonly StateManager _stateManager; + private readonly PersistentQueryId? _persistentQueryId; + private readonly string _tableName; + private readonly string _condition; + private readonly WorkerThread _workerThread; + private Action? _onDispose; + private IDisposable? _endpointSubscriptionDisposer = null; + private IDisposable? _upstreamSubscriptionDisposer = null; + private readonly ObserverContainer> _observers = new(); + private StatusOr _tableHandle = StatusOr.OfStatus(UnsetTableHandleText); + + public DefaultEndpointTableProvider(StateManager stateManager, + PersistentQueryId? persistentQueryId, string tableName, string condition, + Action onDispose) { + _stateManager = stateManager; + _workerThread = stateManager.WorkerThread; + _persistentQueryId = persistentQueryId; + _tableName = tableName; + _condition = condition; + _onDispose = onDispose; + } + + public void Init() { + _endpointSubscriptionDisposer = _stateManager.SubscribeToDefaultEndpointSelection(this); + } + + public IDisposable Subscribe(IObserver> observer) { + _workerThread.EnqueueOrRun(() => { + _observers.Add(observer, out _); + observer.OnNext(_tableHandle); + }); + + return _workerThread.EnqueueOrRunWhenDisposed(() => { + _observers.Remove(observer, out var isLast); + if (!isLast) { + return; + } + + Utility.Exchange(ref _endpointSubscriptionDisposer, null)?.Dispose(); + Utility.Exchange(ref _onDispose, null)?.Invoke(); + }); + } + + public void OnNext(EndpointId? endpointId) { + // Unsubscribe from old upstream + Utility.Exchange(ref _upstreamSubscriptionDisposer, null)?.Dispose(); + + // If endpoint is null, then don't subscribe to anything. + if (endpointId == null) { + _observers.SetAndSendStatus(ref _tableHandle, UnsetTableHandleText); + return; + } + + var tq = new TableQuad(endpointId, _persistentQueryId, _tableName, _condition); + _upstreamSubscriptionDisposer = _stateManager.SubscribeToTable(tq, this); + } + + public void OnNext(StatusOr value) { + _observers.SetAndSend(ref _tableHandle, value); + } + + public void OnCompleted() { + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + throw new NotImplementedException(); + } +} diff --git a/csharp/ExcelAddIn/providers/DefaultSessionProvider.cs b/csharp/ExcelAddIn/providers/DefaultSessionProvider.cs deleted file mode 100644 index f38f8bc8a8e..00000000000 --- a/csharp/ExcelAddIn/providers/DefaultSessionProvider.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Deephaven.DeephavenClient.ExcelAddIn.Util; -using Deephaven.ExcelAddIn.Models; -using Deephaven.ExcelAddIn.Util; - -namespace Deephaven.ExcelAddIn.Providers; - -internal class DefaultSessionProvider(WorkerThread workerThread) : - IObserver>, IObserver>, - IObservable>, IObservable> { - private StatusOr _credentials = StatusOr.OfStatus("[Not set]"); - private StatusOr _session = StatusOr.OfStatus("[Not connected]"); - private readonly ObserverContainer> _credentialsObservers = new(); - private readonly ObserverContainer> _sessionObservers = new(); - private SessionProvider? _parent = null; - private IDisposable? _credentialsSubDisposer = null; - private IDisposable? _sessionSubDisposer = null; - - public IDisposable Subscribe(IObserver> observer) { - workerThread.Invoke(() => { - // New observer gets added to the collection and then notified of the current status. - _credentialsObservers.Add(observer, out _); - observer.OnNext(_credentials); - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - _credentialsObservers.Remove(observer, out _); - }); - }); - } - - public IDisposable Subscribe(IObserver> observer) { - workerThread.Invoke(() => { - // New observer gets added to the collection and then notified of the current status. - _sessionObservers.Add(observer, out _); - observer.OnNext(_session); - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - _sessionObservers.Remove(observer, out _); - }); - }); - } - - public void OnNext(StatusOr value) { - if (workerThread.InvokeIfRequired(() => OnNext(value))) { - return; - } - _credentials = value; - _credentialsObservers.OnNext(_credentials); - } - - public void OnNext(StatusOr value) { - if (workerThread.InvokeIfRequired(() => OnNext(value))) { - return; - } - _session = value; - _sessionObservers.OnNext(_session); - } - - public void OnCompleted() { - // TODO(kosak) - throw new NotImplementedException(); - } - - public void OnError(Exception error) { - // TODO(kosak) - throw new NotImplementedException(); - } - - public void SetParent(SessionProvider? newParent) { - if (workerThread.InvokeIfRequired(() => SetParent(newParent))) { - return; - } - - _parent = newParent; - Utility.Exchange(ref _credentialsSubDisposer, null)?.Dispose(); - Utility.Exchange(ref _sessionSubDisposer, null)?.Dispose(); - - if (_parent == null) { - return; - } - - _credentialsSubDisposer = _parent.Subscribe((IObserver>)this); - _sessionSubDisposer = _parent.Subscribe((IObserver>)this); - } -} diff --git a/csharp/ExcelAddIn/providers/FilteredTableProvider.cs b/csharp/ExcelAddIn/providers/FilteredTableProvider.cs new file mode 100644 index 00000000000..2774ec31c94 --- /dev/null +++ b/csharp/ExcelAddIn/providers/FilteredTableProvider.cs @@ -0,0 +1,106 @@ +using System.Diagnostics; +using Deephaven.DeephavenClient; +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Util; + +namespace Deephaven.ExcelAddIn.Providers; + +internal class FilteredTableProvider : + IObserver>, + // IObservable>, // redundant, part of ITableProvider + ITableProvider { + + private readonly StateManager _stateManager; + private readonly WorkerThread _workerThread; + private readonly EndpointId _endpointId; + private readonly PersistentQueryId? _persistentQueryId; + private readonly string _tableName; + private readonly string _condition; + private Action? _onDispose; + private IDisposable? _tableHandleSubscriptionDisposer = null; + private readonly ObserverContainer> _observers = new(); + private StatusOr _filteredTableHandle = StatusOr.OfStatus("[No Filtered Table]"); + + public FilteredTableProvider(StateManager stateManager, + EndpointId endpointId, PersistentQueryId? persistentQueryId, string tableName, string condition, + Action onDispose) { + _stateManager = stateManager; + _workerThread = stateManager.WorkerThread; + _endpointId = endpointId; + _persistentQueryId = persistentQueryId; + _tableName = tableName; + _condition = condition; + _onDispose = onDispose; + } + + public void Init() { + // Subscribe to a condition-free table + var tq = new TableQuad(_endpointId, _persistentQueryId, _tableName, ""); + Debug.WriteLine($"FTP is subscribing to TableHandle with {tq}"); + _tableHandleSubscriptionDisposer = _stateManager.SubscribeToTable(tq, this); + } + + public IDisposable Subscribe(IObserver> observer) { + _workerThread.EnqueueOrRun(() => { + _observers.Add(observer, out _); + observer.OnNext(_filteredTableHandle); + }); + + return _workerThread.EnqueueOrRunWhenDisposed(() => { + _observers.Remove(observer, out var isLast); + if (!isLast) { + return; + } + + Utility.Exchange(ref _tableHandleSubscriptionDisposer, null)?.Dispose(); + Utility.Exchange(ref _onDispose, null)?.Invoke(); + DisposeTableHandleState(); + }); + } + + public void OnNext(StatusOr tableHandle) { + // Get onto the worker thread if we're not already on it. + if (_workerThread.EnqueueOrNop(() => OnNext(tableHandle))) { + return; + } + + DisposeTableHandleState(); + + // If the new state is just a status message, make that our state and transmit to our observers + if (!tableHandle.GetValueOrStatus(out var th, out var status)) { + _observers.SetAndSendStatus(ref _filteredTableHandle, status); + return; + } + + // It's a real TableHandle so start fetching the table. First notify our observers. + _observers.SetAndSendStatus(ref _filteredTableHandle, "Filtering"); + + try { + var filtered = th.Where(_condition); + _observers.SetAndSendValue(ref _filteredTableHandle, filtered); + } catch (Exception ex) { + _observers.SetAndSendStatus(ref _filteredTableHandle, ex.Message); + } + } + + private void DisposeTableHandleState() { + if (_workerThread.EnqueueOrNop(DisposeTableHandleState)) { + return; + } + + _ = _filteredTableHandle.GetValueOrStatus(out var oldTh, out _); + _observers.SetAndSendStatus(ref _filteredTableHandle, "Disposing TableHandle"); + + if (oldTh != null) { + Utility.RunInBackground(oldTh.Dispose); + } + } + + public void OnCompleted() { + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + throw new NotImplementedException(); + } +} diff --git a/csharp/ExcelAddIn/providers/ITableProvider.cs b/csharp/ExcelAddIn/providers/ITableProvider.cs new file mode 100644 index 00000000000..f0af94f4caa --- /dev/null +++ b/csharp/ExcelAddIn/providers/ITableProvider.cs @@ -0,0 +1,11 @@ +using Deephaven.DeephavenClient; +using Deephaven.ExcelAddIn.Util; + +namespace Deephaven.ExcelAddIn.Providers; + +/// +/// Common interface for TableProvider, FilteredTableProvider, and DefaultEndpointTableProvider +/// +public interface ITableProvider : IObservable> { + void Init(); +} diff --git a/csharp/ExcelAddIn/providers/PersistentQueryProvider.cs b/csharp/ExcelAddIn/providers/PersistentQueryProvider.cs new file mode 100644 index 00000000000..11c240be9ff --- /dev/null +++ b/csharp/ExcelAddIn/providers/PersistentQueryProvider.cs @@ -0,0 +1,110 @@ +using Deephaven.DeephavenClient; +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Util; + +namespace Deephaven.ExcelAddIn.Providers; + +internal class PersistentQueryProvider : + IObserver>, IObservable> { + + private readonly StateManager _stateManager; + private readonly WorkerThread _workerThread; + private readonly EndpointId _endpointId; + private readonly PersistentQueryId? _pqId; + private Action? _onDispose; + private IDisposable? _upstreamSubscriptionDisposer = null; + private readonly ObserverContainer> _observers = new(); + private StatusOr _client = StatusOr.OfStatus("[No Client]"); + private Client? _ownedDndClient = null; + + public PersistentQueryProvider(StateManager stateManager, + EndpointId endpointId, PersistentQueryId? pqId, Action onDispose) { + _stateManager = stateManager; + _workerThread = stateManager.WorkerThread; + _endpointId = endpointId; + _pqId = pqId; + _onDispose = onDispose; + } + + public void Init() { + _upstreamSubscriptionDisposer = _stateManager.SubscribeToSession(_endpointId, this); + } + + public IDisposable Subscribe(IObserver> observer) { + _workerThread.EnqueueOrRun(() => { + _observers.Add(observer, out _); + observer.OnNext(_client); + }); + + return _workerThread.EnqueueOrRunWhenDisposed(() => { + _observers.Remove(observer, out var isLast); + if (!isLast) { + return; + } + + Utility.Exchange(ref _upstreamSubscriptionDisposer, null)?.Dispose(); + Utility.Exchange(ref _onDispose, null)?.Invoke(); + DisposeClientState(); + }); + } + + public void OnNext(StatusOr sessionBase) { + if (_workerThread.EnqueueOrNop(() => OnNext(sessionBase))) { + return; + } + + DisposeClientState(); + + // If the new state is just a status message, make that our state and transmit to our observers + if (!sessionBase.GetValueOrStatus(out var sb, out var status)) { + _observers.SetAndSendStatus(ref _client, status); + return; + } + + // It's a real Session so start fetching it. Also do some validity checking on the PQ id. + _ = sb.Visit( + core => { + var result = _pqId == null + ? StatusOr.OfValue(core.Client) + : StatusOr.OfStatus("PQ specified, but Community Core cannot connect to a PQ"); + _observers.SetAndSend(ref _client, result); + return Unit.Instance; + }, + corePlus => { + if (_pqId == null) { + _observers.SetAndSendStatus(ref _client, "Enterprise Core+ requires a PQ to be specified"); + return Unit.Instance; + } + + _observers.SetAndSendStatus(ref _client, $"Attaching to \"{_pqId}\""); + + try { + _ownedDndClient = corePlus.SessionManager.ConnectToPqByName(_pqId.Id, false); + _observers.SetAndSendValue(ref _client, _ownedDndClient); + } catch (Exception ex) { + _observers.SetAndSendStatus(ref _client, ex.Message); + } + return Unit.Instance; + }); + } + + private void DisposeClientState() { + if (_workerThread.EnqueueOrNop(DisposeClientState)) { + return; + } + + _observers.SetAndSendStatus(ref _client, "Disposing Client"); + var oldClient = Utility.Exchange(ref _ownedDndClient, null); + if (oldClient != null) { + Utility.RunInBackground(oldClient.Dispose); + } + } + + public void OnCompleted() { + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + throw new NotImplementedException(); + } +} diff --git a/csharp/ExcelAddIn/providers/SessionProvider.cs b/csharp/ExcelAddIn/providers/SessionProvider.cs index 16a60e935a1..f34fefdb6d8 100644 --- a/csharp/ExcelAddIn/providers/SessionProvider.cs +++ b/csharp/ExcelAddIn/providers/SessionProvider.cs @@ -1,154 +1,101 @@ -using System.Diagnostics; -using Deephaven.DeephavenClient.ExcelAddIn.Util; -using Deephaven.ExcelAddIn.Factories; +using Deephaven.ExcelAddIn.Factories; using Deephaven.ExcelAddIn.Models; using Deephaven.ExcelAddIn.Util; namespace Deephaven.ExcelAddIn.Providers; -internal class SessionProvider(WorkerThread workerThread) : IObservable>, IObservable>, IDisposable { - private StatusOr _credentials = StatusOr.OfStatus("[Not set]"); +internal class SessionProvider : IObserver>, IObservable> { + private readonly StateManager _stateManager; + private readonly WorkerThread _workerThread; + private readonly EndpointId _endpointId; + private Action? _onDispose; + private IDisposable? _upstreamSubscriptionDisposer = null; private StatusOr _session = StatusOr.OfStatus("[Not connected]"); - private readonly ObserverContainer> _credentialsObservers = new(); - private readonly ObserverContainer> _sessionObservers = new(); - /// - /// This is used to track the results from multiple invocations of "SetCredentials" and - /// to keep only the latest. - /// - private readonly SimpleAtomicReference _sharedSetCredentialsCookie = new(new object()); - - public void Dispose() { - // Get on the worker thread if not there already. - if (workerThread.InvokeIfRequired(Dispose)) { - return; - } - - // TODO(kosak) - // I feel like we should send an OnComplete to any remaining observers - - if (!_session.GetValueOrStatus(out var sess, out _)) { - return; - } - - _sessionObservers.SetAndSendStatus(ref _session, "Disposing"); - sess.Dispose(); + private readonly ObserverContainer> _observers = new(); + private readonly VersionTracker _versionTracker = new(); + + public SessionProvider(StateManager stateManager, EndpointId endpointId, Action onDispose) { + _stateManager = stateManager; + _workerThread = stateManager.WorkerThread; + _endpointId = endpointId; + _onDispose = onDispose; } - /// - /// Subscribe to credentials changes - /// - /// - /// - public IDisposable Subscribe(IObserver> observer) { - workerThread.Invoke(() => { - // New observer gets added to the collection and then notified of the current status. - _credentialsObservers.Add(observer, out _); - observer.OnNext(_credentials); - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - _credentialsObservers.Remove(observer, out _); - }); - }); + public void Init() { + _upstreamSubscriptionDisposer = _stateManager.SubscribeToCredentials(_endpointId, this); } /// /// Subscribe to session changes /// - /// - /// public IDisposable Subscribe(IObserver> observer) { - workerThread.Invoke(() => { - // New observer gets added to the collection and then notified of the current status. - _sessionObservers.Add(observer, out _); + _workerThread.EnqueueOrRun(() => { + _observers.Add(observer, out _); observer.OnNext(_session); }); - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - _sessionObservers.Remove(observer, out var isLast); - Debug.WriteLine(isLast); - }); + return _workerThread.EnqueueOrRunWhenDisposed(() => { + _observers.Remove(observer, out var isLast); + if (!isLast) { + return; + } + + Utility.Exchange(ref _upstreamSubscriptionDisposer, null)?.Dispose(); + Utility.Exchange(ref _onDispose, null)?.Invoke(); + DisposeSessionState(); }); } - public void SetCredentials(CredentialsBase credentials) { - // Get on the worker thread if not there already. - if (workerThread.InvokeIfRequired(() => SetCredentials(credentials))) { + public void OnNext(StatusOr credentials) { + if (_workerThread.EnqueueOrNop(() => OnNext(credentials))) { return; } - // Dispose existing session - if (_session.GetValueOrStatus(out var sess, out _)) { - _sessionObservers.SetAndSendStatus(ref _session, "Disposing session"); - sess.Dispose(); - } - - _credentialsObservers.SetAndSendValue(ref _credentials, credentials); - - _sessionObservers.SetAndSendStatus(ref _session, "Trying to connect"); + DisposeSessionState(); - Utility.RunInBackground(() => CreateSessionBaseInSeparateThread(credentials)); - } - - public void SwitchOnEmpty(Action callerOnEmpty, Action callerOnNotEmpty) { - if (workerThread.InvokeIfRequired(() => SwitchOnEmpty(callerOnEmpty, callerOnNotEmpty))) { + if (!credentials.GetValueOrStatus(out var cbase, out var status)) { + _observers.SetAndSendStatus(ref _session, status); return; } - if (_credentialsObservers.Count != 0 || _sessionObservers.Count != 0) { - callerOnNotEmpty(); - return; - } + _observers.SetAndSendStatus(ref _session, "Trying to connect"); - callerOnEmpty(); + var cookie = _versionTracker.SetNewVersion(); + Utility.RunInBackground(() => CreateSessionBaseInSeparateThread(cbase, cookie)); } - 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; - + private void CreateSessionBaseInSeparateThread(CredentialsBase credentials, VersionTrackerCookie versionCookie) { + SessionBase? sb = null; StatusOr result; try { // This operation might take some time. - var sb = SessionBaseFactory.Create(credentials, workerThread); + sb = SessionBaseFactory.Create(credentials, _workerThread); result = StatusOr.OfValue(sb); } catch (Exception ex) { 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(); - } + // Some time has passed. It's possible that the VersionTracker has been reset + // with a newer version. If so, we should throw away our work and leave. + if (!versionCookie.IsCurrent) { + 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)); + _workerThread.EnqueueOrRun(() => _observers.SetAndSend(ref _session, result)); } - public void Reconnect() { - // Get on the worker thread if not there already. - if (workerThread.InvokeIfRequired(Reconnect)) { + private void DisposeSessionState() { + if (_workerThread.EnqueueOrNop(DisposeSessionState)) { return; } - // We implement this as a SetCredentials call, with credentials we already have. - if (_credentials.GetValueOrStatus(out var creds, out _)) { - SetCredentials(creds); + _ = _session.GetValueOrStatus(out var oldSession, out _); + _observers.SetAndSendStatus(ref _session, "Disposing Session"); + + if (oldSession != null) { + Utility.RunInBackground(oldSession.Dispose); } } diff --git a/csharp/ExcelAddIn/providers/SessionProviders.cs b/csharp/ExcelAddIn/providers/SessionProviders.cs deleted file mode 100644 index 042d0a3d1a7..00000000000 --- a/csharp/ExcelAddIn/providers/SessionProviders.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Diagnostics; -using Deephaven.DeephavenClient.ExcelAddIn.Util; -using Deephaven.ExcelAddIn.Models; -using Deephaven.ExcelAddIn.Util; - -namespace Deephaven.ExcelAddIn.Providers; - -internal class SessionProviders(WorkerThread workerThread) : IObservable> { - private readonly DefaultSessionProvider _defaultProvider = new(workerThread); - private readonly Dictionary _providerMap = new(); - private readonly ObserverContainer> _endpointsObservers = new(); - - 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(() => { - _endpointsObservers.Add(observer, out _); - // To avoid any further possibility of reentrancy while iterating over the dict, - // make a copy of the keys - var keys = _providerMap.Keys.ToArray(); - foreach (var endpointId in keys) { - observer.OnNext(AddOrRemove.OfAdd(endpointId)); - } - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - _endpointsObservers.Remove(observer, out _); - }); - }); - } - - public IDisposable SubscribeToSession(EndpointId id, IObserver> observer) { - IDisposable? disposable = null; - ApplyTo(id, sp => disposable = sp.Subscribe(observer)); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - Utility.Exchange(ref disposable, null)?.Dispose(); - }); - }); - } - - public IDisposable SubscribeToCredentials(EndpointId id, IObserver> observer) { - IDisposable? disposable = null; - ApplyTo(id, sp => disposable = sp.Subscribe(observer)); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - Utility.Exchange(ref disposable, null)?.Dispose(); - }); - }); - } - - public IDisposable SubscribeToDefaultSession(IObserver> observer) { - IDisposable? disposable = null; - workerThread.Invoke(() => { - disposable = _defaultProvider.Subscribe(observer); - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - Utility.Exchange(ref disposable, null)?.Dispose(); - }); - }); - } - - public IDisposable SubscribeToDefaultCredentials(IObserver> observer) { - IDisposable? disposable = null; - workerThread.Invoke(() => { - disposable = _defaultProvider.Subscribe(observer); - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - Utility.Exchange(ref disposable, null)?.Dispose(); - }); - }); - } - - public void SetCredentials(CredentialsBase credentials) { - ApplyTo(credentials.Id, sp => { - sp.SetCredentials(credentials); - }); - } - - public void SetDefaultCredentials(CredentialsBase credentials) { - ApplyTo(credentials.Id, _defaultProvider.SetParent); - } - - public void Reconnect(EndpointId id) { - ApplyTo(id, sp => sp.Reconnect()); - } - - public void SwitchOnEmpty(EndpointId id, Action callerOnEmpty, Action callerOnNotEmpty) { - if (workerThread.InvokeIfRequired(() => SwitchOnEmpty(id, callerOnEmpty, callerOnNotEmpty))) { - return; - } - - Debug.WriteLine("It's SwitchOnEmpty time"); - if (!_providerMap.TryGetValue(id, out var sp)) { - // No provider. That's weird. callerOnEmpty I guess - callerOnEmpty(); - return; - } - - // Make a wrapped onEmpty that removes stuff from my dictionary and invokes - // the observer, then calls the caller's onEmpty - - Action? myOnEmpty = null; - myOnEmpty = () => { - if (workerThread.InvokeIfRequired(myOnEmpty!)) { - return; - } - _providerMap.Remove(id); - _endpointsObservers.OnNext(AddOrRemove.OfRemove(id)); - callerOnEmpty(); - }; - - sp.SwitchOnEmpty(myOnEmpty, callerOnNotEmpty); - } - - - private void ApplyTo(EndpointId id, Action action) { - if (workerThread.InvokeIfRequired(() => ApplyTo(id, action))) { - return; - } - - if (!_providerMap.TryGetValue(id, out var sp)) { - sp = new SessionProvider(workerThread); - _providerMap.Add(id, sp); - _endpointsObservers.OnNext(AddOrRemove.OfAdd(id)); - } - - action(sp); - } -} diff --git a/csharp/ExcelAddIn/providers/TableHandleProvider.cs b/csharp/ExcelAddIn/providers/TableHandleProvider.cs deleted file mode 100644 index db7ddccd4e9..00000000000 --- a/csharp/ExcelAddIn/providers/TableHandleProvider.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Deephaven.DeephavenClient; -using Deephaven.ExcelAddIn.Models; -using Deephaven.ExcelAddIn.Util; -using Deephaven.DeephavenClient.ExcelAddIn.Util; - -namespace Deephaven.ExcelAddIn.Providers; - -internal class TableHandleProvider( - WorkerThread workerThread, - TableTriple descriptor, - string filter) : IObserver>, IObservable>, IDisposable { - - private readonly ObserverContainer> _observers = new(); - private StatusOr _tableHandle = StatusOr.OfStatus("[no TableHandle]"); - - 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(_tableHandle); - }); - - return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { - _observers.Remove(observer, out _); - }); - }); - } - - public void Dispose() { - // Get onto the worker thread if we're not already on it. - if (workerThread.InvokeIfRequired(Dispose)) { - return; - } - - DisposePqAndThState(); - } - - public void OnNext(StatusOr client) { - // Get onto the worker thread if we're not already on it. - if (workerThread.InvokeIfRequired(() => OnNext(client))) { - return; - } - - try { - // Dispose whatever state we had before. - DisposePqAndThState(); - - // If the new state is just a status message, make that our state and transmit to our observers - if (!client.GetValueOrStatus(out var cli, out var status)) { - _observers.SetAndSendStatus(ref _tableHandle, status); - return; - } - - // It's a real client so start fetching the table. First notify our observers. - _observers.SetAndSendStatus(ref _tableHandle, $"Fetching \"{descriptor.TableName}\""); - - // Now fetch the table. This might block but we're on the worker thread. In the future - // we might move this to yet another thread. - var th = cli.Manager.FetchTable(descriptor.TableName); - if (filter != "") { - // If there's a filter, take this table handle and surround it with a Where. - var temp = th; - th = temp.Where(filter); - temp.Dispose(); - } - - // Success! Make this our state and send the table handle to our observers. - _observers.SetAndSendValue(ref _tableHandle, th); - } catch (Exception ex) { - // Some exception. Make the exception message our state and send it to our observers. - _observers.SetAndSendStatus(ref _tableHandle, ex.Message); - } - } - - private void DisposePqAndThState() { - // 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(); - } - } - - public void OnCompleted() { - throw new NotImplementedException(); - } - - public void OnError(Exception error) { - throw new NotImplementedException(); - } -} diff --git a/csharp/ExcelAddIn/providers/TableProvider.cs b/csharp/ExcelAddIn/providers/TableProvider.cs new file mode 100644 index 00000000000..d4da46fef3a --- /dev/null +++ b/csharp/ExcelAddIn/providers/TableProvider.cs @@ -0,0 +1,100 @@ +using Deephaven.DeephavenClient; +using Deephaven.ExcelAddIn.Models; +using Deephaven.ExcelAddIn.Util; + +namespace Deephaven.ExcelAddIn.Providers; + +internal class TableProvider : + IObserver>, + // IObservable>, // redundant, part of ITableProvider + ITableProvider { + private const string UnsetTableHandleText = "[No Table]"; + + private readonly StateManager _stateManager; + private readonly WorkerThread _workerThread; + private readonly EndpointId _endpointId; + private readonly PersistentQueryId? _persistentQueryId; + private readonly string _tableName; + private Action? _onDispose; + private IDisposable? _pqSubscriptionDisposer = null; + private readonly ObserverContainer> _observers = new(); + private StatusOr _tableHandle = StatusOr.OfStatus(UnsetTableHandleText); + + public TableProvider(StateManager stateManager, EndpointId endpointId, + PersistentQueryId? persistentQueryId, string tableName, Action onDispose) { + _stateManager = stateManager; + _workerThread = stateManager.WorkerThread; + _endpointId = endpointId; + _persistentQueryId = persistentQueryId; + _tableName = tableName; + _onDispose = onDispose; + } + + public void Init() { + _pqSubscriptionDisposer = _stateManager.SubscribeToPersistentQuery( + _endpointId, _persistentQueryId, this); + } + + public IDisposable Subscribe(IObserver> observer) { + _workerThread.EnqueueOrRun(() => { + _observers.Add(observer, out _); + observer.OnNext(_tableHandle); + }); + + return _workerThread.EnqueueOrRunWhenDisposed(() => { + _observers.Remove(observer, out var isLast); + if (!isLast) { + return; + } + + Utility.Exchange(ref _pqSubscriptionDisposer, null)?.Dispose(); + Utility.Exchange(ref _onDispose, null)?.Invoke(); + DisposeTableHandleState(); + }); + } + + public void OnNext(StatusOr client) { + if (_workerThread.EnqueueOrNop(() => OnNext(client))) { + return; + } + + DisposeTableHandleState(); + + // If the new state is just a status message, make that our state and transmit to our observers + if (!client.GetValueOrStatus(out var cli, out var status)) { + _observers.SetAndSendStatus(ref _tableHandle, status); + return; + } + + // It's a real client so start fetching the table. First notify our observers. + _observers.SetAndSendStatus(ref _tableHandle, $"Fetching \"{_tableName}\""); + + try { + var th = cli.Manager.FetchTable(_tableName); + _observers.SetAndSendValue(ref _tableHandle, th); + } catch (Exception ex) { + _observers.SetAndSendStatus(ref _tableHandle, ex.Message); + } + } + + private void DisposeTableHandleState() { + if (_workerThread.EnqueueOrNop(DisposeTableHandleState)) { + return; + } + + _ = _tableHandle.GetValueOrStatus(out var oldTh, out _); + _observers.SetAndSendStatus(ref _tableHandle, UnsetTableHandleText); + + if (oldTh != null) { + Utility.RunInBackground(oldTh.Dispose); + } + } + + public void OnCompleted() { + throw new NotImplementedException(); + } + + public void OnError(Exception error) { + throw new NotImplementedException(); + } +} diff --git a/csharp/ExcelAddIn/util/ObservableConverter.cs b/csharp/ExcelAddIn/util/ObservableConverter.cs index 530f0785a63..b31defc40ee 100644 --- a/csharp/ExcelAddIn/util/ObservableConverter.cs +++ b/csharp/ExcelAddIn/util/ObservableConverter.cs @@ -33,10 +33,10 @@ public void OnError(Exception error) { } public IDisposable Subscribe(IObserver observer) { - workerThread.Invoke(() => _observers.Add(observer, out _)); + workerThread.EnqueueOrRun(() => _observers.Add(observer, out _)); return ActionAsDisposable.Create(() => { - workerThread.Invoke(() => { + workerThread.EnqueueOrRun(() => { _observers.Remove(observer, out _); }); }); diff --git a/csharp/ExcelAddIn/util/SimpleAtomicReference.cs b/csharp/ExcelAddIn/util/SimpleAtomicReference.cs deleted file mode 100644 index d5a2f76cd5b..00000000000 --- a/csharp/ExcelAddIn/util/SimpleAtomicReference.cs +++ /dev/null @@ -1,19 +0,0 @@ -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/TableDescriptor.cs b/csharp/ExcelAddIn/util/TableDescriptor.cs deleted file mode 100644 index caa52c74e2a..00000000000 --- a/csharp/ExcelAddIn/util/TableDescriptor.cs +++ /dev/null @@ -1,2 +0,0 @@ -namespace Deephaven.ExcelAddIn.Util; - diff --git a/csharp/ExcelAddIn/util/Utility.cs b/csharp/ExcelAddIn/util/Utility.cs index 96e910b43c9..4b9de77ca3a 100644 --- a/csharp/ExcelAddIn/util/Utility.cs +++ b/csharp/ExcelAddIn/util/Utility.cs @@ -1,4 +1,6 @@  +using System.Diagnostics; + namespace Deephaven.ExcelAddIn.Util; internal static class Utility { @@ -9,21 +11,35 @@ public static T Exchange(ref T item, T newValue) { } public static void RunInBackground(Action a) { - new Thread(() => a()) { IsBackground = true }.Start(); - } - - public static void IgnoreExceptions(Action action) { - try { - action(); - } catch { - // Ignore errors + void Doit() { + try { + a(); + } catch (Exception e) { + Debug.WriteLine($"Ignoring exception {e}"); + } } + new Thread(Doit) { IsBackground = true }.Start(); } } public class Unit { - public static readonly Unit Instance = new Unit(); + public static readonly Unit Instance = new (); private Unit() { } } + +public class ValueHolder where T : class { + private T? _value = null; + + public T Value { + get { + if (_value == null) { + throw new Exception("Value is unset"); + } + + return _value; + } + set => _value = value; + } +} diff --git a/csharp/ExcelAddIn/util/VersionTracker.cs b/csharp/ExcelAddIn/util/VersionTracker.cs new file mode 100644 index 00000000000..a0fcbe48c71 --- /dev/null +++ b/csharp/ExcelAddIn/util/VersionTracker.cs @@ -0,0 +1,27 @@ +namespace Deephaven.ExcelAddIn.Util; + +internal class VersionTracker { + private readonly object _sync = new(); + private VersionTrackerCookie _cookie; + + public VersionTracker() { + _cookie = new VersionTrackerCookie(this); + } + + public VersionTrackerCookie SetNewVersion() { + lock (_sync) { + _cookie = new VersionTrackerCookie(this); + return _cookie; + } + } + + public bool HasCookie(VersionTrackerCookie cookie) { + lock (_sync) { + return ReferenceEquals(_cookie, cookie); + } + } +} + +internal class VersionTrackerCookie(VersionTracker owner) { + public bool IsCurrent => owner.HasCookie(this); +} diff --git a/csharp/ExcelAddIn/util/WorkerThread.cs b/csharp/ExcelAddIn/util/WorkerThread.cs index dc1d2858754..eceea724114 100644 --- a/csharp/ExcelAddIn/util/WorkerThread.cs +++ b/csharp/ExcelAddIn/util/WorkerThread.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Deephaven.DeephavenClient.ExcelAddIn.Util; namespace Deephaven.ExcelAddIn.Util; @@ -18,13 +19,15 @@ public static WorkerThread Create() { private WorkerThread() { } - public void Invoke(Action action) { - if (!InvokeIfRequired(action)) { + // enquee or run + public void EnqueueOrRun(Action action) { + if (!EnqueueOrNop(action)) { action(); } } - public bool InvokeIfRequired(Action action) { + // conditionalenqueue + public bool EnqueueOrNop(Action action) { if (ReferenceEquals(Thread.CurrentThread, _thisThread)) { // Appending to thread queue was not required. Return false. return false; @@ -43,6 +46,10 @@ public bool InvokeIfRequired(Action action) { return true; } + public IDisposable EnqueueOrRunWhenDisposed(Action action) { + return ActionAsDisposable.Create(() => EnqueueOrRun(action)); + } + private void Doit() { while (true) { Action action; diff --git a/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs b/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs index 8692d3836ac..a34b9a6fbdb 100644 --- a/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs +++ b/csharp/ExcelAddIn/viewmodels/ConnectionManagerDialogRow.cs @@ -11,8 +11,9 @@ public sealed class ConnectionManagerDialogRow(string id) : INotifyPropertyChang private readonly object _sync = new(); private StatusOr _credentials = StatusOr.OfStatus("[Not set]"); private StatusOr _session = StatusOr.OfStatus("[Not connected]"); - private StatusOr _defaultCredentials = StatusOr.OfStatus("[Not set]"); + private EndpointId? _defaultEndpointId = null; + [DisplayName("Name")] public string Id { get; init; } = id; public string Status { @@ -25,6 +26,7 @@ public string Status { } } + [DisplayName("Server Type")] public string ServerType { get { var creds = GetCredentialsSynced(); @@ -38,13 +40,12 @@ public string ServerType { } } + [DisplayName("Default")] public bool IsDefault { get { - var creds = GetCredentialsSynced(); - var defaultCreds = GetDefaultCredentialsSynced(); - return creds.GetValueOrStatus(out var creds1, out _) && - defaultCreds.GetValueOrStatus(out var creds2, out _) && - creds1.Id == creds2.Id; + var id = Id; // readonly so no synchronization needed. + var defaultEp = GetDefaultEndpointIdSynced(); + return defaultEp != null && defaultEp.Id == id; } } @@ -63,15 +64,15 @@ public void SetCredentialsSynced(StatusOr value) { OnPropertyChanged(nameof(IsDefault)); } - public StatusOr GetDefaultCredentialsSynced() { + public EndpointId? GetDefaultEndpointIdSynced() { lock (_sync) { - return _defaultCredentials; + return _defaultEndpointId; } } - public void SetDefaultCredentialsSynced(StatusOr value) { + public void SetDefaultEndpointIdSynced(EndpointId? value) { lock (_sync) { - _defaultCredentials = value; + _defaultEndpointId = value; } OnPropertyChanged(nameof(IsDefault)); } diff --git a/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs b/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs index 2b3e613c7d3..86cae1c39e0 100644 --- a/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs +++ b/csharp/ExcelAddIn/viewmodels/CredentialsDialogViewModel.cs @@ -76,11 +76,10 @@ void CheckMissing(string field, string name) { CheckMissing(JsonUrl, "JSON URL"); CheckMissing(UserId, "User Id"); CheckMissing(Password, "Password"); - CheckMissing(OperateAsToUse, "Operate As"); } if (missingFields.Count > 0) { - errorText = string.Join(", ", missingFields); + errorText = string.Join(Environment.NewLine, missingFields); return false; } diff --git a/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs b/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs index a3177133202..d56b79f950a 100644 --- a/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs +++ b/csharp/ExcelAddIn/views/ConnectionManagerDialog.cs @@ -5,25 +5,15 @@ namespace Deephaven.ExcelAddIn.Views; using SelectedRowsAction = Action; public partial class ConnectionManagerDialog : Form { - private const string IsDefaultColumnName = "IsDefault"; - private readonly Action _onNewButtonClicked; - private readonly SelectedRowsAction _onDeleteButtonClicked; - private readonly SelectedRowsAction _onReconnectButtonClicked; - private readonly SelectedRowsAction _onMakeDefaultButtonClicked; - private readonly SelectedRowsAction _onEditButtonClicked; - private readonly BindingSource _bindingSource = new(); + public event Action? OnNewButtonClicked; + public event SelectedRowsAction? OnDeleteButtonClicked; + public event SelectedRowsAction? OnReconnectButtonClicked; + public event SelectedRowsAction? OnMakeDefaultButtonClicked; + public event SelectedRowsAction? OnEditButtonClicked; - public ConnectionManagerDialog(Action onNewButtonClicked, - SelectedRowsAction onDeleteButtonClicked, - SelectedRowsAction onReconnectButtonClicked, - SelectedRowsAction onMakeDefaultButtonClicked, - SelectedRowsAction onEditButtonClicked) { - _onNewButtonClicked = onNewButtonClicked; - _onDeleteButtonClicked = onDeleteButtonClicked; - _onReconnectButtonClicked = onReconnectButtonClicked; - _onMakeDefaultButtonClicked = onMakeDefaultButtonClicked; - _onEditButtonClicked = onEditButtonClicked; + private readonly BindingSource _bindingSource = new(); + public ConnectionManagerDialog() { InitializeComponent(); _bindingSource.DataSource = typeof(ConnectionManagerDialogRow); @@ -48,27 +38,27 @@ public void RemoveRow(ConnectionManagerDialogRow row) { } private void newButton_Click(object sender, EventArgs e) { - _onNewButtonClicked(); + OnNewButtonClicked?.Invoke(); } private void reconnectButton_Click(object sender, EventArgs e) { var selections = GetSelectedRows(); - _onReconnectButtonClicked(selections); + OnReconnectButtonClicked?.Invoke(selections); } private void editButton_Click(object sender, EventArgs e) { var selections = GetSelectedRows(); - _onEditButtonClicked(selections); + OnEditButtonClicked?.Invoke(selections); } private void deleteButton_Click(object sender, EventArgs e) { var selections = GetSelectedRows(); - _onDeleteButtonClicked(selections); + OnDeleteButtonClicked?.Invoke(selections); } private void makeDefaultButton_Click(object sender, EventArgs e) { var selections = GetSelectedRows(); - _onMakeDefaultButtonClicked(selections); + OnMakeDefaultButtonClicked?.Invoke(selections); } private ConnectionManagerDialogRow[] GetSelectedRows() { @@ -78,7 +68,6 @@ private ConnectionManagerDialogRow[] GetSelectedRows() { for (var i = 0; i != count; ++i) { result.Add((ConnectionManagerDialogRow)sr[i].DataBoundItem); } - return result.ToArray(); } } diff --git a/csharp/ExcelAddIn/views/CredentialsDialog.cs b/csharp/ExcelAddIn/views/CredentialsDialog.cs index 51062db9a63..f3b664cab31 100644 --- a/csharp/ExcelAddIn/views/CredentialsDialog.cs +++ b/csharp/ExcelAddIn/views/CredentialsDialog.cs @@ -1,16 +1,11 @@ -using System.Diagnostics; -using Deephaven.ExcelAddIn.ViewModels; +using Deephaven.ExcelAddIn.ViewModels; namespace ExcelAddIn.views { public partial class CredentialsDialog : Form { - private readonly Action _onSetCredentialsButtonClicked; - private readonly Action _onTestCredentialsButtonClicked; - - public CredentialsDialog(CredentialsDialogViewModel vm, Action onSetCredentialsButtonClicked, - Action onTestCredentialsButtonClicked) { - _onSetCredentialsButtonClicked = onSetCredentialsButtonClicked; - _onTestCredentialsButtonClicked = onTestCredentialsButtonClicked; + public event Action? OnSetCredentialsButtonClicked = null; + public event Action? OnTestCredentialsButtonClicked = null; + public CredentialsDialog(CredentialsDialogViewModel vm) { InitializeComponent(); // Need to fire these bindings on property changed rather than simply on validation, // because on validation is not responsive enough. Also, painful technical note: @@ -71,11 +66,11 @@ public void SetTestResultsBox(string testResultsState) { } private void setCredentialsButton_Click(object sender, EventArgs e) { - _onSetCredentialsButtonClicked(); + OnSetCredentialsButtonClicked?.Invoke(); } private void testCredentialsButton_Click(object sender, EventArgs e) { - _onTestCredentialsButtonClicked(); + OnTestCredentialsButtonClicked?.Invoke(); } } } diff --git a/csharp/ExcelAddIn/views/DeephavenMessageBox.Designer.cs b/csharp/ExcelAddIn/views/DeephavenMessageBox.Designer.cs new file mode 100644 index 00000000000..624a860a9af --- /dev/null +++ b/csharp/ExcelAddIn/views/DeephavenMessageBox.Designer.cs @@ -0,0 +1,111 @@ +namespace ExcelAddIn.views { + partial class DeephavenMessageBox { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) { + if (disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + captionLabel = new Label(); + contentsBox = new TextBox(); + okButton = new Button(); + captionPanel = new Panel(); + cancelButton = new Button(); + captionPanel.SuspendLayout(); + SuspendLayout(); + // + // captionLabel + // + captionLabel.Dock = DockStyle.Fill; + captionLabel.Font = new Font("Segoe UI", 20F, FontStyle.Regular, GraphicsUnit.Point, 0); + captionLabel.Location = new Point(0, 0); + captionLabel.Name = "captionLabel"; + captionLabel.Size = new Size(751, 73); + captionLabel.TabIndex = 0; + captionLabel.Text = "Caption"; + captionLabel.TextAlign = ContentAlignment.MiddleCenter; + // + // contentsBox + // + contentsBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + contentsBox.Location = new Point(40, 105); + contentsBox.Multiline = true; + contentsBox.Name = "contentsBox"; + contentsBox.ReadOnly = true; + contentsBox.Size = new Size(716, 207); + contentsBox.TabIndex = 1; + contentsBox.TabStop = false; + // + // okButton + // + okButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + okButton.Location = new Point(644, 343); + okButton.Name = "okButton"; + okButton.Size = new Size(112, 34); + okButton.TabIndex = 2; + okButton.Text = "OK"; + okButton.UseVisualStyleBackColor = true; + okButton.Click += okButton_Click; + // + // captionPanel + // + captionPanel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + captionPanel.Controls.Add(captionLabel); + captionPanel.Location = new Point(24, 12); + captionPanel.Name = "captionPanel"; + captionPanel.Size = new Size(751, 73); + captionPanel.TabIndex = 3; + // + // cancelButton + // + cancelButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + cancelButton.Location = new Point(513, 343); + cancelButton.Name = "cancelButton"; + cancelButton.Size = new Size(112, 34); + cancelButton.TabIndex = 4; + cancelButton.Text = "Cancel"; + cancelButton.UseVisualStyleBackColor = true; + cancelButton.Click += cancelButton_Click; + // + // DeephavenMessageBox + // + AutoScaleDimensions = new SizeF(10F, 25F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(800, 401); + Controls.Add(cancelButton); + Controls.Add(captionPanel); + Controls.Add(okButton); + Controls.Add(contentsBox); + Name = "DeephavenMessageBox"; + Text = "Deephaven Message"; + captionPanel.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label captionLabel; + private TextBox contentsBox; + private Button okButton; + private Panel captionPanel; + private Button cancelButton; + } +} \ No newline at end of file diff --git a/csharp/ExcelAddIn/views/DeephavenMessageBox.cs b/csharp/ExcelAddIn/views/DeephavenMessageBox.cs new file mode 100644 index 00000000000..8476f689b05 --- /dev/null +++ b/csharp/ExcelAddIn/views/DeephavenMessageBox.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace ExcelAddIn.views { + public partial class DeephavenMessageBox : Form { + public DeephavenMessageBox(string caption, string text, bool cancelVisible) { + InitializeComponent(); + + captionLabel.Text = caption; + contentsBox.Text = text; + cancelButton.Visible = cancelVisible; + + AcceptButton = AcceptButton; + CancelButton = cancelButton; + } + + private void okButton_Click(object sender, EventArgs e) { + DialogResult = DialogResult.OK; + Close(); + } + + private void cancelButton_Click(object sender, EventArgs e) { + DialogResult = DialogResult.Cancel; + Close(); + } + } +} diff --git a/csharp/ExcelAddIn/views/DeephavenMessageBox.resx b/csharp/ExcelAddIn/views/DeephavenMessageBox.resx new file mode 100644 index 00000000000..4f24d55cd6b --- /dev/null +++ b/csharp/ExcelAddIn/views/DeephavenMessageBox.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file