Files
PowerToys/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/FallbackSettingsViewModel.cs
Michael Jolley 4337f8e5ff 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>
2026-03-27 17:54:58 +00:00

90 lines
3.0 KiB
C#

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class FallbackSettingsViewModel : ObservableObject
{
private readonly ISettingsService _settingsService;
private readonly ProviderSettingsViewModel _providerSettingsViewModel;
private FallbackSettings _fallbackSettings;
public string DisplayName { get; private set; } = string.Empty;
public IconInfoViewModel Icon { get; private set; } = new(null);
public string Id { get; private set; } = string.Empty;
public bool IsEnabled
{
get => _fallbackSettings.IsEnabled;
set
{
if (value != _fallbackSettings.IsEnabled)
{
var newSettings = _fallbackSettings with { IsEnabled = value };
if (!newSettings.IsEnabled)
{
newSettings = newSettings with { IncludeInGlobalResults = false };
}
_fallbackSettings = newSettings;
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
OnPropertyChanged(nameof(IsEnabled));
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
}
}
}
public bool IncludeInGlobalResults
{
get => _fallbackSettings.IncludeInGlobalResults;
set
{
if (value != _fallbackSettings.IncludeInGlobalResults)
{
var newSettings = _fallbackSettings with { IncludeInGlobalResults = value };
if (!newSettings.IsEnabled)
{
newSettings = newSettings with { IsEnabled = true };
}
_fallbackSettings = newSettings;
_providerSettingsViewModel.UpdateFallbackSettings(Id, _fallbackSettings);
OnPropertyChanged(nameof(IncludeInGlobalResults));
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
}
}
}
public FallbackSettingsViewModel(
TopLevelViewModel fallback,
FallbackSettings fallbackSettings,
ProviderSettingsViewModel providerSettings,
ISettingsService settingsService)
{
_settingsService = settingsService;
_providerSettingsViewModel = providerSettings;
_fallbackSettings = fallbackSettings;
Id = fallback.Id;
DisplayName = string.IsNullOrWhiteSpace(fallback.DisplayTitle)
? (string.IsNullOrWhiteSpace(fallback.Title) ? providerSettings.DisplayName : fallback.Title)
: fallback.DisplayTitle;
Icon = new(fallback.InitialIcon);
Icon.InitializeProperties();
}
}