Skip to content

Commit

Permalink
feat(csharp/ExcelAddIn): ExcelAddIn demo v6: Respond to demo feedback (
Browse files Browse the repository at this point in the history
…#6030)

Respond to demo feedback, make a variety of changes.
  • Loading branch information
kosak authored Sep 7, 2024
1 parent 1bb5f09 commit 29184e8
Show file tree
Hide file tree
Showing 17 changed files with 666 additions and 311 deletions.
4 changes: 4 additions & 0 deletions csharp/ExcelAddIn/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,8 @@ public void SetDefaultCredentials(CredentialsBase credentials) {
public void Reconnect(EndpointId id) {
_sessionProviders.Reconnect(id);
}

public void SwitchOnEmpty(EndpointId id, Action onEmpty, Action onNotEmpty) {
_sessionProviders.SwitchOnEmpty(id, onEmpty, onNotEmpty);
}
}
122 changes: 47 additions & 75 deletions csharp/ExcelAddIn/factories/ConnectionManagerDialogFactory.cs
Original file line number Diff line number Diff line change
@@ -1,102 +1,74 @@
using Deephaven.ExcelAddIn.Viewmodels;
using System.Collections.Concurrent;
using Deephaven.ExcelAddIn.Managers;
using Deephaven.ExcelAddIn.Viewmodels;
using Deephaven.ExcelAddIn.ViewModels;
using Deephaven.ExcelAddIn.Views;
using System.Diagnostics;
using Deephaven.ExcelAddIn.Models;
using Deephaven.ExcelAddIn.Util;

namespace Deephaven.ExcelAddIn.Factories;

internal static class ConnectionManagerDialogFactory {
public static void CreateAndShow(StateManager sm) {
var rowToManager = new ConcurrentDictionary<ConnectionManagerDialogRow, ConnectionManagerDialogRowManager>();

// The "new" button creates a "New/Edit Credentials" dialog
void OnNewButtonClicked() {
var cvm = CredentialsDialogViewModel.OfEmpty();
var dialog = CredentialsDialogFactory.Create(sm, cvm);
dialog.Show();
}

var cmDialog = new ConnectionManagerDialog(OnNewButtonClicked);
cmDialog.Show();
var cmso = new ConnectionManagerSessionObserver(sm, cmDialog);
var disposer = sm.SubscribeToSessions(cmso);

cmDialog.Closed += (_, _) => {
disposer.Dispose();
cmso.Dispose();
};
}
}

internal class ConnectionManagerSessionObserver(
StateManager stateManager,
ConnectionManagerDialog cmDialog) : IObserver<AddOrRemove<EndpointId>>, IDisposable {
private readonly List<IDisposable> _disposables = new();

public void OnNext(AddOrRemove<EndpointId> aor) {
if (!aor.IsAdd) {
// TODO(kosak)
Debug.WriteLine("Remove is not handled");
return;
void OnDeleteButtonClicked(ConnectionManagerDialogRow[] rows) {
foreach (var row in rows) {
if (!rowToManager.TryGetValue(row, out var manager)) {
continue;
}
manager.DoDelete();
}
}

var endpointId = aor.Value;
void OnReconnectButtonClicked(ConnectionManagerDialogRow[] rows) {
foreach (var row in rows) {
if (!rowToManager.TryGetValue(row, out var manager)) {
continue;
}
manager.DoReconnect();
}
}

var statusRow = new ConnectionManagerDialogRow(endpointId.Id, stateManager);
// We watch for session and credential state changes in our ID
var sessDisposable = stateManager.SubscribeToSession(endpointId, statusRow);
var credDisposable = stateManager.SubscribeToCredentials(endpointId, statusRow);
void OnMakeDefaultButtonClicked(ConnectionManagerDialogRow[] rows) {
// Make the last selected row the default
if (rows.Length == 0) {
return;
}

// And we also watch for credentials changes in the default session (just to keep
// track of whether we are still the default)
var dct = new DefaultCredentialsTracker(statusRow);
var defaultCredDisposable = stateManager.SubscribeToDefaultCredentials(dct);
var row = rows[^1];
if (!rowToManager.TryGetValue(row, out var manager)) {
return;
}

// We'll do our AddRow on the GUI thread, and, while we're on the GUI thread, we'll add
// our disposables to our saved disposables.
cmDialog.Invoke(() => {
_disposables.Add(sessDisposable);
_disposables.Add(credDisposable);
_disposables.Add(defaultCredDisposable);
cmDialog.AddRow(statusRow);
});
}
manager.DoSetAsDefault();
}

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();
void OnEditButtonClicked(ConnectionManagerDialogRow[] rows) {
foreach (var row in rows) {
if (!rowToManager.TryGetValue(row, out var manager)) {
continue;
}
manager.DoEdit();
}
});
}
}

