Compare commits

...

1 Commits

Author SHA1 Message Date
Clint Rutkas
8090a990f5 Settings: Performance fixes and MVVM infrastructure upgrade
Phase 1 - Critical Performance Fixes:
- Convert blocking Task.Wait() to async in GeneralViewModel.ViewDiagnosticDataAsync
- Replace Thread.Sleep() with Task.Delay() in SettingsRepository, GeneralViewModel, GeneralPage
- Defer diagnostic cleanup to background task in GeneralViewModel constructor
- Move DryRunBackup to Loaded event with low priority dispatch
- Implement lazy initialization for hidden MainWindow (defer InitializeComponent until shown)

Phase 2 - Dependency Injection Infrastructure:
- Add ServiceProvider static wrapper for gradual DI migration
- Add ServiceCollectionExtensions for service registration
- Add INavigationService interface with NavigationServiceWrapper
- Add ISettingsUtils interface for SettingsUtils abstraction
- Initialize DI container in App.xaml.cs

Phase 3 - MVVM Toolkit Integration:
- Add CommunityToolkit.Mvvm and Microsoft.Extensions.DependencyInjection packages
- Add ViewModelBase with ObservableRecipient and lifecycle methods
- Add ModuleViewModelBase with common module properties and hotkey conflict handling
- Migrate FileLocksmithViewModel to use [ObservableProperty] and [RelayCommand]

Phase 5 - Messaging System:
- Add message types for cross-component communication (ModuleEnabledChanged,
  SettingsSaved, ThemeChanged, IPCMessageReceived, etc.)

Phase 6 - Async Settings Repository:
- Add IAsyncSettingsRepository interface with ValueTask async methods
- Add AsyncSettingsRepository implementation with caching and SemaphoreSlim

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 13:44:33 -08:00
25 changed files with 1457 additions and 130 deletions

View File

@@ -0,0 +1,259 @@
// 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 System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Async settings repository implementation with caching and thread-safe access.
/// Provides non-blocking settings operations suitable for UI applications.
/// </summary>
/// <typeparam name="T">The settings type.</typeparam>
public sealed class AsyncSettingsRepository<T> : IAsyncSettingsRepository<T>, IDisposable
where T : class, ISettingsConfig, new()
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly ISettingsUtils _settingsUtils;
private readonly string _moduleName;
private readonly string _fileName;
private T _cachedSettings;
private FileSystemWatcher _watcher;
private bool _isDisposed;
/// <inheritdoc/>
public event Action<T> SettingsChanged;
/// <summary>
/// Initializes a new instance of the <see cref="AsyncSettingsRepository{T}"/> class.
/// </summary>
/// <param name="settingsUtils">The settings utilities instance.</param>
/// <param name="fileName">The settings file name.</param>
public AsyncSettingsRepository(ISettingsUtils settingsUtils, string fileName = "settings.json")
{
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
_fileName = fileName;
// Get module name from type
var settingsItem = new T();
_moduleName = settingsItem.GetModuleName();
InitializeWatcher();
}
/// <inheritdoc/>
public T SettingsConfig
{
get
{
if (_cachedSettings == null)
{
_semaphore.Wait();
try
{
if (_cachedSettings == null)
{
_cachedSettings = LoadSettingsInternal();
}
}
finally
{
_semaphore.Release();
}
}
return _cachedSettings;
}
private set => _cachedSettings = value;
}
/// <inheritdoc/>
public async ValueTask<T> GetSettingsAsync(bool forceRefresh = false, CancellationToken cancellationToken = default)
{
if (!forceRefresh && _cachedSettings != null)
{
return _cachedSettings;
}
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!forceRefresh && _cachedSettings != null)
{
return _cachedSettings;
}
_cachedSettings = await Task.Run(() => LoadSettingsInternal(), cancellationToken).ConfigureAwait(false);
return _cachedSettings;
}
finally
{
_semaphore.Release();
}
}
/// <inheritdoc/>
public async ValueTask SaveSettingsAsync(T settings, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(settings);
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Temporarily stop watching to avoid self-triggered events
StopWatching();
await Task.Run(
() => _settingsUtils.SaveSettings(settings.ToJsonString(), _moduleName, _fileName),
cancellationToken).ConfigureAwait(false);
_cachedSettings = settings;
}
finally
{
StartWatching();
_semaphore.Release();
}
}
/// <inheritdoc/>
public bool ReloadSettings()
{
_semaphore.Wait();
try
{
var newSettings = LoadSettingsInternal();
if (newSettings != null)
{
_cachedSettings = newSettings;
return true;
}
return false;
}
catch (Exception ex)
{
Logger.LogError($"Failed to reload settings for {_moduleName}", ex);
return false;
}
finally
{
_semaphore.Release();
}
}
/// <inheritdoc/>
public async ValueTask<bool> ReloadSettingsAsync(CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var newSettings = await Task.Run(() => LoadSettingsInternal(), cancellationToken).ConfigureAwait(false);
if (newSettings != null)
{
_cachedSettings = newSettings;
return true;
}
return false;
}
catch (Exception ex)
{
Logger.LogError($"Failed to reload settings for {_moduleName}", ex);
return false;
}
finally
{
_semaphore.Release();
}
}
/// <inheritdoc/>
public void StopWatching()
{
if (_watcher != null)
{
_watcher.EnableRaisingEvents = false;
}
}
/// <inheritdoc/>
public void StartWatching()
{
if (_watcher != null)
{
_watcher.EnableRaisingEvents = true;
}
}
private T LoadSettingsInternal()
{
try
{
return _settingsUtils.GetSettingsOrDefault<T>(_moduleName, _fileName);
}
catch (Exception ex)
{
Logger.LogError($"Failed to load settings for {_moduleName}", ex);
return new T();
}
}
private void InitializeWatcher()
{
try
{
var filePath = _settingsUtils.GetSettingsFilePath(_moduleName, _fileName);
var directory = Path.GetDirectoryName(filePath);
var fileName = Path.GetFileName(filePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
_watcher = new FileSystemWatcher(directory, fileName);
_watcher.NotifyFilter = NotifyFilters.LastWrite;
_watcher.Changed += OnWatcherChanged;
_watcher.EnableRaisingEvents = true;
}
catch (Exception ex)
{
Logger.LogError($"Failed to initialize settings watcher for {typeof(T).Name}", ex);
}
}
private async void OnWatcherChanged(object sender, FileSystemEventArgs e)
{
// Wait a bit for the file write to complete and retry if needed
for (int i = 0; i < 5; i++)
{
await Task.Delay(100).ConfigureAwait(false);
if (await ReloadSettingsAsync().ConfigureAwait(false))
{
SettingsChanged?.Invoke(_cachedSettings);
return;
}
}
}
/// <inheritdoc/>
public void Dispose()
{
if (!_isDisposed)
{
_watcher?.Dispose();
_semaphore?.Dispose();
_isDisposed = true;
}
}
}
}

