Skip to content

Commit a6dcc56

Browse files
sharpninjaCopilot
andcommitted
Reactive workspace propagation via WorkspacePathChanged event
- Add WorkspacePathChanged event to MainWindowViewModel (SSOT notification) - Fire event from ApplyActiveMcpBaseUrl after setting workspace path - Subscribe TodoListViewModel, WorkspaceViewModel, VoiceConversationViewModel at creation time — reactive refresh replaces imperative ordering - Remove imperative child VM refresh from RefreshAllViewsForConnectionChangeAsync - Remove premature auto-load from OnLoaded in PhoneTodoView, TodoListView (Android + Desktop) — workspace event handles initial load - Fix _suppressWorkspaceSelectionChanged exception safety with try-finally Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5029aaa commit a6dcc56

4 files changed

Lines changed: 26 additions & 63 deletions

File tree

src/McpServerManager.Android/Views/PhoneTodoView.axaml.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArg
5555

5656
private async void OnLoaded(object? sender, RoutedEventArgs e)
5757
{
58+
// No auto-load here — workspace-change event triggers the initial load
59+
// after the correct workspace path is set on the shared MCP client.
5860
_hasAutoLoaded = true;
59-
if (DataContext is TodoListViewModel vm && vm.GroupedItems.Count == 0 && !vm.IsLoading)
60-
await vm.LoadTodosCommand.ExecuteAsync(null);
6161
}
6262

6363
private void OnDataContextChanged(object? sender, EventArgs e)
@@ -77,9 +77,6 @@ private void OnDataContextChanged(object? sender, EventArgs e)
7777

7878
SyncEditorFromViewModel(_currentVm);
7979
RefreshFormattedDetailFromViewModel();
80-
81-
if (_hasAutoLoaded && _currentVm.GroupedItems.Count == 0 && !_currentVm.IsLoading)
82-
_ = _currentVm.LoadTodosCommand.ExecuteAsync(null);
8380
}
8481

8582
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)

src/McpServerManager.Android/Views/TodoListView.axaml.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,10 @@ public void SaveSettings()
3434
SaveCurrentSplitterToSettings(_wasPortrait.Value);
3535
}
3636

37-
private async void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
37+
private void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
3838
{
39+
// No auto-load — workspace-change event triggers the initial load.
3940
_hasAutoLoaded = true;
40-
if (DataContext is TodoListViewModel vm && vm.GroupedItems.Count == 0 && !vm.IsLoading)
41-
await vm.LoadTodosCommand.ExecuteAsync(null);
4241
}
4342

4443
private void OnDataContextChanged(object? sender, EventArgs e)
@@ -47,9 +46,6 @@ private void OnDataContextChanged(object? sender, EventArgs e)
4746
{
4847
vm.GetEditorText = () => Editor.Text;
4948
vm.PropertyChanged += OnViewModelPropertyChanged;
50-
// Auto-load if DataContext arrived after Loaded event
51-
if (_hasAutoLoaded && vm.GroupedItems.Count == 0 && !vm.IsLoading)
52-
_ = vm.LoadTodosCommand.ExecuteAsync(null);
5349
}
5450
}
5551

src/McpServerManager.Core/ViewModels/MainWindowViewModel.cs

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ public partial class MainWindowViewModel : ViewModelBase, Commands.ICommandTarge
6868
internal readonly Mediator _mediator = new();
6969
private static readonly ILogger _logger = AppLogService.Instance.CreateLogger("ViewModel");
7070

71+
/// <summary>Raised when the active workspace path changes. Child VMs subscribe to refresh reactively.</summary>
72+
public event Action<string>? WorkspacePathChanged;
73+
7174
/// <summary>ViewModel for the Todo tab. Created lazily on first access.</summary>
7275
public TodoListViewModel TodoViewModel => _todoViewModel ??= CreateTodoViewModel();
7376
private TodoListViewModel? _todoViewModel;
@@ -76,6 +79,7 @@ private TodoListViewModel CreateTodoViewModel()
7679
{
7780
var vm = new TodoListViewModel(_clipboardService, _mcpTodoService);
7881
vm.GlobalStatusChanged += msg => DispatchToUi(() => StatusMessage = msg);
82+
WorkspacePathChanged += path => DispatchToUi(() => _ = vm.RefreshForConnectionChangeAsync());
7983
return vm;
8084
}
8185

@@ -88,6 +92,7 @@ private WorkspaceViewModel CreateWorkspaceViewModel()
8892
var vm = new WorkspaceViewModel(_clipboardService, _mcpWorkspaceService);
8993
vm.GlobalStatusChanged += msg => DispatchToUi(() => StatusMessage = msg);
9094
vm.WorkspaceCatalogChanged += change => _ = RefreshWorkspacePickerAfterCatalogChangeAsync(change);
95+
WorkspacePathChanged += path => DispatchToUi(() => _ = vm.RefreshForConnectionChangeAsync());
9196
return vm;
9297
}
9398

