Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(csharp/ExcelAddIn): ExcelAddIn demo v6: Respond to demo feedback #6030

Merged
merged 1 commit into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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