View File

@@ -0,0 +1,69 @@
// 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 System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces
{
/// <summary>
/// Interface for asynchronous settings repository operations.
/// Provides non-blocking access to settings with caching support.
/// </summary>
/// <typeparam name="T">The settings type.</typeparam>
public interface IAsyncSettingsRepository<T>
where T : class, ISettingsConfig, new()
{
/// <summary>
/// Occurs when the settings have been externally changed (e.g., by another process).
/// </summary>
event Action<T> SettingsChanged;
/// <summary>
/// Gets the current cached settings synchronously.
/// Returns the cached value immediately, or loads if not cached.
/// </summary>
T SettingsConfig { get; }
/// <summary>
/// Gets the settings asynchronously with optional refresh from disk.
/// </summary>
/// <param name="forceRefresh">If true, bypasses cache and reads from disk.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The settings object.</returns>
ValueTask<T> GetSettingsAsync(bool forceRefresh = false, CancellationToken cancellationToken = default);
/// <summary>
/// Saves the settings asynchronously.
/// </summary>
/// <param name="settings">The settings to save.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the save operation.</returns>
ValueTask SaveSettingsAsync(T settings, CancellationToken cancellationToken = default);
/// <summary>
/// Reloads the settings from disk, updating the cache.
/// </summary>
/// <returns>True if the reload was successful.</returns>
bool ReloadSettings();
/// <summary>
/// Reloads the settings from disk asynchronously, updating the cache.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>True if the reload was successful.</returns>
ValueTask<bool> ReloadSettingsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Stops watching for external file changes.
/// </summary>
void StopWatching();
/// <summary>
/// Starts watching for external file changes.
/// </summary>
void StartWatching();
}
}

View File

@@ -0,0 +1,62 @@
// 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.
namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces
{
/// <summary>
/// Interface for settings utility operations to enable dependency injection and testability.
/// </summary>
public interface ISettingsUtils
{
/// <summary>
/// Checks if a settings file exists.
/// </summary>
/// <param name="powertoy">The module name.</param>
/// <param name="fileName">The settings file name.</param>
/// <returns>True if the settings file exists.</returns>
bool SettingsExists(string powertoy = "", string fileName = "settings.json");
/// <summary>
/// Deletes settings for the specified module.
/// </summary>
/// <param name="powertoy">The module name.</param>
void DeleteSettings(string powertoy = "");
/// <summary>
/// Gets settings for the specified module.
/// </summary>
/// <typeparam name="T">The settings type.</typeparam>
/// <param name="powertoy">The module name.</param>
/// <param name="fileName">The settings file name.</param>
/// <returns>The deserialized settings object.</returns>
T GetSettings<T>(string powertoy = "", string fileName = "settings.json")
where T : ISettingsConfig, new();
/// <summary>
/// Gets settings for the specified module, or returns default if not found.
/// </summary>
/// <typeparam name="T">The settings type.</typeparam>
/// <param name="powertoy">The module name.</param>
/// <param name="fileName">The settings file name.</param>
/// <returns>The deserialized settings object or default.</returns>
T GetSettingsOrDefault<T>(string powertoy = "", string fileName = "settings.json")
where T : ISettingsConfig, new();
/// <summary>
/// Saves settings to a JSON file.
/// </summary>
/// <param name="jsonSettings">The JSON settings string.</param>
/// <param name="powertoy">The module name.</param>
/// <param name="fileName">The settings file name.</param>
void SaveSettings(string jsonSettings, string powertoy = "", string fileName = "settings.json");
/// <summary>
/// Gets the file path to the settings file.
/// </summary>
/// <param name="powertoy">The module name.</param>
/// <param name="fileName">The settings file name.</param>
/// <returns>The full path to the settings file.</returns>
string GetSettingsFilePath(string powertoy = "", string fileName = "settings.json");
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -77,12 +78,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private void Watcher_Changed(object sender, FileSystemEventArgs e)
private async void Watcher_Changed(object sender, FileSystemEventArgs e)
{
// Wait a bit for the file write to complete and retry if needed
for (int i = 0; i < 5; i++)
{
Thread.Sleep(100);
await Task.Delay(100).ConfigureAwait(false);
if (ReloadSettings())
{
SettingsChanged?.Invoke(SettingsConfig);

View File

@@ -16,7 +16,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
// Some functions are marked as virtual to allow mocking in unit tests.
public class SettingsUtils
public class SettingsUtils : ISettingsUtils
{
public const string DefaultFileName = "settings.json";
private const string DefaultModuleName = "";

View File

@@ -0,0 +1,21 @@
// 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.Messaging.Messages;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent when backup/restore operations complete.
/// </summary>
public sealed class BackupRestoreCompletedMessage : ValueChangedMessage<BackupRestoreCompletedMessage.ResultData>
{
public BackupRestoreCompletedMessage(bool success, string message, bool isBackup)
: base(new ResultData(success, message, isBackup))
{
}
public record ResultData(bool Success, string Message, bool IsBackup);
}
}

View File

@@ -0,0 +1,20 @@
// 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.Messaging.Messages;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent when general settings are updated.
/// </summary>
public sealed class GeneralSettingsUpdatedMessage : ValueChangedMessage<GeneralSettings>
{
public GeneralSettingsUpdatedMessage(GeneralSettings settings)
: base(settings)
{
}
}
}

View File

@@ -0,0 +1,21 @@
// 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.Messaging.Messages;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent when hotkey conflicts are detected.
/// </summary>
public sealed class HotkeyConflictDetectedMessage : ValueChangedMessage<HotkeyConflictDetectedMessage.ConflictData>
{
public HotkeyConflictDetectedMessage(string moduleName, string hotkeyDescription, bool isSystemConflict)
: base(new ConflictData(moduleName, hotkeyDescription, isSystemConflict))
{
}
public record ConflictData(string ModuleName, string HotkeyDescription, bool IsSystemConflict);
}
}

View File

@@ -0,0 +1,19 @@
// 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.Messaging.Messages;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent when an IPC message is received from the PowerToys runner.
/// </summary>
public sealed class IPCMessageReceivedMessage : ValueChangedMessage<string>
{
public IPCMessageReceivedMessage(string message)
: base(message)
{
}
}
}

View File

@@ -0,0 +1,21 @@
// 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.Messaging.Messages;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent when a module's enabled state changes.
/// </summary>
public sealed class ModuleEnabledChangedMessage : ValueChangedMessage<ModuleEnabledChangedMessage.ModuleStateData>
{
public ModuleEnabledChangedMessage(string moduleName, bool isEnabled)
: base(new ModuleStateData(moduleName, isEnabled))
{
}
public record ModuleStateData(string ModuleName, bool IsEnabled);
}
}

View File

@@ -0,0 +1,19 @@
// 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.Messaging.Messages;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent to request navigation to a specific page.
/// </summary>
public sealed class NavigateToPageMessage : ValueChangedMessage<System.Type>
{
public NavigateToPageMessage(System.Type pageType)
: base(pageType)
{
}
}
}