public void OnCompleted() {
// TODO(kosak)
throw new NotImplementedException();
}
var cmDialog = new ConnectionManagerDialog(OnNewButtonClicked, OnDeleteButtonClicked,
OnReconnectButtonClicked, OnMakeDefaultButtonClicked, OnEditButtonClicked);
cmDialog.Show();
var dm = new ConnectionManagerDialogManager(cmDialog, rowToManager, sm);
var disposer = sm.SubscribeToSessions(dm);

public void OnError(Exception error) {
// TODO(kosak)
throw new NotImplementedException();
cmDialog.Closed += (_, _) => {
disposer.Dispose();
dm.Dispose();
};
}
}

internal class DefaultCredentialsTracker(ConnectionManagerDialogRow statusRow) : IObserver<StatusOr<CredentialsBase>> {
public void OnNext(StatusOr<CredentialsBase> value) {
statusRow.SetDefaultCredentials(value);
}

public void OnCompleted() {
// TODO(kosak)
throw new NotImplementedException();
}

public void OnError(Exception error) {
// TODO(kosak)
throw new NotImplementedException();
}
}
3 changes: 2 additions & 1 deletion csharp/ExcelAddIn/factories/CredentialsDialogFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Deephaven.ExcelAddIn.Models;
using System.Diagnostics;
using Deephaven.ExcelAddIn.Models;
using Deephaven.ExcelAddIn.Util;
using Deephaven.ExcelAddIn.ViewModels;
using ExcelAddIn.views;
Expand Down
66 changes: 66 additions & 0 deletions csharp/ExcelAddIn/managers/ConnectionManagerDialogManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Collections.Concurrent;
using Deephaven.ExcelAddIn.Models;
using Deephaven.ExcelAddIn.Viewmodels;
using Deephaven.ExcelAddIn.Views;
using System.Diagnostics;
using Deephaven.ExcelAddIn.Util;

namespace Deephaven.ExcelAddIn.Managers;

internal class ConnectionManagerDialogManager(
ConnectionManagerDialog cmDialog,
ConcurrentDictionary<ConnectionManagerDialogRow, ConnectionManagerDialogRowManager> rowToManager,
StateManager stateManager) : IObserver<AddOrRemove<EndpointId>>, IDisposable {
private readonly WorkerThread _workerThread = stateManager.WorkerThread;
private readonly Dictionary<EndpointId, ConnectionManagerDialogRow> _idToRow = new();
private readonly List<IDisposable> _disposables = new();

public void OnNext(AddOrRemove<EndpointId> aor) {
if (_workerThread.InvokeIfRequired(() => 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);
_idToRow.Add(endpointId, row);
_disposables.Add(statusRowManager);

cmDialog.AddRow(row);
return;
}

// Remove!
if (!_idToRow.Remove(aor.Value, out var rowToDelete) ||
!rowToManager.TryRemove(rowToDelete, out var rowManager)) {
return;
}

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();
}
});
}

public void OnCompleted() {
// TODO(kosak)
throw new NotImplementedException();
}

public void OnError(Exception error) {
// TODO(kosak)
throw new NotImplementedException();
}
}
140 changes: 140 additions & 0 deletions csharp/ExcelAddIn/managers/ConnectionManagerDialogRowManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using Deephaven.ExcelAddIn.Factories;
using Deephaven.ExcelAddIn.Models;
using Deephaven.ExcelAddIn.Util;
using Deephaven.ExcelAddIn.Viewmodels;
using Deephaven.ExcelAddIn.ViewModels;