@@ -128,6 +133,7 @@ private VoiceConversationViewModel CreateVoiceConversationViewModel()
128133
?? string.Empty
129134
};
130135
vm.GlobalStatusChanged += msg => DispatchToUi(() => StatusMessage = msg);
136+
WorkspacePathChanged += path => DispatchToUi(() => _ = vm.RefreshForConnectionChangeAsync());
131137
return vm;
132138
}
133139

@@ -434,6 +440,9 @@ private void ApplyActiveMcpBaseUrl(string mcpBaseUrl, string? mcpApiKey = null,
434440

435441
if (_hasRegisteredCqrsHandlers)
436442
RegisterMcpServiceHandlers();
443+
444+
// Notify child VMs reactively — they self-refresh without imperative ordering.
445+
WorkspacePathChanged?.Invoke(resolvedPath);
437446
}
438447

439448
private async Task<string?> ResolveActiveConnectionApiKeyAsync(WorkspaceConnectionOption option, string baseUrl)
@@ -1062,8 +1071,14 @@ private void ApplyWorkspaceConnectionOptions(
10621071
?? WorkspaceConnections[0];
10631072

10641073
_suppressWorkspaceSelectionChanged = true;
1065-
SelectedWorkspaceConnection = selected;
1066-
_suppressWorkspaceSelectionChanged = false;
1074+
try
1075+
{
1076+
SelectedWorkspaceConnection = selected;
1077+
}
1078+
finally
1079+
{
1080+
_suppressWorkspaceSelectionChanged = false;
1081+
}
10671082

10681083
// Initial picker population suppresses selection-changed switching to avoid duplicate work,
10691084
// but we still need one real connection switch to set workspace path and refresh views.
@@ -1164,6 +1179,7 @@ await DispatchToUiAsync(() =>
11641179

11651180
private async Task RefreshAllViewsForConnectionChangeAsync()
11661181
{
1182+
// Session logs are owned by MainWindowViewModel — refresh directly.
11671183
_logger.LogDebug("[Workspace Switch] Refreshing session logs...");
11681184
try
11691185
{
@@ -1176,52 +1192,8 @@ private async Task RefreshAllViewsForConnectionChangeAsync()
11761192
ex.GetType().Name, ex.Message);
11771193
}
11781194

1179-
if (_todoViewModel != null)
1180-
{
1181-
_logger.LogDebug("[Workspace Switch] Refreshing todo view...");
1182-
try
1183-
{
1184-
await _todoViewModel.RefreshForConnectionChangeAsync().ConfigureAwait(true);
1185-
_logger.LogDebug("[Workspace Switch] Todo view refreshed OK");
1186-
}
1187-
catch (Exception ex)
1188-
{
1189-
_logger.LogWarning(ex, "[Workspace Switch] Todo refresh failed ({ExType}: {ExMsg}); continuing",
1190-
ex.GetType().Name, ex.Message);
1191-
}
1192-
}
1193-
1194-
if (_workspaceViewModel != null)
1195-
{
1196-
_logger.LogDebug("[Workspace Switch] Refreshing workspace view...");
1197-
try
1198-
{
1199-
await _workspaceViewModel.RefreshForConnectionChangeAsync().ConfigureAwait(true);
1200-
_logger.LogDebug("[Workspace Switch] Workspace view refreshed OK");
1201-
}
1202-
catch (Exception ex)
1203-
{
1204-
_logger.LogWarning(ex, "[Workspace Switch] Workspace view refresh failed ({ExType}: {ExMsg}); continuing",
1205-
ex.GetType().Name, ex.Message);
1206-
}
1207-
}
1208-
1209-
if (_voiceConversationViewModel != null)
1210-
{
1211-
_logger.LogDebug("[Workspace Switch] Refreshing voice view...");
1212-
try
1213-
{
1214-
_voiceConversationViewModel.WorkspacePath = _mcpClient.WorkspacePath ?? string.Empty;
1215-
await _voiceConversationViewModel.RefreshForConnectionChangeAsync().ConfigureAwait(true);
1216-
_logger.LogDebug("[Workspace Switch] Voice view refreshed OK");
1217-
}
1218-
catch (Exception ex)
1219-
{
1220-
_logger.LogWarning(ex, "[Workspace Switch] Voice refresh failed ({ExType}: {ExMsg}); continuing",
1221-
ex.GetType().Name, ex.Message);
1222-
}
1223-
}
1224-
1195+
// Child VMs (Todo, Workspace, Voice) are refreshed reactively via
1196+
// WorkspacePathChanged event — no imperative calls needed here.
12251197
_logger.LogDebug("[Workspace Switch] All views refreshed.");
12261198
}
12271199

src/McpServerManager.Desktop/Views/TodoListView.axaml.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,10 @@ public void OnHostSizeChanged(Size newSize)
5959
}
6060
}
6161

62-
private async void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
62+
private void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
6363
{
64-
if (_hasAutoLoaded) return;
64+
// No auto-load — workspace-change event triggers the initial load.
6565
_hasAutoLoaded = true;
66-
if (DataContext is TodoListViewModel vm)
67-
await vm.LoadTodosCommand.ExecuteAsync(null);
6866
}
6967

7068
private void OnDataContextChanged(object? sender, EventArgs e)

0 commit comments

Comments
 (0)