View File

@@ -0,0 +1,19 @@
// 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.
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent to request a restart of PowerToys.
/// </summary>
public sealed class RestartRequestedMessage
{
public bool MaintainElevation { get; }
public RestartRequestedMessage(bool maintainElevation = true)
{
MaintainElevation = maintainElevation;
}
}
}

View File

@@ -0,0 +1,21 @@
// 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.Messaging.Messages;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent when settings are saved.
/// </summary>
public sealed class SettingsSavedMessage : ValueChangedMessage<SettingsSavedMessage.SettingsSaveData>
{
public SettingsSavedMessage(string moduleName)
: base(new SettingsSaveData(moduleName))
{
}
public record SettingsSaveData(string ModuleName);
}
}

View File

@@ -0,0 +1,19 @@
// 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.Messaging.Messages;
namespace Microsoft.PowerToys.Settings.UI.Messages
{
/// <summary>
/// Message sent when the application theme changes.
/// </summary>
public sealed class ThemeChangedMessage : ValueChangedMessage<string>
{
public ThemeChangedMessage(string themeName)
: base(themeName)
{
}
}
}

View File

@@ -74,7 +74,9 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />

View File

@@ -0,0 +1,131 @@
// 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 System;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.Services
{
/// <summary>
/// Interface for navigation services to enable testability and abstraction
/// from the static NavigationService class.
/// </summary>
public interface INavigationService
{
/// <summary>
/// Occurs when navigation has completed.
/// </summary>
event NavigatedEventHandler Navigated;
/// <summary>
/// Occurs when navigation has failed.
/// </summary>
event NavigationFailedEventHandler NavigationFailed;
/// <summary>
/// Gets or sets the navigation frame.
/// </summary>
Frame Frame { get; set; }
/// <summary>
/// Gets a value indicating whether navigation can go back.
/// </summary>
bool CanGoBack { get; }
/// <summary>
/// Gets a value indicating whether navigation can go forward.
/// </summary>
bool CanGoForward { get; }
/// <summary>
/// Navigates back in the navigation stack.
/// </summary>
/// <returns>True if navigation was successful.</returns>
bool GoBack();
/// <summary>
/// Navigates forward in the navigation stack.
/// </summary>
void GoForward();
/// <summary>
/// Navigates to the specified page type.
/// </summary>
/// <param name="pageType">The type of page to navigate to.</param>
/// <param name="parameter">Optional navigation parameter.</param>
/// <param name="infoOverride">Optional navigation transition override.</param>
/// <returns>True if navigation was successful.</returns>
bool Navigate(Type pageType, object parameter = null, NavigationTransitionInfo infoOverride = null);
/// <summary>
/// Navigates to the specified page type.
/// </summary>
/// <typeparam name="T">The type of page to navigate to.</typeparam>
/// <param name="parameter">Optional navigation parameter.</param>
/// <param name="infoOverride">Optional navigation transition override.</param>
/// <returns>True if navigation was successful.</returns>
bool Navigate<T>(object parameter = null, NavigationTransitionInfo infoOverride = null)
where T : Page;
/// <summary>
/// Ensures a page of the specified type is selected.
/// </summary>
/// <param name="pageType">The type of page to ensure is selected.</param>
void EnsurePageIsSelected(Type pageType);
}
/// <summary>
/// Wrapper around the static NavigationService to implement INavigationService.
/// Allows for gradual migration and testability.
/// </summary>
public class NavigationServiceWrapper : INavigationService
{
/// <inheritdoc/>
public event NavigatedEventHandler Navigated
{
add => NavigationService.Navigated += value;
remove => NavigationService.Navigated -= value;
}
/// <inheritdoc/>
public event NavigationFailedEventHandler NavigationFailed
{
add => NavigationService.NavigationFailed += value;
remove => NavigationService.NavigationFailed -= value;
}
/// <inheritdoc/>
public Frame Frame
{
get => NavigationService.Frame;
set => NavigationService.Frame = value;
}
/// <inheritdoc/>
public bool CanGoBack => NavigationService.CanGoBack;
/// <inheritdoc/>
public bool CanGoForward => NavigationService.CanGoForward;
/// <inheritdoc/>
public bool GoBack() => NavigationService.GoBack();
/// <inheritdoc/>
public void GoForward() => NavigationService.GoForward();
/// <inheritdoc/>
public bool Navigate(Type pageType, object parameter = null, NavigationTransitionInfo infoOverride = null)
=> NavigationService.Navigate(pageType, parameter, infoOverride);
/// <inheritdoc/>
public bool Navigate<T>(object parameter = null, NavigationTransitionInfo infoOverride = null)
where T : Page
=> NavigationService.Navigate<T>(parameter, infoOverride);
/// <inheritdoc/>
public void EnsurePageIsSelected(Type pageType) => NavigationService.EnsurePageIsSelected(pageType);
}
}

