mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
CmdPal: Make settings and app state immutable (#46451)
## Summary This PR refactors CmdPal settings/state to be immutable end-to-end. ### Core changes - Convert model types to immutable records / init-only properties: - `SettingsModel` - `AppStateModel` - `ProviderSettings` - `DockSettings` - `RecentCommandsManager` - supporting settings types (fallback/hotkey/alias/top-level hotkey/history items, etc.) - Replace mutable collections with immutable equivalents where appropriate: - `ImmutableDictionary<,>` - `ImmutableList<>` - Move mutation flow to atomic service updates: - `ISettingsService.UpdateSettings(Func<SettingsModel, SettingsModel>)` - `IAppStateService.UpdateState(Func<AppStateModel, AppStateModel>)` - Update ViewModels/managers/services to use copy-on-write (`with`) patterns instead of in-place mutation. - Update serialization context + tests for immutable model graph compatibility. ## Why Issue #46437 is caused by mutable shared state being updated from different execution paths/threads, leading to race-prone behavior during persistence/serialization. By making settings/app state immutable and using atomic swap/update patterns, we remove in-place mutation and eliminate this class of concurrency bug. ## Validation - Built successfully: - `Microsoft.CmdPal.UI.ViewModels` - `Microsoft.CmdPal.UI` - `Microsoft.CmdPal.UI.ViewModels.UnitTests` - Updated unit tests for immutable update patterns. Fixes #46437 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,9 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class FallbackSettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly FallbackSettings _fallbackSettings;
|
||||
private readonly ProviderSettingsViewModel _providerSettingsViewModel;
|
||||
|
||||
private FallbackSettings _fallbackSettings;
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
@@ -27,15 +29,18 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
{
|
||||
if (value != _fallbackSettings.IsEnabled)
|
||||
{
|
||||
_fallbackSettings.IsEnabled = value;
|
||||
var newSettings = _fallbackSettings with { IsEnabled = value };
|
||||
|
||||
if (!_fallbackSettings.IsEnabled)
|
||||
if (!newSettings.IsEnabled)
|
||||
{
|
||||
_fallbackSettings.IncludeInGlobalResults = false;
|
||||
newSettings = newSettings with { IncludeInGlobalResults = false };
|
||||
}
|
||||
|
||||
Save();
|
||||
_fallbackSettings = newSettings;
|
||||
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
|
||||
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,15 +52,18 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
{
|
||||
if (value != _fallbackSettings.IncludeInGlobalResults)
|
||||
{
|
||||
_fallbackSettings.IncludeInGlobalResults = value;
|
||||
var newSettings = _fallbackSettings with { IncludeInGlobalResults = value };
|
||||
|
||||
if (!_fallbackSettings.IsEnabled)
|
||||
if (!newSettings.IsEnabled)
|
||||
{
|
||||
_fallbackSettings.IsEnabled = true;
|
||||
newSettings = newSettings with { IsEnabled = true };
|
||||
}
|
||||
|
||||
Save();
|
||||
_fallbackSettings = newSettings;
|
||||
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
|
||||
|
||||
OnPropertyChanged(nameof(IncludeInGlobalResults));
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +75,7 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_providerSettingsViewModel = providerSettings;
|
||||
_fallbackSettings = fallbackSettings;
|
||||
|
||||
Id = fallback.Id;
|
||||
@@ -77,10 +86,4 @@ public partial class FallbackSettingsViewModel : ObservableObject
|
||||
Icon = new(fallback.InitialIcon);
|
||||
Icon.InitializeProperties();
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
_settingsService.Save();
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user