namespace Deephaven.ExcelAddIn.Managers;

public sealed class ConnectionManagerDialogRowManager : IObserver<StatusOr<CredentialsBase>>,
IObserver<StatusOr<SessionBase>>, IObserver<ConnectionManagerDialogRowManager.MyWrappedSocb>, IDisposable {

public static ConnectionManagerDialogRowManager Create(ConnectionManagerDialogRow row,
EndpointId endpointId, StateManager stateManager) {
var result = new ConnectionManagerDialogRowManager(row, endpointId, stateManager);
result.Resubscribe();
return result;
}

private readonly ConnectionManagerDialogRow _row;
private readonly EndpointId _endpointId;
private readonly StateManager _stateManager;
private readonly WorkerThread _workerThread;
private readonly List<IDisposable> _disposables = new();

private ConnectionManagerDialogRowManager(ConnectionManagerDialogRow row, EndpointId endpointId,
StateManager stateManager) {
_row = row;
_endpointId = endpointId;
_stateManager = stateManager;
_workerThread = stateManager.WorkerThread;
}

public void Dispose() {
Unsubcribe();
}

private void Resubscribe() {
if (_workerThread.InvokeIfRequired(Resubscribe)) {
return;
}

if (_disposables.Count != 0) {
throw new Exception("State error: already subscribed");
}
// 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<StatusOr<SessionBase>>)
// as the specific session we are watching. To work around this,
// we create an Observer that translates StatusOr<SessionBase> to
// MyWrappedSOSB and then we subscribe to that.
var converter = ObservableConverter.Create(
(StatusOr<CredentialsBase> socb) => new MyWrappedSocb(socb), _workerThread);
var d3 = _stateManager.SubscribeToDefaultCredentials(converter);
var d4 = converter.Subscribe(this);

_disposables.AddRange(new[] { d1, d2, d3, d4 });
}

private void Unsubcribe() {
if (_workerThread.InvokeIfRequired(Unsubcribe)) {
return;
}
var temp = _disposables.ToArray();
_disposables.Clear();

foreach (var disposable in temp) {
disposable.Dispose();
}
}

public void OnNext(StatusOr<CredentialsBase> value) {
_row.SetCredentialsSynced(value);
}

public void OnNext(StatusOr<SessionBase> value) {
_row.SetSessionSynced(value);
}

public void OnNext(MyWrappedSocb value) {
_row.SetDefaultCredentialsSynced(value.Value);
}

public void DoEdit() {
var creds = _row.GetCredentialsSynced();
// If we have valid credentials, then make a populated viewmodel.
// If we don't, then make an empty viewmodel with only Id populated.
var cvm = creds.AcceptVisitor(
crs => CredentialsDialogViewModel.OfIdAndCredentials(_endpointId.Id, crs),
_ => CredentialsDialogViewModel.OfIdButOtherwiseEmpty(_endpointId.Id));
var cd = CredentialsDialogFactory.Create(_stateManager, cvm);
cd.Show();
}

public void DoDelete() {
// Strategy:
// 1. Unsubscribe to everything
// 2. If it turns out that we were the last subscriber to the session, then great, the
// delete can proceed.
// 3. Otherwise (there is some other subscriber to the session), then the delete operation
// should be denied. In that case we restore our state by resubscribing to everything.
Unsubcribe();

_stateManager.SwitchOnEmpty(_endpointId, () => { }, Resubscribe);
}

public void DoReconnect() {
_stateManager.Reconnect(_endpointId);
}

public void DoSetAsDefault() {
// If the connection is already the default, do nothing.
if (_row.IsDefault) {
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);
}

public void OnCompleted() {
// TODO(kosak)
throw new NotImplementedException();
}

public void OnError(Exception error) {
// TODO(kosak)
throw new NotImplementedException();
}

public record MyWrappedSocb(StatusOr<CredentialsBase> Value) {
}
}
Loading

0 comments on commit 29184e8

Please sign in to comment.