View File

@@ -0,0 +1,73 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.ViewModels;
namespace Microsoft.PowerToys.Settings.UI.Services
{
/// <summary>
/// Extension methods for configuring services in the dependency injection container.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds core Settings UI services to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddSettingsServices(this IServiceCollection services)
{
// Register singleton services
services.AddSingleton<ISettingsUtils>(SettingsUtils.Default);
services.AddSingleton<ThemeService>(App.ThemeService);
services.AddSingleton<INavigationService, NavigationServiceWrapper>();
// Register settings repositories as singletons
services.AddSingleton(sp =>
{
var settingsUtils = sp.GetRequiredService<ISettingsUtils>();
return SettingsRepository<GeneralSettings>.GetInstance((SettingsUtils)settingsUtils);
});
return services;
}
/// <summary>
/// Adds ViewModel registrations to the service collection.
/// ViewModels are registered as transient to create new instances per request.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddViewModels(this IServiceCollection services)
{
// Register ViewModels as transient for fresh instances
// These can be migrated incrementally as each ViewModel is updated
// Tier 1 ViewModels (low complexity) - to be migrated first
// services.AddTransient<FileLocksmithViewModel>();
// services.AddTransient<RegistryPreviewViewModel>();
// services.AddTransient<CropAndLockViewModel>();
// Tier 2 ViewModels (medium complexity)
// services.AddTransient<ColorPickerViewModel>();
// services.AddTransient<AlwaysOnTopViewModel>();
// services.AddTransient<PowerOcrViewModel>();
// services.AddTransient<HostsViewModel>();
// Tier 3 ViewModels (medium-high complexity)
// services.AddTransient<FancyZonesViewModel>();
// services.AddTransient<PowerLauncherViewModel>();
// services.AddTransient<KeyboardManagerViewModel>();
// Tier 4 ViewModels (high complexity) - migrate last
// services.AddTransient<GeneralViewModel>();
// services.AddTransient<DashboardViewModel>();
// services.AddTransient<ShellViewModel>();
return services;
}
}
}

View File

@@ -0,0 +1,113 @@
// 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 System;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.PowerToys.Settings.UI.Services
{
/// <summary>
/// Provides a static wrapper around <see cref="Microsoft.Extensions.DependencyInjection.ServiceProvider"/>
/// to enable gradual migration to dependency injection.
/// </summary>
public static class ServiceProvider
{
private static readonly object _lock = new object();
private static IServiceProvider _serviceProvider;
/// <summary>
/// Gets a value indicating whether the service provider has been initialized.
/// </summary>
public static bool IsInitialized => _serviceProvider != null;
/// <summary>
/// Initializes the service provider with the specified service collection.
/// This should be called once during application startup.
/// </summary>
/// <param name="services">The service collection containing all registered services.</param>
/// <exception cref="InvalidOperationException">Thrown if the service provider is already initialized.</exception>
public static void Initialize(IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
lock (_lock)
{
if (_serviceProvider != null)
{
throw new InvalidOperationException("ServiceProvider is already initialized.");
}
_serviceProvider = services.BuildServiceProvider();
}
}
/// <summary>
/// Gets a service of the specified type.
/// </summary>
/// <typeparam name="T">The type of service to get.</typeparam>
/// <returns>The service instance, or null if not registered.</returns>
public static T GetService<T>()
where T : class
{
EnsureInitialized();
return _serviceProvider.GetService<T>();
}
/// <summary>
/// Gets a required service of the specified type.
/// </summary>
/// <typeparam name="T">The type of service to get.</typeparam>
/// <returns>The service instance.</returns>
/// <exception cref="InvalidOperationException">Thrown if the service is not registered.</exception>
public static T GetRequiredService<T>()
where T : class
{
EnsureInitialized();
return _serviceProvider.GetRequiredService<T>();
}
/// <summary>
/// Gets a service of the specified type.
/// </summary>
/// <param name="serviceType">The type of service to get.</param>
/// <returns>The service instance, or null if not registered.</returns>
public static object GetService(Type serviceType)
{
ArgumentNullException.ThrowIfNull(serviceType);
EnsureInitialized();
return _serviceProvider.GetService(serviceType);
}
/// <summary>
/// Gets a required service of the specified type.
/// </summary>
/// <param name="serviceType">The type of service to get.</param>
/// <returns>The service instance.</returns>
/// <exception cref="InvalidOperationException">Thrown if the service is not registered.</exception>
public static object GetRequiredService(Type serviceType)
{
ArgumentNullException.ThrowIfNull(serviceType);
EnsureInitialized();
return _serviceProvider.GetRequiredService(serviceType);
}
/// <summary>
/// Creates a new scope for scoped services.
/// </summary>
/// <returns>A new service scope.</returns>
public static IServiceScope CreateScope()
{
EnsureInitialized();
return _serviceProvider.CreateScope();
}
private static void EnsureInitialized()
{
if (_serviceProvider == null)
{
throw new InvalidOperationException("ServiceProvider has not been initialized. Call Initialize() first.");
}
}
}
}

View File

@@ -10,6 +10,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
@@ -83,6 +84,9 @@ namespace Microsoft.PowerToys.Settings.UI
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
// Initialize dependency injection
InitializeServices();
InitializeComponent();
UnhandledException += App_UnhandledException;
@@ -95,6 +99,14 @@ namespace Microsoft.PowerToys.Settings.UI
});
}
private static void InitializeServices()
{
var services = new ServiceCollection();
services.AddSettingsServices();
services.AddViewModels();
Services.ServiceProvider.Initialize(services);
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);

View File

@@ -22,15 +22,19 @@ namespace Microsoft.PowerToys.Settings.UI
{
public sealed partial class MainWindow : WindowEx
{
private bool _isFullyInitialized;
private bool _createHidden;
public MainWindow(bool createHidden = false)
{
_createHidden = createHidden;
var bootTime = new System.Diagnostics.Stopwatch();
bootTime.Start();
this.Activated += Window_Activated_SetIcon;
App.ThemeService.ThemeChanged += OnThemeChanged;
App.ThemeService.ApplyTheme();
this.ExtendsContentIntoTitleBar = true;
@@ -43,8 +47,13 @@ namespace Microsoft.PowerToys.Settings.UI
{
placement.ShowCmd = NativeMethods.SW_HIDE;
// Restore the last known placement on the first activation
this.Activated += Window_Activated;
// Defer full initialization until window is shown
this.Activated += Window_Activated_LazyInit;
}
else
{
// Full initialization for visible windows
CompleteInitialization();
}
NativeMethods.SetWindowPlacement(hWnd, ref placement);
@@ -52,6 +61,16 @@ namespace Microsoft.PowerToys.Settings.UI
var loader = ResourceLoaderInstance.ResourceLoader;
Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title");
// IPC callbacks must be set up immediately so messages can be received even when hidden
SetupIPCCallbacks();
bootTime.Stop();
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
}
private void SetupIPCCallbacks()
{
// send IPC Message
ShellPage.SetDefaultSndMessageCallback(msg =>
{
@@ -128,13 +147,10 @@ namespace Microsoft.PowerToys.Settings.UI
App.GetScoobeWindow().Activate();
});
this.InitializeComponent();
SetAppTitleBar();
// receive IPC Message
App.IPCMessageReceivedCallback = (string msg) =>
{
if (ShellPage.ShellHandler.IPCResponseHandleList != null)
if (ShellPage.ShellHandler?.IPCResponseHandleList != null)
{
var success = JsonObject.TryParse(msg, out JsonObject json);
if (success)
@@ -150,10 +166,33 @@ namespace Microsoft.PowerToys.Settings.UI
}
}
};
}
bootTime.Stop();
private void CompleteInitialization()
{
if (_isFullyInitialized)
{
return;
}
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
_isFullyInitialized = true;
App.ThemeService.ApplyTheme();
this.InitializeComponent();
SetAppTitleBar();
}
private void Window_Activated_LazyInit(object sender, WindowActivatedEventArgs args)
{
if (args.WindowActivationState != WindowActivationState.Deactivated && !_isFullyInitialized)
{
CompleteInitialization();
// After lazy init, also restore placement
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
NativeMethods.SetWindowPlacement(hWnd, ref placement);
}
}
private void SetAppTitleBar()
@@ -165,6 +204,8 @@ namespace Microsoft.PowerToys.Settings.UI
public void NavigateToSection(System.Type type)
{
// Ensure full initialization before navigation
CompleteInitialization();
ShellPage.Navigate(type);
}
@@ -222,6 +263,8 @@ namespace Microsoft.PowerToys.Settings.UI
internal void EnsurePageIsSelected()
{
// Ensure full initialization before page selection
CompleteInitialization();
ShellPage.EnsurePageIsSelected();
}
}

View File

@@ -41,9 +41,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
Action stateUpdatingAction = () =>
{
this.DispatcherQueue.TryEnqueue(() =>
this.DispatcherQueue.TryEnqueue(async () =>
{
ViewModel.RefreshUpdatingState();
await ViewModel.RefreshUpdatingStateAsync().ConfigureAwait(true);
});
};
@@ -92,9 +92,16 @@ namespace Microsoft.PowerToys.Settings.UI.Views
CheckBugReportStatus();
doRefreshBackupRestoreStatus(100);
this.Loaded += (s, e) =>
{
ViewModel.OnPageLoaded();
this.Loaded += (s, e) => ViewModel.OnPageLoaded();
// Defer backup status check to after page is loaded with low priority
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
doRefreshBackupRestoreStatus(100);
});
};
}
private void OpenColorsSettings_Click(object sender, RoutedEventArgs e)
@@ -123,11 +130,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void RefreshBackupRestoreStatus(int delayMs = 0)
{
Task.Run(() =>
Task.Run(async () =>
{
if (delayMs > 0)
{
Thread.Sleep(delayMs);
await Task.Delay(delayMs).ConfigureAwait(false);
}
var settingsBackupAndRestoreUtils = SettingsBackupAndRestoreUtils.Instance;
@@ -171,7 +178,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private async void ViewDiagnosticData_Click(object sender, RoutedEventArgs e)
{
await Task.Run(ViewModel.ViewDiagnosticData);
await ViewModel.ViewDiagnosticDataAsync().ConfigureAwait(false);
}
private void BugReportToolClicked(object sender, RoutedEventArgs e)

View File

@@ -1,43 +1,92 @@
// Copyright (c) Microsoft Corporation
// 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 System;
using System.Globalization;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.UI.Services;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class FileLocksmithViewModel : Observable
/// <summary>
/// ViewModel for the File Locksmith settings page.
/// Uses CommunityToolkit.Mvvm for MVVM pattern implementation.
/// </summary>
public partial class FileLocksmithViewModel : ObservableObject
{
private GeneralSettings GeneralSettingsConfig { get; set; }
private readonly SettingsUtils _settingsUtils;
private readonly ISettingsUtils _settingsUtils;
private FileLocksmithSettings Settings { get; set; }
private const string ModuleName = FileLocksmithSettings.ModuleName;
private const string ModuleNameConst = FileLocksmithSettings.ModuleName;
private string _settingsConfigFileFolder = string.Empty;
private GpoRuleConfigured _enabledGpoRuleConfiguration;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEnabledGpoConfigured))]
private bool _enabledStateIsGPOConfigured;
[ObservableProperty]
private bool _isFileLocksmithEnabled;
[ObservableProperty]
private bool _enabledOnContextExtendedMenu;
private Func<string, int> SendConfigMSG { get; }
/// <summary>
/// Initializes a new instance of the <see cref="FileLocksmithViewModel"/> class
/// using dependency injection (for new code).
/// </summary>
/// <param name="settingsUtils">The settings utilities.</param>
/// <param name="generalSettingsRepository">The general settings repository.</param>
/// <param name="sendConfigMSG">The IPC message callback.</param>
public FileLocksmithViewModel(
ISettingsUtils settingsUtils,
ISettingsRepository<GeneralSettings> generalSettingsRepository,
Func<string, int> sendConfigMSG)
{
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
ArgumentNullException.ThrowIfNull(generalSettingsRepository);
GeneralSettingsConfig = generalSettingsRepository.SettingsConfig;
SendConfigMSG = sendConfigMSG ?? throw new ArgumentNullException(nameof(sendConfigMSG));
LoadSettings();
}
/// <summary>
/// Initializes a new instance of the <see cref="FileLocksmithViewModel"/> class
/// (backward compatible constructor for existing code).
/// </summary>
public FileLocksmithViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, string configFileSubfolder = "")
{
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
// To obtain the general settings configurations of PowerToys Settings.
ArgumentNullException.ThrowIfNull(settingsRepository);
_settingsConfigFileFolder = configFileSubfolder;
GeneralSettingsConfig = settingsRepository.SettingsConfig;
SendConfigMSG = ipcMSGCallBackFunc;
LoadSettings();
}
private void LoadSettings()
{
try
{
FileLocksmithLocalProperties localSettings = _settingsUtils.GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
FileLocksmithLocalProperties localSettings = ((SettingsUtils)_settingsUtils).GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
Settings = new FileLocksmithSettings(localSettings);
}
catch (Exception)
@@ -48,16 +97,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
InitializeEnabledValue();
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
_fileLocksmithEnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
EnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
}
public string GetSettingsSubPath()
{
return _settingsConfigFileFolder + "\\" + ModuleName;
return _settingsConfigFileFolder + "\\" + ModuleNameConst;
}
private void InitializeEnabledValue()
@@ -66,65 +111,40 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
{
// Get the enabled state from GPO.
_enabledStateIsGPOConfigured = true;
_isFileLocksmithEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
EnabledStateIsGPOConfigured = true;
IsFileLocksmithEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
}
else
{
_isFileLocksmithEnabled = GeneralSettingsConfig.Enabled.FileLocksmith;
IsFileLocksmithEnabled = GeneralSettingsConfig.Enabled.FileLocksmith;
}
}
public bool IsFileLocksmithEnabled
/// <summary>
/// Gets a value indicating whether the enabled state is configured by GPO.
/// </summary>
public bool IsEnabledGpoConfigured => EnabledStateIsGPOConfigured;
partial void OnIsFileLocksmithEnabledChanged(bool value)
{
get => _isFileLocksmithEnabled;
set
if (EnabledStateIsGPOConfigured)
{
if (_enabledStateIsGPOConfigured)
{
// If it's GPO configured, shouldn't be able to change this state.
return;
}
if (_isFileLocksmithEnabled != value)
{
_isFileLocksmithEnabled = value;
GeneralSettingsConfig.Enabled.FileLocksmith = value;
OnPropertyChanged(nameof(IsFileLocksmithEnabled));
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
// TODO: Implement when this module has properties.
NotifySettingsChanged();
}
// If it's GPO configured, shouldn't be able to change this state.
return;
}
GeneralSettingsConfig.Enabled.FileLocksmith = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
NotifySettingsChanged();
}
public bool EnabledOnContextExtendedMenu
partial void OnEnabledOnContextExtendedMenuChanged(bool value)
{
get
{
return _fileLocksmithEnabledOnContextExtendedMenu;
}
set
{
if (value != _fileLocksmithEnabledOnContextExtendedMenu)
{
_fileLocksmithEnabledOnContextExtendedMenu = value;
Settings.Properties.ExtendedContextMenuOnly.Value = value;
OnPropertyChanged(nameof(EnabledOnContextExtendedMenu));
NotifySettingsChanged();
}
}
}
public bool IsEnabledGpoConfigured
{
get => _enabledStateIsGPOConfigured;
Settings.Properties.ExtendedContextMenuOnly.Value = value;
NotifySettingsChanged();
}
private void NotifySettingsChanged()
@@ -138,13 +158,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.FileLocksmithSettings)));
}
private Func<string, int> SendConfigMSG { get; }
private GpoRuleConfigured _enabledGpoRuleConfiguration;
private bool _enabledStateIsGPOConfigured;
private bool _isFileLocksmithEnabled;
private bool _fileLocksmithEnabledOnContextExtendedMenu;
/// <summary>
/// Refreshes the enabled state by re-reading GPO configuration.
/// </summary>
[RelayCommand]
public void RefreshEnabledState()
{
InitializeEnabledValue();

View File

@@ -213,14 +213,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_fileWatcher = Helper.GetFileWatcher(string.Empty, UpdatingSettings.SettingsFile, dispatcherAction);
}
// Diagnostic data retention policy
// Defer diagnostic data cleanup to background task to avoid blocking UI
_ = Task.Run(CleanupDiagnosticDataAsync);
InitializeLanguages();
}
private void CleanupDiagnosticDataAsync()
{
// Diagnostic data retention policy - runs on background thread
string etwDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\etw");
DeleteDiagnosticDataOlderThan28Days(etwDirPath);
string localLowEtwDirPath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw");
DeleteDiagnosticDataOlderThan28Days(localLowEtwDirPath);
InitializeLanguages();
}
// Supported languages. Taken from Resources.wxs + default + en-US
@@ -1356,52 +1362,54 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
NotifyAllBackupAndRestoreProperties();
}
public void RefreshUpdatingState()
public async Task RefreshUpdatingStateAsync()
{
object oLock = new object();
lock (oLock)
// Load settings with retry on background thread
var config = await Task.Run(async () =>
{
var config = UpdatingSettings.LoadSettings();
var loadedConfig = UpdatingSettings.LoadSettings();
// Retry loading if failed
for (int i = 0; i < 3 && config == null; i++)
for (int i = 0; i < 3 && loadedConfig == null; i++)
{
System.Threading.Thread.Sleep(100);
config = UpdatingSettings.LoadSettings();
await Task.Delay(100).ConfigureAwait(false);
loadedConfig = UpdatingSettings.LoadSettings();
}
if (config == null)
{
return;
}
return loadedConfig;
}).ConfigureAwait(true);
UpdatingSettingsConfig = config;
if (PowerToysUpdatingState != config.State)
{
IsNewVersionDownloading = false;
}
else
{
bool dateChanged = UpdateCheckedDate == UpdatingSettingsConfig.LastCheckedDateLocalized;
bool fileDownloaded = string.IsNullOrEmpty(UpdatingSettingsConfig.DownloadedInstallerFilename);
IsNewVersionDownloading = !(dateChanged || fileDownloaded);
}
PowerToysUpdatingState = UpdatingSettingsConfig.State;
PowerToysNewAvailableVersion = UpdatingSettingsConfig.NewVersion;
PowerToysNewAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
UpdateCheckedDate = UpdatingSettingsConfig.LastCheckedDateLocalized;
_isNoNetwork = PowerToysUpdatingState == UpdatingSettings.UpdatingState.NetworkError;
NotifyPropertyChanged(nameof(IsNoNetwork));
NotifyPropertyChanged(nameof(IsNewVersionDownloading));
NotifyPropertyChanged(nameof(IsUpdatePanelVisible));
_isNewVersionChecked = PowerToysUpdatingState == UpdatingSettings.UpdatingState.UpToDate && !IsNewVersionDownloading;
NotifyPropertyChanged(nameof(IsNewVersionCheckedAndUpToDate));
NotifyPropertyChanged(nameof(IsDownloadAllowed));
if (config == null)
{
return;
}
UpdatingSettingsConfig = config;
if (PowerToysUpdatingState != config.State)
{
IsNewVersionDownloading = false;
}
else
{
bool dateChanged = UpdateCheckedDate == UpdatingSettingsConfig.LastCheckedDateLocalized;
bool fileDownloaded = string.IsNullOrEmpty(UpdatingSettingsConfig.DownloadedInstallerFilename);
IsNewVersionDownloading = !(dateChanged || fileDownloaded);
}
PowerToysUpdatingState = UpdatingSettingsConfig.State;
PowerToysNewAvailableVersion = UpdatingSettingsConfig.NewVersion;
PowerToysNewAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
UpdateCheckedDate = UpdatingSettingsConfig.LastCheckedDateLocalized;
_isNoNetwork = PowerToysUpdatingState == UpdatingSettings.UpdatingState.NetworkError;
NotifyPropertyChanged(nameof(IsNoNetwork));
NotifyPropertyChanged(nameof(IsNewVersionDownloading));
NotifyPropertyChanged(nameof(IsUpdatePanelVisible));
_isNewVersionChecked = PowerToysUpdatingState == UpdatingSettings.UpdatingState.UpToDate && !IsNewVersionDownloading;
NotifyPropertyChanged(nameof(IsNewVersionCheckedAndUpToDate));
NotifyPropertyChanged(nameof(IsDownloadAllowed));
}
private void InitializeLanguages()
@@ -1489,7 +1497,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
internal void ViewDiagnosticData()
internal async Task ViewDiagnosticDataAsync()
{
string localLowEtwDirPath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw");
string etwDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\etw");
@@ -1522,7 +1530,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
string tracerptPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "system32");
ETLConverter converter = new ETLConverter(etwDirPath, tracerptPath);
Task.Run(() => converter.ConvertDiagnosticsETLsAsync()).Wait();
await converter.ConvertDiagnosticsETLsAsync().ConfigureAwait(false);
if (Directory.Exists(etwDirPath))
{

View File

@@ -0,0 +1,242 @@
// 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 System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Services;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
/// <summary>
/// Base class for module-specific ViewModels that provides common functionality
/// such as enabled state, GPO management, and hotkey conflict handling.
/// </summary>
public abstract partial class ModuleViewModelBase : ViewModelBase
{
private readonly Dictionary<string, bool> _hotkeyConflictStatus = new Dictionary<string, bool>();
private readonly Dictionary<string, string> _hotkeyConflictTooltips = new Dictionary<string, string>();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEnabledGpoConfigured))]
private bool _isEnabled;
[ObservableProperty]
private bool _isGpoManaged;
/// <summary>
/// Initializes a new instance of the <see cref="ModuleViewModelBase"/> class.
/// </summary>
protected ModuleViewModelBase()
{
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ModuleViewModelBase"/> class with a custom messenger.
/// </summary>
/// <param name="messenger">The messenger instance to use.</param>
protected ModuleViewModelBase(IMessenger messenger)
: base(messenger)
{
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
}
}
/// <summary>
/// Gets the module name used for settings and conflict detection.
/// </summary>
protected abstract string ModuleName { get; }
/// <summary>
/// Gets a value indicating whether the enabled state is configured by GPO.
/// </summary>
public bool IsEnabledGpoConfigured => IsGpoManaged;
/// <inheritdoc/>
public override void OnPageLoaded()
{
base.OnPageLoaded();
Debug.WriteLine($"=== PAGE LOADED: {ModuleName} ===");
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
}
/// <summary>
/// Gets all hotkey settings for this module.
/// Override in derived classes to return module-specific hotkey settings.
/// </summary>
/// <returns>A dictionary of module names to hotkey settings arrays.</returns>
public virtual Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
{
return null;
}
/// <summary>
/// Handles updates to hotkey conflicts for the module.
/// </summary>
protected virtual void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
{
UpdateHotkeyConflictStatus(e.Conflicts);
var allHotkeySettings = GetAllHotkeySettings();
void UpdateConflictProperties()
{
if (allHotkeySettings != null)
{
foreach (KeyValuePair<string, HotkeySettings[]> kvp in allHotkeySettings)
{
var module = kvp.Key;
var hotkeySettingsList = kvp.Value;
for (int i = 0; i < hotkeySettingsList.Length; i++)
{
var key = $"{module.ToLowerInvariant()}_{i}";
hotkeySettingsList[i].HasConflict = GetHotkeyConflictStatus(key);
hotkeySettingsList[i].ConflictDescription = GetHotkeyConflictTooltip(key);
}
}
}
}
_ = Task.Run(() =>
{
try
{
var settingsWindow = App.GetSettingsWindow();
settingsWindow.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, UpdateConflictProperties);
}
catch
{
UpdateConflictProperties();
}
});
}
/// <summary>
/// Gets module-related conflicts from all conflicts data.
/// </summary>
protected ModuleConflictsData GetModuleRelatedConflicts(AllHotkeyConflictsData allConflicts)
{
var moduleConflicts = new ModuleConflictsData();
if (allConflicts.InAppConflicts != null)
{
foreach (var conflict in allConflicts.InAppConflicts)
{
if (IsModuleInvolved(conflict))
{
moduleConflicts.InAppConflicts.Add(conflict);
}
}
}
if (allConflicts.SystemConflicts != null)
{
foreach (var conflict in allConflicts.SystemConflicts)
{
if (IsModuleInvolved(conflict))
{
moduleConflicts.SystemConflicts.Add(conflict);
}
}
}
return moduleConflicts;
}
/// <summary>
/// Updates the hotkey conflict status based on all conflicts data.
/// </summary>
protected virtual void UpdateHotkeyConflictStatus(AllHotkeyConflictsData allConflicts)
{
_hotkeyConflictStatus.Clear();
_hotkeyConflictTooltips.Clear();
if (allConflicts.InAppConflicts.Count > 0)
{
foreach (var conflictGroup in allConflicts.InAppConflicts)
{
foreach (var conflict in conflictGroup.Modules)
{
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
{
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
_hotkeyConflictStatus[keyName] = true;
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
}
}
}
}
if (allConflicts.SystemConflicts.Count > 0)
{
foreach (var conflictGroup in allConflicts.SystemConflicts)
{
foreach (var conflict in conflictGroup.Modules)
{
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
{
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
_hotkeyConflictStatus[keyName] = true;
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText");
}
}
}
}
}
/// <summary>
/// Gets whether a specific hotkey has a conflict.
/// </summary>
protected virtual bool GetHotkeyConflictStatus(string key)
{
return _hotkeyConflictStatus.ContainsKey(key) && _hotkeyConflictStatus[key];
}
/// <summary>
/// Gets the conflict tooltip for a specific hotkey.
/// </summary>
protected virtual string GetHotkeyConflictTooltip(string key)
{
return _hotkeyConflictTooltips.TryGetValue(key, out string value) ? value : null;
}
private bool IsModuleInvolved(HotkeyConflictGroupData conflict)
{
if (conflict.Modules == null)
{
return false;
}
return conflict.Modules.Any(module =>
string.Equals(module.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase));
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
}
}
base.Dispose(disposing);
}
}
}

View File

@@ -0,0 +1,108 @@
// 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 System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
/// <summary>
/// Base class for all ViewModels using CommunityToolkit.Mvvm.
/// Provides lifecycle methods and messaging support.
/// </summary>
public abstract class ViewModelBase : ObservableRecipient, IDisposable
{
private bool _isDisposed;
private bool _isActive;
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
/// </summary>
protected ViewModelBase()
: base(WeakReferenceMessenger.Default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBase"/> class with a custom messenger.
/// </summary>
/// <param name="messenger">The messenger instance to use.</param>
protected ViewModelBase(IMessenger messenger)
: base(messenger)
{
}
/// <summary>
/// Gets a value indicating whether this ViewModel is currently active (visible).
/// </summary>
public bool IsViewModelActive => _isActive;
/// <summary>
/// Called when the view associated with this ViewModel is navigated to.
/// Override this method to perform initialization when the page is displayed.
/// </summary>
public virtual void OnNavigatedTo()
{
_isActive = true;
IsActive = true; // Activates message subscriptions in ObservableRecipient
}
/// <summary>
/// Called when the view associated with this ViewModel is navigated to.
/// Override this method to perform async initialization when the page is displayed.
/// </summary>
/// <returns>A task representing the async operation.</returns>
public virtual Task OnNavigatedToAsync()
{
OnNavigatedTo();
return Task.CompletedTask;
}
/// <summary>
/// Called when the view associated with this ViewModel is navigated away from.
/// Override this method to perform cleanup when the page is no longer displayed.
/// </summary>
public virtual void OnNavigatedFrom()
{
_isActive = false;
IsActive = false; // Deactivates message subscriptions in ObservableRecipient
}
/// <summary>
/// Called when the page is loaded. This is triggered from the Page's Loaded event.
/// </summary>
public virtual void OnPageLoaded()
{
// Default implementation does nothing.
// Override in derived classes to perform actions when the page is loaded.
}
/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing">True to release both managed and unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
// Deactivate message subscriptions
IsActive = false;
}
_isDisposed = true;
}
}
}
}